tests in typescript

This commit is contained in:
grimhilt 2023-04-01 22:36:51 +02:00
parent 9fbf5e5cf3
commit a5d325818b
22 changed files with 1258 additions and 412 deletions

7
.gitignore vendored
View File

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

View File

@ -1,6 +1,6 @@
import statusCode from "../utils/statusCodes"; import statusCode from "../utils/statusCodes";
import { getRooms } from "../db/api"; import { getRooms } from "../db/api";
import { logger } from "../system/Logger"; import logger from "../system/Logger";
export async function rooms(body, res) { export async function rooms(body, res) {
const { mailboxId, offset, limit } = body; const { mailboxId, offset, limit } = body;

View File

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

View File

@ -1,6 +1,7 @@
import { execQueryAsync, execQueryAsyncWithId } from "./db"; import { execQueryAsync, execQueryAsyncWithId } from "./db";
export 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 localpart = email.split("@")[0];
const domain = email.split("@")[1]; const domain = email.split("@")[1];
const query = `INSERT INTO address const query = `INSERT INTO address
@ -10,19 +11,19 @@ export async function getAddresseId(email, name) {
return await execQueryAsyncWithId(query, values); return await execQueryAsyncWithId(query, values);
} }
export 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 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); return await execQueryAsyncWithId(query, values);
} }
export 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 query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
const values = [ownerId]; const values = [ownerId];
return await execQueryAsync(query, values); return await execQueryAsync(query, values);
} }
export async function getUserIdOfMailbox(boxId) { export async function getUserIdOfMailbox(boxId: number): Promise<{ user_id: number }[]> {
const query = ` const query = `
SELECT app_account.user_id SELECT app_account.user_id
FROM mailbox FROM mailbox

View File

@ -2,7 +2,7 @@ import { transformEmojis } from "../utils/string";
import { db, execQueryAsync, execQueryAsyncWithId, execQuery } from "./db"; import { db, execQueryAsync, execQueryAsyncWithId, execQuery } from "./db";
import { queryFromId, queryToId, queryCcId } from "./utils/addressQueries"; import { queryFromId, queryToId, queryCcId } from "./utils/addressQueries";
export async function getAllMembers(messageId) { export async function getAllMembers(messageId: number) {
const query = ` const query = `
SELECT GROUP_CONCAT(address.address_id) AS is SELECT GROUP_CONCAT(address.address_id) AS is
FROM address FROM address
@ -15,13 +15,18 @@ export async function getAllMembers(messageId) {
return await execQueryAsync(query, values); return await execQueryAsync(query, values);
} }
export async function registerMember(roomId, memberId) { export async function registerMember(roomId: number, memberId: number) {
const query = `INSERT IGNORE INTO app_room_member (room_id, member_id) VALUES (?, ?)`; const query = `INSERT IGNORE INTO app_room_member (room_id, member_id) VALUES (?, ?)`;
const values = [roomId, memberId]; const values = [roomId, memberId];
return await execQueryAsync(query, values); return await execQueryAsync(query, values);
} }
export async function createRoom(roomName, ownerId, messageId, roomType) { export async function createRoom(
roomName: string | null | undefined,
ownerId: number,
messageId: number,
roomType: number,
) {
if (!roomName) roomName = "No room name"; if (!roomName) roomName = "No room name";
roomName = transformEmojis(roomName); roomName = transformEmojis(roomName);
const query = `INSERT IGNORE INTO app_room (room_name, owner_id, message_id, room_type) VALUES (?, ?, ?, ?)`; const query = `INSERT IGNORE INTO app_room (room_name, owner_id, message_id, room_type) VALUES (?, ?, ?, ?)`;
@ -29,7 +34,14 @@ export async function createRoom(roomName, ownerId, messageId, roomType) {
return await execQueryAsyncWithId(query, values); return await execQueryAsyncWithId(query, values);
} }
export async function registerMessageInRoom(messageId, roomId, isSeen, idate) { // 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 query = `INSERT IGNORE INTO app_room_message (message_id, room_id) VALUES (?, ?)`;
const values = [messageId, roomId]; const values = [messageId, roomId];
await execQueryAsync(query, values); await execQueryAsync(query, values);
@ -40,17 +52,17 @@ export async function registerMessageInRoom(messageId, roomId, isSeen, idate) {
// } // }
} }
export function updateLastUpdateRoom(roomId, idate) { export function updateLastUpdateRoom(roomId: number, idate: string) {
const query = `UPDATE app_room SET lastUpdate = ? WHERE room_id = ?`; const query = `UPDATE app_room SET lastUpdate = ? WHERE room_id = ?`;
const values = [idate, roomId]; const values = [idate, roomId];
execQuery(query, values); execQuery(query, values);
} }
export function incrementNotSeenRoom(roomId) { export function incrementNotSeenRoom(roomId: number) {
// todo // todo
} }
export async function getRoomInfo(messageID) { export async function getRoomInfo(messageID: string): Promise<{ room_id: number; root_id: number }[]> {
const query = ` const query = `
SELECT SELECT
app_room.room_id app_room.room_id
@ -65,13 +77,13 @@ export async function getRoomInfo(messageID) {
return await execQueryAsync(query, values); return await execQueryAsync(query, values);
} }
export async function registerThread(roomId, parentId, rootId) { 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 query = `INSERT IGNORE INTO app_thread (room_id, parent_id, root_id) VALUES (?, ?, ?)`;
const values = [roomId, parentId, rootId]; const values = [roomId, parentId, rootId];
return await execQueryAsync(query, values); return await execQueryAsync(query, values);
} }
export async function isRoomGroup(roomId) { export async function isRoomGroup(roomId: number): Promise<boolean> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const query = `SELECT isGroup FROM app_room WHERE room_id = '${roomId}'`; const query = `SELECT isGroup FROM app_room WHERE room_id = '${roomId}'`;
db.query(query, (err, results, fields) => { db.query(query, (err, results, fields) => {
@ -81,13 +93,14 @@ export async function isRoomGroup(roomId) {
}); });
} }
export 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 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); return await execQueryAsync(query, values);
} }
export async function hasSameMembersAsParent(messageId, messageID) { export async function hasSameMembersAsParent(messageId: number, messageID: string) {
const query1 = ` const query1 = `
SELECT SELECT
GROUP_CONCAT(fromT.address_id) AS fromA, GROUP_CONCAT(fromT.address_id) AS fromA,
@ -129,4 +142,4 @@ export async function hasSameMembersAsParent(messageId, messageID) {
addressesMsg1.length == addressesMsg2.length && addressesMsg1.length == addressesMsg2.length &&
addressesMsg1.reduce((a, b) => a && addressesMsg2.includes(b), true) addressesMsg1.reduce((a, b) => a && addressesMsg2.includes(b), true)
); );
} }

View File

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

View File

@ -1,5 +1,46 @@
export interface User { export interface User {
name: string, name: string;
mailbox: string, mailbox: string;
host: 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,5 +1,6 @@
import { ImapMessageAttributes } from "imap"; import Imap, { ImapMessageAttributes, MailBoxes } from "imap";
import { getMailbox, updateMailbox } from "../../db/imap/imap"; import { getMailbox, updateMailbox } from "../../db/imap/imap";
import { Attrs } from "../../interfaces/mail/attrs.interface";
import logger from "../../system/Logger"; import logger from "../../system/Logger";
import RegisterMessageInApp from "../saveMessage"; import RegisterMessageInApp from "../saveMessage";
import { saveMessage } from "../storeMessage"; import { saveMessage } from "../storeMessage";
@ -8,7 +9,7 @@ export default class Box {
imap: Imap; imap: Imap;
boxName: string; boxName: string;
id: number; id: number;
box: Object; box: MailBoxes;
constructor(_imap, _boxId, _boxName) { constructor(_imap, _boxId, _boxName) {
this.imap = _imap; this.imap = _imap;

View File

@ -7,17 +7,19 @@ import {
registerThread, registerThread,
registerMember, registerMember,
getAllMembers, getAllMembers,
getRoomInfo,
} from "../db/saveMessageApp"; } from "../db/saveMessageApp";
import { findRoomByOwner, getAddresseId, getUserIdOfMailbox } from "../db/mail"; import { findRoomByOwner, getAddresseId, getUserIdOfMailbox } from "../db/mail";
import { nbMembers } from "./utils/envelopeUtils"; import { nbMembers } from "./utils/envelopeUtils";
import logger from "../system/Logger"; import logger from "../system/Logger";
import { ImapMessageAttributes } from "imap"; 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 * take object address and join mailbox and host to return mailbox@host
*/ */
function createAddress(elt) { function createAddress(elt: User): string {
return `${elt.mailbox}@${elt.host}`; return `${elt.mailbox}@${elt.host}`;
} }
@ -31,47 +33,58 @@ export const roomType = {
export default class RegisterMessageInApp { export default class RegisterMessageInApp {
messageId: number; messageId: number;
attrs: ImapMessageAttributes; attrs: Attrs;
envelope?: ImapMessageAttributes.envelope; envelope: Envelope;
messageID?: string; messageID?: string;
boxId: number; boxId: number;
isSeen: boolean; isSeen: boolean;
ownerId: number; ownerId: number;
userId: number; userId: number;
inReplyTo: string;
constructor(_messageId, _attrs, _boxId) {
constructor(_messageId: number, _attrs: Attrs, _boxId: number) {
this.messageId = _messageId; this.messageId = _messageId;
this.attrs = _attrs; this.attrs = _attrs;
if (!this.attrs.envelope) throw new Error("Envelope must exist in attributes");
this.envelope = this.attrs.envelope; this.envelope = this.attrs.envelope;
this.messageID = this.envelope?.messageId; this.messageID = this.envelope?.messageId;
this.boxId = _boxId; this.boxId = _boxId;
this.isSeen = this.attrs.flags.includes("\\Seen") ? true : false; this.isSeen = this.attrs.flags.includes("\\Seen") ? true : false;
this.ownerId; this.ownerId = -1;
this.userId; this.userId = -1;
this.inReplyTo = "";
} }
async init() { async init() {
this.ownerId = await getAddresseId(createAddress(this.envelope.from[0])); // todo use sender or from ? 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; isDm = () => nbMembers(this.envelope) == 2;
async isFromUs() { async isFromUs() {
if (!this.userId) this.userId = (await getUserIdOfMailbox(this.boxId))[0]?.user_id; if (this.userId == -1) {
await getUserIdOfMailbox(this.boxId).then((res) => {
this.userId = res[0]?.user_id;
});
}
return this.ownerId == this.userId; return this.ownerId == this.userId;
} }
async registerMembers(roomId) { async registerMembers(roomId: number) {
getAllMembers(this.messageId).then((res) => { getAllMembers(this.messageId).then((res) => {
res[0].id.split(",").foreach(async (memberId) => { res[0].id.split(",").foreach(async (memberId: number) => {
await registerMember(roomId, memberId); await registerMember(roomId, memberId);
}); });
}); });
} }
async initiateRoom(owner, type) { async initiateRoom(owner: number, roomType: number) {
try { try {
const roomId = await createRoom(this.envelope.subject, owner, this.messageId, type); const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType);
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date); await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
this.registerMembers(roomId); this.registerMembers(roomId);
return roomId; return roomId;
@ -80,7 +93,7 @@ export default class RegisterMessageInApp {
} }
} }
async createOrRegisterOnExistence(owner, roomType) { async createOrRegisterOnExistence(owner: number, roomType: number) {
await findRoomByOwner(owner).then(async (res) => { await findRoomByOwner(owner).then(async (res) => {
if (res.length == 0) { if (res.length == 0) {
// first message with this sender // first message with this sender
@ -93,42 +106,41 @@ export default class RegisterMessageInApp {
} }
async initiateThread() { async initiateThread() {
await createRoom(this.envelope.subject, owner, this.messageId, roomType.THREAD).then(async (roomId) => { await createRoom(this.envelope.subject, this.ownerId, this.messageId, roomType.THREAD).then(
// find parent room infos async (roomId: number) => {
await getRoomInfo(this.envelope.inReplyTo).then(async (room) => { // find parent room infos
// todo room not lenght, reply to transfer ? await getRoomInfo(this.inReplyTo).then(async (room) => {
let root_id = room[0].root_id; // todo room not lenght, reply to transfer ?
if (!root_id) root_id = room[0].room_id; let root_id = room[0].root_id;
await registerThread(roomId, room[0].room_id, 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); // impl register previous message ?
await this.registerMembers(roomId); await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
}); await this.registerMembers(roomId);
},
);
} }
async createOrRegisterOnMembers(roomId) { async createOrRegisterOnMembers(roomId: number) {
const hasSameMembers = await hasSameMembersAsParent(this.messageID, this.envelope.inReplyTo); const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo);
if (hasSameMembers) { if (hasSameMembers) {
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date); await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
} else { } else {
await this.initiateThread(); await this.initiateThread();
await createThread(this.envelope.subject, this.ownerId, this.messageId, roomId, this.isDm()).then(
async (threadId) => {
await registerMessageInThread(this.messageId, threadId, this.isSeen);
},
);
} }
} }
async save() { async save() {
await this.init(); await this.init();
if (this.envelope.inReplyTo) { if (this.envelope.inReplyTo) {
this.inReplyTo = this.envelope.inReplyTo;
this.saveReply(); this.saveReply();
} else { } else {
if (await this.isFromUs()) { if (await this.isFromUs()) {
if (this.isDm()) { if (this.isDm()) {
// create or add new message to DM // 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])); const userTo = await getAddresseId(createAddress(this.envelope.to[0]));
await this.createOrRegisterOnExistence(userTo, roomType.DM); await this.createOrRegisterOnExistence(userTo, roomType.DM);
} else { } else {
@ -143,15 +155,14 @@ export default class RegisterMessageInApp {
} }
async saveReply() { async saveReply() {
const messageID = envelope.messageId; await findRoomsFromMessage(this.inReplyTo).then(async (rooms) => {
await findRoomsFromMessage(messageId).then(async (rooms) => {
if (rooms.length == 0) { if (rooms.length == 0) {
// no rooms, so is a transfer // no rooms, so is a transfer
// todo test if members of transferred message are included // todo test if members of transferred message are included
} else if (rooms.length === 1) { } else if (rooms.length === 1) {
// only one room so message is only in a room and not in a thread // only one room so message is only in a room and not in a thread
// as a thread is associated to a room // as a thread is associated to a room
await isRoomGroup(rooms[0].room_id).then(async (isGroup) => { await isRoomGroup(rooms[0].room_id).then(async (isGroup: boolean) => {
if (isGroup) { if (isGroup) {
this.createOrRegisterOnMembers(rooms[0].room_id); this.createOrRegisterOnMembers(rooms[0].room_id);
} else { } else {

View File

@ -1,5 +1,5 @@
import { getAddresseId } from "../db/mail"; import { getAddresseId } from "../db/mail";
import mailParser from "mailparser"; import {simpleParser} from "mailparser";
import moment from "moment"; import moment from "moment";
import { import {
registerMessage, registerMessage,
@ -41,7 +41,7 @@ export function saveMessage(attrs, mailboxId, imap) {
// saveSource(messageId, buffer); // saveSource(messageId, buffer);
// parse data // parse data
mailParser(buffer, async (err, parsed) => { simpleParser(buffer, async (err, parsed) => {
saveFromParsedData(parsed, messageId) saveFromParsedData(parsed, messageId)
.then(() => { .then(() => {
resolve(messageId); resolve(messageId);
@ -68,7 +68,7 @@ export function saveMessage(attrs, mailboxId, imap) {
} }
async function saveFromParsedData(parsed, messageId) { async function saveFromParsedData(parsed, messageId) {
const promises = []; const promises: Promise<any>[] = [];
Object.keys(parsed).forEach((key) => { Object.keys(parsed).forEach((key) => {
if (["from", "to", "cc", "bcc", "replyTo"].includes(key)) { if (["from", "to", "cc", "bcc", "replyTo"].includes(key)) {
promises.push( promises.push(

View File

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

1009
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
"ajv": "^8.12.0", "ajv": "^8.12.0",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"imap": "^0.8.19", "imap": "^0.8.19",
"imap-simple": "^5.1.0", "imap-simple": "^5.1.0",
@ -18,18 +19,22 @@
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-typescript": "^7.21.4",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/imap": "^0.8.35", "@types/imap": "^0.8.35",
"@types/jest": "^29.5.0",
"@types/mailparser": "^3.0.2", "@types/mailparser": "^3.0.2",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",
"@types/node": "^18.15.11", "@types/node": "^18.15.11",
"concurrently": "^8.0.1", "concurrently": "^8.0.1",
"jest": "^29.5.0", "jest": "^29.5.0",
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"ts-sql-plugin": "^0.9.0", "typescript": "^4.9.5"
"typescript": "^5.0.3"
}, },
"jest": { "jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"testMatch": [ "testMatch": [
"<rootDir>/test//**/*-test.[jt]s?(x)" "<rootDir>/test//**/*-test.[jt]s?(x)"
] ]

View File

@ -1,90 +0,0 @@
const { getAddresseId, getUserIdOfMailbox } from ";
const { generateAttrs, generateUsers } = require("../test-utils/test-attrsUtils");
const { registerMessageInApp, roomType } from ";
jest.mock("../../db/mail");
// 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
describe("saveMessage", () => {
describe("new first message", () => {
const users = generateUsers(5);
const ownUser = users[0];
const messageId = 1;
const boxId = 1;
getUserIdOfMailbox.mockReturnValue([{ user_id: ownUser.id }]);
getAddresseId.mockImplementation((email) => {
const match = users.find((user) => user.user.mailbox + "@" + user.user.host == email);
return match.id;
});
describe("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(() => undefined);
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(() => undefined);
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("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(() => undefined);
await register.save();
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(users[1].id, roomType.ROOM);
});
});
});
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

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

View File

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

View File

@ -1,61 +0,0 @@
const { names } from ";
function generateAttrs(options) {
const attrs = {
"size": 42,
"envelope": {
date: "2023-03-21T15:25:42.000Z",
subject: options.subject ?? "subject" + randomString(10),
from: options.from ?? null,
sender: options.sender ?? null,
replyTo: options.replyTo ?? null,
to: options.to ?? null,
cc: options.cc ?? null,
bcc: options.bcc ?? null,
inReplyTo: options.inReplyTo ?? null,
messageId: options.messageId ?? randomString(10),
},
"date": options.date ?? new Date(),
"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;
}
function generateUsers(nb) {
const users = [];
for (let i = 0; i < nb; i++) {
users.push({
user: {
name: "",
mailbox: names[i],
host: "provider.com",
},
id: i,
});
}
return users;
}
function randomString(length) {
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) {
return (Math.random() * Math.pow(10, length)).toFixed();
}
module.exports = {
generateAttrs,
generateUsers,
};

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

View File

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