Compare commits

...

4 Commits

Author SHA1 Message Date
grimhilt
a5d325818b tests in typescript 2023-04-01 22:36:51 +02:00
grimhilt
9fbf5e5cf3 started to convert to typescript 2023-04-01 16:32:29 +02:00
grimhilt
1d761fa6df add logic and more test to saveMessage 2023-04-01 15:07:49 +02:00
grimhilt
7f28823758 implement save of thread and members 2023-03-31 16:07:02 +02:00
45 changed files with 2020 additions and 3902 deletions

7
.gitignore vendored
View File

@ -1,11 +1,10 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
.env
# Log files
npm-debug.log*
@ -22,15 +21,13 @@ pnpm-debug.log*
*.sln
*.sw?
build
.tmp
.s.*
log*
config.json
.direnv
.envrc
*.txt
*.json
tmp
test.*
*.png

View File

@ -1,8 +1,8 @@
const statusCode = require("../utils/statusCodes").statusCodes;
const { registerAccount } = require("../db/api");
const { getAddresseId } = require("../db/mail");
import statusCode from "../utils/statusCodes";
import { registerAccount } from "../db/api";
import { getAddresseId } from "../db/mail";
async function addAccount(body, res) {
export 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)
@ -15,8 +15,4 @@ async function addAccount(body, res) {
});
}
module.exports = {
addAccount,
};
// todo change mailbox to account

View File

@ -1,8 +1,8 @@
const statusCode = require("../utils/statusCodes").statusCodes;
const { getMembers } = require("../db/api.js");
const { logger } = require("../system/Logger");
import statusCode from "../utils/statusCodes";
import { getMembers } from "../db/api";
import logger from "../system/Logger";
async function members(body, res) {
export async function members(body, res) {
const { roomId } = body;
getMembers(roomId).then((addresses) => {
res.status(statusCode.OK).json(addresses);
@ -11,7 +11,3 @@ async function members(body, res) {
res.status(statusCode.INTERNAL_SERVER_ERROR);
});
}
module.exports = {
members,
};

View File

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

View File

@ -0,0 +1,13 @@
import statusCode from "../utils/statusCodes";
import { getMessages } from "../db/api";
import logger from "../system/Logger";
export async function messages(body, res) {
const { roomId } = body;
getMessages(roomId).then((messages) => {
res.status(statusCode.OK).json(messages);
}).catch((err) => {
logger.error(err)
res.status(statusCode.INTERNAL_SERVER_ERROR);
});
}

View File

@ -1,8 +1,8 @@
const statusCode = require("../utils/statusCodes").statusCodes;
const { getRooms } = require("../db/api.js");
const { logger } = require("../system/Logger");
import statusCode from "../utils/statusCodes";
import { getRooms } from "../db/api";
import logger from "../system/Logger";
async function rooms(body, res) {
export async function rooms(body, res) {
const { mailboxId, offset, limit } = body;
getRooms(mailboxId).then((rooms) => {
res.status(statusCode.OK).json(rooms);
@ -11,7 +11,3 @@ async function rooms(body, res) {
res.status(statusCode.INTERNAL_SERVER_ERROR);
});
}
module.exports = {
rooms,
};

View File

@ -1,7 +1,7 @@
const { execQueryAsync, execQueryAsyncWithId } = require("./db.js");
const { queryCcId, queryToId, queryFromId } = require("./utils/addressQueries.js");
import { execQueryAsync, execQueryAsyncWithId } from "./db";
import { queryCcId, queryToId, queryFromId } from "./utils/addressQueries";
async function registerAccount(userId, pwd, xoauth, xoauth2, host, port, tls) {
export 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 (?, ?, ?, ?, ?, ?, ?)
@ -10,7 +10,7 @@ async function registerAccount(userId, pwd, xoauth, xoauth2, host, port, tls) {
return await execQueryAsyncWithId(query, values);
}
async function getAccounts() {
export async function getAccounts() {
// todo mailbox or account id ?
const query = `
SELECT
@ -27,7 +27,7 @@ async function getAccounts() {
return await execQueryAsync(query, values);
}
async function getRooms(mailboxId) {
export async function getRooms(mailboxId) {
const query = `
SELECT
app_room.room_id AS id,
@ -51,7 +51,7 @@ async function getRooms(mailboxId) {
return await execQueryAsync(query, values);
}
async function getMessages(roomId) {
export async function getMessages(roomId) {
// todo attachements name
const query = `
SELECT
@ -97,7 +97,7 @@ async function getMessages(roomId) {
return await execQueryAsync(query, values);
}
async function getMembers(roomId) {
export async function getMembers(roomId) {
const query = `
SELECT
address.address_id,
@ -109,12 +109,4 @@ async function getMembers(roomId) {
`;
const values = [roomId];
return await execQueryAsync(query, values);
}
module.exports = {
registerAccount,
getAccounts,
getRooms,
getMessages,
getMembers
};
}

View File

@ -1,12 +1,13 @@
const mysql = require("mysql");
const { logger } = require("../system/Logger");
const MYSQL = require("./config.json").mysql;
import mysql from "mysql";
import logger from "../system/Logger";
require("dotenv").config();
const db = mysql.createConnection({
host: MYSQL.host,
user: MYSQL.user,
password: MYSQL.pwd,
database: MYSQL.database,
// todo remove export
export const db = mysql.createConnection({
host: process.env.HOST_DB,
user: process.env.USER_DB,
password: process.env.PASSWORD_DB,
database: process.env.NAME_DB,
});
db.connect(function (err) {
@ -17,7 +18,7 @@ db.connect(function (err) {
}
});
function execQueryAsync(query, values) {
export function execQueryAsync(query: string, values: any[]): Promise<any> {
return new Promise((resolve, reject) => {
db.query(query, values, (err, results, fields) => {
if (err) {
@ -29,7 +30,7 @@ function execQueryAsync(query, values) {
});
}
function execQueryAsyncWithId(query, values) {
export function execQueryAsyncWithId(query: string, values: any[]): Promise<number> {
return new Promise((resolve, reject) => {
db.query(query, values, (err, results, fields) => {
if (err) {
@ -41,19 +42,12 @@ function execQueryAsyncWithId(query, values) {
});
}
function execQuery(query, values) {
export function execQuery(query: string, values: any[]) {
db.query(query, values, (err, results, fields) => {
if (err) {
logger.error(err);
throw (err);
throw err;
}
return results;
});
}
module.exports = {
db, // todo remove this
execQuery,
execQueryAsync,
execQueryAsyncWithId
};

View File

@ -1,6 +1,6 @@
const { execQueryAsyncWithId, execQueryAsync, execQuery } = require("../db");
import { execQueryAsyncWithId, execQueryAsync, execQuery } from "../db";
async function getAllAccounts() {
export async function getAllAccounts() {
const query = `
SELECT
app_account.account_id AS id,
@ -16,34 +16,26 @@ async function getAllAccounts() {
return await execQueryAsync(query, values);
}
async function getAllMailboxes(accountId) {
export 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) {
export 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) {
export async function getMailbox(mailboxId) {
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
const values = [mailboxId];
return await execQueryAsync(query, values);
}
function updateMailbox(mailboxId, uidnext) {
export 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
}

View File

@ -1,6 +1,7 @@
const { execQueryAsync, execQueryAsyncWithId } = require("./db.js");
import { execQueryAsync, execQueryAsyncWithId } from "./db";
async function getAddresseId(email, name) {
export async function getAddresseId(email: string, name?: string): Promise<number> {
console.log("get address id")
const localpart = email.split("@")[0];
const domain = email.split("@")[1];
const query = `INSERT INTO address
@ -10,19 +11,19 @@ async function getAddresseId(email, name) {
return await execQueryAsyncWithId(query, values);
}
async function getFieldId(field) {
export async function getFieldId(field: string): Promise<number> {
const query = `INSERT INTO field_name (field_name) VALUES (?) ON DUPLICATE KEY UPDATE field_id=LAST_INSERT_ID(field_id)`;
const values = [field]
const values = [field];
return await execQueryAsyncWithId(query, values);
}
async function findRoomByOwner(ownerId) {
export async function findRoomByOwner(ownerId: number): Promise<{ room_id: number }[]> {
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
const values = [ownerId];
return await execQueryAsync(query, values);
}
async function getUserIdOfMailbox(boxId) {
export async function getUserIdOfMailbox(boxId: number): Promise<{ user_id: number }[]> {
const query = `
SELECT app_account.user_id
FROM mailbox
@ -32,10 +33,3 @@ async function getUserIdOfMailbox(boxId) {
const values = [boxId];
return await execQueryAsync(query, values);
}
module.exports = {
getAddresseId,
getFieldId,
findRoomByOwner,
getUserIdOfMailbox
};

View File

@ -1,7 +1,7 @@
const { transformEmojis } = require("../utils/string.js");
const { db, execQuery, execQueryAsync, execQueryAsyncWithId } = require("./db.js");
import { transformEmojis } from "../utils/string";
import { execQuery, execQueryAsync, execQueryAsyncWithId } from "./db";
async function registerMessage(timestamp, rfc822size, messageId) {
export async function registerMessage(timestamp, rfc822size, messageId) {
const query = `
INSERT INTO message
(idate, messageID, rfc822size) VALUES (?, ?, ?)
@ -11,7 +11,7 @@ async function registerMessage(timestamp, rfc822size, messageId) {
return await execQueryAsyncWithId(query, values);
}
function registerMailbox_message(mailboxId, uid, messageId, modseq, seen, deleted) {
export 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 (?, ?, ?, ?, ?, ?)
@ -20,7 +20,7 @@ function registerMailbox_message(mailboxId, uid, messageId, modseq, seen, delete
execQuery(query, values);
}
function registerBodypart(messageId, part, bodypartId, bytes, nbLines) {
export function registerBodypart(messageId, part, bodypartId, bytes, nbLines) {
const query = `
INSERT IGNORE INTO part_number
(message_id, part, bodypart_id, bytes, nb_lines) VALUES (?, ?, ?, ?, ?)
@ -29,14 +29,14 @@ function registerBodypart(messageId, part, bodypartId, bytes, nbLines) {
execQuery(query, values);
}
async function saveBodypart(bytes, hash, text, data) {
export 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) {
export async function saveHeader_fields(messageId, fieldId, bodypartId, part, value) {
value = transformEmojis(value);
const query = `
INSERT IGNORE INTO header_field
@ -46,7 +46,7 @@ async function saveHeader_fields(messageId, fieldId, bodypartId, part, value) {
return await execQueryAsync(query, values);
}
async function saveAddress_fields(messageId, fieldId, addressId, number) {
export async function saveAddress_fields(messageId, fieldId, addressId, number) {
const query = `
INSERT IGNORE INTO address_field
(message_id , field_id, address_id, number) VALUES (?, ?, ?, ?)
@ -55,7 +55,7 @@ async function saveAddress_fields(messageId, fieldId, addressId, number) {
return await execQueryAsync(query, values);
}
function saveSource(messageId, content) {
export function saveSource(messageId, content) {
content = transformEmojis(content);
const query = `
INSERT INTO source (message_id, content) VALUES (?, ?)
@ -63,14 +63,4 @@ function saveSource(messageId, content) {
`;
const values = [messageId, content, content];
execQuery(query, values);
}
module.exports = {
registerMessage,
registerMailbox_message,
saveHeader_fields,
saveAddress_fields,
registerBodypart,
saveBodypart,
saveSource
}

View File

@ -1,64 +1,89 @@
const { transformEmojis } = require("../utils/string.js");
const { db, execQueryAsync, execQueryAsyncWithId, execQuery } = require("./db.js");
const { queryFromId, queryToId, queryCcId } = require("./utils/addressQueries.js");
import { transformEmojis } from "../utils/string";
import { db, execQueryAsync, execQueryAsyncWithId, execQuery } from "./db";
import { queryFromId, queryToId, queryCcId } from "./utils/addressQueries";
async function createRoom(roomName, ownerId, messageId) {
if (!roomName) roomName = "No room name";
roomName = transformEmojis(roomName);
const query = `INSERT IGNORE 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
export async function getAllMembers(messageId: number) {
const query = `
SELECT GROUP_CONCAT(address.address_id) AS is
FROM address
INNER JOIN address_field ON
address_field.address_id = address.address_id AND
address_field.message_id = ?
GROUP BY address_field.message_id
`;
const values = [messageId];
return await execQueryAsync(query, values);
}
async function registerMessageInRoom(messageId, roomId, isSeen, idate) {
export async function registerMember(roomId: number, memberId: number) {
const query = `INSERT IGNORE INTO app_room_member (room_id, member_id) VALUES (?, ?)`;
const values = [roomId, memberId];
return await execQueryAsync(query, values);
}
export async function createRoom(
roomName: string | null | undefined,
ownerId: number,
messageId: number,
roomType: number,
) {
if (!roomName) roomName = "No room name";
roomName = transformEmojis(roomName);
const query = `INSERT IGNORE INTO app_room (room_name, owner_id, message_id, room_type) VALUES (?, ?, ?, ?)`;
const values = [roomName.substring(0, 255), ownerId, messageId, roomType];
return await execQueryAsyncWithId(query, values);
}
// todo date not good
export async function registerMessageInRoom(
messageId: number,
roomId: number,
isSeen: boolean,
idate: string | undefined | null,
) {
if (!idate) idate = new Date().toString();
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);
}
// if (!isSeen) {
// incrementNotSeenRoom(roomId);
// }
}
function updateLastUpdateRoom(roomId, idate) {
export function updateLastUpdateRoom(roomId: number, idate: string) {
const query = `UPDATE app_room SET lastUpdate = ? WHERE room_id = ?`;
const values = [idate, roomId];
execQuery(query, values);
}
function incrementNotSeenRoom(roomId) {
export function incrementNotSeenRoom(roomId: number) {
// 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];
export async function getRoomInfo(messageID: string): Promise<{ room_id: number; root_id: number }[]> {
const query = `
SELECT
app_room.room_id
app_thread.root_id
FROM app_room
LEFT JOIN app_thread ON app_thread.room_id = app_room.room_id
INNER JOIN app_room_message ON app_room_message.room_id = app_room.room_id
INNER JOIN message ON message.message_id = app_room_message.message_id
WHERE message.messageID = ?
`;
const values = [messageID];
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")
export async function registerThread(roomId: number, parentId: number, rootId: number) {
const query = `INSERT IGNORE INTO app_thread (room_id, parent_id, root_id) VALUES (?, ?, ?)`;
const values = [roomId, parentId, rootId];
return await execQueryAsync(query, values);
}
function updateLastUpdateThread(threadId) {
// todo
// check for parent
}
function incrementNotSeenThread(threadId) {
// todo
// also increment root room
}
async function isRoomGroup(roomId) {
export async function isRoomGroup(roomId: number): Promise<boolean> {
return new Promise((resolve, reject) => {
const query = `SELECT isGroup FROM app_room WHERE room_id = '${roomId}'`;
db.query(query, (err, results, fields) => {
@ -68,13 +93,14 @@ async function isRoomGroup(roomId) {
});
}
async function findRoomsFromMessage(messageId) {
export async function findRoomsFromMessage(messageID: string) {
// todo find message in room not started
const query = `SELECT room_id FROM app_room_message WHERE message_id = ? ORDER BY room_id`;
const values = [messageId];
const values = [messageID];
return await execQueryAsync(query, values);
}
async function hasSameMembersAsParent(messageId, messageID) {
export async function hasSameMembersAsParent(messageId: number, messageID: string) {
const query1 = `
SELECT
GROUP_CONCAT(fromT.address_id) AS fromA,
@ -117,13 +143,3 @@ async function hasSameMembersAsParent(messageId, messageID) {
addressesMsg1.reduce((a, b) => a && addressesMsg2.includes(b), true)
);
}
module.exports = {
createRoom,
registerMessageInRoom,
createThread,
registerMessageInThread,
isRoomGroup,
findRoomsFromMessage,
hasSameMembersAsParent,
};

View File

@ -122,11 +122,11 @@ CREATE TABLE app_room (
room_name VARCHAR(255) NOT NULL,
owner_id INT NOT NULL,
message_id INT NOT NULL,
isGroup BOOLEAN NOT NULL DEFAULT false,
room_type INT NOT NULL DEFAULT 0,
notSeen INT NOT NULL DEFAULT 0,
lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
PRIMARY KEY (room_id),
UNIQUE KEY (owner_id, message_id, isGroup),
UNIQUE KEY (owner_id, message_id, room_type),
FOREIGN KEY (owner_id) REFERENCES address(address_id),
FOREIGN KEY (message_id) REFERENCES message(message_id)
);
@ -134,14 +134,13 @@ CREATE TABLE app_room (
-- 12
CREATE TABLE app_thread (
room_id INT NOT NULL,
parent_room_id INT,
root_room_id INT,
isDm BOOLEAN NOT NULL DEFAULT false,
parent_id INT,
root_id INT,
PRIMARY KEY (room_id),
UNIQUE KEY (room_id, parent_room_id, root_room_id),
UNIQUE KEY (room_id, parent_id, root_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
FOREIGN KEY (parent_id) REFERENCES app_room(room_id) ON DELETE SET NULL,
FOREIGN KEY (root_id) REFERENCES app_room(room_id) ON DELETE SET NULL
);
-- 13
@ -157,6 +156,7 @@ CREATE TABLE app_room_message (
CREATE TABLE app_room_member (
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)
);

View File

@ -1,4 +1,4 @@
const queryAddress = (type) => `
const queryAddress = (type: string): string => `
LEFT JOIN (
SELECT address_field.address_id, address_field.message_id
FROM address_field
@ -9,12 +9,6 @@ const queryAddress = (type) => `
)
`;
const queryFromId = queryAddress("from");
const queryToId = queryAddress("to");
const queryCcId = queryAddress("cc");
module.exports = {
queryFromId,
queryToId,
queryCcId
}
export const queryFromId = queryAddress("from");
export const queryToId = queryAddress("to");
export const queryCcId = queryAddress("cc");

View File

@ -0,0 +1,46 @@
export interface User {
name: string;
mailbox: string;
host: string;
}
export interface Envelope {
date?: string | null;
subject?: string | null;
from?: User[] | null;
sender?: User[] | null;
replyTo?: User[] | null;
to?: User[] | null;
cc?: User[] | null;
bcc?: User[] | null;
inReplyTo?: string | null;
messageId: string;
}
export interface Attrs {
/** A 32-bit ID that uniquely identifies this message within its mailbox. */
uid: number;
/** A list of flags currently set on this message. */
flags: string[];
/** The internal server date for the message. */
date: string;
/** The message's body structure (only set if requested with fetch()). */
struct?: any[] | undefined;
envelope?: Envelope;
/** The RFC822 message size (only set if requested with fetch()). */
size?: number | undefined;
}
export interface AttrsWithEnvelope {
/** A 32-bit ID that uniquely identifies this message within its mailbox. */
uid: number;
/** A list of flags currently set on this message. */
flags: string[];
/** The internal server date for the message. */
date: string;
/** The message's body structure (only set if requested with fetch()). */
struct?: any[] | undefined;
envelope: Envelope;
/** The RFC822 message size (only set if requested with fetch()). */
size?: number | undefined;
}

View File

@ -1,14 +0,0 @@
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,
};

View File

@ -1,9 +1,16 @@
const { getMailbox, updateMailbox } = require("../../db/imap/imap");
const { logger } = require("../../system/Logger");
const { registerMessageInApp } = require("../saveMessage");
const { saveMessage } = require("../storeMessage");
import Imap, { ImapMessageAttributes, MailBoxes } from "imap";
import { getMailbox, updateMailbox } from "../../db/imap/imap";
import { Attrs } from "../../interfaces/mail/attrs.interface";
import logger from "../../system/Logger";
import RegisterMessageInApp from "../saveMessage";
import { saveMessage } from "../storeMessage";
export default class Box {
imap: Imap;
boxName: string;
id: number;
box: MailBoxes;
class Box {
constructor(_imap, _boxId, _boxName) {
this.imap = _imap;
this.boxName = _boxName;
@ -23,16 +30,18 @@ class Box {
}
sync(savedUid, currentUid) {
const promises = [];
const mails = [];
const promises: Promise<unknown>[] = [];
const mails: ImapMessageAttributes[] = [];
logger.log(`Syncing from ${savedUid} to ${currentUid} uid`);
const f = this.imap.seq.fetch(`${savedUid}:${currentUid}`, {
// const f = this.imap.seq.fetch(`${savedUid}:${currentUid}`, {
size: true,
envelope: true,
});
f.on("message", (msg, seqno) => {
msg.once("attributes", (attrs) => {
msg.once("attributes", (attrs: Attrs) => {
console.log(attrs.envelope)
mails.push(attrs);
promises.push(saveMessage(attrs, this.id, this.imap));
});
@ -44,14 +53,15 @@ class Box {
f.once("end", async () => {
let step = 20;
logger.log(promises.length)
for (let i = 0; i < promises.length; i += step) {
for (let j = i; j < (i + step && promises.length); j++) {
console.log(j, promises.length, promises[j])
await new Promise((resolve, reject) => {
promises[j]
.then(async (res) => {
await registerMessageInApp(res, mails[j], this.id);
resolve();
const register = new RegisterMessageInApp(res, mails[j], this.id);
await register.save();
resolve("");
})
.catch((err) => {
reject(err);
@ -61,16 +71,7 @@ class Box {
logger.log(`Saved messages ${i + step > promises.length ? promises.length : i + step}/${mails.length}`);
updateMailbox(this.id, mails[i].uid);
}
// await Promise.all(promises).then(async (res) => {
// for (let i = 0; i < mails.length; i++) {
// logger.log(`Saved message ${i}/${mails.length}`);
// }
// });
updateMailbox(this.id, currentUid);
});
}
}
module.exports = {
Box,
};
}

View File

@ -1,9 +1,15 @@
const Imap = require("imap");
const { getAllMailboxes, registerMailbox } = require("../../db/imap/imap");
const { logger } = require("../../system/Logger");
const { Box } = require("./Box");
import { Account } from "./ImapSync";
import Imap from "imap";
import { getAllMailboxes, registerMailbox } from "../../db/imap/imap";
import logger from "../../system/Logger";
import Box from "./Box";
export class ImapInstance {
imap: Imap;
account: Account;
boxes: Box[];
class ImapInstance {
constructor(account) {
this.imap = new Imap({
user: account.user,
@ -68,8 +74,4 @@ class ImapInstance {
if (!allBox.includes("/")) logger.warn("Did not find 'All' mailbox");
return allBox;
}
}
module.exports = {
ImapInstance,
};
}

View File

@ -1,28 +0,0 @@
const { getAllAccounts } = require("../../db/imap/imap");
const { logger } = require("../../system/Logger");
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) => {
logger.error(err);
});
}
addInstance(config) {
this.instances.push(new ImapInstance(config));
}
}
module.exports = {
ImapSync
}

View File

@ -0,0 +1,33 @@
import { getAllAccounts } from "../../db/imap/imap";
import logger from "../../system/Logger";
import { ImapInstance } from "./ImapInstance";
export interface Account {
id: number;
user: string
password?: string
}
export default class ImapSync {
instances: ImapInstance[]
constructor() {
this.instances = [];
}
init() {
getAllAccounts().then((accounts: Account[]) => {
for (let i = 0; i < accounts.length; i++) {
accounts[i].password = accounts[i]?.password?.toString().replace(/[\u{0080}-\u{FFFF}]/gu,"");
if (accounts[i].id == 2) continue; //debug_todo
this.addInstance(accounts[i]);
}
}).catch((err) => {
logger.error(err);
});
}
addInstance(config) {
this.instances.push(new ImapInstance(config));
}
}

View File

@ -1,145 +0,0 @@
const {
createRoom,
registerMessageInRoom,
createThread,
registerMessageInThread,
isRoomGroup,
findRoomsFromMessage,
hasSameMembersAsParent,
} = require("../db/saveMessageApp");
const { findRoomByOwner, getAddresseId, getUserIdOfMailbox } = require("../db/mail");
const { nbMembers } = 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 initiateRoom(envelope, ownerId, messageId, isSeen) {
await createRoom(envelope.subject, ownerId, messageId).then(async (roomId) => {
// todo register members
await registerMessageInRoom(messageId, roomId, isSeen, envelope.date);
});
}
const roomType = {
ROOM: 'room',
CHANNEL: 'channel',
GROUP: 'group',
DM: 'dm',
THREAD: 'thread'
}
class registerMessageInApp {
constructor(_messageId, _attrs, _boxId) {
this.messageId = _messageId;
this.attrs = _attrs;
this.envelope = this.attrs.envelope;
this.messageID = this.envelope.messageId;
this.boxId = _boxId;
this.isSeen = this.attrs.flags.includes("\\Seen") ? 1 : 0;
this.ownerId;
this.userId;
}
async init() {
this.ownerId = await getAddresseId(createAddress(this.envelope.from[0])); // todo use sender or from ?
}
isDm = () => nbMembers(this.envelope) == 2;
async isFromUs() {
if (!this.userId) this.userId = (await getUserIdOfMailbox(this.boxId))[0]?.user_id;
return this.ownerId == this.userId;
}
async initiateRoom(owner, roomType) {
// todo roomType
await createRoom(this.envelope.subject, owner, this.messageId).then(async (roomId) => {
// todo register members
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
});
}
async createOrRegisterOnExistence(owner, roomType) {
await findRoomByOwner(owner).then(async (res) => {
if (res.length == 0) {
// first message with this sender
await 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.isSeen, this.envelope.date);
}
});
}
async createOrRegisterOnMembers(roomId) {
const hasSameMembers = await hasSameMembersAsParent(this.messageID, this.envelope.inReplyTo);
if (hasSameMembers) {
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
} else {
await createThread(this.envelope.subject, this.ownerId, this.messageId, roomId, this.isDm()).then(
async (threadId) => {
await registerMessageInThread(this.messageId, threadId, this.isSeen);
},
);
}
}
async save() {
await this.init();
if (this.envelope.inReplyTo) {
this.saveReply();
} else {
if (await this.isFromUs()) {
if (this.isDm()) {
// create or add new message to DM
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
initiateRoom(this.envelope, this.ownerId, this.messageId, this.isSeen);
}
} else {
await this.createOrRegisterOnExistence(this.ownerId, roomType.ROOM);
}
}
}
async saveReply() {
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
await isRoomGroup(rooms[0].room_id).then(async (isGroup) => {
if (isGroup) {
this.createOrRegisterOnMembers(rooms[0].room_id);
} 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 roomId = rooms[rooms.length - 1].room_id;
this.createOrRegisterOnMembers(roomId);
}
});
}
}
module.exports = {
registerMessageInApp,
roomType
};

183
back/mails/saveMessage.ts Normal file
View File

@ -0,0 +1,183 @@
import {
createRoom,
registerMessageInRoom,
isRoomGroup,
findRoomsFromMessage,
hasSameMembersAsParent,
registerThread,
registerMember,
getAllMembers,
getRoomInfo,
} from "../db/saveMessageApp";
import { findRoomByOwner, getAddresseId, getUserIdOfMailbox } from "../db/mail";
import { nbMembers } from "./utils/envelopeUtils";
import logger from "../system/Logger";
import { ImapMessageAttributes } from "imap";
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 const 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 registerMembers(roomId: number) {
getAllMembers(this.messageId).then((res) => {
res[0].id.split(",").foreach(async (memberId: number) => {
await registerMember(roomId, memberId);
});
});
}
async initiateRoom(owner: number, roomType: number) {
try {
const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType);
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
this.registerMembers(roomId);
return roomId;
} catch (err) {
logger.error(err);
}
}
async createOrRegisterOnExistence(owner: number, roomType: number) {
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.isSeen, this.envelope.date);
}
});
}
async initiateThread() {
await createRoom(this.envelope.subject, this.ownerId, this.messageId, roomType.THREAD).then(
async (roomId: number) => {
// find parent room infos
await getRoomInfo(this.inReplyTo).then(async (room) => {
// todo room not lenght, reply to transfer ?
let root_id = room[0].root_id;
if (!root_id) root_id = room[0].room_id;
await registerThread(roomId, room[0].room_id, root_id);
});
// impl register previous message ?
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
await this.registerMembers(roomId);
},
);
}
async createOrRegisterOnMembers(roomId: number) {
const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo);
if (hasSameMembers) {
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
} else {
await this.initiateThread();
}
}
async save() {
await this.init();
if (this.envelope.inReplyTo) {
this.inReplyTo = this.envelope.inReplyTo;
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
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 == 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
await isRoomGroup(rooms[0].room_id).then(async (isGroup: boolean) => {
if (isGroup) {
this.createOrRegisterOnMembers(rooms[0].room_id);
} 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 roomId = rooms[rooms.length - 1].room_id;
this.createOrRegisterOnMembers(roomId);
}
});
}
}

View File

@ -1,7 +1,7 @@
const { getAddresseId } = require("../db/mail");
const { simpleParser } = require("mailparser");
const moment = require("moment");
const {
import { getAddresseId } from "../db/mail";
import {simpleParser} from "mailparser";
import moment from "moment";
import {
registerMessage,
registerMailbox_message,
saveHeader_fields,
@ -9,12 +9,12 @@ const {
registerBodypart,
saveBodypart,
saveSource,
} = require("../db/saveMessage");
} from "../db/saveMessage";
const { getFieldId } = require("../db/mail");
const { logger } = require("../system/Logger");
import { getFieldId } from "../db/mail";
import logger from "../system/Logger";
function saveMessage(attrs, mailboxId, imap) {
export 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;
@ -68,7 +68,7 @@ function saveMessage(attrs, mailboxId, imap) {
}
async function saveFromParsedData(parsed, messageId) {
const promises = [];
const promises: Promise<any>[] = [];
Object.keys(parsed).forEach((key) => {
if (["from", "to", "cc", "bcc", "replyTo"].includes(key)) {
promises.push(
@ -118,9 +118,6 @@ async function saveFromParsedData(parsed, messageId) {
// todo when transfered
}
module.exports = {
saveMessage,
};
if (process.env["NODE_DEV"] == "TEST") {
module.exports = {

View File

@ -0,0 +1,17 @@
import { Envelope, User } from "../../interfaces/mail/attrs.interface";
export function nbMembers(envelope: Envelope) {
return getMembers(envelope).length;
}
export function getMembers(envelope: Envelope) {
const members: User[] = [];
const fields = ["from", "to", "sender", "replyTo", "cc", "bcc"] as const;
fields.forEach((field) => {
envelope[field]?.forEach((member: User) => {
if (members.find((m) => m.mailbox === member.mailbox && m.host === member.host)) return;
members.push(member);
});
});
return members;
}

View File

@ -1,18 +0,0 @@
function nbMembers(envelope) {
let nbMembers =
(envelope.bcc?.length ?? 0) +
(envelope.cc?.length ?? 0) +
(envelope.to?.length ?? 0) +
(envelope.from?.length ?? 0);
if (
envelope.sender?.length > 0 &&
!(envelope.sender[0].mailbox == envelope.from[0].mailbox && envelope.sender[0].host == envelope.from[0].host)
) {
nbMembers += envelope.sender?.length ?? 0;
}
return nbMembers;
}
module.exports = {
nbMembers,
};

1621
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,15 @@
{
"scripts": {
"build": "npx tsc",
"start": "node build/server.js",
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q build/server.js\"",
"clean": "rm -rf build"
},
"dependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"imap": "^0.8.19",
"imap-simple": "^5.1.0",
@ -12,9 +19,22 @@
"vue-router": "^4.1.6"
},
"devDependencies": {
"jest": "^29.5.0"
"@babel/preset-typescript": "^7.21.4",
"@types/express": "^4.17.17",
"@types/imap": "^0.8.35",
"@types/jest": "^29.5.0",
"@types/mailparser": "^3.0.2",
"@types/moment": "^2.13.0",
"@types/node": "^18.15.11",
"concurrently": "^8.0.1",
"jest": "^29.5.0",
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"testMatch": [
"<rootDir>/test//**/*-test.[jt]s?(x)"
]

View File

@ -1,16 +1,17 @@
const statusCodes = require("../utils/statusCodes.js").statusCodes;
const express = require("express");
import statusCodes from "../utils/statusCodes";
import express from "express";
const router = express.Router();
const Ajv = require("ajv");
const addFormats = require("ajv-formats");
import Ajv from "ajv";
import addFormats from "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");
import schema_account from "../schemas/account_schema.json";
import { addAccount } from "../controllers/addAccount";
import { getAccounts } from "../db/api";
import { rooms } from "../controllers/rooms";
import { messages } from "../controllers/messages";
import { members } from "../controllers/members";
const validate_account = ajv.compile(schema_account);
@ -50,4 +51,4 @@ router.post("/account", async (req, res) => {
}
});
module.exports = router;
export default router;

View File

@ -1,7 +1,7 @@
const express = require("express");
const cors = require("cors");
import express from "express";
import cors from "cors";
const app = express();
const { ImapSync } = require("./mails/imap/ImapSync");
import ImapSync from "./mails/imap/ImapSync";
app.use(express.json());
app.use(
@ -12,8 +12,8 @@ app.use(
app.use(cors());
app.listen(process.env.PORT || 5500);
const mailRouter = require("./routes/mail");
import mailRouter from "./routes/mail";
app.use("/api/mail", mailRouter);
const imapSync = new ImapSync();
imapSync.init()
imapSync.init();

View File

@ -3,19 +3,19 @@ class Logger {
}
log(content) {
console.log(this._prefix("log"), content);
log(...content): void {
// console.log(this._prefix("log"), content);
}
warn(content) {
console.warn(this._prefix("warn"), content);
warn(...content): void {
// console.warn(this._prefix("warn"), content);
}
error(content) {
console.error(this._prefix("err"), content);
error(...content): void {
// console.error(this._prefix("err"), content);
}
_prefix(type) {
_prefix(type: string): string {
let typeStr = "";
switch (type) {
case "log":
@ -39,7 +39,4 @@ class Logger {
}
const logger = new Logger();
module.exports = {
logger,
}
export default logger;

View File

@ -1,41 +0,0 @@
const { getAddresseId, getUserIdOfMailbox } = require("../../db/mail");
const { generateAttrs, generateUsers } = require("../test-utils/test-attrsUtils");
const { registerMessageInApp, roomType } = require("../../mails/saveMessage");
jest.mock("../../db/mail");
// todo mock db
describe("saveMessage", () => {
describe("is not a reply", () => {
it("DM from user should be assigned to other user", async () => {
const users = generateUsers(2);
const attrs = generateAttrs({ from: [users[0].user], to: [users[1].user] });
getUserIdOfMailbox.mockReturnValue([{ user_id: users[0].id }]);
getAddresseId.mockImplementation((email) => {
const match = users.find((user) => user.user.mailbox + "@" + user.user.host == email);
return match.id;
});
const register = new registerMessageInApp(1, attrs, 1);
const createOrRegisterOnExistence = jest
.spyOn(register, "createOrRegisterOnExistence")
.mockImplementation(() => undefined);
await register.save();
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(users[1].id, roomType.DM);
});
it("DM message from user should be added to DM room", async () => {
// todo multiple messages
});
// it("first GROUP message should create a group", () => {});
});
describe("replies", () => {
it("", () => {});
});
describe("", () => {});
});

View File

@ -0,0 +1,112 @@
import { generateAttrs, generateUsers } from "../test-utils/test-attrsUtils";
import registerMessageInApp, { roomType } from "../../mails/saveMessage";
import { jest, describe, it, expect } from "@jest/globals";
import { getAddresseId, getUserIdOfMailbox } from "../../db/mail";
// todo esbuild
// todo mock db
// new message from us
// to multiple people -> room
// if response has same member => group
// if response is dm => channel
// to one person => dm
// new message from other
// to only me -> room
// if no reply to multiple message => channel
// else => dm
// to multiple people -> room
// // make it better
// if multiple members reply -> group
// if only me reply -> channel
const users = generateUsers(5);
const ownUser = users[0];
const messageId = 1;
const boxId = 1;
jest.mock("../../db/mail", () => ({
getAddresseId: jest.fn().mockImplementation((email) => {
const match = users.find((user) => user.user.mailbox + "@" + user.user.host == email);
return new Promise((resolve, reject) => resolve(match?.id));
}),
getUserIdOfMailbox: jest.fn().mockImplementation((boxId) => {
return new Promise((resolve, reject) => resolve([{ user_id: ownUser.id }]));
}),
}));
describe("saveMessage", () => {
describe("functions", () => {
it("isFromUs", async () => {
const attrs = generateAttrs({ from: [ownUser.user], to: [users[1].user] });
const register = new registerMessageInApp(messageId, attrs, boxId);
await register.init();
const res = await register.isFromUs();
expect(res).toBe(true);
const attrs2 = generateAttrs({ from: [users[2].user], to: [users[1].user] });
const register2 = new registerMessageInApp(messageId, attrs2, boxId);
await register2.init();
const res2 = await register2.isFromUs();
expect(res2).toBe(false);
});
});
describe("implementation", () => {
describe("new first message from us", () => {
it("new first message from us to one recipient should create a DM", async () => {
const attrs = generateAttrs({ from: [ownUser.user], to: [users[1].user] });
const register = new registerMessageInApp(messageId, attrs, boxId);
const createOrRegisterOnExistence = jest
.spyOn(register, "createOrRegisterOnExistence")
.mockImplementation(
(owner: number, roomType: number) => new Promise((resolve, reject) => resolve()),
);
await register.save();
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(users[1].id, roomType.DM);
});
it("new first message from us to multiple recipients should create a ROOM", async () => {
const attrs = generateAttrs({ from: [ownUser.user], to: [users[1].user, users[2].user] });
const register = new registerMessageInApp(messageId, attrs, boxId);
const initiateRoom = jest
.spyOn(register, "initiateRoom")
.mockImplementation((owner: number, roomType: number) => Promise.resolve(1));
await register.save();
expect(initiateRoom).toHaveBeenCalledWith(ownUser.id, roomType.ROOM);
});
// it("response to new first message to multiple recipients with same members should change room type to GROUP", () => {});
// it("response to new first message to multiple recipients with different members should change room type to CHANNEL", () => {});
});
describe("new first message from other", () => {
it("new first message from other to me only should create a room", async () => {
const attrs = generateAttrs({ from: [users[1].user], to: [ownUser.user] });
const register = new registerMessageInApp(messageId, attrs, boxId);
const createOrRegisterOnExistence = jest
.spyOn(register, "createOrRegisterOnExistence")
.mockImplementation((owner: number, roomType: number) => {
return new Promise((resolve, reject) => resolve());
});
await register.save();
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(users[1].id, roomType.ROOM);
});
});
// describe("replies", () => {
// it("", () => {});
// });
// describe("", () => {});
});
});

View File

@ -0,0 +1,35 @@
import { nbMembers } from "../../../mails/utils/envelopeUtils";
import { generateAttrs, generateUsers } from "../../test-utils/test-attrsUtils";
import { describe, it, expect } from '@jest/globals';
describe("envelopeUtils", () => {
const names = generateUsers(6);
describe("nbMembers", () => {
it("sender and from shouldn't be counted twice if there are the same", () => {
const envelope = generateAttrs({
from: [names[0].user],
sender: [names[0].user],
}).envelope;
expect(nbMembers(envelope)).toBe(1);
});
it("sender and from shoud be counted twice if there are the same", () => {
const envelope = generateAttrs({
from: [names[0].user],
sender: [names[1].user],
}).envelope;
expect(nbMembers(envelope)).toBe(2);
});
it("should count every members", () => {
// todo should merge identic members
const envelope = generateAttrs({
from: [names[0].user],
sender: [names[1].user],
replyTo: [names[2].user],
to: [names[3].user],
cc: [names[4].user],
bcc: [names[5].user],
}).envelope;
expect(nbMembers(envelope)).toBe(6);
});
});
});

View File

@ -1,40 +0,0 @@
const { nbMembers } = require("../../../mails/utils/statusUtils");
describe("statusUtils", () => {
it("sender and from shouldn't be counted twice if there are the same", () => {
const envelope = {
from: [{ name: "", mailbox: "user_1", host: "provider.com" }],
sender: [{ name: "", mailbox: "user_1", host: "provider.com" }],
replyTo: [{ name: "", mailbox: "user_1", host: "provider.com" }],
to: null,
cc: null,
bcc: null,
inReplyTo: null,
};
expect(nbMembers(envelope)).toBe(1);
});
it("sender and from shoud be counted twice if there are the same", () => {
const envelope = {
from: [{ name: "", mailbox: "user_1", host: "provider.com" }],
sender: [{ name: "", mailbox: "user_2", host: "provider.com" }],
replyTo: [{ name: "", mailbox: "user_1", host: "provider.com" }],
to: null,
cc: null,
bcc: null,
inReplyTo: null,
};
expect(nbMembers(envelope)).toBe(2);
});
it("should count every members", () => {
const envelope = {
from: [{ name: "", mailbox: "user_1", host: "provider.com" }],
sender: [{ name: "", mailbox: "user_2", host: "provider.com" }],
replyTo: [{ name: "", mailbox: "user_1", host: "provider.com" }],
to: [{ name: "", mailbox: "user_1", host: "provider.com" }],
cc: [{ name: "", mailbox: "user_1", host: "provider.com" }],
bcc: [{ name: "", mailbox: "user_1", host: "provider.com" }],
inReplyTo: null,
};
expect(nbMembers(envelope)).toBe(5);
});
});

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

@ -1,19 +0,0 @@
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

@ -1,4 +1,4 @@
const mysql = require("mysql");
const mysql from ";
export class TestDb {
constructor (options) {

View File

@ -201,6 +201,4 @@ const names = [
"Lori",
];
module.exports = {
names,
};
export default names;

View File

@ -0,0 +1,73 @@
import { AttrsWithEnvelope, User } from "../../interfaces/mail/attrs.interface";
import names from "./names";
interface Options {
subject?: string;
from?: User[];
sender?: User[];
replyTo?: User[];
to?: User[];
cc?: User[];
bcc?: User[];
inReplyTo?: string;
messageId?: string;
date?: string;
flags?: string[];
uid?: number;
modseq?: number;
}
export function generateAttrs(options: Options): AttrsWithEnvelope {
const attrs = {
"size": 42,
"envelope": {
date: "2023-03-21T15:25:42.000Z",
subject: options.subject ?? "subject" + randomString(10),
from: options.from ?? undefined,
sender: options.sender ?? undefined,
replyTo: options.replyTo ?? undefined,
to: options.to ?? undefined,
cc: options.cc ?? undefined,
bcc: options.bcc ?? undefined,
inReplyTo: options.inReplyTo ?? undefined,
messageId: options.messageId ?? randomString(10),
},
"date": options.date ?? new Date().toDateString(),
"flags": options.flags ?? [],
"uid": options.uid ?? randomInt(3),
"modseq": options.modseq ?? randomInt(7),
"x-gm-labels": ["\\Inbox"],
"x-gm-msgid": "1760991478422670209",
"x-gm-thrid": "1760991478422670209",
};
return attrs;
}
export function generateUsers(nb: number) {
const users: {user: User, id: number}[] = [];
for (let i = 0; i < nb; i++) {
users.push({
user: {
name: "",
mailbox: names[i],
host: "provider.com",
},
id: i,
});
}
return users;
}
function randomString(length: number): string {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function randomInt(length: number): number {
return parseInt((Math.random() * Math.pow(10, length)).toFixed());
}

17
back/tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"outDir": "./build",
"target": "es6",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node", "jest"],
"paths": {
"*": ["node_modules/*", "src/types/*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}

View File

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

View File

@ -1,5 +1,4 @@
// from https://github.com/prettymuchbryce/http-status-codes/blob/master/src/utils.ts
const statusCodes = {
"CONTINUE": 100,
"SWITCHING_PROTOCOLS": 101,
@ -59,6 +58,4 @@ const statusCodes = {
"NETWORK_AUTHENTICATION_REQUIRED": 511,
}
module.exports = {
statusCodes: statusCodes
};
export default statusCodes;

View File

@ -1,4 +1,4 @@
function transformEmojis(str) {
export function transformEmojis(str :string): string {
if (!str) return str;
// Use a regular expression to match emojis in the string
const regex =
@ -6,20 +6,14 @@ function transformEmojis(str) {
// Replace each matched emoji with its Unicode code point
const transformedStr = str.replace(regex, (match) => {
return "\\u{" + match.codePointAt(0).toString(16).toUpperCase() + "}";
return "\\u{" + match.codePointAt(0)?.toString(16).toUpperCase() + "}";
});
return transformedStr;
}
function decodeEmojis(text) {
export function decodeEmojis(text: string): string {
const regex = /\\u{([^}]+)}/g;
const decodedText = text.replace(regex, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
return decodedText;
}
module.exports = {
transformEmojis,
decodeEmojis
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};