diff --git a/back/db/api.js b/back/db/api.js index 7741109..09ea2a7 100644 --- a/back/db/api.js +++ b/back/db/api.js @@ -41,11 +41,12 @@ async function getRooms(mailboxId) { 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 + 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); diff --git a/back/db/mail.js b/back/db/mail.js index f7434b4..8e80a8c 100644 --- a/back/db/mail.js +++ b/back/db/mail.js @@ -12,13 +12,6 @@ async function getAddresseId(email, name) { return await execQueryAsyncWithId(query, values); } -function getMailboxId(email) { - return new Promise((resolve, reject) => { - resolve(0) - }); - // todo -} - 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] @@ -33,7 +26,6 @@ async function findRoomByOwner(ownerId) { module.exports = { getAddresseId, - getMailboxId, getFieldId, findRoomByOwner, }; diff --git a/back/db/saveMessage.js b/back/db/saveMessage.js index d493dd0..bf0a82f 100644 --- a/back/db/saveMessage.js +++ b/back/db/saveMessage.js @@ -1,3 +1,4 @@ +const { transformEmojis } = require("../utils/string.js"); const { db, execQuery, execQueryAsync, execQueryAsyncWithId } = require("./db.js"); const DEBUG = require("../utils/debug").DEBUG; @@ -30,12 +31,14 @@ function registerBodypart(messageId, part, bodypartId, bytes, nbLines) { } 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 (?, ?, ?, ?, ?) @@ -54,7 +57,7 @@ async function saveAddress_fields(messageId, fieldId, addressId, number) { } function saveSource(messageId, content) { - content = Buffer.from(content); + content = transformEmojis(content); const query = ` INSERT INTO source (message_id, content) VALUES (?, ?) ON DUPLICATE KEY UPDATE content = ? diff --git a/back/db/saveMessageApp.js b/back/db/saveMessageApp.js index ece6ab4..e556f7b 100644 --- a/back/db/saveMessageApp.js +++ b/back/db/saveMessageApp.js @@ -1,28 +1,32 @@ -const { db, execQueryAsync, execQueryAsyncWithId } = require("./db.js"); +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) { +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); + updateLastUpdateRoom(roomId, idate); if (!isSeen) { incrementNotSeenRoom(roomId); } } -function updateLastUpdateRoom(roomId) { - // todo +function updateLastUpdateRoom(roomId, idate) { + const query = `UPDATE app_room SET lastUpdate = ? WHERE room_id = ?`; + const values = [idate, roomId]; + execQuery(query, values); } function incrementNotSeenRoom(roomId) { @@ -41,18 +45,10 @@ async function createThread(threadName, ownerId, messageId, parentRoomId, isDm) async function registerMessageInThread(messageId, threadId, isSeen) { // todo check if it is still a thread or should be a room // todo isdm - const query = `INSERT IGNORE INTO app_space_message - (message_id, thread_id) VALUES (?, ?)`; - const values = [messageId, threadId]; - await execQueryAsync(query, values); - updateLastUpdateThread(threadId); - - if (!isSeen) { - incrementNotSeenThread(threadId); - } + console.log("register message in thread") } -function updateLastUpdateRoom(threadId) { +function updateLastUpdateThread(threadId) { // todo // check for parent } diff --git a/back/db/structureV2.sql b/back/db/structureV2.sql index 91333c1..11eb9fa 100644 --- a/back/db/structureV2.sql +++ b/back/db/structureV2.sql @@ -67,7 +67,7 @@ CREATE TABLE bodypart ( bodypart_id INT AUTO_INCREMENT, bytes INT NOT NULL, hash TEXT NOT NULL, - text TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + text TEXT, data BINARY, PRIMARY KEY (bodypart_id) ); @@ -75,10 +75,10 @@ CREATE TABLE bodypart ( -- 7 CREATE TABLE source ( message_id INT NOT NULL, - content BLOB 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 ( @@ -94,7 +94,7 @@ CREATE TABLE header_field ( field_id INT NOT NULL, bodypart_id INT, part VARCHAR(128), - value TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + 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, diff --git a/back/mails/imap/Box.js b/back/mails/imap/Box.js index 9329148..9b3e592 100644 --- a/back/mails/imap/Box.js +++ b/back/mails/imap/Box.js @@ -25,7 +25,6 @@ class Box { sync(savedUid, currentUid) { const promises = []; const mails = []; - console.log(savedUid, currentUid); const f = this.imap.seq.fetch(`${savedUid}:${currentUid}`, { size: true, envelope: true, diff --git a/back/mails/index.js b/back/mails/index.js deleted file mode 100644 index f0ba06f..0000000 --- a/back/mails/index.js +++ /dev/null @@ -1,87 +0,0 @@ -const Imap = require("imap"); -const { simpleParser } = require("mailparser"); -const inspect = require("util").inspect; -const saveMessage = require("./storeMessage").saveMessage; -const registerMessageInApp = require("./saveMessage").registerMessageInApp; -const imapConfig = require("./config.json").mail; - -const fs = require("fs"); -const { DEBUG } = require("../utils/debug"); -const imap = new Imap({ - user: imapConfig.user, - password: imapConfig.password, - tlsOptions: { servername: "imap.gmail.com" }, - host: "imap.gmail.com", - port: 993, - tls: true, -}); - -// reset table; - -let shouldReset = false; -// let shouldReset = true; - -if (shouldReset) { - const { execQuery, execQueryAsync } = require("../db/db"); - const query = "SELECT table_name FROM INFORMATION_SCHEMA.tables WHERE table_schema = 'mail'"; - execQueryAsync(query).then((results) => { - execQuery("SET FOREIGN_KEY_CHECKS=0"); - results.map((table) => { - execQuery("DELETE FROM " + table.table_name); - // execQuery("DROP TABLE " + table.table_name); - }); - }); - return; -} - -imap.once("ready", function () { - const readOnly = true; - imap.openBox("INBOX", readOnly, (err, box) => { - // console.log(box); // uidvalidty uidnext, messages total and new - // imap.search(["ALL"], function (err, results) { - // console.log(results[results.length - 1]); - // }); - - // const f = imap.fetch(970, { - // size: true, - // envelope: true, - // }); - const promises = []; - const mails = []; - const f = imap.seq.fetch('1:10', { - size: true, - envelope: true - }); - f.on("message", function (msg, seqno) { - msg.once("attributes", (attrs) => { - // todo find boxId - const boxId = 1; - // mails.push(attrs); - // promises.push(saveMessage(attrs, boxId, imap)); - }); - }); - - f.once("error", function (err) { - DEBUG.log("Fetch error: " + err); - }); - f.once("end", async function () { - Promise.all(promises).then(async (res) => { - DEBUG.log("Done fetching all messages!"); - for (let i = 0; i < mails.length; i++) { - await registerMessageInApp(res[i], mails[i]); - } - }); - }); - // imap.end() - }); -}); - -imap.once("error", function (err) { - console.log(err); -}); - -imap.once("end", function () { - console.log("Connection ended"); -}); - -imap.connect(); diff --git a/back/mails/saveMessage.js b/back/mails/saveMessage.js index 9e72ecb..ff6e687 100644 --- a/back/mails/saveMessage.js +++ b/back/mails/saveMessage.js @@ -30,11 +30,11 @@ async function registerMessageInApp(messageId, attrs) { if (res.length == 0) { // first message of this sender await createRoom(envelope.subject, ownerId, messageId).then(async (roomId) => { - await registerMessageInRoom(messageId, roomId, isSeen); + 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); + await registerMessageInRoom(messageId, res[0].room_id, isSeen, envelope.date); } }); } @@ -54,7 +54,7 @@ async function registerReplyMessage(envelope, messageId, isSeen, ownerId) { if (isGroup) { const hasSameMembers = await hasSameMembersAsParent(messageID, envelope.inReplyTo); if (hasSameMembers) { - await registerMessageInRoom(messageId, rooms[0].room_id, isSeen); + 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 diff --git a/back/mails/storeMessage.js b/back/mails/storeMessage.js index f5d34b0..fc18a56 100644 --- a/back/mails/storeMessage.js +++ b/back/mails/storeMessage.js @@ -2,7 +2,6 @@ const { getAddresseId } = require("../db/mail"); const { DEBUG } = require("../utils/debug"); const { simpleParser } = require("mailparser"); const moment = require("moment"); -const fs = require("fs"); const { registerMessage, registerMailbox_message, diff --git a/back/utils/string.js b/back/utils/string.js new file mode 100644 index 0000000..5683f89 --- /dev/null +++ b/back/utils/string.js @@ -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 +} \ No newline at end of file diff --git a/front/package.json b/front/package.json index 98804be..45ab08e 100644 --- a/front/package.json +++ b/front/package.json @@ -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" diff --git a/front/src/components/Badge.vue b/front/src/components/Badge.vue new file mode 100644 index 0000000..1bedfd8 --- /dev/null +++ b/front/src/components/Badge.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/front/src/store/models/Room.js b/front/src/store/models/Room.js index 114ec85..9f3aa68 100644 --- a/front/src/store/models/Room.js +++ b/front/src/store/models/Room.js @@ -1,13 +1,23 @@ export default class Room { - constructor(id, user, userId, roomName, mailboxId) { - this.id = id; - this.user = user; - this.userId = userId; - this.roomName = roomName; - this.mailboxId = mailboxId; + 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; - this.threads = []; + + 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 = []; + } } } \ No newline at end of file diff --git a/front/src/store/models/Thread.js b/front/src/store/models/Thread.js new file mode 100644 index 0000000..e69de29 diff --git a/front/src/store/store.js b/front/src/store/store.js index a074af1..7c8b3fc 100644 --- a/front/src/store/store.js +++ b/front/src/store/store.js @@ -6,7 +6,7 @@ import Account from "./models/Account"; const store = createStore({ state() { return { - rooms: [], + rooms: [new Room({id:12, user: "user", roomName: "room name", mailbboxId:2})], messages: [], accounts: [new Account(0, "ALL")], activeAccount: 0, @@ -21,23 +21,8 @@ const store = createStore({ }, setActiveRoom(state, payload) { state.activeRoom = payload; - // todo call actions - // fetch messages for this room if not already fetched const room = state.rooms.find((room) => room.id == payload); - if (!room || room?.fetched == false) { - console.log("add messages"); - API.getMessages(payload) - .then((res) => { - // todo add if not exist - room.fetched = true; - res.data.forEach((msg) => { - state.messages.push(msg); - }); - }) - .catch((err) => { - console.log(err); - }); - } + store.dispatch("fetchMessages", { roomId: payload, room: room }); }, addAccounts(state, payload) { payload.forEach((account) => { @@ -47,11 +32,13 @@ const store = createStore({ addRooms(state, payload) { // todo add if not exist payload.rooms.forEach((room) => { - state.rooms.push(new Room(room.id, room.user, room.userId, room.roomName, room.mailboxId)); + 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); }); @@ -65,6 +52,7 @@ const store = createStore({ }, messages: (state) => (roomId) => { const room = state.rooms.find((room) => room.id == roomId); + if (!room) return []; if (!room.messagesFetched) { store.dispatch("fetchMessages", { roomId: room.id }); } @@ -94,15 +82,15 @@ const store = createStore({ } }, fetchMessages: async (context, data) => { - console.log(data) - API.getMessages(data.roomId) - .then((res) => { - console.log(res.data); - context.commit("addMessages", { messages: res.data, roomId: data.roomId }); - }) - .catch((err) => { - console.log(err); - }); + 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); + }); + } }, }, }); diff --git a/front/src/utils/string.js b/front/src/utils/string.js new file mode 100644 index 0000000..00f0b1d --- /dev/null +++ b/front/src/utils/string.js @@ -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; +} diff --git a/front/src/views/room/Message.vue b/front/src/views/room/Message.vue index 5b0b403..d0f070b 100644 --- a/front/src/views/room/Message.vue +++ b/front/src/views/room/Message.vue @@ -1,8 +1,35 @@ @@ -13,7 +40,7 @@ const date = new Date(props.data.date);
Carrefour@emailing .carrefor.fr "carrefour"
-
{{ props.data.subject }}
+
{{ decodeEmojis(props.data.subject) }}
{{ date.toLocaleString("en-GB", { @@ -28,9 +55,7 @@ const date = new Date(props.data.date); }}
-
-
-
+
@@ -46,25 +71,22 @@ const date = new Date(props.data.date); display: flex; flex-direction: row; justify-content: space-between; - align-items: center; } -#content { - overflow: auto; +iframe { + overflow-y: auto; max-height: 300px; - width: 750px; /* template width being 600px to 640px up to 750px (experiment and test) */ + width: 100%; + max-width: 750px; /* template width being 600px to 640px up to 750px (experiment and test) */ } .left, .right { - display: flex; - align-items: center; + white-space: nowrap; } .middle { margin: 0 10px; - flex: 1; - align-self: center; - display: contents; + text-align: center; } diff --git a/front/src/views/sidebar/rooms/Room.vue b/front/src/views/sidebar/rooms/Room.vue index 91063da..b5f728e 100644 --- a/front/src/views/sidebar/rooms/Room.vue +++ b/front/src/views/sidebar/rooms/Room.vue @@ -1,9 +1,11 @@ \ No newline at end of file + diff --git a/front/src/views/sidebar/rooms/Rooms.vue b/front/src/views/sidebar/rooms/Rooms.vue index 2622b1c..c74462b 100644 --- a/front/src/views/sidebar/rooms/Rooms.vue +++ b/front/src/views/sidebar/rooms/Rooms.vue @@ -1,5 +1,5 @@ @@ -23,10 +23,11 @@ export default { \ No newline at end of file diff --git a/front/src/views/sidebar/rooms/threads/Thread.vue b/front/src/views/sidebar/rooms/threads/Thread.vue new file mode 100644 index 0000000..f666b9c --- /dev/null +++ b/front/src/views/sidebar/rooms/threads/Thread.vue @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/front/src/views/sidebar/rooms/threads/ThreadList.vue b/front/src/views/sidebar/rooms/threads/ThreadList.vue index 37f18b7..ba28253 100644 --- a/front/src/views/sidebar/rooms/threads/ThreadList.vue +++ b/front/src/views/sidebar/rooms/threads/ThreadList.vue @@ -1,17 +1,16 @@ + + - \ No newline at end of file diff --git a/front/yarn.lock b/front/yarn.lock index 60d5865..dc782d5 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2821,6 +2821,11 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +dompurify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.1.tgz#a0933f38931b3238934dd632043b727e53004289" + integrity sha512-60tsgvPKwItxZZdfLmamp0MTcecCta3avOhsLgPZ0qcWt96OasFfhkeIRbJ6br5i0fQawT1/RBGB5L58/Jpwuw== + domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" resolved "https://registry.npmmirror.com/domutils/-/domutils-2.8.0.tgz"