diff --git a/back/db/message/storeMessage-db.ts b/back/db/message/storeMessage-db.ts index 77578d6..2c6f2d3 100644 --- a/back/db/message/storeMessage-db.ts +++ b/back/db/message/storeMessage-db.ts @@ -27,6 +27,12 @@ export function registerMailbox_message( execQuery(query, values); } +export function registerFlag(messageId: number, flagId: number) { + const query = `INSERT IGNORE INTO flag_name (message_id, flag_id) VALUES (?, ?)`; + const values = [messageId, flagId]; + execQuery(query, values); +} + export function registerBodypart(messageId: number, part: string, bodypartId: number, bytes: number, nbLines: null) { const query = ` INSERT IGNORE INTO part_number diff --git a/back/db/structureV2.sql b/back/db/structureV2.sql index e87342c..2f9f7c0 100644 --- a/back/db/structureV2.sql +++ b/back/db/structureV2.sql @@ -155,9 +155,27 @@ CREATE TABLE app_room_message ( -- 14 -- todo needed ? CREATE TABLE app_room_member ( - room_id INT NOT NULL, + room_id INT NOT NULL, member_id INT NOT NULL, UNIQUE KEY (room_id, member_id), FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE, FOREIGN KEY (member_id) REFERENCES address(address_id) ); + +-- 15 +create table flag_name ( + flag_id INT NOT NULL, + flag_name VARCHAR(255) NOT NULL, + PRIMARY KEY (flag_id), + UNIQUE KEY (flag_name) +); + +-- 16 +create table flag ( + message_id INT NOT NULL, + flag_id INT NOT NULL, + UNIQUE KEY (message_id, flag_id), + FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE, + FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE, + FOREIGN KEY (flag_id) REFERENCES flag_name(flag_id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/back/db/utils/mail.ts b/back/db/utils/mail.ts index 1d643a4..166db17 100644 --- a/back/db/utils/mail.ts +++ b/back/db/utils/mail.ts @@ -16,6 +16,12 @@ export async function getFieldId(field: string): Promise { return await execQueryAsyncWithId(query, values); } +export async function getFlagId(flag: string): Promise { + const query = `INSERT INTO flag_name (flag_name) VALUES (?) ON DUPLICATE KEY UPDATE flag_id=LAST_INSERT_ID(flag_id)`; + const values = [flag]; + return await execQueryAsyncWithId(query, values); +} + export async function findRoomByOwner(ownerId: number): Promise<{ room_id: number }[]> { const query = `SELECT room_id FROM app_room WHERE owner_id = ?`; const values = [ownerId]; diff --git a/back/mails/message/storeMessage.ts b/back/mails/message/storeMessage.ts index a4a3e5d..411905b 100644 --- a/back/mails/message/storeMessage.ts +++ b/back/mails/message/storeMessage.ts @@ -1,5 +1,5 @@ -import { getAddresseId } from "../../db/utils/mail"; -import {simpleParser} from "mailparser"; +import { getAddresseId, getFlagId } from "../../db/utils/mail"; +import { EmailAddress, ParsedMail, simpleParser } from "mailparser"; import moment from "moment"; import Imap from "imap"; import { @@ -10,6 +10,7 @@ import { registerBodypart, saveBodypart, saveSource, + registerFlag, } from "../../db/message/storeMessage-db"; import { getFieldId } from "../../db/utils/mail"; @@ -25,11 +26,13 @@ export function saveMessage(attrs: AttrsWithEnvelope, mailboxId: number, imap: I return new Promise((resolve, reject) => { registerMessage(ts, rfc822size, messageID) .then((messageId) => { - const isSeen: boolean = attrs.flags.includes("\\Seen"); const deleted: boolean = attrs.flags.includes("\\Deleted"); registerMailbox_message(mailboxId, attrs.uid, messageId, attrs?.modseq || 0, isSeen, deleted); + registerFlags(messageId, attrs.flags); + + // fetch message to save everything const f = imap.fetch(attrs.uid, { bodies: "" }); let buffer = ""; @@ -70,14 +73,24 @@ export function saveMessage(attrs: AttrsWithEnvelope, mailboxId: number, imap: I }); } -async function saveFromParsedData(parsed, messageId) { +function registerFlags(messageId: number, flags: string[]) { + flags.forEach((flag) => { + getFlagId(flag).then((flagId) => { + registerFlag(messageId, flagId); + }).catch((err: Error) => { + logger.err(err); + }); + }); +} + +async function saveFromParsedData(parsed: ParsedMail, messageId: number) { const promises: Promise[] = []; 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) => { + parsed[key].value.forEach((addr: EmailAddress, nb: number) => { getAddresseId(addr.address, addr.name).then(async (addressId) => { await saveAddress_fields(messageId, fieldId, addressId, nb); }); @@ -121,7 +134,6 @@ async function saveFromParsedData(parsed, messageId) { // todo when transfered } - if (process.env["NODE_DEV"] == "TEST") { module.exports = { saveFromParsedData, diff --git a/back/mails/saveMessage.ts b/back/mails/saveMessage.ts deleted file mode 100644 index 736e809..0000000 --- a/back/mails/saveMessage.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { - createRoom, - registerMessageInRoom, - getRoomType, - findRoomsFromMessage, - hasSameMembersAsParent, - registerThread, - registerMember, - getAllMembers, - getThreadInfo, - incrementNotSeenRoom, - getThreadInfoOnId, -} from "../db/saveMessage-db"; - -import { findRoomByOwner, getAddresseId, getUserIdOfMailbox } from "../db/utils/mail"; -import { nbMembers } from "./utils/envelopeUtils"; -import logger from "../system/Logger"; -import { Attrs, Envelope, User } from "../interfaces/mail/attrs.interface"; - -/** - * take object address and join mailbox and host to return mailbox@host - */ -function createAddress(elt: User): string { - return `${elt.mailbox}@${elt.host}`; -} - -export enum RoomType { - ROOM = 0, - CHANNEL = 1, - GROUP = 2, - DM = 3, - THREAD = 4, -} - -export default class RegisterMessageInApp { - messageId: number; - attrs: Attrs; - envelope: Envelope; - messageID?: string; - boxId: number; - isSeen: boolean; - ownerId: number; - userId: number; - inReplyTo: string; - - constructor(_messageId: number, _attrs: Attrs, _boxId: number) { - this.messageId = _messageId; - this.attrs = _attrs; - if (!this.attrs.envelope) throw new Error("Envelope must exist in attributes"); - this.envelope = this.attrs.envelope; - this.messageID = this.envelope?.messageId; - this.boxId = _boxId; - this.isSeen = this.attrs.flags.includes("\\Seen") ? true : false; - this.ownerId = -1; - this.userId = -1; - this.inReplyTo = ""; - } - - async init() { - if (this.envelope.from) { - this.ownerId = await getAddresseId(createAddress(this.envelope.from[0])); // todo use sender or from ? - } else { - throw new Error("Envelope must have a 'from' field"); - } - } - - isDm = () => nbMembers(this.envelope) == 2; - - async isFromUs() { - if (this.userId == -1) { - await getUserIdOfMailbox(this.boxId).then((res) => { - this.userId = res[0]?.user_id; - }); - } - return this.ownerId == this.userId; - } - - async incrementNotSeen(roomId: number) { - // todo it appears there is an error with notifications - if (!this.isSeen) { - await incrementNotSeenRoom(roomId); - } - } - - async registerMembers(roomId: number) { - getAllMembers(this.messageId).then((res) => { - if (res.lenght == 0) return; - const data = res[0].id.split(","); - data.forEach(async (memberId: number) => { - await registerMember(roomId, memberId); - }); - }); - } - - async initiateRoom(owner: number, roomType: RoomType) { - try { - const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType); - await registerMessageInRoom(this.messageId, roomId, this.envelope.date); - await this.incrementNotSeen(roomId); - await this.registerMembers(roomId); - return roomId; - } catch (err) { - logger.err(err); - } - } - - async createOrRegisterOnExistence(owner: number, roomType: RoomType) { - await findRoomByOwner(owner).then(async (res) => { - if (res.length == 0) { - // first message with this sender - await this.initiateRoom(owner, roomType); - } else { - // not a reply, add to the list of message if this sender - await registerMessageInRoom(this.messageId, res[0].room_id, this.envelope.date); - await this.incrementNotSeen(res[0].room_id); - } - }); - } - - async initiateThread() { - await createRoom(this.envelope.subject, this.ownerId, this.messageId, RoomType.THREAD).then( - async (threadId: number) => { - // find parent room infos - let roomId: number; - let root_id: number; - await getThreadInfo(this.inReplyTo).then(async (room) => { - // todo room not lenght, reply to transfer ? - roomId = room[0].room_id; - root_id = room[0].root_id; - if (root_id === undefined) root_id = roomId; - await registerThread(threadId, roomId, root_id); - }); - // impl register previous message or go back - await registerMessageInRoom(this.messageId, threadId, this.envelope.date); - await this.incrementNotSeen(root_id); - await this.incrementNotSeen(threadId); - await this.registerMembers(threadId); - }, - ); - } - - async createOrRegisterOnMembers(roomId: number, isThread?: boolean) { - const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo); - if (hasSameMembers) { - await registerMessageInRoom(this.messageId, roomId, this.envelope.date); - await this.incrementNotSeen(roomId); - if (isThread) { - await getThreadInfoOnId(roomId).then(async (res) => { - let root_id = res[0].root_id; - if (root_id == undefined) root_id = res[0].room_id; - await this.incrementNotSeen(res[0].root_id); - }); - } - } else { - await this.initiateThread(); - } - } - - async save() { - await this.init(); - if (this.envelope.inReplyTo) { - this.inReplyTo = this.envelope.inReplyTo; - await this.saveReply(); - } else { - if (await this.isFromUs()) { - if (this.isDm()) { - // create or add new message to DM - if (!this.envelope.to) throw new Error("Who send a DM and put the recipient in cc ?"); - const userTo = await getAddresseId(createAddress(this.envelope.to[0])); - await this.createOrRegisterOnExistence(userTo, RoomType.DM); - } else { - // it is not a reply and not a dm - // so it is a channel, which can be possibly a group - await this.initiateRoom(this.ownerId, RoomType.ROOM); - } - } else { - await this.createOrRegisterOnExistence(this.ownerId, RoomType.ROOM); - } - } - } - - async saveReply() { - await findRoomsFromMessage(this.inReplyTo).then(async (rooms) => { - if (rooms.length < 1) { - // 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 - const roomType = (await getRoomType(rooms[0].room_id))[0].room_type; - if (roomType == RoomType.GROUP || roomType == RoomType.THREAD) { - await this.createOrRegisterOnMembers(rooms[0].room_id, roomType == RoomType.THREAD); - } else { - // reply from CHANNEL or DM or ROOM - await this.initiateThread(); - // 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 roomId = rooms[rooms.length - 1].room_id; - await this.createOrRegisterOnMembers(roomId); - } - }); - } -} diff --git a/back/mails/storeMessage.ts b/back/mails/storeMessage.ts deleted file mode 100644 index bed526a..0000000 --- a/back/mails/storeMessage.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { getAddresseId } from "../db/utils/mail"; -import {simpleParser} from "mailparser"; -import moment from "moment"; -import Imap from "imap"; -import { - registerMessage, - registerMailbox_message, - saveHeader_fields, - saveAddress_fields, - registerBodypart, - saveBodypart, - saveSource, -} from "../db/imap/storeMessage-db"; - -import { getFieldId } from "../db/utils/mail"; -import logger from "../system/Logger"; -import { AttrsWithEnvelope } from "../interfaces/mail/attrs.interface"; - -export function saveMessage(attrs: AttrsWithEnvelope, mailboxId: number, imap: Imap): Promise { - 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) { - logger.warn("Fetch error: " + err); - }); - f.once("end", function () { - // logger.log("Done fetching data of " + messageID); // todo - }); - }) - .catch((err) => { - logger.warn("Unable to register message: " + err); - reject(err); - }); - }); -} - -async function saveFromParsedData(parsed, messageId) { - const promises: Promise[] = []; - 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", "references"].includes(key)) { - // todo : "references" (array) - if (key == "references") return; - 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"; - let partType = "text/plain"; - if (key == "html") { - partType = "text/html"; - } else if (key == "textAsHtml") { - partType = "text/TexAsHtml"; - } - saveBodypart(size, hash, parsed[key], "").then((bodypartId) => { - getFieldId(key).then((fieldId) => { - saveHeader_fields(messageId, fieldId, bodypartId, partType, 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 { - logger.warn("doesn't know key: " + key); - return; - } - }); - return Promise.all(promises); - // todo when transfered -} - - -if (process.env["NODE_DEV"] == "TEST") { - module.exports = { - saveFromParsedData, - }; -}