diff --git a/back/imap/index.js b/back/imap/index.js index 4719bb6..4902388 100644 --- a/back/imap/index.js +++ b/back/imap/index.js @@ -22,7 +22,7 @@ imap.once("ready", function () { console.log(results[results.length - 1]); }); - const f = imap.fetch(970, { bodies: ['TEXT'], size: true, struct: true, envelope: true }); + const f = imap.fetch(970, { size: true, struct: true, envelope: true }); // var f = imap.seq.fetch('1:3', { // bodies: 'HEADER.FIELDS (FROM TO SUBJECT DATE)', // struct: true @@ -30,23 +30,23 @@ imap.once("ready", function () { f.on("message", function (msg, seqno) { // console.log("Message #%d", seqno); // var prefix = "(#" + seqno + ") "; - msg.on("body", function (stream, info) { - simpleParser(stream, async (err, parsed) => { - // find box id; - console.log(parsed) - const boxId = 1; - // saveMessage(parsed, boxId); - console.log(parsed.subject); - fs.writeFileSync("./test.txt", JSON.stringify(parsed)); - }); - // console.log(prefix + 'Body'); - // stream.pipe(fs.createWriteStream('msg-' + seqno + '-body.txt')); - }); + // msg.on("body", function (stream, info) { + // simpleParser(stream, async (err, parsed) => { + // // find box id; + // // console.log(parsed) + // const boxId = 1; + // saveMessage(parsed, boxId); + // // console.log(parsed.subject); + // // fs.writeFileSync("./test.txt", JSON.stringify(parsed)); + // }); + // // console.log(prefix + 'Body'); + // // stream.pipe(fs.createWriteStream('msg-' + seqno + '-body.txt')); + // }); msg.once('attributes', attrs => { // todo find boxId const boxId = 1; - console.log(attrs) - // saveMessage(attrs, boxId, imap); + // console.log(attrs) + saveMessage(attrs, boxId, imap); }); }); @@ -58,7 +58,7 @@ imap.once("ready", function () { }); f.once("end", function () { console.log("Done fetching all messages!"); - imap.end(); + // imap.end(); }); // }); return; @@ -135,66 +135,6 @@ imap.once("end", function () { 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' => '', -// // '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 diff --git a/back/imap/storeMessage.js b/back/imap/storeMessage.js index 555aba3..891d4ed 100644 --- a/back/imap/storeMessage.js +++ b/back/imap/storeMessage.js @@ -8,9 +8,17 @@ const { saveHeader_fields, saveAddress_fields, registerBodypart, - saveBodypart + saveBodypart, } = require("../sql/saveMessage"); -const { getFieldId } = require("../sql/mail"); + +const { + createRoom, + registerMessageInRoom, + createThread, + registerMessageInThread, + isRoomGroup +} = require("../sql/saveMessageApp"); +const { getFieldId, findRoomByOwner } = require("../sql/mail"); function saveMessage(attrs, mailboxId, imap) { const envelope = attrs.envelope; @@ -95,34 +103,77 @@ function saveMessage(attrs, mailboxId, imap) { } }); - // todo check for different provider - if (envelope?.inReplyTo) { - `SELECT app_room_messages.room, app_room_messages.thread FROM app_room_messages INNER JOIN messages WHERE messages.messageID = '${envelope.inReplyTo}' AND app_room_messages.message = messages.id`; - // parent message is in a thread, so register this message only in a thread - if (thread) { - `INSERT IGNORE INTO app_room_messages (thread, message) VALUES('${thread}', '${messageId}')`; - // increment not read counter and lastUpdate is not read (both) - } else if (room) { - - if (!isGroup) { - // check if create thread - } else { - // check if create new group and delete thread - } - } else { - - } - } - /** - * if reply to then add to same group as previous and update type if necessary - * else add to sender group - * - */ - + // todo check for different provider name of inreplyto + registerMessageInApp() + } ); } +function haveSameReceivers() { + // take cc and to +} + +function registerMessageInApp() { + if (envelope.inReplyTo) { + registerReplyMessage(); + } else { + findRoomByOwner(ownerId).then((res) => { + if (res.length == 0) { + registerMessageInRoom(messageId, res[0].id); + } else { + createRoom(envelope.subject, ownerId, envelope.flags.includes["seen"]).then((roomId) => { + registerMessageInRoom(messageId, roomId); + }); + } + }); + } +} + +function registerReplyMessage() { + findSpaceFromMessage(messageId).then((spaces) => { + if(spaces.length == 0) { // no space, so is a transfer + // todo test if members of transferred message are included + } else if (spaces[0].room) { // message in room + isRoomGroup(spaces[0].room_id).then((isGroup) => { + if (isGroup) { + if (hasSameMembersAsParent(messageId)) { + registerMessageInRoom(messageId, spaces[0].room_id); + } else { + // group and not the same member as the reply + // some recipient has been removed create a thread + const notSeen = 0; // todo + const isDm = 0; // todo + createThread(space[0].room_id, envelope.subject, notSeen, isDm).then((threadId) => { + registerMessageInThread(messageId, threadId); + }); + } + } else { // reply from channel + // todo + // 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 if (spaces[0].thread) { // message in thread + // todo + // if (hasSameMembersAsParent(messageId)) { + // // register new message in thread + // // possibly convert to room only if parent is channel + // } else { + // // create sub thread + // } + } + }); + + // `SELECT app_room_messages.room, app_room_messages.thread FROM app_room_messages INNER JOIN messages WHERE messages.messageID = '${envelope.inReplyTo}' AND app_room_messages.message = messages.id`; + +} + function findTextPart(struct) { for (var i = 0, len = struct.length, r; i < len; ++i) { if (Array.isArray(struct[i])) { diff --git a/back/routers/mail.js b/back/routers/mail.js index 000f834..b1b0705 100644 --- a/back/routers/mail.js +++ b/back/routers/mail.js @@ -23,7 +23,7 @@ router.get("/{mailboxId}/messages", (req, res) => { // todo check token // todo use offset const query = ``; - SELECT header_fields.value FROM header_fields INNER JOIN field_names WHERE header_fields.field = field_names.id AND (field_names.name = "subject" OR field_names.name = "date"); ORDER BY messages.idate + // SELECT header_fields.value FROM header_fields INNER JOIN field_names WHERE header_fields.field = field_names.id AND (field_names.name = "subject" OR field_names.name = "date"); ORDER BY messages.idate // number of message missed in the room }); diff --git a/back/sql/mail.js b/back/sql/mail.js new file mode 100644 index 0000000..57463e2 --- /dev/null +++ b/back/sql/mail.js @@ -0,0 +1,57 @@ +const bdd = require("./bdd.js").bdd; +const DEBUG = require("../utils/debug").DEBUG; + +function isValidEmail(email) { + // todo + return true; +} + + +function getAddresseId(email, name) { + const localpart = email.split("@")[0]; + const domain = email.split("@")[1]; + return new Promise((resolve, reject) => { + if (!isValidEmail(email)) reject("Not a valid email"); + const query = `INSERT INTO address (address_name, localpart, domain, email) VALUES ('${name}', '${localpart}', '${domain}', '${email}') + ON DUPLICATE KEY UPDATE email = '${email}', id = LAST_INSERT_ID(id)`; + bdd.query(query, (err, results, fields) => { + if (err) reject(err); + resolve(results.insertId); + }); + }); +} + +function getMailboxId(email) { + return new Promise((resolve, reject) => { + resolve(0) + }); + // todo +} + +function getFieldId(field) { + return new Promise((resolve, reject) => { + const query = `INSERT INTO field_name (field_name) VALUES ('${field}') ON DUPLICATE KEY UPDATE field_id=LAST_INSERT_ID(field_id); + `; + bdd.query(query, (err, results, fields) => { + if (err) reject(err); + resolve(results.insertId); + }); + }); +} + +function findRoomByOwner(ownerId) { + return new Promise((resolve, reject) => { + const query = `SELECT room_id FROM app_room WHERE owner_id = '${ownerId}'`; + bdd.query(query, (err, results, fields) => { + if (err) reject(err); + resolve(results); + }); + }); +} + +module.exports = { + getAddresseId, + getMailboxId, + getFieldId, + findRoomByOwner, +}; diff --git a/back/sql/saveMessage.js b/back/sql/saveMessage.js index d8dd097..4f3e741 100644 --- a/back/sql/saveMessage.js +++ b/back/sql/saveMessage.js @@ -2,9 +2,8 @@ const bdd = require("./bdd.js").bdd; const DEBUG = require("../utils/debug").DEBUG; function registerMessage(timestamp, rfc822size, messageId) { - // todo messageId return new Promise((resolve, reject) => { - const query = `INSERT INTO messages (idate, rfc822size) VALUES (${timestamp}, '${rfc822size}')`; + const query = `INSERT INTO message (idate, messageID, rfc822size) VALUES (${timestamp}, '${messageId}', '${rfc822size}')`; bdd.query(query, (err, results, fields) => { if (err) reject(err); resolve(results.insertId); @@ -13,14 +12,14 @@ function registerMessage(timestamp, rfc822size, messageId) { } function registerMailbox_message(mailboxId, uid, messageId, modseq, seen, deleted) { - const query = `INSERT IGNORE INTO mailbox_messages (mailbox, uid, message, modseq, seen, deleted) VALUES ('${mailboxId}', '${uid}', '${messageId}', '${modseq}', '${seen}', '${deleted}')`; + const query = `INSERT IGNORE INTO mailbox_message (mailbox_id, uid, message_id, modseq, seen, deleted) VALUES ('${mailboxId}', '${uid}', '${messageId}', '${modseq}', '${seen}', '${deleted}')`; bdd.query(query, (err, results, fields) => { if (err) DEBUG.log(err); }); } function registerBodypart(messageId, part, bodypartId, bytes, nbLines) { - const query = `INSERT IGNORE INTO part_numbers (message, part, bodypart, bytes, nbLines) VALUES ('${messageId}', '${part}', '${bodypartId}', '${bytes}', '${nbLines}')`; + const query = `INSERT IGNORE INTO part_number (message_id, part, bodypart, bytes, nbLines) VALUES ('${messageId}', '${part}', '${bodypartId}', '${bytes}', '${nbLines}')`; bdd.query(query, (err, results, fields) => { if (err) DEBUG.log(err); }); @@ -29,7 +28,7 @@ function registerBodypart(messageId, part, bodypartId, bytes, nbLines) { function saveBodypart(bytes, hash, text, data) { return new Promise((resolve, reject) => { - const query = `INSERT IGNORE INTO bodyparts (bytes, hash, text, data) VALUES ('${bytes}', '${hash}', '${text}', '${data}')`; + const query = `INSERT IGNORE INTO bodypart (bytes, hash, text, data) VALUES ('${bytes}', '${hash}', '${text}', '${data}')`; bdd.query(query, (err, results, fields) => { if (err) reject(err); resolve(results.insertId); @@ -38,14 +37,14 @@ function saveBodypart(bytes, hash, text, data) { } 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}')`; + const query = `INSERT IGNORE INTO header_field (message_id, part, position, field_id, 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}')`; + const query = `INSERT IGNORE INTO address_field (message_id , part, position, field_id, number, address_id) VALUES ('${message}', '${part}', '${position}', '${field}', '${number}', '${address}')`; bdd.query(query, (err, results, fields) => { if (err) throw err; }); @@ -57,5 +56,5 @@ module.exports = { saveHeader_fields, saveAddress_fields, registerBodypart, - saveBodypart + saveBodypart, } \ No newline at end of file diff --git a/back/sql/saveMessageApp.js b/back/sql/saveMessageApp.js new file mode 100644 index 0000000..08e30d3 --- /dev/null +++ b/back/sql/saveMessageApp.js @@ -0,0 +1,92 @@ +const bdd = require("./bdd.js").bdd; +const DEBUG = require("../utils/debug").DEBUG; + +function createRoom(roomName, ownerId, notSeen) { + return new Promise((resolve, reject) => { + const query = `INSERT INTO app_room (room_name, owner_id, notSeen) VALUES ('${roomName}', '${ownerId}', '${notSeen}')`; + bdd.query(query, (err, results, fields) => { + if (err) reject(err); + resolve(results.insertId); + }); + }); +} + +function registerMessageInRoom(messageId, roomId, isSeen) { + const query = `INSERT INTO app_room_message (message_id, room_id) VALUES ('${messageId}', '${roomId}')`; + bdd.query(query, (err, results, fields) => { + if (err) throw err; + }); + + updateLastUpdateRoom(roomId); + + if (!isSeen) { + incrementNotSeenRoom(roomId); + } +} + +function updateLastUpdateRoom(roomId) { + // todo +} + +function incrementNotSeenRoom(roomId) { + // todo +} + +function createThread(roomId, threadName, notSeen, isDm) { + return new Promise((resolve, reject) => { + const query = `INSERT INTO app_thread + (room_id, thread_name, notSeen, isDm) + VALUES ( + '${roomId}', + '${threadName}', + '${notSeen}', + '${isDm}', + )`; + bdd.query(query, (err, results, fields) => { + if (err) reject(err); + resolve(results.insertId); + }); + }); +} + +function registerMessageInThread(messageId, threadId, isSeen) { + // todo check if it is still a thread or should be a room + const query = `INSERT IGNORE INTO app_room_messages + (message_id, thread_id) VALUES ('${messageId}', '${threadId}')`; + bdd.query(query, (err, results, fields) => { + if (err) throw err; + }); + updateLastUpdateThread(threadId); + + if (!isSeen) { + incrementNotSeenThread(threadId); + } +} + +function updateLastUpdateRoom(threadId) { + // todo + // check for parent +} + +function incrementNotSeenThread(threadId) { + // todo + // also increment parent room +} + +function isRoomGroup(roomId) { + return new Promise((resolve, reject) => { + const query = `SELECT isGroup FROM app_room WHERE room_id = '${roomId}'`; + bdd.query(query, (err, results, fields) => { + if (err) reject(err); + resolve(results[0].isGroup); + }); + }); +} + +module.exports = { + createRoom, + registerMessageInRoom, + createThread, + registerMessageInThread, + isRoomGroup +}; \ No newline at end of file diff --git a/back/sql/structureV2.sql b/back/sql/structureV2.sql new file mode 100644 index 0000000..cae8ae0 --- /dev/null +++ b/back/sql/structureV2.sql @@ -0,0 +1,175 @@ +/** + * 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) +); + +-- 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), + UNIQUE KEY (mailbox_name), + 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 BIT(1) NOT NULL DEFAULT 0, + deleted BIT(1) NOT NULL DEFAULT 0, + 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 part_number ( + message_id INT NOT NULL, + part VARCHAR(128) NOT NULL, + bodypart_id INT NOT NULL, + bytes INT, + nb_lines INT, + PRIMARY KEY (message_id, part), + FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE, + FOREIGN KEY (bodypart_id) REFERENCES bodypart(bodypart_id) ON DELETE CASCADE +); + +-- 7 +CREATE TABLE bodypart ( + bodypart_id INT AUTO_INCREMENT, + bytes INT NOT NULL, + hash TEXT NOT NULL, + text TEXT, + data BINARY, + PRIMARY KEY (bodypart_id) +); + +-- 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, + part VARCHAR(128) NOT NULL, + position INT NOT NULL, + field_id INT NOT NULL, + value TEXT NOT NULL, + UNIQUE KEY (message_id, part, position, field_id), + FOREIGN KEY (message_id, part) REFERENCES part_number(message_id, part) ON DELETE CASCADE, + FOREIGN KEY (field_id) REFERENCES field_name(field_id) +); + +-- 10 +CREATE TABLE address_field ( + message_id INT NOT NULL, + part VARCHAR(128) NOT NULL, + position INT NOT NULL, + field_id INT NOT NULL, + number INT, + address_id INT NOT NULL, + FOREIGN KEY (message_id, part) REFERENCES part_number(message_id, part) ON DELETE CASCADE, + FOREIGN KEY (field_id) REFERENCES field_name(field_id), + FOREIGN KEY (address_id) REFERENCES address(address_id) +); + +/** + * App table + */ + +-- 2 +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 BIT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (account_id), + FOREIGN KEY (user_id) REFERENCES address(address_id) ON DELETE CASCADE +); + +-- 11 +CREATE TABLE app_room ( + room_id INT AUTO_INCREMENT, + room_name VARCHAR(30) NOT NULL, + owner_id INT NOT NULL, + isGroup BIT(1) NOT NULL DEFAULT 0, + 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) +); + +-- 12 +CREATE TABLE app_thread ( + thread_id INT AUTO_INCREMENT, + room_id INT NOT NULL, + thread_name VARCHAR(30) NOT NULL, + notSeen INT NOT NULL DEFAULT 0, + lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(), + isDm BIT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (thread_id), + FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE +); + +-- 13 +CREATE TABLE app_room_message ( + message_id INT NOT NULL, + room_id INT, + thread_id INT, + FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE, + FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE SET NULL, + FOREIGN KEY (thread_id) REFERENCES app_thread(thread_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) +); + +-- 14 +CREATE TABLE app_thread_member ( + thread_id INT NOT NULL, + member_id INT NOT NULL, + FOREIGN KEY (thread_id) REFERENCES app_thread(thread_id) ON DELETE CASCADE, + FOREIGN KEY (member_id) REFERENCES address(address_id) +); diff --git a/doc.js b/doc.js new file mode 100644 index 0000000..be061f7 --- /dev/null +++ b/doc.js @@ -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 + } +}