Compare commits

...

20 Commits

Author SHA1 Message Date
grimhilt
bffcdafe7a display mail in iframe, add design for thread and unseen 2023-03-27 01:04:43 +02:00
grimhilt
a9d15027aa apply difference between mailbox and account 2023-03-26 14:55:13 +02:00
grimhilt
d0d666f4cb link imap sync to server and show email on front 2023-03-26 14:20:16 +02:00
grimhilt
b156c5954d advancements in tests and storing messages 2023-03-25 16:47:23 +01:00
grimhilt
097dd8bf21 load message in front 2023-03-25 13:06:59 +01:00
grimhilt
cb5021750a fix modal background opacity 2023-03-23 23:59:49 +01:00
grimhilt
7008e24941 start to load messages from rooms 2023-03-20 21:28:13 +01:00
grimhilt
0f87bdc715 basic routing for roomview 2023-03-20 15:00:15 +01:00
grimhilt
47b8c54122 fetch rooms 2023-03-20 14:43:07 +01:00
grimhilt
ace2063309 fetching mailboxes from api 2023-03-17 13:31:27 +01:00
grimhilt
6b96815b93 save message working without reply 2023-03-16 16:14:25 +01:00
grimhilt
520eb95d37 save message sync 2023-03-15 14:48:15 +01:00
grimhilt
3e029a26d4 advancement on logic of app 2023-03-13 19:12:57 +01:00
grimhilt
28b2b69dc8 add queries for app functionnalities 2023-03-13 00:54:44 +01:00
grimhilt
c81042a223 started some app structure 2023-03-13 00:13:17 +01:00
grimhilt
749127ac19 update database structure 2023-03-11 14:52:08 +01:00
grimhilt
61d7eb386c gloablly save messages 2023-03-10 17:07:05 +01:00
grimhilt
02e3af693a solution clean not working 2023-03-10 16:18:04 +01:00
grimhilt
ac8211defd save 2023-03-10 16:08:50 +01:00
grimhilt
5f2cbd82b6 remove password 2023-03-01 16:57:11 +01:00
85 changed files with 17531 additions and 11203 deletions

14
.gitignore vendored
View File

@@ -21,3 +21,17 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
.tmp
.s.*
log*
config.json
.direnv
.envrc
*.txt
*.json
tmp
test.*
*.png
!*/schemas/*

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"printWidth": 120,
"tabWidth": 4,
"quoteProps": "consistent",
"trailingComma": "all",
"useTabs": false
}

View File

@@ -1,24 +0,0 @@
# mail
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -1,13 +0,0 @@
const statusCodes = require("../utils/statusCodes.js").statusCodes;
/**
* Return all mailboxes and folders for an user
*/
function getMailboxes(req, res) {
const {token} = req.params;
const query = ``;
}
module.exports = {
getFolders,
}

View File

@@ -0,0 +1,22 @@
const statusCode = require("../utils/statusCodes").statusCodes;
const { registerAccount } = require("../db/api");
const { getAddresseId } = require("../db/mail");
async function addAccount(body, res) {
const { email, pwd, xoauth, xoauth2, host, port, tls } = body;
getAddresseId(email).then((addressId) => {
registerAccount(addressId, pwd, xoauth, xoauth2, host, port, tls)
.then((mailboxId) => {
res.status(statusCode.OK).json({ id: mailboxId });
})
.catch(() => {
res.status(statusCode.INTERNAL_SERVER_ERROR);
});
});
}
module.exports = {
addAccount,
};
// todo change mailbox to account

View File

@@ -0,0 +1,16 @@
const statusCode = require("../utils/statusCodes").statusCodes;
const { getMessages } = require("../db/api.js");
async function messages(body, res) {
const { roomId } = body;
getMessages(roomId).then((messages) => {
res.status(statusCode.OK).json(messages);
}).catch((err) => {
console.log(err)
res.status(statusCode.INTERNAL_SERVER_ERROR);
});
}
module.exports = {
messages,
};

16
back/controllers/rooms.js Normal file
View File

@@ -0,0 +1,16 @@
const statusCode = require("../utils/statusCodes").statusCodes;
const { getRooms } = require("../db/api.js");
async function rooms(body, res) {
const { mailboxId, offset, limit } = body;
getRooms(mailboxId).then((rooms) => {
res.status(statusCode.OK).json(rooms);
}).catch((err) => {
console.log(err)
res.status(statusCode.INTERNAL_SERVER_ERROR);
});
}
module.exports = {
rooms,
};

106
back/db/api.js Normal file
View File

@@ -0,0 +1,106 @@
const { db, execQueryAsync, execQueryAsyncWithId } = require("./db.js");
const { queryCcId, queryToId, queryFromId } = require("./utils/addressQueries.js");
const DEBUG = require("../utils/debug").DEBUG;
async function registerAccount(userId, pwd, xoauth, xoauth2, host, port, tls) {
const query = `
INSERT INTO app_account
(user_id, account_pwd, xoauth, xoauth2, host, port, tls) VALUES (?, ?, ?, ?, ?, ?, ?)
`;
const values = [userId, pwd, xoauth, xoauth2, host, port, tls];
return await execQueryAsyncWithId(query, values);
}
async function getAccounts() {
// todo mailbox or account id ?
const query = `
SELECT
mailbox.mailbox_id AS id,
address.email
FROM app_account
INNER JOIN address
INNER JOIN mailbox
WHERE
address.address_id = app_account.user_id AND
mailbox.account_id = app_account.account_id
`;
const values = [];
return await execQueryAsync(query, values);
}
async function getRooms(mailboxId) {
const query = `
SELECT
app_room.room_id AS id,
app_room.room_name AS roomName,
address.email AS user,
app_room.owner_id AS userId,
app_room.notSeen,
mailbox_message.mailbox_id AS mailboxId
FROM app_room
INNER JOIN message
INNER JOIN mailbox_message
INNER JOIN address
WHERE
message.message_id = app_room.message_id AND
mailbox_message.mailbox_id = ? AND
mailbox_message.message_id = message.message_id AND
address.address_id = app_room.owner_id
ORDER BY app_room.lastUpdate DESC
`;
const values = [mailboxId];
return await execQueryAsync(query, values);
}
async function getMessages(roomId) {
// todo attachements name
const query = `
SELECT
msg.message_id AS id,
GROUP_CONCAT(fromT.address_id) AS fromA,
GROUP_CONCAT(toT.address_id) AS toA,
GROUP_CONCAT(ccT.address_id) AS ccA,
subjectT.value AS subject,
content.text AS content,
message.idate AS date
FROM app_room_message msg
${queryFromId} fromT ON msg.message_id = fromT.message_id
${queryToId} toT ON msg.message_id = toT.message_id
${queryCcId} ccT ON msg.message_id = ccT.message_id
LEFT JOIN (
SELECT header_field.message_id, header_field.value
FROM header_field
INNER JOIN field_name
WHERE
field_name.field_id = header_field.field_id AND
field_name.field_name = 'subject'
) subjectT ON msg.message_id = subjectT.message_id
LEFT JOIN (
SELECT bodypart.text, header_field.message_id FROM bodypart
INNER JOIN header_field
INNER JOIN field_name
WHERE
field_name.field_id = header_field.field_id AND
field_name.field_name = 'html' AND
bodypart.bodypart_id = header_field.bodypart_id
) content ON msg.message_id = content.message_id
INNER JOIN message ON message.message_id = msg.message_id
WHERE msg.room_id = ?
GROUP BY msg.message_id
ORDER BY message.idate;
`;
const values = [roomId];
return await execQueryAsync(query, values);
}
module.exports = {
registerAccount,
getAccounts,
getRooms,
getMessages
};

140
back/db/database.dart Normal file
View File

@@ -0,0 +1,140 @@
Table "addresses" {
"id" int [pk, not null, increment]
"name" text
"localpart" text [not null]
"domain" text [not null]
"email" text [not null]
Indexes {
email [unique]
}
}
Table "mailboxes" {
"id" int [pk, not null, increment]
"name" text [not null]
"uidnext" int [not null, default: 1]
"nextmodseq" bigint [not null, default: 1]
"first_recent" int [not null, default: 1]
"uidvalidity" int [not null, default: 1]
Indexes {
name [unique]
}
}
Table "messages" {
"id" int [pk, not null, increment]
"messageID" text [pk, not null]
"idate" timestamp [not null]
"rfc822size" int
}
Table "mailbox_messages" {
"mailbox" int [not null]
"uid" int [pk, not null]
"message" int [pk, not null]
"modseq" bigint [not null]
"seen" boolean [not null, default: false]
"deleted" boolean [not null, default: false]
}
Ref: mailbox_messages.mailbox > mailboxes.id
Ref: mailbox_messages.message - messages.id
Table "bodyparts" {
"id" int [pk, not null, increment]
"bytes" int [not null]
"hash" text [not null]
"text" text
"data" binary
}
Table "part_numbers" {
"message" int [pk, not null]
"part" text [not null]
"bodypart" int [not null]
"bytes" int
"nb_lines" int
}
// todo on delete cascade
Ref: part_numbers.message > messages.id
Ref: part_numbers.bodypart - bodyparts.id
Table "field_names" {
"id" int [pk, not null, increment]
"name" text [not null]
Indexes {
name [unique]
}
}
Table "header_fields" {
"id" int [pk, not null, increment]
"message" int [pk, not null]
"part" text [not null]
"position" int [not null]
"field" int [not null]
"value" text
Indexes {
message [unique]
part [unique]
position [unique]
field [unique]
}
}
Ref: header_fields.message > messages.id
Ref: header_fields.part > part_numbers.part
Ref: header_fields.field > field_names.id
Table "address_fields" {
"message" int [not null]
"part" text [not null]
"position" int [not null]
"field" int [not null]
"number" int
"address" int [not null]
}
Ref: address_fields.message > messages.id
Ref: address_fields.part > part_numbers.part
Ref: address_fields.field > field_names.id
Ref: address_fields.address > addresses.id
// app table
Table "front_threads" {
"id" int [pk, not null, increment]
"room" int [not null]
"name" text
"notSeen" int [not null, default: true]
"lastUpdate" timestamp [not null]
"isDm" bool [not null, default: true]
}
Ref: front_threads.room > front_rooms.id
Table "front_rooms" {
"id" int [pk, not null, increment]
"name" text
"isGroup" bool [not null, default: false]
"notSeen" int [not null]
"lastUpdate" timestamp [not null]
}
Table "front_room_messages" {
"room" int [not null]
"thread" int [not null]
"message" int [not null]
}
Ref: front_room_messages.room > front_rooms.id
Ref: front_room_messages.message - messages.id
Ref: front_room_messages.thread > front_threads.id

108
back/db/database.sql Normal file
View File

@@ -0,0 +1,108 @@
CREATE TABLE `addresses` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` text,
`localpart` text NOT NULL,
`domain` text NOT NULL,
`email` text NOT NULL
);
CREATE TABLE `mailboxes` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` text NOT NULL,
`uidnext` int NOT NULL DEFAULT 1,
`nextmodseq` bigint NOT NULL DEFAULT 1,
`first_recent` int NOT NULL DEFAULT 1,
`uidvalidity` int NOT NULL DEFAULT 1
);
CREATE TABLE `messages` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`idate` timestamp NOT NULL,
`rfc822size` int
);
CREATE TABLE `mailbox_messages` (
`mailbox` int NOT NULL,
`uid` int NOT NULL,
`message` int NOT NULL,
`modseq` bigint NOT NULL,
`seen` boolean NOT NULL DEFAULT false,
`deleted` boolean NOT NULL DEFAULT false,
PRIMARY KEY (`uid`, `message`)
);
CREATE TABLE `bodyparts` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`bytes` int NOT NULL,
`hash` text NOT NULL,
`text` text,
`data` binary
);
CREATE TABLE `part_numbers` (
`message` int PRIMARY KEY NOT NULL,
`part` text NOT NULL,
`bodypart` int NOT NULL,
`bytes` int,
`nb_lines` int
);
CREATE TABLE `field_names` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` text NOT NULL
);
CREATE TABLE `header_fields` (
`id` int NOT NULL AUTO_INCREMENT,
`message` int NOT NULL,
`part` text NOT NULL,
`position` int NOT NULL,
`field` int NOT NULL,
`value` text,
PRIMARY KEY (`id`, `message`)
);
CREATE TABLE `address_fields` (
`message` int NOT NULL,
`part` text NOT NULL,
`position` int NOT NULL,
`field` int NOT NULL,
`number` int,
`address` int NOT NULL
);
CREATE UNIQUE INDEX `addresses_index_0` ON `addresses` (`email`);
CREATE UNIQUE INDEX `mailboxes_index_1` ON `mailboxes` (`name`);
CREATE UNIQUE INDEX `field_names_index_2` ON `field_names` (`name`);
CREATE UNIQUE INDEX `header_fields_index_3` ON `header_fields` (`message`);
CREATE UNIQUE INDEX `header_fields_index_4` ON `header_fields` (`part`);
CREATE UNIQUE INDEX `header_fields_index_5` ON `header_fields` (`position`);
CREATE UNIQUE INDEX `header_fields_index_6` ON `header_fields` (`field`);
ALTER TABLE `mailbox_messages` ADD FOREIGN KEY (`mailbox`) REFERENCES `mailboxes` (`id`);
ALTER TABLE `messages` ADD FOREIGN KEY (`id`) REFERENCES `mailbox_messages` (`message`);
ALTER TABLE `part_numbers` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
ALTER TABLE `bodyparts` ADD FOREIGN KEY (`id`) REFERENCES `part_numbers` (`bodypart`);
ALTER TABLE `header_fields` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
ALTER TABLE `header_fields` ADD FOREIGN KEY (`part`) REFERENCES `part_numbers` (`part`);
ALTER TABLE `header_fields` ADD FOREIGN KEY (`field`) REFERENCES `field_names` (`id`);
ALTER TABLE `address_fields` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
ALTER TABLE `address_fields` ADD FOREIGN KEY (`part`) REFERENCES `part_numbers` (`part`);
ALTER TABLE `address_fields` ADD FOREIGN KEY (`field`) REFERENCES `field_names` (`id`);
ALTER TABLE `address_fields` ADD FOREIGN KEY (`address`) REFERENCES `addresses` (`id`);

60
back/db/db.js Normal file
View File

@@ -0,0 +1,60 @@
const mysql = require("mysql");
const MYSQL = require("./config.json").mysql;
const DEBUG = require("../utils/debug.js").DEBUG;
const db = mysql.createConnection({
host: MYSQL.host,
user: MYSQL.user,
password: MYSQL.pwd,
database: MYSQL.database,
});
db.connect(function (err) {
if (err) {
DEBUG.log("Impossible de se connecter", err.code);
} else {
DEBUG.log("Database successfully connected");
}
});
function execQueryAsync(query, values) {
return new Promise((resolve, reject) => {
db.query(query, values, (err, results, fields) => {
if (err) {
reject(err);
} else {
resolve(results);
}
});
});
}
function execQueryAsyncWithId(query, values) {
return new Promise((resolve, reject) => {
db.query(query, values, (err, results, fields) => {
if (err) {
reject(err);
} else {
resolve(results.insertId);
}
});
});
}
function execQuery(query, values) {
db.query(query, values, (err, results, fields) => {
if (err) {
DEBUG.log(err);
throw (err);
}
return results;
});
}
module.exports = {
db, // todo remove this
execQuery,
execQueryAsync,
execQueryAsyncWithId
};

49
back/db/imap/imap.js Normal file
View File

@@ -0,0 +1,49 @@
const { execQueryAsyncWithId, execQueryAsync, execQuery } = require("../db");
async function getAllAccounts() {
const query = `
SELECT
app_account.account_id AS id,
address.email AS user,
app_account.account_pwd AS password,
app_account.host AS host,
app_account.port AS port,
app_account.tls AS tls
FROM app_account INNER JOIN address
WHERE address.address_id = app_account.user_id
`;
const values = [];
return await execQueryAsync(query, values);
}
async function getAllMailboxes(accountId) {
const query = 'SELECT * FROM mailbox WHERE mailbox.account_id = ?';
const values = [accountId];
return await execQueryAsync(query, values)
}
async function registerMailbox(accountId, mailboxName) {
const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`;
const values = [accountId, mailboxName];
return await execQueryAsyncWithId(query, values);
}
async function getMailbox(mailboxId) {
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
const values = [mailboxId];
return await execQueryAsync(query, values);
}
function updateMailbox(mailboxId, uidnext) {
const query = `UPDATE mailbox SET uidnext = ? WHERE mailbox_id = ?`;
const values = [uidnext, mailboxId];
execQuery(query, values);
}
module.exports = {
getAllAccounts,
getAllMailboxes,
registerMailbox,
getMailbox,
updateMailbox
}

31
back/db/mail.js Normal file
View File

@@ -0,0 +1,31 @@
const { execQueryAsync, execQueryAsyncWithId } = require("./db.js");
const DEBUG = require("../utils/debug").DEBUG;
async function getAddresseId(email, name) {
const localpart = email.split("@")[0];
const domain = email.split("@")[1];
const query = `INSERT INTO address
(address_name, localpart, domain, email) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE address_name = ?, address_id = LAST_INSERT_ID(address_id)`;
const values = [name, localpart, domain, email, name];
return await execQueryAsyncWithId(query, values);
}
async function getFieldId(field) {
const query = `INSERT INTO field_name (field_name) VALUES (?) ON DUPLICATE KEY UPDATE field_id=LAST_INSERT_ID(field_id)`;
const values = [field]
return await execQueryAsyncWithId(query, values);
}
async function findRoomByOwner(ownerId) {
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
const values = [ownerId];
return await execQueryAsync(query, values);
}
module.exports = {
getAddresseId,
getFieldId,
findRoomByOwner,
};

77
back/db/saveMessage.js Normal file
View File

@@ -0,0 +1,77 @@
const { transformEmojis } = require("../utils/string.js");
const { db, execQuery, execQueryAsync, execQueryAsyncWithId } = require("./db.js");
const DEBUG = require("../utils/debug").DEBUG;
async function registerMessage(timestamp, rfc822size, messageId) {
const query = `
INSERT INTO message
(idate, messageID, rfc822size) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE message_id = LAST_INSERT_ID(message_id)
`;
const values = [timestamp, messageId, rfc822size];
return await execQueryAsyncWithId(query, values);
}
function registerMailbox_message(mailboxId, uid, messageId, modseq, seen, deleted) {
const query = `
INSERT IGNORE INTO mailbox_message
(mailbox_id, uid, message_id, modseq, seen, deleted) VALUES (?, ?, ?, ?, ?, ?)
`;
const values = [mailboxId, uid, messageId, modseq, seen, deleted];
execQuery(query, values);
}
function registerBodypart(messageId, part, bodypartId, bytes, nbLines) {
const query = `
INSERT IGNORE INTO part_number
(message_id, part, bodypart_id, bytes, nb_lines) VALUES (?, ?, ?, ?, ?)
`;
const values = [messageId, part, bodypartId, bytes, nbLines];
execQuery(query, values);
}
async function saveBodypart(bytes, hash, text, data) {
text = transformEmojis(text);
const query = `INSERT IGNORE INTO bodypart (bytes, hash, text, data) VALUES (?, ?, ?, ?)`;
const values = [bytes, hash, text, data];
return await execQueryAsyncWithId(query, values);
}
async function saveHeader_fields(messageId, fieldId, bodypartId, part, value) {
value = transformEmojis(value);
const query = `
INSERT IGNORE INTO header_field
(message_id, field_id, bodypart_id, part, value) VALUES (?, ?, ?, ?, ?)
`;
const values = [messageId, fieldId, bodypartId, part, value];
return await execQueryAsync(query, values);
}
async function saveAddress_fields(messageId, fieldId, addressId, number) {
const query = `
INSERT IGNORE INTO address_field
(message_id , field_id, address_id, number) VALUES (?, ?, ?, ?)
`;
const values = [messageId, fieldId, addressId, number];
return await execQueryAsync(query, values);
}
function saveSource(messageId, content) {
content = transformEmojis(content);
const query = `
INSERT INTO source (message_id, content) VALUES (?, ?)
ON DUPLICATE KEY UPDATE content = ?
`;
const values = [messageId, content, content];
execQuery(query, values);
}
module.exports = {
registerMessage,
registerMailbox_message,
saveHeader_fields,
saveAddress_fields,
registerBodypart,
saveBodypart,
saveSource
}

129
back/db/saveMessageApp.js Normal file
View File

@@ -0,0 +1,129 @@
const { transformEmojis } = require("../utils/string.js");
const { db, execQueryAsync, execQueryAsyncWithId, execQuery } = require("./db.js");
const { queryFromId, queryToId, queryCcId } = require("./utils/addressQueries.js");
const DEBUG = require("../utils/debug").DEBUG;
async function createRoom(roomName, ownerId, messageId) {
roomName = transformEmojis(roomName);
const query = `INSERT INTO app_room (room_name, owner_id, message_id) VALUES (?, ?, ?)`;
const values = [roomName.substring(0, 255), ownerId, messageId];
return await execQueryAsyncWithId(query, values);
// todo add members
}
async function registerMessageInRoom(messageId, roomId, isSeen, idate) {
const query = `INSERT IGNORE INTO app_room_message (message_id, room_id) VALUES (?, ?)`;
const values = [messageId, roomId];
await execQueryAsync(query, values);
updateLastUpdateRoom(roomId, idate);
if (!isSeen) {
incrementNotSeenRoom(roomId);
}
}
function updateLastUpdateRoom(roomId, idate) {
const query = `UPDATE app_room SET lastUpdate = ? WHERE room_id = ?`;
const values = [idate, roomId];
execQuery(query, values);
}
function incrementNotSeenRoom(roomId) {
// todo
}
async function createThread(threadName, ownerId, messageId, parentRoomId, isDm) {
const rootRoomId = -1; // todo
const threadId = await createRoom(threadName, ownerId, messageId);
const query = `INSERT INTO app_thread (room_id, parent_room_id, root_room_id, isDm) VALUES (?, ?, ?, ?)`;
const values = [threadId, parentRoomId, rootRoomId, isDm];
return await execQueryAsync(query, values);
// todo add members
}
async function registerMessageInThread(messageId, threadId, isSeen) {
// todo check if it is still a thread or should be a room
// todo isdm
console.log("register message in thread")
}
function updateLastUpdateThread(threadId) {
// todo
// check for parent
}
function incrementNotSeenThread(threadId) {
// todo
// also increment parent room
}
async function isRoomGroup(roomId) {
return new Promise((resolve, reject) => {
const query = `SELECT isGroup FROM app_room WHERE room_id = '${roomId}'`;
db.query(query, (err, results, fields) => {
if (err) reject(err);
resolve(results[0].isGroup);
});
});
}
async function findRoomsFromMessage(messageId) {
const query = `SELECT room_id FROM app_room_message WHERE message_id = ? ORDER BY room_id`;
const values = [messageId];
return await execQueryAsync(query, values);
}
async function hasSameMembersAsParent(messageId, messageID) {
const query1 = `
SELECT
GROUP_CONCAT(fromT.address_id) AS fromA,
GROUP_CONCAT(toT.address_id) AS toA,
GROUP_CONCAT(ccT.address_id) AS ccA
FROM message msg
${queryFromId} fromT ON msg.message_id = fromT.message_id
${queryToId} toT ON msg.message_id = toT.message_id
${queryCcId} ccT ON msg.message_id = ccT.message_id
WHERE msg.message_id = ?
`;
const values1 = [messageId];
let addressesMsg1 = await execQueryAsync(query1, values1);
const query2 = `
SELECT
GROUP_CONCAT(fromT.address_id) AS fromA,
GROUP_CONCAT(toT.address_id) AS toA,
GROUP_CONCAT(ccT.address_id) AS ccA
FROM message msg
${queryFromId} fromT ON msg.message_id = fromT.message_id
${queryToId} toT ON msg.message_id = toT.message_id
${queryCcId} ccT ON msg.message_id = ccT.message_id
WHERE msg.messageID = ?
`;
const values2 = [messageID];
let addressesMsg2 = await execQueryAsync(query2, values2);
addressesMsg1 = addressesMsg1[0]?.fromA
?.split(",")
.concat(addressesMsg1[0]?.toA?.split(","))
.concat(addressesMsg1[0]?.ccA?.split(","));
addressesMsg2 = addressesMsg2[0]?.fromA
?.split(",")
.concat(addressesMsg2[0]?.toA?.split(","))
.concat(addressesMsg2[0]?.ccA?.split(","));
return (
addressesMsg1.length == addressesMsg2.length &&
addressesMsg1.reduce((a, b) => a && addressesMsg2.includes(b), true)
);
}
module.exports = {
createRoom,
registerMessageInRoom,
createThread,
registerMessageInThread,
isRoomGroup,
findRoomsFromMessage,
hasSameMembersAsParent,
};

View File

@@ -139,3 +139,19 @@ create table app_accounts (
tls int(1) not null default 0,
primary key (id)
);
create table app_rooms (
id int not null auto_increment,
name text not null,
owner int not null,
isGroup BIT(1) not null default 0,
notSeen int not null default 0,
lastUpdate timestamp not null,
primary key (id)
);
create table app_room_messages (
message int [not null]
room int,
primary key()
)

161
back/db/structureV2.sql Normal file
View File

@@ -0,0 +1,161 @@
-- Mail storage
-- 1
CREATE TABLE address (
address_id INT AUTO_INCREMENT,
address_name TEXT,
localpart TEXT NOT NULL,
domain TEXT NOT NULL,
email TEXT NOT NULL,
PRIMARY KEY (address_id),
UNIQUE KEY (email)
);
-- 2 app
CREATE TABLE app_account (
account_id INT AUTO_INCREMENT,
user_id INT NOT NULL,
account_pwd BINARY(22),
xoauth VARCHAR(116),
xoauth2 VARCHAR(116),
host VARCHAR(255) NOT NULL DEFAULT 'localhost',
port INT(5) NOT NULL DEFAULT 143,
tls BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (account_id),
FOREIGN KEY (user_id) REFERENCES address(address_id) ON DELETE CASCADE
);
-- 3
CREATE TABLE mailbox (
mailbox_id INT AUTO_INCREMENT,
account_id INT NOT NULL,
mailbox_name TEXT NOT NULL,
uidnext INT NOT NULL DEFAULT 1,
nextmodseq BIGINT NOT NULL DEFAULT 1,
first_recent INT NOT NULL DEFAULT 1,
uidvalidity INT NOT NULL DEFAULT 1,
PRIMARY KEY (mailbox_id),
FOREIGN KEY (account_id) REFERENCES app_account(account_id) ON DELETE CASCADE
);
-- 4
CREATE TABLE message (
message_id INT AUTO_INCREMENT,
messageID TEXT NOT NULL,
idate TIMESTAMP NOT NULL,
rfc822size INT NOT NULL,
PRIMARY KEY (message_id),
UNIQUE KEY (messageID)
);
-- 5
-- if mailbox_message deleted message is not deleted
CREATE TABLE mailbox_message (
mailbox_id INT NOT NULL,
uid INT,
message_id INT,
modseq BIGINT NOT NULL,
seen BOOLEAN NOT NULL DEFAULT false,
deleted BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (uid, message_id),
FOREIGN KEY (mailbox_id) REFERENCES mailbox(mailbox_id) ON DELETE CASCADE,
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE
);
-- 6
CREATE TABLE bodypart (
bodypart_id INT AUTO_INCREMENT,
bytes INT NOT NULL,
hash TEXT NOT NULL,
text TEXT,
data BINARY,
PRIMARY KEY (bodypart_id)
);
-- 7
CREATE TABLE source (
message_id INT NOT NULL,
content TEXT NOT NULL,
PRIMARY KEY (message_id),
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE
);
-- 8
CREATE TABLE field_name (
field_id INT AUTO_INCREMENT,
field_name VARCHAR (255),
PRIMARY KEY (field_id),
UNIQUE KEY (field_name)
);
-- 9
CREATE TABLE header_field (
message_id INT NOT NULL,
field_id INT NOT NULL,
bodypart_id INT,
part VARCHAR(128),
value TEXT,
UNIQUE KEY (message_id, field_id, bodypart_id),
UNIQUE KEY (message_id, field_id, part),
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
FOREIGN KEY (field_id) REFERENCES field_name(field_id) ON DELETE CASCADE,
FOREIGN KEY (bodypart_id) REFERENCES bodypart(bodypart_id)
);
-- 10
CREATE TABLE address_field (
message_id INT NOT NULL,
field_id INT NOT NULL,
address_id INT NOT NULL,
number INT,
UNIQUE KEY (message_id, field_id, address_id),
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
FOREIGN KEY (field_id) REFERENCES field_name(field_id),
FOREIGN KEY (address_id) REFERENCES address(address_id)
);
-- App table
-- 11
CREATE TABLE app_room (
room_id INT AUTO_INCREMENT,
room_name VARCHAR(255) NOT NULL,
owner_id INT NOT NULL,
message_id INT NOT NULL,
isGroup BOOLEAN NOT NULL DEFAULT false,
notSeen INT NOT NULL DEFAULT 0,
lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
PRIMARY KEY (room_id),
FOREIGN KEY (owner_id) REFERENCES address(address_id),
FOREIGN KEY (message_id) REFERENCES message(message_id)
);
-- 12
CREATE TABLE app_thread (
room_id INT NOT NULL,
parent_room_id INT,
root_room_id INT,
isDm BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (room_id),
UNIQUE KEY (room_id, parent_room_id, root_room_id),
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE,
FOREIGN KEY (parent_room_id) REFERENCES app_room(room_id) ON DELETE SET NULL,
FOREIGN KEY (root_room_id) REFERENCES app_room(room_id) ON DELETE SET NULL
);
-- 13
CREATE TABLE app_room_message (
message_id INT NOT NULL,
room_id INT,
UNIQUE KEY (message_id, room_id),
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE SET NULL
);
-- 14
CREATE TABLE app_room_member (
room_id INT NOT NULL,
member_id INT NOT NULL,
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE,
FOREIGN KEY (member_id) REFERENCES address(address_id)
);

View File

@@ -0,0 +1,20 @@
const queryAddress = (type) => `
LEFT JOIN (
SELECT address_field.address_id, address_field.message_id
FROM address_field
INNER JOIN field_name
WHERE
field_name.field_id = address_field.field_id AND
field_name.field_name = '${type}'
)
`;
const queryFromId = queryAddress("from");
const queryToId = queryAddress("to");
const queryCcId = queryAddress("cc");
module.exports = {
queryFromId,
queryToId,
queryCcId
}

View File

@@ -1,162 +0,0 @@
const Imap = require('imap');
const {simpleParser} = require('mailparser');
const inspect = require('util').inspect;
const saveMessage = require('./storeMessage').saveMessage;
const imap = new Imap({
user: '***REMOVED***',
password: '***REMOVED***',
tlsOptions: {servername: "imap.gmail.com"},
host: 'imap.gmail.com',
port: 993,
tls: true
});
imap.once('ready', function() {
const readOnly = true;
imap.openBox('INBOX', readOnly, (err, box) => {
// console.log(box); // uidvalidty uidnext, messages total and new
if (err) throw err;
const f = imap.seq.fetch('2:2', {
bodies: ['HEADER.FIELDS (FROM)','TEXT'],
struct: true,
envelope: true,
extensions: true
});
f.on('message', function(msg, seqno) {
// console.log('Message #%d', seqno);
var prefix = '(#' + seqno + ') ';
let attributes = undefined;
let body = undefined;
msg.on('body', function(stream, info) {
simpleParser(stream, async (err, parsed) => {
body = parsed;
if (attributes) {
saveMessage(body, attributes);
};
// console.log(parsed.headers)
// const {from, subject, textAsHtml, text} = parsed;
// console.log(parsed.attachments)
// console.log(prefix + parsed.text)
// console.log(parsed.from.value);
// console.log(parsed.subject);
// console.log(parsed.date)
// console.log(parsed.replyTo.value);
// console.log(parsed.messageId);
// console.log(parsed.html);
// console.log(parsed.text);
// console.log(parsed.textAsHtml);
});
});
msg.once('attributes', attrs => {
attributes = attrs;
if (body) {
saveMessage(body, attributes);
};
// console.log(prefix + 'Attributes: %s', inspect(attrs, false, 8));
});
msg.once('end', function() {
console.log(prefix + 'Finished');
});
});
f.once('error', function(err) {
console.log('Fetch error: ' + err);
});
f.once('end', function() {
console.log('Done fetching all messages!');
imap.end();
});
});
});
imap.once('error', function(err) {
console.log(err);
});
imap.once('end', function() {
console.log('Connection ended');
});
imap.connect();
// const getEmails = () => {
// imap.once('ready', () => {
// imap.openBox('INBOX', false, () => {
// imap.search(['UNSEEN'], (err, results) => {
// const f = imap.fetch(results, {bodies: ''});
// f.on('message', msg => {
// msg.on('body', stream => {
// simpleParser(stream, async (err, parsed) => {
// // const {from, subject, textAsHtml, text} = parsed;
// // console.log(parsed.from.value);
// // console.log(parsed.subject);
// // console.log(parsed.date)
// // console.log(parsed.replyTo.value);
// // console.log(parsed.messageId);
// // console.log(parsed.html);
// // console.log(parsed.text);
// // console.log(parsed.textAsHtml);
// // 'x-emsg-mtaselection' => 'prod_5_emailing.carrefour.fr',
// // 'message-id' => '<emsg.6584.7d09.15dd1ec64c1@ukmme02.em.unica.net>',
// // 'feedback-id' => 'emailing.carrefour.fr:10070-6584:emsg-x',
// // 'list' => { unsubscribe: [Object] },
// // 'mime-version' => '1.0',
// // 'content-type' => { value: 'text/html', params: [Object] },
// // 'content-transfer-encoding' => 'quoted-printable'
// });
// });
// msg.once('attributes', attrs => {
// const {uid} = attrs;
// console.log(uid)
// // imap.addFlags(uid, ['\\Seen'], () => {
// // // Mark the email as read after reading it
// // console.log('Marked as read!');
// // });
// });
// });
// f.once('error', ex => {
// return Promise.reject(ex);
// });
// f.once('end', () => {
// console.log('Done fetching all messages!');
// imap.end();
// });
// });
// });
// });
// imap.once('error', err => {
// console.log(err);
// });
// imap.once('end', () => {
// console.log('Connection ended');
// });
// imap.connect();
// };
// getEmails();
function isValidEmail(email) {
// todo
return true;
}
module.exports = {
isValidEmail
}

View File

@@ -1,72 +0,0 @@
const { getAddresseId } = require("../sql/mail");
const { DEBUG } = require("../utils/debug");
const { registerMessage, registerMailbox_message, saveHeader_fields, saveAddress_fields } = require('../sql/saveMessage');
const { getMailboxId, getField } = require('../sql/mail');
function saveMessage(message, attributes, mailbox) {
const timestamp = new Date(attributes.envelope.date).getTime();
const rfc822size = 0; // todo
registerMessage(timestamp, rfc822size).then((messageId) => {
getMailboxId(mailbox).then((mailboxId) => {
const seen = attributes.flags.includes('Seen') ? 1 : 0; // todo verify
const deleted = attributes.flags.includes('Deleted') ? 1 : 0; // todo verify
registerMailbox_message(mailboxId, attributes.uid, messageId, attributes.modseq, seen, deleted).then(() => {
attributes.struct.forEach(part => {
// saveBodyparts().then((bodypartId) => {
// const partText = undefined;
// savePart_numbers(messageId, partText, bodypartId, part.size, part.lines)
// });
});
const part = ''; // todo
Object.keys(attributes.envelope).forEach(key => {
const newKey = keyNormalizer(key);
if (isHeader(newKey)) {
getField(newKey).then((fieldId) => {
saveHeader_fields(messageId, part, 2, fieldId, attributes.envelope[key]);
});
} else {
getField(newKey).then((fieldId) => {
if (attributes.envelope[key]) {
attributes.envelope[key].forEach((elt, index) => {
saveAddress_fields(messageId, part, fieldId, index, getAddresseId(`${elt.mailbox}@${elt.host}`, elt.name));
});
}
});
}
});
// todo add date field
});
});
});
}
function isHeader(key) {
switch (key) {
case 'date':
case 'subject':
case 'messageId':
return true;
case 'from':
case 'sender':
case 'replyTo':
case 'to':
case 'cc':
case 'bcc':
case 'inReplyTo':
return false;
default:
DEBUG.log("Unknown header key: "+key);
return true;
}
}
function keyNormalizer(key) {
// todo
return key;
}
module.exports = {
saveMessage
}

14
back/jest-mysql-config.js Normal file
View File

@@ -0,0 +1,14 @@
const MYSQL = require("./sql/config.json").mysql;
module.exports = {
databaseOptions: {
host: "localhost",
port: 3306,
user: MYSQL.user,
password: MYSQL.pwd,
database: "mail_test",
},
createDatabase: true,
dbSchema: "./sql/structureV2.sql",
truncateDatabase: true,
};

58
back/mails/imap/Box.js Normal file
View File

@@ -0,0 +1,58 @@
const { getMailbox, updateMailbox } = require("../../db/imap/imap");
const { DEBUG } = require("../../utils/debug");
const { registerMessageInApp } = require("../saveMessage");
const { saveMessage } = require("../storeMessage");
class Box {
constructor(_imap, boxId, _boxName) {
this.imap = _imap;
this.boxName = _boxName;
this.id = boxId;
this.box;
this.init();
}
async init() {
this.box = (await getMailbox(this.id))[0];
const readOnly = true;
this.imap.openBox(this.boxName, readOnly, (err, box) => {
if (err) DEBUG.log(err);
this.sync(this.box.uidnext, box.uidnext);
});
}
sync(savedUid, currentUid) {
const promises = [];
const mails = [];
const f = this.imap.seq.fetch(`${savedUid}:${currentUid}`, {
size: true,
envelope: true,
});
f.on("message", (msg, seqno) => {
msg.once("attributes", (attrs) => {
mails.push(attrs);
promises.push(saveMessage(attrs, this.id, this.imap));
});
});
f.once("error", (err) => {
DEBUG.log("Fetch error: " + err);
});
f.once("end", async () => {
await Promise.all(promises).then(async (res) => {
for (let i = 0; i < mails.length; i++) {
console.log(i, mails[i].uid)
await registerMessageInApp(res[i], mails[i]);
}
});
updateMailbox(this.id, currentUid);
});
}
}
module.exports = {
Box,
};

View File

@@ -0,0 +1,73 @@
const Imap = require("imap");
const { getAllMailboxes, registerMailbox } = require("../../db/imap/imap");
const { DEBUG } = require("../../utils/debug");
const { Box } = require("./Box");
class ImapInstance {
constructor(account) {
this.imap = new Imap({
user: account.user,
password: account.password,
tlsOptions: { servername: account.host },
host: account.host,
port: account.port,
tls: account.tls,
});
this.account = account;
this.boxes = [];
/**
* IMAP
*/
this.imap.once("ready", () => {
DEBUG.log("imap connected")
this.imapReady();
});
this.imap.once("error", function (err) {
DEBUG.log(err);
});
this.imap.once("end", function () {
DEBUG.log("Connection ended");
});
this.imap.connect();
}
imapReady() {
getAllMailboxes(this.account.id).then((mailboxes) => {
if (mailboxes.length > 0) {
this.boxes.push(new Box(this.imap, mailboxes[0].mailbox_id, mailboxes[0].mailbox_name));
} else {
this.imap.getBoxes('', (err, boxes) => {
if (err) DEBUG.log(err);
const allBoxName = this.getAllBox(boxes);
registerMailbox(this.account.id, allBoxName).then((mailboxId) => {
this.boxes.push(new Box(this.imap, mailboxId, allBoxName));
});
});
}
});
}
getAllBox(boxes) {
// ideally we should get the all box to get all messages
let allBox;
Object.keys(boxes).forEach(key => {
if (key === 'INBOX') return;
allBox = key;
Object.keys(boxes[key].children).forEach((childBoxes) => {
if (boxes[key].children[childBoxes].attribs.includes('\\All')) {
allBox += '/' + childBoxes;
}
});
});
return allBox;
}
}
module.exports = {
ImapInstance
}

View File

@@ -0,0 +1,28 @@
const { getAllAccounts } = require("../../db/imap/imap");
const { DEBUG } = require("../../utils/debug");
const { ImapInstance } = require("./ImapInstance");
class ImapSync {
constructor() {
this.instances = [];
}
init() {
getAllAccounts().then((accounts) => {
for (let i = 0; i < accounts.length; i++) {
accounts[i].password = accounts[i]?.password.toString().replace(/[\u{0080}-\u{FFFF}]/gu,"");
this.addInstance(accounts[i]);
}
}).catch((err) => {
DEBUG.log(err);
});
}
addInstance(config) {
this.instances.push(new ImapInstance(config));
}
}
module.exports = {
ImapSync
}

95
back/mails/saveMessage.js Normal file
View File

@@ -0,0 +1,95 @@
const {
createRoom,
registerMessageInRoom,
createThread,
registerMessageInThread,
isRoomGroup,
findRoomsFromMessage,
hasSameMembersAsParent,
} = require("../db/saveMessageApp");
const { findRoomByOwner, getAddresseId } = require("../db/mail");
const { isDmOnEnvelope } = require("./utils/statusUtils");
/**
* take object address and join mailbox and host to return mailbox@host
*/
function createAddress(elt) {
return `${elt.mailbox}@${elt.host}`;
}
async function registerMessageInApp(messageId, attrs) {
const isSeen = attrs.flags.includes("Seen") ? 1 : 0; // todo verify
const envelope = attrs.envelope;
await getAddresseId(createAddress(envelope.sender[0])).then(async (ownerId) => {
if (envelope.inReplyTo) {
await registerReplyMessage(envelope, messageId, isSeen, ownerId);
} else {
await findRoomByOwner(ownerId).then(async (res) => {
if (res.length == 0) {
// first message of this sender
await createRoom(envelope.subject, ownerId, messageId).then(async (roomId) => {
await registerMessageInRoom(messageId, roomId, isSeen, envelope.date);
});
} else {
// not a reply, add to the list of message if this sender
await registerMessageInRoom(messageId, res[0].room_id, isSeen, envelope.date);
}
});
}
});
}
async function registerReplyMessage(envelope, messageId, isSeen, ownerId) {
const messageID = envelope.messageId;
await findRoomsFromMessage(messageId).then(async (rooms) => {
if (rooms.length == 0) {
// no rooms, so is a transfer
// todo test if members of transferred message are included
} else if (rooms.length === 1) {
// only one room so message is only in a room and not in a thread
// as a thread is associated to a room to begin
await isRoomGroup(rooms[0].room_id).then(async (isGroup) => {
if (isGroup) {
const hasSameMembers = await hasSameMembersAsParent(messageID, envelope.inReplyTo);
if (hasSameMembers) {
await registerMessageInRoom(messageId, rooms[0].room_id, isSeen, envelope.date);
} else {
// is a group and has not the same member as the previous message
// some recipient has been removed create a thread
const isDm = isDmOnEnvelope(envelope);
await createThread(envelope.subject, ownerId, messageId, rooms[0].room_id, isDm).then(async (threadId) => {
await registerMessageInThread(messageId, threadId, isSeen);
});
}
} else {
// reply from channel
// todo
// if (sender == owner) { // correction from the original sender
// // leave in the same channel
// }
}
});
} else if (rooms.length > 1) {
// get the lowest thread (order by room_id)
const room = rooms[rooms.length-1];
const hasSameMembers = await hasSameMembersAsParent(messageID, envelope.inReplyTo);
if (hasSameMembers) {
await registerMessageInThread(messageId, room.room_id, isSeen);
} else {
// has not the same members so it is a derivation of this thread
// todo put this in a function and add default message in the reply chain
const isDm = isDmOnEnvelope(envelope);
await createThread(envelope.subject, ownerId, messageId, room.room_id, isDm).then(async (threadId) => {
await registerMessageInThread(messageId, threadId, isSeen);
});
}
}
});
}
module.exports = {
registerMessageInApp
};

128
back/mails/storeMessage.js Normal file
View File

@@ -0,0 +1,128 @@
const { getAddresseId } = require("../db/mail");
const { DEBUG } = require("../utils/debug");
const { simpleParser } = require("mailparser");
const moment = require("moment");
const {
registerMessage,
registerMailbox_message,
saveHeader_fields,
saveAddress_fields,
registerBodypart,
saveBodypart,
saveSource,
} = require("../db/saveMessage");
const { getFieldId } = require("../db/mail");
function saveMessage(attrs, mailboxId, imap) {
const envelope = attrs.envelope;
const ts = moment(new Date(envelope.date).getTime()).format("YYYY-MM-DD HH:mm:ss");
const rfc822size = attrs.size;
const messageID = envelope.messageId;
return new Promise((resolve, reject) => {
registerMessage(ts, rfc822size, messageID)
.then((messageId) => {
const isSeen = attrs.flags.includes("Seen") ? 1 : 0; // todo verify
const deleted = attrs.flags.includes("Deleted") ? 1 : 0; // todo verify
registerMailbox_message(mailboxId, attrs.uid, messageId, attrs.modseq, isSeen, deleted);
const f = imap.fetch(attrs.uid, { bodies: "" });
let buffer = "";
f.on("message", function (msg, seqno) {
msg.on("body", function (stream, info) {
stream.on("data", function (chunk) {
buffer += chunk.toString("utf8");
});
stream.once("end", () => {
// save raw data todo
// saveSource(messageId, buffer);
// parse data
simpleParser(buffer, async (err, parsed) => {
saveFromParsedData(parsed, messageId)
.then(() => {
resolve(messageId);
})
.catch((err) => {
reject(err);
});
});
});
});
});
f.once("error", function (err) {
console.log("Fetch error: " + err);
});
f.once("end", function () {
DEBUG.log("Done fetching data of "+messageID);
});
})
.catch((err) => {
DEBUG.log("Unable to register message: " + err);
reject(err);
});
});
}
async function saveFromParsedData(parsed, messageId) {
const promises = [];
Object.keys(parsed).forEach((key) => {
if (["from", "to", "cc", "bcc", "replyTo"].includes(key)) {
promises.push(
// save address field
getFieldId(key).then((fieldId) => {
parsed[key].value.forEach((addr, nb) => {
getAddresseId(addr.address, addr.name).then(async (addressId) => {
await saveAddress_fields(messageId, fieldId, addressId, nb);
});
});
}),
);
} else if (["subject", "inReplyTo"].includes(key)) {
// todo : "references" (array)
promises.push(
getFieldId(key).then(async (fieldId) => {
await saveHeader_fields(messageId, fieldId, undefined, undefined, parsed[key]);
}),
);
} else if (["html", "text", "textAsHtml"].includes(key)) {
const hash = "0";
const size = "0";
saveBodypart(size, hash, parsed[key], "").then((bodypartId) => {
getFieldId(key).then((fieldId) => {
saveHeader_fields(
messageId,
fieldId,
bodypartId,
undefined, // todo ?
undefined
);
});
});
} else if (key == "attachments") {
// todo
} else if (["date", "messageId", "headers", "headerLines"].includes(key)) {
// messageId and date are already saved
// other field are not important and can be retrieved in source
return;
} else {
DEBUG.log("doesn't know key: " + key);
return;
}
});
return Promise.all(promises);
// todo when transfered
}
module.exports = {
saveMessage,
};
if (process.env['NODE_DEV'] == 'TEST') {
module.exports = {
saveFromParsedData
};
}

View File

@@ -0,0 +1,13 @@
function isDmOnEnvelope(envelope) {
const members =
envelope.bcc?.length +
envelope.cc?.length +
envelope.to?.length +
envelope.sender?.length +
envelope.from?.length;
return members === 2;
}
module.exports = {
isDmOnEnvelope,
};

7904
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,24 @@
{
"dependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"imap": "^0.8.19",
"imap-simple": "^5.1.0",
"mailparser": "^3.6.3",
"moment": "^2.29.4",
"mysql": "^2.18.1",
"vue-router": "^4.1.6"
},
"devDependencies": {
"jest": "^29.5.0",
"jest-mysql": "^2.0.0"
},
"jest": {
"preset": "jest-mysql",
"testMatch": [
"<rootDir>/test//**/*-test.[jt]s?(x)"
]
}
}

48
back/routes/mail.js Normal file
View File

@@ -0,0 +1,48 @@
const statusCodes = require("../utils/statusCodes.js").statusCodes;
const express = require("express");
const router = express.Router();
const Ajv = require("ajv");
const addFormats = require("ajv-formats");
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
const schema_account = require("../schemas/account_schema.json");
const { addAccount } = require("../controllers/addAccount.js");
const { getAccounts } = require("../db/api.js");
const { rooms } = require("../controllers/rooms.js");
const { messages } = require("../controllers/messages.js");
const validate_account = ajv.compile(schema_account);
/**
* Return all mailboxes and folders for an user
*/
router.get("/accounts", (req, res) => {
getAccounts().then((data) => {
res.status(statusCodes.OK).json(data);
});
});
router.get("/:mailboxId/rooms", async (req, res) => {
// todo use offset limit
await rooms(req.params, res);
});
router.get("/:roomId/messages", async (req, res) => {
const { roomId } = req.params;
await messages(req.params, res);
});
/**
* Register a new mailbox inside the app
*/
router.post("/account", async (req, res) => {
const valid = validate_account(req.body);
if (!valid) {
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validate_account.errors });
} else {
await addAccount(req.body, res);
}
});
module.exports = router;

View File

@@ -0,0 +1,14 @@
{
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"pwd": { "type": "string" },
"xoauth": { "type": "string" },
"xoauth2": { "type": "string" },
"host": { "type": "string", "format": "hostname" },
"port": { "type": "number", "maximum": 65535 },
"tls": { "type": "boolean" }
},
"required": ["email", "host", "port", "tls"],
"additionalProperties": false
}

View File

@@ -1,20 +1,19 @@
const mails = require("./api/mails.js");
const express = require('express');
const cors = require('cors')
const express = require("express");
const cors = require("cors");
const app = express();
const { ImapSync } = require("./mails/imap/ImapSync");
app.use(express.json());
app.use(
express.urlencoded({
extended: true,
})
express.urlencoded({
extended: true,
}),
);
app.use(cors());
app.listen(process.env.PORT || 5500);
const mailRouter = require("./routes/mail");
app.use("/api/mail", mailRouter);
// anecdote
app.get("/api/mails/mailboxes", mails.getMailboxes);
const imapSync = new ImapSync();
imapSync.init()

View File

@@ -1,23 +0,0 @@
const mysql = require("mysql");
const MYSQL = require("./config.json").mysql;
const DEBUG = require("../utils/debug.js").DEBUG;
const bdd = mysql.createConnection({
host: MYSQL.host,
user: MYSQL.user,
password: MYSQL.pwd,
database: MYSQL.database,
});
bdd.connect(function (err) {
if (err) {
DEBUG.log("Impossible de se connecter", err.code);
} else {
DEBUG.log("Database successfully connected");
}
});
module.exports = {
bdd: bdd,
};

View File

@@ -1,48 +0,0 @@
const bdd = require("./bdd.js").bdd;
const DEBUG = require("../utils/debug").DEBUG;
function registerMessage(timestamp, rfc822size) {
return new Promise((resolve, reject) => {
resolve(0);
//todo
const query = `INSERT INTO messages (idate, rfc822size) VALUES (UNIX_TIMESTAMP('${timestamp}'), '${rfc822size}')`;
bdd.query(query, (err, results, fields) => {
if (err) reject(err);
// resolve(results.insertId);
});
});
}
function registerMailbox_message(mailboxId, uid, messageId, modseq, seen, deleted) {
return new Promise((resolve, reject) => {
const query = `INSERT IGNORE INTO mailbox_messages (mailbox, uid, message, modseq, seen, deleted) VALUES ('${mailboxId}', '${uid}', '${messageId}', '${modseq}', '${seen}', '${deleted}')`;
bdd.query(query, (err, results, fields) => {
if (err) reject(err);
resolve();
// resolve(results.insertId);
});
});
}
function saveBodyparts() {}
function saveHeader_fields(message, part, position, field, value) {
const query = `INSERT IGNORE INTO header_fields (message, part, position, field, value) VALUES ('${message}', '${part}', '${position}', '${field}', '${value}')`;
bdd.query(query, (err, results, fields) => {
if (err) throw err;
});
}
function saveAddress_fields(message, part, position, field, number, address) {
const query = `INSERT IGNORE INTO address_fields (message, part, position, field, number, address) VALUES ('${message}', '${part}', '${position}', '${field}', '${number}', '${address}')`;
bdd.query(query, (err, results, fields) => {
if (err) throw err;
});
}
module.exports = {
registerMessage,
registerMailbox_message,
saveHeader_fields,
saveAddress_fields,
}

View File

@@ -0,0 +1,52 @@
process.env["NODE_DEV"] = "TEST";
const { saveFromParsedData } = require("../../mails/storeMessage");
const { TestDb } = require("../sql/test-utilsDb");
const MYSQL = require("../../db/config.json").MYSQL;
let db;
beforeAll(async () => {
const options = {
databaseOptions: {
host: "localhost",
port: 3306,
user: MYSQL.user,
password: MYSQL.pwd,
database: "mail_test",
},
dbSchema: "../../sql/structureV2.sql",
};
db = new TestDb(options);
await db.init();
});
afterAll(async () => {
db.cleanTables();
});
describe("saveMessage", async () => {
describe("rooms", async () => {
it("messages not related and from same sender should be in the same room", async () => {
await saveFromParsedData(
{
to: { value: [{ address: "address1@email.com" }] },
from: { value: [{ address: "address2@email.com" }] },
messageId: "<messageId1>",
},
1,
);
await saveFromParsedData(
{
to: { value: [{ address: "address1@email.com" }] },
from: { value: [{ address: "address2@email.com" }] },
messageId: "<messageId2>",
},
2,
);
// todo call parser
const query = ""
db.execQueryAsync().then((res) => {
expect(res.length).toBe(2);
})
});
});
});

View File

@@ -0,0 +1,19 @@
beforeAll(async () => {
// const schema = fs.readFileSync('../../sql/structureV2.sql', 'utf8');
// await setupDB(mysqlConfig, schema);
});
afterAll(async () => {
global.db.query("DROP database mail_test");
});
describe('saveMessageApp', async () => {
beforeEach(() => {
});
it("", () => {
});
});

View File

@@ -0,0 +1,62 @@
const mysql = require("mysql");
export class TestDb {
constructor (options) {
this.options = options;
this.db;
}
async init () {
return new Promise((resolve, reject) => {
this.db = mysql.createConnection({
host: this.options.databaseOptions.host,
user: this.options.databaseOptions.user,
password: this.options.databaseOptions.pwd,
database: this.options.databaseOptions.database,
});
this.db.connect(async function (err) {
if (err) {
reject("Impossible de se connecter", err.code)
} else {
if (this.options.dbSchema) {
const schema = fs.readFileSync(this.options.dbSchema, 'utf8');
await this.execQueryAsync(schema, []);
resolve("Database successfully connected and created !")
} else {
resolve("Database successfully connected");
}
}
});
});
}
cleanTables() {
const query = "SELECT table_name FROM INFORMATION_SCHEMA.tables WHERE table_schema = ?";
const values = [this.options.databaseOptions.database];
this.execQueryAsync(query, values).then((results) => {
this.execQuery("SET FOREIGN_KEY_CHECKS=0");
results.map((table) => {
execQuery("DROP TABLE " + table.table_name);
});
});
}
execQuery(query, values) {
db.query(query, values, (err, results, fields) => {
});
}
async execQueryAsync(query, values) {
return new Promise((resolve, reject) => {
db.query(query, values, (err, results, fields) => {
if (err) {
reject(err);
} else {
resolve(results);
}
});
});
}
}

13
back/utils/array.js Normal file
View File

@@ -0,0 +1,13 @@
function removeDuplicates(array) {
let unique = [];
for (let i = 0; i < array.length; i++) {
if (!unique.includes(array[i])) {
unique.push(array[i]);
}
}
return unique;
}
module.exports = {
removeDuplicates,
};

View File

@@ -1,9 +1,9 @@
const DEBUG = (function() {
const timestamp = function(){};
timestamp.toString = () => "[DEBUG " + new Date().toLocaleDateString() + "]";
return { log: console.log.bind(console, "%s", timestamp) };
const DEBUG = (function () {
const timestamp = function () {};
timestamp.toString = () => "[" + new Date().toLocaleString() + "]";
return { log: console.log.bind(console, "%s", timestamp) };
})();
module.exports = {
DEBUG: DEBUG
}
DEBUG,
};

25
back/utils/string.js Normal file
View File

@@ -0,0 +1,25 @@
function transformEmojis(str) {
if (!str) return str;
// Use a regular expression to match emojis in the string
const regex =
/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F900}-\u{1F9FF}\u{1F1E0}-\u{1F1FF}]/gu;
// Replace each matched emoji with its Unicode code point
const transformedStr = str.replace(regex, (match) => {
return "\\u{" + match.codePointAt(0).toString(16).toUpperCase() + "}";
});
return transformedStr;
}
function decodeEmojis(text) {
const regex = /\\u{([^}]+)}/g;
const decodedText = text.replace(regex, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
return decodedText;
}
module.exports = {
transformEmojis,
decodeEmojis
}

3173
back/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

34
doc.js Normal file
View File

@@ -0,0 +1,34 @@
if (isReply) {
if (inThread) {
if (hasSameMembers) {
// register new message in thread
// possibly convert to room only if parent is channel
} else {
// create sub thread
}
} else if (inRoom) {
if (isGroup) {
if (hasSameMembers) {
// register new message in group
} else {
// create thread
}
} else { // reply from channel
if (messageInRoom == 1) { // was new channel transform to group
// register new message in group
} else if (sender == owner) { // correction from the original sender
// leave in the same channel
} else { // user response to announcement
// create new thread
}
}
} else { // transfer
// todo test if members of transferred message are included
}
} else {
if (inRoomByOwner) {
// register message in room
} else {
// create room
}
}

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.2756 4.69628C21.8922 4.07969 21.8922 3.08 21.2756 2.46342C20.659 1.84683 19.6593 1.84683 19.0427 2.46342L11.8917 9.61447L4.74063 2.46342C4.12404 1.84683 3.12436 1.84683 2.50777 2.46342C1.89118 3.08 1.89118 4.07969 2.50777 4.69628L9.65882 11.8473L2.20145 19.3047C1.58487 19.9213 1.58487 20.921 2.20145 21.5376C2.81804 22.1541 3.81773 22.1541 4.43431 21.5376L11.8917 14.0802L19.349 21.5375C19.9656 22.1541 20.9653 22.1541 21.5819 21.5375C22.1985 20.921 22.1985 19.9213 21.5819 19.3047L14.1245 11.8473L21.2756 4.69628Z" fill="#737D8C"/>
</svg>

Before

Width:  |  Height:  |  Size: 650 B

After

Width:  |  Height:  |  Size: 0 B

View File

@@ -12,6 +12,7 @@
"@vueuse/core": "^9.13.0",
"axios": "^1.3.4",
"core-js": "^3.8.3",
"dompurify": "^3.0.1",
"vue": "^3.2.13",
"vue-router": "^4.1.6",
"vuex": "^4.0.2"

View File

@@ -4,22 +4,18 @@ import { RouterView } from "vue-router";
<template>
<div id="app">
<!-- <router-link to="/">Home</router-link> -->
<RouterView/>
<Sidebar />
<RoomView />
<RouterView/>
</div>
</template>
<script>
import Sidebar from './views/sidebar/Sidebar'
import RoomView from './views/room/RoomView'
export default {
name: 'App',
components: {
Sidebar,
RoomView,
},
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="wrapper">
<slot class="badge" name="body">
0
</slot>
</div>
</template>
<style scoped>
.wrapper {
display: flex;
justify-content: center;
align-items: center;
background-color: #4d5970;
height: 1.6rem;
width: 1.6rem;
min-width: 1.6rem;
min-height: 1.6rem;
border-radius: 1.6rem;
}
.badge {
color: #fff;
font-size: 1.1rem;
line-height: 1.4rem;
}
</style>

View File

@@ -1,11 +1,11 @@
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
import roomsStore from './store/rooms'
import store from './store/store'
const app = createApp(App);
app.use(router);
app.use(roomsStore);
app.use(store);
app.mount('#app');

View File

@@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home";
import RoomView from "../views/room/RoomView";
const routes = [
{
@@ -16,6 +17,10 @@ const routes = [
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"),
},
{
path: "/:id",
component: RoomView
}
];
const router = createRouter({

View File

@@ -1,6 +1,6 @@
import axios from 'axios'
export default(url='localhost') => {
export default(url='/api') => {
return axios.create({
baseURL: url,
});

View File

@@ -1,7 +1,16 @@
import API from './API'
export default {
getQuote() {
return API().get('/');
registerAccount(data) {
return API().post('/mail/account', data);
},
getAccounts() {
return API().get('/mail/accounts');
},
getRooms(mailboxId) {
return API().get(`/mail/${mailboxId}/rooms`);
},
getMessages(roomId) {
return API().get(`/mail/${roomId}/messages`);
}
}

View File

@@ -0,0 +1,7 @@
export default class Account {
constructor(_id, _mail) {
this.id = _id;
this.email = _mail;
this.fetched = false;
}
}

View File

@@ -0,0 +1,23 @@
export default class Room {
constructor(room, thread) {
this.id = room.id;
this.user = room.user;
this.userId = room.userId;
this.roomName = room.roomName;
this.mailboxId = room.mailboxId;
this.unseen = room.unseen;
this.messages = [];
this.messagesFetched = false;
if (!thread) {
this.threads = [
new Room({id:12, user: this.user, roomName: "thread 1", mailbboxId:this.mailboxId}, true),
new Room({id:12, user: this.user, roomName: "thread 1", mailbboxId:this.mailboxId}, true),
new Room({id:12, user: this.user, roomName: "thread 1", mailbboxId:this.mailboxId}, true),
];
} else {
this.threads = [];
}
}
}

View File

View File

@@ -1,52 +0,0 @@
import { createStore } from "vuex";
const roomsStore = createStore({
state() {
return {
rooms: [
{
users: "clemnce",
object:
"Lorem magna minim cillum labore ex eiusmod proident excepteur sint irure ipsum.",
mailbox: 1,
},
{
users: "juliette",
object:
"Lorem magna minim cillum labore ex eiusmod proident excepteur sint irure ipsum.",
mailbox: 1,
},
{
users: "jean",
object:
"Lorem magna minim cillum labore ex eiusmod proident excepteur sint irure ipsum.",
mailbox: 2,
},
{
users: "luc",
object:
"Lorem magna minim cillum labore ex eiusmod proident excepteur sint irure ipsum.",
mailbox: 2,
},
],
mailboxes: [
{ mail: "mail@domain.com", id: 1 },
{ mail: "name@example.com", id: 2 },
],
activeMailbox: -1
};
},
mutations: {
setActiveMailbox(state, payload) {
state.activeMailbox = payload;
}
},
getters: {
folderRooms: (state) => () => {
if (state.activeMailbox == -1) return state.rooms;
return state.rooms.filter(room => room.mailbox == state.activeMailbox);
}
}
});
export default roomsStore;

98
front/src/store/store.js Normal file
View File

@@ -0,0 +1,98 @@
import API from "@/services/imapAPI";
import { createStore } from "vuex";
import Room from "./models/Room";
import Account from "./models/Account";
const store = createStore({
state() {
return {
rooms: [new Room({id:12, user: "user", roomName: "room name", mailbboxId:2})],
messages: [],
accounts: [new Account(0, "ALL")],
activeAccount: 0,
activeRoom: 0,
};
},
mutations: {
setactiveAccount(state, payload) {
state.activeAccount = payload;
const account = state.accounts.find((account) => account.id == payload);
store.dispatch("fetchRooms", { accountId: payload, account: account });
},
setActiveRoom(state, payload) {
state.activeRoom = payload;
const room = state.rooms.find((room) => room.id == payload);
store.dispatch("fetchMessages", { roomId: payload, room: room });
},
addAccounts(state, payload) {
payload.forEach((account) => {
state.accounts.push(new Account(account.id, account.email));
});
},
addRooms(state, payload) {
// todo add if not exist
payload.rooms.forEach((room) => {
state.rooms.push(new Room(room));
});
},
addMessages(state, payload) {
// todo add if not exist
const room = state.rooms.find((room) => room.id == payload.roomId);
if (!room) return;
payload.messages.forEach((message) => {
room.messages.push(message);
});
room.messagesFetched = true;
},
},
getters: {
rooms: (state) => () => {
if (state.activeAccount === 0) return state.rooms;
return state.rooms.filter((room) => room.mailboxId == state.activeAccount);
},
messages: (state) => (roomId) => {
const room = state.rooms.find((room) => room.id == roomId);
if (!room) return [];
if (!room.messagesFetched) {
store.dispatch("fetchMessages", { roomId: room.id });
}
return room.messages;
},
},
actions: {
fetchAccounts: async (context) => {
API.getAccounts()
.then((res) => {
context.commit("addAccounts", res.data);
})
.catch((err) => {
console.log(err);
});
},
fetchRooms: async (context, data) => {
if (data.account?.fetched == false) {
API.getRooms(data.accountId)
.then((res) => {
data.account.fetched = true;
context.commit("addRooms", { rooms: res.data });
})
.catch((err) => {
console.log(err);
});
}
},
fetchMessages: async (context, data) => {
if (!data.room || data.room?.fetched == false) {
API.getMessages(data.roomId)
.then((res) => {
context.commit("addMessages", { messages: res.data, roomId: data.roomId });
})
.catch((err) => {
console.log(err);
});
}
},
},
});
export default store;

View File

@@ -0,0 +1,5 @@
export function decodeEmojis(text) {
const regex = /\\u{([^}]+)}/g;
const decodedText = text.replace(regex, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
return decodedText;
}

View File

@@ -3,14 +3,14 @@ import { ref, computed, watchEffect } from 'vue'
import Modal from './Modal'
import API from '../../services/imapAPI';
const modal = ref(true);
const modal = ref(false);
const email = ref("");
const pwd = ref("");
const xoauth = ref("");
const xoauth2 = ref("");
const host = ref("");
const port = ref("993");
const port = ref(993);
const error = ref("");
@@ -64,10 +64,20 @@ function checkError() {
}
}
function addMailboxRequest(event) {
console.log(event.target.disabled = true)
function addAccountRequest() {
checkError();
API.getQuote().then((res) => {
const data = {
email: email.value,
pwd: pwd.value,
xoauth: xoauth.value,
xoauth2: xoauth2.value,
host: host.value,
port: port.value,
tls: true
};
API.registerAccount(data).then((res) => {
console.log(res.status);
}).catch((err) => {
console.log(err.request.status)
@@ -103,7 +113,7 @@ function mailChange() {
</button>
<Modal
v-if="modal"
title="Add mailbox"
title="Add new account"
@close-modal="modal = false"
>
<template v-slot:body>
@@ -135,7 +145,7 @@ function mailChange() {
</div>
<!-- tls -->
<div>
<button :disabled="error != ''" @click="addMailboxRequest">Add</button>
<button :disabled="error != ''" @click="addAccountRequest">Add</button>
{{ error }}
</div>
</template>

View File

@@ -5,6 +5,7 @@ import { defineEmits, defineProps } from 'vue'
const emit = defineEmits(['close-modal']);
const props = defineProps({ title: String });
// todo close on escape
function close() {
emit('close-modal');
}
@@ -42,8 +43,7 @@ function close() {
top: 0;
z-index: 4000;
background-color: #000;
opacity: .7;
background-color: rgba(0, 0, 0, 0.8);
}
.modal {

View File

@@ -19,7 +19,7 @@ export default {
<style scoped>
#main {
background-color: blue;
border-bottom: 1px solid #505050;
width: 100%;
height: 51px;
}

View File

@@ -0,0 +1,92 @@
<script setup>
import { defineProps, onMounted, ref } from "vue";
import { decodeEmojis } from "../../utils/string";
import DOMPurify from 'dompurify';
const props = defineProps({ data: Object });
const date = new Date(props.data.date);
const iframe = ref(null);
onMounted(() => {
const doc = iframe.value.contentDocument || iframe.value.contentWindow.document;
const html = DOMPurify.sanitize(props.data.content);
doc.open();
// todo dompurify
// background vs color
doc.write(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body style="margin: 0;">
${html}
</body>
</html>
`);
doc.close();
});
</script>
<!-- to if to is more than me
cc -->
<!-- object (channel only)
content
attachments -->
<template>
<div class="message">
<div id="context">
<div class="left" id="profile">Carrefour@emailing .carrefor.fr "carrefour"</div>
<div class="middle">{{ decodeEmojis(props.data.subject) }}</div>
<div class="right" id="date">
{{
date.toLocaleString("en-GB", {
weekday: "short",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
timezone: "UTC+1",
})
}}
</div>
</div>
<iframe ref="iframe"></iframe>
</div>
</template>
<style scoped>
.message {
width: auto;
border: white 1px solid;
padding: 10px;
margin: 5px;
}
#context {
display: flex;
flex-direction: row;
justify-content: space-between;
}
iframe {
overflow-y: auto;
max-height: 300px;
width: 100%;
max-width: 750px; /* template width being 600px to 640px up to 750px (experiment and test) */
}
.left,
.right {
white-space: nowrap;
}
.middle {
margin: 0 10px;
text-align: center;
}
</style>

View File

@@ -1,31 +1,73 @@
<script setup>
import { useStore } from "vuex";
import { useRoute, onBeforeRouteUpdate } from "vue-router";
import { onBeforeMount } from "vue";
import Header from "./Header.vue";
import Message from "./Message.vue";
const store = useStore();
const route = useRoute();
let { id } = route.params;
onBeforeMount(async () => {
// get data
console.log(store.state.rooms.find((room) => room.id === id)?.fetched);
let room = store.state.rooms.find((room) => room.id === id);
if (!room || room?.fetched === false) {
// todo
// await store.dispatch("fetchMessages", );
}
store.commit("setActiveRoom", id);
});
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
id = to.params.id;
store.commit("setActiveRoom", id);
}
});
</script>
<template>
<div>
<div id="main">
<Header></Header>
<div>
Room
is thread
<div id="RoomViewBody">
<div class="content">
<Message v-for="(message, index) in store.getters.messages(id)" :key="index" :data="message" />
</div>
<div id="composer">COMPOSER</div>
</div>
</div>
</template>
<script>
import Header from './Header.vue'
export default {
name: 'RoomView',
components: {
Header
}
}
</script>
<style scoped>
div {
background-color: #1D1D23;
#main {
background-color: #1d1d23;
color: white;
width: 100%;
}
</style>
#RoomViewBody {
display: flex;
flex-direction: column;
height: 100%;
}
#composer {
position: absolute;
bottom: 0;
width: 100%;
padding-top: 10px;
height: 35px;
background-color: red;
}
.content {
display: flex;
flex-direction: column;
overflow: auto;
margin-bottom: 100px;
}
</style>

View File

@@ -1,19 +1,19 @@
<template>
<div>
<Mailboxes />
<Users id="users"/>
<Accounts />
<Rooms id="rooms"/>
</div>
</template>
<script>
import Mailboxes from './mailboxes/Mailboxes'
import Users from './users/Users.vue'
import Accounts from './accounts/Accounts'
import Rooms from './rooms/Rooms.vue'
export default {
name: 'Sidebar',
components: {
Mailboxes,
Users,
Accounts,
Rooms,
}
}
</script>
@@ -24,7 +24,7 @@ div {
height: 100%;
}
#users {
#rooms {
max-width: 300px;
min-width: 250px;
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="container" @click="setActiveMailbox(data.id)" :class="activeMailbox == data.id && 'active'">
{{ data.mail }}
<div class="container" @click="setactiveAccount(data.id)" :class="activeAccount == data.id && 'active'">
{{ data.email }}
</div>
</template>
@@ -8,17 +8,17 @@
import { mapMutations, mapState } from 'vuex'
export default {
name: 'Mailbox',
name: 'Account',
components: {
},
props: {
data: {mail: String, id: Number}
},
computed: {
...mapState(['activeMailbox'])
...mapState(['activeAccount'])
},
methods: {
...mapMutations(['setActiveMailbox'])
...mapMutations(['setactiveAccount'])
}
}
</script>

View File

@@ -4,30 +4,34 @@
<!-- deconnect -->
</div>
<span class="divider"></span>
<Mailbox :data="{'id': -1, 'mail': 'all'}"/>
<Mailbox v-for="(mailbox, index) in mailboxes" :key="index" :data="mailbox"/>
<Account v-for="(account, index) in accounts" :key="index" :data="account"/>
<span class="divider"></span>
<AddMailboxModal />
<AddAccountModal />
</div>
</template>
<script>
import { mapState } from 'vuex'
import Mailbox from './Mailbox'
import AddMailboxModal from '../../modals/AddMailboxModal'
import Account from './Account'
import AddAccountModal from '../../modals/AddAccountModal'
import store from '@/store/store'
export default {
name: 'Mailboxes',
name: 'Accounts',
components: {
Mailbox,
AddMailboxModal
Account,
AddAccountModal
},
computed: {
...mapState(['mailboxes'])
...mapState(['accounts'])
},
created() {
store.dispatch('fetchAccounts');
}
}
</script>
<style scoped>

View File

@@ -0,0 +1,93 @@
<script setup>
import { useRouter } from "vue-router";
import { defineProps } from "vue";
import BaseAvatar from "../../avatars/BaseAvatar.vue";
import Badge from "../../../components/Badge.vue";
import ThreadList from "./threads/ThreadList.vue";
import store from "@/store/store";
import { decodeEmojis } from "@/utils/string";
const props = defineProps({
data: {
id: Number,
roomName: String,
user: String,
userId: Number,
notSeen: Number,
mailboxId: Number,
threads: [Object],
},
});
const router = useRouter();
</script>
<template>
<div>
<div
class="room"
@click="router.push(`/${props.data.id}`)"
v-bind:class="store.state.activeRoom == props.data.id ? 'selected' : ''"
>
<BaseAvatar url="vue.png" />
<div id="content">
<div id="sender">{{ props.data.user }}</div>
<div id="object">{{ decodeEmojis(props.data.roomName) }}</div>
</div>
<Badge class="badge" v-if="props.data.unseen > 0"
><template v-slot:body>{{ props.data.unseen }}</template>
</Badge>
</div>
<ThreadList :threads="props.data.threads" />
</div>
</template>
<style scoped>
.badge {
margin: auto 7px auto 1px;
}
.room {
box-sizing: border-box;
contain: content;
display: flex;
margin: 4px;
padding: 4px;
cursor: pointer;
}
.room:hover,
.selected {
background-color: #41474f;
border-radius: 8px;
}
#content {
display: flex;
flex-grow: 1;
flex-direction: column;
height: 32px;
justify-content: center;
margin-left: 8px;
min-width: 0;
}
#sender {
font-size: 1.4rem;
line-height: 1.8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
color: white;
}
#object {
color: #a9b2bc;
line-height: 1.8rem;
font-size: 1.3rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 100%;
}
</style>

View File

@@ -1,32 +1,33 @@
<template>
<div>
<User v-for="(room, index) in folderRooms()" :key="index" :sender="room.users" :object="room.object" />
<div class="test">
<Room v-for="(room, index) in rooms()" :key="index" :data="room" />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import User from './User'
import Room from './Room'
export default {
name: 'Users',
name: 'Rooms',
props: {
},
components: {
User
Room
},
computed: {
...mapGetters(['folderRooms'])
...mapGetters(['rooms'])
}
}
</script>
<style scoped>
div {
.test {
display: flex;
flex-direction: column;
background-color: #24242B;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,38 @@
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
thread: Object
})
console.log(props.thread)
</script>
<template>
<div class="room">
{{props.thread.roomName}}
</div>
</template>
<style scoped>
.room {
box-sizing: border-box;
contain: content;
display: flex;
margin: -4px 4px 1px 4px;
padding: 4px;
cursor: pointer;
color: #a9b2bc;
}
.room:hover, .selected {
background-color: #41474f;;
border-radius: 8px;
}
.room::before {
content: "|";
margin: 0 10px;
color: white;
}
</style>

View File

@@ -0,0 +1,16 @@
<script setup>
import Thread from './Thread.vue';
import { defineProps } from 'vue'
const props = defineProps({ threads: [Object] });
console.log(props.threads)
</script>
<template>
<div>
<Thread v-for="(thread, index) in props.threads" :key="index" :thread="thread" />
</div>
</template>
<style scoped>
</style>

View File

@@ -1,77 +0,0 @@
<template>
<div>
<div id="user">
<BaseAvatar url="vue.png"/>
<div id="content">
<div id="sender">{{ sender }}</div>
<div id="object">{{ object }}</div>
</div>
</div>
<ThreadList />
</div>
</template>
<script>
import BaseAvatar from '../../avatars/BaseAvatar.vue'
import ThreadList from './threads/ThreadList.vue'
export default {
name: 'User',
props: {
sender: String,
object: String
},
components: {
BaseAvatar,
ThreadList
}
}
</script>
<style scoped>
#user {
box-sizing: border-box;
contain: content;
display: flex;
margin-bottom: 4px;
margin: 4px;
padding: 4px;
cursor: pointer;
}
#user:hover {
background-color: #41474f;;
border-radius: 8px;
}
#content {
display: flex;
flex-grow: 1;
flex-direction: column;
height: 32px;
justify-content: center;
margin-left: 8px;
min-width: 0;
}
#sender {
font-size: 1.4rem;
line-height: 1.8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
color: white;
}
#object {
color: #a9b2bc;
line-height: 1.8rem;
font-size: 1.3rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 100%;
}
</style>

View File

@@ -1,17 +0,0 @@
<template>
<div>
</div>
</template>
<script>
export default {
name: 'ThreadList',
props: {
},
}
</script>
<style scoped>
</style>

View File

@@ -1,4 +1,11 @@
const { defineConfig } = require('@vue/cli-service')
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true
transpileDependencies: true,
devServer: {
proxy: {
"/api": {
target: "http://localhost:5500",
}
}
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

View File

@@ -1,43 +0,0 @@
{
"name": "mail",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"vue": "^3.2.13"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -1,26 +0,0 @@
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,58 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -1,4 +0,0 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View File

@@ -1,4 +0,0 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})

6105
yarn.lock

File diff suppressed because it is too large Load Diff