Compare commits
134 Commits
bffcdafe7a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adb7a1161a | ||
|
|
3c069ed2f8 | ||
|
|
ae73326820 | ||
|
|
f40b6758de | ||
|
|
43e8cc7d3e | ||
|
|
edddf9afbf | ||
|
|
3bffd88108 | ||
|
|
0094783a4e | ||
|
|
f42d819e45 | ||
|
|
467b0eebe9 | ||
|
|
e8e8555c2f | ||
|
|
af4cc2f6a0 | ||
|
|
2cae8f12a7 | ||
|
|
7be2e84691 | ||
|
|
737a22e1f8 | ||
|
|
4ecd723cec | ||
|
|
53c79aebc4 | ||
|
|
b2b0949353 | ||
|
|
ffcfc57bbe | ||
|
|
f7c95b3a36 | ||
|
|
843659b495 | ||
|
|
1a7828b281 | ||
|
|
b821c89e20 | ||
|
|
c3374a612e | ||
|
|
686e6a4911 | ||
|
|
b137263bef | ||
|
|
2c7b4f1c78 | ||
|
|
3dab9c8db1 | ||
|
|
5aef5ab7b0 | ||
|
|
8f980748b5 | ||
|
|
22fb12e6d6 | ||
|
|
12e508c7cb | ||
|
|
29b4b7bfeb | ||
|
|
91a52a29ce | ||
|
|
6e8e3bf1f4 | ||
|
|
dd8f9210a9 | ||
|
|
10fc3fd628 | ||
|
|
dc20ccfec0 | ||
|
|
ca096dac89 | ||
|
|
cd996d851a | ||
|
|
0f063deff9 | ||
|
|
614f7d9802 | ||
|
|
318748c984 | ||
|
|
4d3fbe292e | ||
|
|
2594f6042b | ||
|
|
8c7cc1f316 | ||
|
|
85b63f0ea3 | ||
|
|
e44584df0a | ||
|
|
956bc35158 | ||
|
|
79e17ad24f | ||
|
|
4799e477be | ||
|
|
7ad22e55c1 | ||
|
|
5b62fce48a | ||
|
|
b48c834d36 | ||
|
|
8cb1271f9a | ||
|
|
0b950ba7a7 | ||
|
|
e43ab6cfe1 | ||
|
|
4e79ab12dc | ||
|
|
1ab74d67ca | ||
|
|
9842ebeb12 | ||
|
|
8cc738c9b2 | ||
|
|
49e8ec64e0 | ||
|
|
dcb7075dca | ||
|
|
4cc53752d7 | ||
|
|
a80873b617 | ||
|
|
4d7a919054 | ||
|
|
3357009d6a | ||
|
|
4b21168547 | ||
|
|
160bb0c605 | ||
|
|
b9cc543b64 | ||
|
|
5b6995d6a6 | ||
|
|
65db4d8b7e | ||
|
|
9d12e81e07 | ||
|
|
af76e8f2f9 | ||
|
|
46ed3a1f41 | ||
|
|
d641b01758 | ||
|
|
20fe48974f | ||
|
|
398d243eac | ||
|
|
649bccb01e | ||
|
|
7e0e27c2b6 | ||
|
|
7c98c1eb0c | ||
|
|
8c6a2bcfd7 | ||
|
|
b14a0ca586 | ||
|
|
6d9d67905c | ||
|
|
97768e3695 | ||
|
|
51003b494b | ||
|
|
8b4210914b | ||
|
|
41aabb868f | ||
|
|
16d0fafb1a | ||
|
|
86f321c0a1 | ||
|
|
65631f8e9a | ||
|
|
9fc31f8686 | ||
|
|
5a71e104cd | ||
|
|
9c16e06446 | ||
|
|
de94bd4bab | ||
|
|
fd253197cc | ||
|
|
6b4264fccc | ||
|
|
4e48c5d813 | ||
|
|
4bff5be6c1 | ||
|
|
e3d8d3cf9b | ||
|
|
7f535f2e95 | ||
|
|
948ec3c7b4 | ||
|
|
3042ed972b | ||
|
|
11ab6a6a21 | ||
|
|
90dd16ee0d | ||
|
|
aced3b8914 | ||
|
|
68e1dfe7d8 | ||
|
|
a82ff9b85b | ||
|
|
8306543ddd | ||
|
|
6507d466ad | ||
|
|
44125fc55d | ||
|
|
14dd6b36f8 | ||
|
|
91898e25a5 | ||
|
|
185f051a63 | ||
|
|
838550b6cc | ||
|
|
5447557f91 | ||
|
|
62dd43c3d5 | ||
|
|
0ea7f5865b | ||
|
|
4d4ef54bcb | ||
|
|
926dc60920 | ||
|
|
d6f06f3ca6 | ||
|
|
9b3ddd291e | ||
|
|
d7029854b4 | ||
|
|
095efb5440 | ||
|
|
14e64c1fc3 | ||
|
|
95f39cf53a | ||
|
|
f9fbab3a21 | ||
|
|
aa9a69e17f | ||
|
|
3286a2e52b | ||
|
|
9046ccf137 | ||
|
|
29bf4bbdbd | ||
|
|
df69a7dbd9 | ||
|
|
427ffba725 | ||
|
|
d3893c682e |
8
.gitignore
vendored
@@ -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,16 +21,15 @@ 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
|
||||||
!*/schemas/*
|
!*/schemas/*
|
||||||
|
todo
|
||||||
31
README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# TRIFORM
|
||||||
|
|
||||||
|
_WORK IN PROGRESS_
|
||||||
|
|
||||||
|
TRIFORM (Threaded Rooms Interface For Organised Relational Mails) is a mail client which sorts mails into conversations based on the members present in each mails.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [x] Multi account (tested only with gmail)
|
||||||
|
- [x] Live syncing of mails
|
||||||
|
- [x] Live syncing of flags
|
||||||
|
- [x] Mark as read/unread
|
||||||
|
- [x] Flag important
|
||||||
|
- [ ] Attachments
|
||||||
|
- [ ] Send new mails
|
||||||
|
- [ ] Respond to mails
|
||||||
|
- [x] Delete mails
|
||||||
|
- [ ] Delete rooms
|
||||||
|
- [ ] Live sync with the ui
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
- Room: General space where messages are grouped.
|
||||||
|
- Channel: Space use for newsletters or other conversations when there is a low need for reply.
|
||||||
|
- Group: Space contening several users that are part of the conversation.
|
||||||
|
- DM: Space defining a conversation with only one other member.
|
||||||
|
- Thread: Sub-Space generally created when changing the number of member in a space.
|
||||||
|
|
||||||
|
## Examples
|
||||||
39
back/abl/Account-abl.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
import { getAccounts, getRooms, registerAccount } from "../db/api-db";
|
||||||
|
import { getAddressId } from "../db/utils/mail";
|
||||||
|
import logger from "../system/Logger";
|
||||||
|
import statusCodes from "../utils/statusCodes";
|
||||||
|
|
||||||
|
export default class Account {
|
||||||
|
static async getAll(body, res: Response) {
|
||||||
|
getAccounts().then((data) => {
|
||||||
|
res.status(statusCodes.OK).json(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async register(body, res: Response) {
|
||||||
|
const { email, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls } = body;
|
||||||
|
getAddressId(email).then((addressId) => {
|
||||||
|
registerAccount(addressId, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls)
|
||||||
|
.then((mailboxId) => {0
|
||||||
|
res.status(statusCodes.OK).json({ id: mailboxId });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
res.status(statusCodes.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// todo change mailbox to account
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getRooms(body, res: Response) {
|
||||||
|
const { mailboxId, offset, limit } = body;
|
||||||
|
getRooms(mailboxId)
|
||||||
|
.then((rooms) => {
|
||||||
|
res.status(statusCodes.OK).json(rooms);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.err(err);
|
||||||
|
res.status(statusCodes.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
120
back/abl/Message-abl.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import statusCode from "../utils/statusCodes";
|
||||||
|
import { Response } from "express";
|
||||||
|
import logger from "../system/Logger";
|
||||||
|
import Message from "../mails/message/Message";
|
||||||
|
import Room from "../mails/room/Room";
|
||||||
|
|
||||||
|
export default class MessageAbl {
|
||||||
|
static async changeFlag(body, res: Response, isDelete: boolean) {
|
||||||
|
const { mailboxId, messageId, flag } = body;
|
||||||
|
const message = new Message().setMessageId(messageId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.useUid();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.useMailbox(mailboxId);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isDelete) {
|
||||||
|
await message.mailbox.removeFlag(message.uid.toString(), flag);
|
||||||
|
} else {
|
||||||
|
await message.mailbox.addFlag(message.uid.toString(), flag);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
}
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addFlag(body, res: Response) {
|
||||||
|
await MessageAbl.changeFlag(body, res, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeFlag(body, res: Response) {
|
||||||
|
await MessageAbl.changeFlag(body, res, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteRemoteUtil(message: Message, mailboxId: number, res, isFull: boolean): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await message.useUid();
|
||||||
|
} catch (error) {
|
||||||
|
logger.err(error);
|
||||||
|
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.useMailbox(mailboxId);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.mailbox.addFlag(message.uid.toString(), ["\\Deleted"]);
|
||||||
|
await message.mailbox.moveToTrash(message.uid.toString(), (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.err(err);
|
||||||
|
}
|
||||||
|
// throw err;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.log(err);
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFull) {
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static deleteRemoteOnly = async (body, res: Response) => {
|
||||||
|
const { mailboxId, messageId } = body;
|
||||||
|
const message = new Message().setMessageId(messageId);
|
||||||
|
await MessageAbl.deleteRemoteUtil(message, mailboxId, res, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
static deleteEverywhere = async (body, res: Response) => {
|
||||||
|
const { mailboxId, messageId } = body;
|
||||||
|
const message = new Message().setMessageId(messageId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.useFlags();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
logger.err(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if message not deleted remotly, delete it
|
||||||
|
if (!message.isDeleted) {
|
||||||
|
const success = await MessageAbl.deleteRemoteUtil(message, mailboxId, res, false);
|
||||||
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await new Room().setRoomIdOnMessageId(messageId);
|
||||||
|
try {
|
||||||
|
await message.delete();
|
||||||
|
if (room.roomId && await room.shouldDelete()) {
|
||||||
|
await room.delete();
|
||||||
|
res.status(statusCode.OK).json({ deleteRoom: true }).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
};
|
||||||
|
}
|
||||||
93
back/abl/Room-abl.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import statusCode from "../utils/statusCodes";
|
||||||
|
import { Response } from "express";
|
||||||
|
import { RoomType } from "../mails/message/saveMessage";
|
||||||
|
import { getRoomType } from "../db/message/saveMessage-db";
|
||||||
|
import { getLastMsgData, getRoomOwner } from "../db/Room-db";
|
||||||
|
import emailManager from "../mails/EmailManager";
|
||||||
|
import MailBuilder from "../mails/utils/mailBuilder";
|
||||||
|
import { getAddresses } from "../db/utils/mail";
|
||||||
|
import { getMembers, getMessages, getRooms } from "../db/api-db";
|
||||||
|
import logger from "../system/Logger";
|
||||||
|
import Room from "../mails/room/Room";
|
||||||
|
|
||||||
|
function rmUserFromAddrs(addresses: { email: string }[], user: string) {
|
||||||
|
let index = addresses.findIndex((a) => a.email == user);
|
||||||
|
if (index != -1) {
|
||||||
|
addresses.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default class RoomAbl {
|
||||||
|
// todo change name of reponse
|
||||||
|
static async response(body, res: Response) {
|
||||||
|
const { user, roomId, text, html } = body;
|
||||||
|
const roomType = (await getRoomType(roomId))[0].room_type;
|
||||||
|
|
||||||
|
if (roomType === RoomType.DM) {
|
||||||
|
const ownerEmail = (await getRoomOwner(roomId))[0].email;
|
||||||
|
|
||||||
|
const mailBuilder = new MailBuilder();
|
||||||
|
mailBuilder.from(user).to(ownerEmail).text(text).html(html);
|
||||||
|
|
||||||
|
emailManager.getSmtp(user)?.sendMail(mailBuilder.message);
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
} else if (roomType === RoomType.GROUP || roomType === RoomType.THREAD) {
|
||||||
|
const lastMsgData = (await getLastMsgData(roomId))[0];
|
||||||
|
const mailBuilder = new MailBuilder();
|
||||||
|
mailBuilder.inReplySubject(lastMsgData.subject).inReplyTo(lastMsgData.messageID).text(text).html(html);
|
||||||
|
|
||||||
|
const from = await getAddresses(lastMsgData.fromA);
|
||||||
|
let to = lastMsgData.toA ? await getAddresses(lastMsgData.toA) : [];
|
||||||
|
let cc = lastMsgData.ccA ? await getAddresses(lastMsgData.ccA) : [];
|
||||||
|
|
||||||
|
// remove us from recipients
|
||||||
|
rmUserFromAddrs(to, user);
|
||||||
|
rmUserFromAddrs(from, user);
|
||||||
|
|
||||||
|
// add sender of previous as recipient if it is not us
|
||||||
|
if (from.findIndex((a) => a.email == user) == -1) {
|
||||||
|
to = to.concat(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
mailBuilder
|
||||||
|
.from(user)
|
||||||
|
.to(to.map((a) => a.email))
|
||||||
|
.cc(cc.map((a) => a.email));
|
||||||
|
|
||||||
|
emailManager.getSmtp(user)?.sendMail(mailBuilder.message);
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
} else {
|
||||||
|
res.status(statusCode.FORBIDDEN).send({ error: "Cannot add a new message in a room or a channel." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getMessages(body, res: Response) {
|
||||||
|
const { roomId } = body;
|
||||||
|
getMessages(roomId)
|
||||||
|
.then((messages) => {
|
||||||
|
res.status(statusCode.OK).json(messages);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.err(err);
|
||||||
|
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getMembers(body, res: Response) {
|
||||||
|
const { roomId } = body;
|
||||||
|
getMembers(roomId)
|
||||||
|
.then((addresses) => {
|
||||||
|
res.status(statusCode.OK).json(addresses);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.err(err);
|
||||||
|
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(body, res: Response) {
|
||||||
|
const { roomId } = body;
|
||||||
|
console.log("delete", roomId);
|
||||||
|
const room = new Room().setRoomId(roomId);
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const statusCode = require("../utils/statusCodes").statusCodes;
|
|
||||||
const { registerAccount } = require("../db/api");
|
|
||||||
const { getAddresseId } = require("../db/mail");
|
|
||||||
|
|
||||||
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)
|
|
||||||
.then((mailboxId) => {
|
|
||||||
res.status(statusCode.OK).json({ id: mailboxId });
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
addAccount,
|
|
||||||
};
|
|
||||||
|
|
||||||
// todo change mailbox to account
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const statusCode = require("../utils/statusCodes").statusCodes;
|
|
||||||
const { getMessages } = require("../db/api.js");
|
|
||||||
|
|
||||||
async function messages(body, res) {
|
|
||||||
const { roomId } = body;
|
|
||||||
getMessages(roomId).then((messages) => {
|
|
||||||
res.status(statusCode.OK).json(messages);
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
messages,
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const statusCode = require("../utils/statusCodes").statusCodes;
|
|
||||||
const { getRooms } = require("../db/api.js");
|
|
||||||
|
|
||||||
async function rooms(body, res) {
|
|
||||||
const { mailboxId, offset, limit } = body;
|
|
||||||
getRooms(mailboxId).then((rooms) => {
|
|
||||||
res.status(statusCode.OK).json(rooms);
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
rooms,
|
|
||||||
};
|
|
||||||
85
back/db/Room-db.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { execQueryAsync } from "./db";
|
||||||
|
import { queryCcId, queryFromId, queryToId } from "./utils/addressQueries";
|
||||||
|
|
||||||
|
export async function getRoomOwner(roomId: number) {
|
||||||
|
const query = `
|
||||||
|
SELECT address.email
|
||||||
|
FROM app_room
|
||||||
|
INNER JOIN address ON address.address_id = app_room.owner_id
|
||||||
|
WHERE app_room.room_id = ?
|
||||||
|
`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get all the data needed to reply to a message in a room
|
||||||
|
*/
|
||||||
|
export async function getLastMsgData(roomId: number) {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
msg.message_id AS id,
|
||||||
|
GROUP_CONCAT(fromT.address_id) AS fromA,
|
||||||
|
GROUP_CONCAT(toT.address_id) AS toA,
|
||||||
|
GROUP_CONCAT(ccT.address_id) AS ccA,
|
||||||
|
subjectT.value AS subject,
|
||||||
|
content.text AS content,
|
||||||
|
message.idate AS date,
|
||||||
|
message.messageID AS messageID
|
||||||
|
FROM app_room_message msg
|
||||||
|
|
||||||
|
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||||
|
${queryToId} toT ON msg.message_id = toT.message_id
|
||||||
|
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT header_field.message_id, header_field.value
|
||||||
|
FROM header_field
|
||||||
|
INNER JOIN field_name
|
||||||
|
WHERE
|
||||||
|
field_name.field_id = header_field.field_id AND
|
||||||
|
field_name.field_name = 'subject'
|
||||||
|
) subjectT ON msg.message_id = subjectT.message_id
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT bodypart.text, header_field.message_id FROM bodypart
|
||||||
|
INNER JOIN header_field
|
||||||
|
INNER JOIN field_name
|
||||||
|
WHERE
|
||||||
|
field_name.field_id = header_field.field_id AND
|
||||||
|
field_name.field_name = 'html' AND
|
||||||
|
bodypart.bodypart_id = header_field.bodypart_id
|
||||||
|
) content ON msg.message_id = content.message_id
|
||||||
|
|
||||||
|
INNER JOIN message ON message.message_id = msg.message_id
|
||||||
|
|
||||||
|
WHERE msg.room_id = ?
|
||||||
|
GROUP BY msg.message_id
|
||||||
|
ORDER BY message.idate DESC
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRoomOnMessageId(messageId: number) {
|
||||||
|
const query = `SELECT room_id FROM app_room_message WHERE message_id = ?`;
|
||||||
|
const values = [messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRoomNbMessageAndThread(roomId: number): Promise<{ nbMessage: number; nbThread: number }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT COUNT(arm.room_id) AS nbMessage, COUNT(app_thread.room_id) AS nbThread
|
||||||
|
FROM app_room_message arm
|
||||||
|
INNER JOIN app_thread ON (app_thread.root_id = arm.room_id OR app_thread.parent_id = arm.room_id)
|
||||||
|
WHERE arm.room_id = ?`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRoom(roomId: number) {
|
||||||
|
const query = `DELETE FROM app_room WHERE room_id = ?`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
157
back/db/api-db.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { execQueryAsync, execQueryAsyncWithId } from "./db";
|
||||||
|
import { queryCcId, queryToId, queryFromId } from "./utils/addressQueries";
|
||||||
|
|
||||||
|
export async function registerAccount(userId, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls) {
|
||||||
|
const query = `
|
||||||
|
INSERT INTO app_account
|
||||||
|
(user_id, account_pwd, xoauth, xoauth2, imap_host, smtp_host, imap_port, smtp_port, tls) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
const values = [userId, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls];
|
||||||
|
return await execQueryAsyncWithId(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAccounts() {
|
||||||
|
// todo mailbox or account id ?
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
mailbox.mailbox_id AS id,
|
||||||
|
address.email
|
||||||
|
FROM app_account
|
||||||
|
INNER JOIN address
|
||||||
|
INNER JOIN mailbox
|
||||||
|
WHERE
|
||||||
|
address.address_id = app_account.user_id AND
|
||||||
|
mailbox.account_id = app_account.account_id
|
||||||
|
`;
|
||||||
|
const values = [];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRooms(mailboxId: number) {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
room.room_id AS id,
|
||||||
|
room.room_name AS roomName,
|
||||||
|
address.email AS user,
|
||||||
|
room.owner_id AS userId,
|
||||||
|
(COUNT(notSeenThreads.message_id) + COUNT(notSeenRoom.message_id)) AS notSeen,
|
||||||
|
room.room_type AS roomType,
|
||||||
|
mailbox_message.mailbox_id AS mailboxId,
|
||||||
|
app_thread.parent_id
|
||||||
|
|
||||||
|
FROM app_room room
|
||||||
|
INNER JOIN message ON message.message_id = room.message_id
|
||||||
|
INNER JOIN mailbox_message ON mailbox_message.message_id = message.message_id
|
||||||
|
INNER JOIN address ON address.address_id = room.owner_id
|
||||||
|
LEFT JOIN app_thread ON room.room_id = app_thread.room_id
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT app_room_message.room_id, app_room_message.message_id
|
||||||
|
FROM app_room_message
|
||||||
|
WHERE
|
||||||
|
"\\\\Seen" NOT IN (
|
||||||
|
SELECT flag_name FROM flag_name
|
||||||
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id AND flag.message_id = app_room_message.message_id
|
||||||
|
WHERE flag_name.flag_id = flag.flag_id
|
||||||
|
)
|
||||||
|
) notSeenRoom ON notSeenRoom.room_id = room.room_id
|
||||||
|
|
||||||
|
-- get not seen in thread
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT app_room_message.message_id, app_thread.parent_id
|
||||||
|
FROM app_room
|
||||||
|
INNER 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
|
||||||
|
WHERE
|
||||||
|
"\\\\Seen" NOT IN (
|
||||||
|
SELECT flag_name FROM flag_name
|
||||||
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id AND flag.message_id = app_room_message.message_id
|
||||||
|
WHERE flag_name.flag_id = flag.flag_id
|
||||||
|
)
|
||||||
|
) notSeenThreads ON notSeenThreads.parent_id = room.room_id
|
||||||
|
|
||||||
|
WHERE
|
||||||
|
mailbox_message.mailbox_id = ?
|
||||||
|
GROUP BY room.room_id
|
||||||
|
ORDER BY room.lastUpdate DESC
|
||||||
|
`;
|
||||||
|
// todo parent_id replace to root_id
|
||||||
|
const values = [mailboxId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessages(roomId: number) {
|
||||||
|
// todo attachements name
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
msg.message_id AS id,
|
||||||
|
GROUP_CONCAT(fromT.address_id) AS fromA,
|
||||||
|
GROUP_CONCAT(toT.address_id) AS toA,
|
||||||
|
GROUP_CONCAT(ccT.address_id) AS ccA,
|
||||||
|
subjectT.value AS subject,
|
||||||
|
content.text AS content,
|
||||||
|
message.idate AS date,
|
||||||
|
GROUP_CONCAT(flagT.flag_name) AS flags,
|
||||||
|
thread.room_id AS thread
|
||||||
|
FROM app_room_message msg
|
||||||
|
|
||||||
|
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||||
|
${queryToId} toT ON msg.message_id = toT.message_id
|
||||||
|
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT header_field.message_id, header_field.value
|
||||||
|
FROM header_field
|
||||||
|
INNER JOIN field_name
|
||||||
|
WHERE
|
||||||
|
field_name.field_id = header_field.field_id AND
|
||||||
|
field_name.field_name = 'subject'
|
||||||
|
) subjectT ON msg.message_id = subjectT.message_id
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT bodypart.text, header_field.message_id
|
||||||
|
FROM bodypart
|
||||||
|
INNER JOIN header_field
|
||||||
|
INNER JOIN field_name
|
||||||
|
WHERE
|
||||||
|
field_name.field_id = header_field.field_id AND
|
||||||
|
field_name.field_name = 'html' AND
|
||||||
|
bodypart.bodypart_id = header_field.bodypart_id
|
||||||
|
) content ON msg.message_id = content.message_id
|
||||||
|
|
||||||
|
LEFT JOIN flag ON flag.message_id = msg.message_id
|
||||||
|
LEFT JOIN flag_name flagT ON flagT.flag_id = flag.flag_id
|
||||||
|
|
||||||
|
INNER JOIN message ON message.message_id = msg.message_id
|
||||||
|
|
||||||
|
-- get room_id of thread room with this message as origin
|
||||||
|
LEFT JOIN app_room thread ON (
|
||||||
|
thread.message_id = msg.message_id AND
|
||||||
|
msg.room_id != thread.room_id AND
|
||||||
|
thread.room_type = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
WHERE msg.room_id = ?
|
||||||
|
GROUP BY msg.message_id
|
||||||
|
ORDER BY message.idate ASC;
|
||||||
|
`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMembers(roomId: number) {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
address.address_id AS id,
|
||||||
|
address.address_name AS name,
|
||||||
|
address.email AS email,
|
||||||
|
field_name.field_name as type
|
||||||
|
FROM app_room
|
||||||
|
INNER JOIN address_field ON address_field.message_id = app_room.message_id
|
||||||
|
INNER JOIN address ON address.address_id = address_field.address_id
|
||||||
|
INNER JOIN field_name ON field_name.field_id = address_field.field_id
|
||||||
|
WHERE app_room.room_id = ?;
|
||||||
|
`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
106
back/db/api.js
@@ -1,106 +0,0 @@
|
|||||||
const { db, execQueryAsync, execQueryAsyncWithId } = require("./db.js");
|
|
||||||
const { queryCcId, queryToId, queryFromId } = require("./utils/addressQueries.js");
|
|
||||||
const DEBUG = require("../utils/debug").DEBUG;
|
|
||||||
|
|
||||||
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 (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`;
|
|
||||||
const values = [userId, pwd, xoauth, xoauth2, host, port, tls];
|
|
||||||
return await execQueryAsyncWithId(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAccounts() {
|
|
||||||
// todo mailbox or account id ?
|
|
||||||
const query = `
|
|
||||||
SELECT
|
|
||||||
mailbox.mailbox_id AS id,
|
|
||||||
address.email
|
|
||||||
FROM app_account
|
|
||||||
INNER JOIN address
|
|
||||||
INNER JOIN mailbox
|
|
||||||
WHERE
|
|
||||||
address.address_id = app_account.user_id AND
|
|
||||||
mailbox.account_id = app_account.account_id
|
|
||||||
`;
|
|
||||||
const values = [];
|
|
||||||
return await execQueryAsync(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRooms(mailboxId) {
|
|
||||||
const query = `
|
|
||||||
SELECT
|
|
||||||
app_room.room_id AS id,
|
|
||||||
app_room.room_name AS roomName,
|
|
||||||
address.email AS user,
|
|
||||||
app_room.owner_id AS userId,
|
|
||||||
app_room.notSeen,
|
|
||||||
mailbox_message.mailbox_id AS mailboxId
|
|
||||||
FROM app_room
|
|
||||||
INNER JOIN message
|
|
||||||
INNER JOIN mailbox_message
|
|
||||||
INNER JOIN address
|
|
||||||
WHERE
|
|
||||||
message.message_id = app_room.message_id AND
|
|
||||||
mailbox_message.mailbox_id = ? AND
|
|
||||||
mailbox_message.message_id = message.message_id AND
|
|
||||||
address.address_id = app_room.owner_id
|
|
||||||
ORDER BY app_room.lastUpdate DESC
|
|
||||||
`;
|
|
||||||
const values = [mailboxId];
|
|
||||||
return await execQueryAsync(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMessages(roomId) {
|
|
||||||
// todo attachements name
|
|
||||||
const query = `
|
|
||||||
SELECT
|
|
||||||
msg.message_id AS id,
|
|
||||||
GROUP_CONCAT(fromT.address_id) AS fromA,
|
|
||||||
GROUP_CONCAT(toT.address_id) AS toA,
|
|
||||||
GROUP_CONCAT(ccT.address_id) AS ccA,
|
|
||||||
subjectT.value AS subject,
|
|
||||||
content.text AS content,
|
|
||||||
message.idate AS date
|
|
||||||
FROM app_room_message msg
|
|
||||||
|
|
||||||
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
|
||||||
${queryToId} toT ON msg.message_id = toT.message_id
|
|
||||||
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
|
||||||
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT header_field.message_id, header_field.value
|
|
||||||
FROM header_field
|
|
||||||
INNER JOIN field_name
|
|
||||||
WHERE
|
|
||||||
field_name.field_id = header_field.field_id AND
|
|
||||||
field_name.field_name = 'subject'
|
|
||||||
) subjectT ON msg.message_id = subjectT.message_id
|
|
||||||
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT bodypart.text, header_field.message_id FROM bodypart
|
|
||||||
INNER JOIN header_field
|
|
||||||
INNER JOIN field_name
|
|
||||||
WHERE
|
|
||||||
field_name.field_id = header_field.field_id AND
|
|
||||||
field_name.field_name = 'html' AND
|
|
||||||
bodypart.bodypart_id = header_field.bodypart_id
|
|
||||||
) content ON msg.message_id = content.message_id
|
|
||||||
|
|
||||||
INNER JOIN message ON message.message_id = msg.message_id
|
|
||||||
|
|
||||||
WHERE msg.room_id = ?
|
|
||||||
GROUP BY msg.message_id
|
|
||||||
ORDER BY message.idate;
|
|
||||||
`;
|
|
||||||
const values = [roomId];
|
|
||||||
return await execQueryAsync(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
registerAccount,
|
|
||||||
getAccounts,
|
|
||||||
getRooms,
|
|
||||||
getMessages
|
|
||||||
};
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
Table "addresses" {
|
|
||||||
"id" int [pk, not null, increment]
|
|
||||||
"name" text
|
|
||||||
"localpart" text [not null]
|
|
||||||
"domain" text [not null]
|
|
||||||
"email" text [not null]
|
|
||||||
|
|
||||||
Indexes {
|
|
||||||
email [unique]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Table "mailboxes" {
|
|
||||||
"id" int [pk, not null, increment]
|
|
||||||
"name" text [not null]
|
|
||||||
"uidnext" int [not null, default: 1]
|
|
||||||
"nextmodseq" bigint [not null, default: 1]
|
|
||||||
"first_recent" int [not null, default: 1]
|
|
||||||
"uidvalidity" int [not null, default: 1]
|
|
||||||
|
|
||||||
Indexes {
|
|
||||||
name [unique]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Table "messages" {
|
|
||||||
"id" int [pk, not null, increment]
|
|
||||||
"messageID" text [pk, not null]
|
|
||||||
"idate" timestamp [not null]
|
|
||||||
"rfc822size" int
|
|
||||||
}
|
|
||||||
|
|
||||||
Table "mailbox_messages" {
|
|
||||||
"mailbox" int [not null]
|
|
||||||
"uid" int [pk, not null]
|
|
||||||
"message" int [pk, not null]
|
|
||||||
"modseq" bigint [not null]
|
|
||||||
"seen" boolean [not null, default: false]
|
|
||||||
"deleted" boolean [not null, default: false]
|
|
||||||
}
|
|
||||||
|
|
||||||
Ref: mailbox_messages.mailbox > mailboxes.id
|
|
||||||
Ref: mailbox_messages.message - messages.id
|
|
||||||
|
|
||||||
Table "bodyparts" {
|
|
||||||
"id" int [pk, not null, increment]
|
|
||||||
"bytes" int [not null]
|
|
||||||
"hash" text [not null]
|
|
||||||
"text" text
|
|
||||||
"data" binary
|
|
||||||
}
|
|
||||||
|
|
||||||
Table "part_numbers" {
|
|
||||||
"message" int [pk, not null]
|
|
||||||
"part" text [not null]
|
|
||||||
"bodypart" int [not null]
|
|
||||||
"bytes" int
|
|
||||||
"nb_lines" int
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo on delete cascade
|
|
||||||
Ref: part_numbers.message > messages.id
|
|
||||||
Ref: part_numbers.bodypart - bodyparts.id
|
|
||||||
|
|
||||||
Table "field_names" {
|
|
||||||
"id" int [pk, not null, increment]
|
|
||||||
"name" text [not null]
|
|
||||||
|
|
||||||
Indexes {
|
|
||||||
name [unique]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Table "header_fields" {
|
|
||||||
"id" int [pk, not null, increment]
|
|
||||||
"message" int [pk, not null]
|
|
||||||
"part" text [not null]
|
|
||||||
"position" int [not null]
|
|
||||||
"field" int [not null]
|
|
||||||
"value" text
|
|
||||||
|
|
||||||
Indexes {
|
|
||||||
message [unique]
|
|
||||||
part [unique]
|
|
||||||
position [unique]
|
|
||||||
field [unique]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ref: header_fields.message > messages.id
|
|
||||||
Ref: header_fields.part > part_numbers.part
|
|
||||||
Ref: header_fields.field > field_names.id
|
|
||||||
|
|
||||||
Table "address_fields" {
|
|
||||||
"message" int [not null]
|
|
||||||
"part" text [not null]
|
|
||||||
"position" int [not null]
|
|
||||||
"field" int [not null]
|
|
||||||
"number" int
|
|
||||||
"address" int [not null]
|
|
||||||
}
|
|
||||||
|
|
||||||
Ref: address_fields.message > messages.id
|
|
||||||
Ref: address_fields.part > part_numbers.part
|
|
||||||
Ref: address_fields.field > field_names.id
|
|
||||||
Ref: address_fields.address > addresses.id
|
|
||||||
|
|
||||||
// app table
|
|
||||||
Table "front_threads" {
|
|
||||||
"id" int [pk, not null, increment]
|
|
||||||
"room" int [not null]
|
|
||||||
"name" text
|
|
||||||
"notSeen" int [not null, default: true]
|
|
||||||
"lastUpdate" timestamp [not null]
|
|
||||||
"isDm" bool [not null, default: true]
|
|
||||||
}
|
|
||||||
|
|
||||||
Ref: front_threads.room > front_rooms.id
|
|
||||||
|
|
||||||
Table "front_rooms" {
|
|
||||||
"id" int [pk, not null, increment]
|
|
||||||
"name" text
|
|
||||||
"isGroup" bool [not null, default: false]
|
|
||||||
"notSeen" int [not null]
|
|
||||||
"lastUpdate" timestamp [not null]
|
|
||||||
}
|
|
||||||
|
|
||||||
Table "front_room_messages" {
|
|
||||||
"room" int [not null]
|
|
||||||
"thread" int [not null]
|
|
||||||
"message" int [not null]
|
|
||||||
}
|
|
||||||
|
|
||||||
Ref: front_room_messages.room > front_rooms.id
|
|
||||||
Ref: front_room_messages.message - messages.id
|
|
||||||
Ref: front_room_messages.thread > front_threads.id
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,108 +1,181 @@
|
|||||||
CREATE TABLE `addresses` (
|
-- Mail storage
|
||||||
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
|
||||||
`name` text,
|
-- 1
|
||||||
`localpart` text NOT NULL,
|
CREATE TABLE address (
|
||||||
`domain` text NOT NULL,
|
address_id INT AUTO_INCREMENT,
|
||||||
`email` text NOT NULL
|
address_name TEXT,
|
||||||
|
localpart TEXT NOT NULL,
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (address_id),
|
||||||
|
UNIQUE KEY (email)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE `mailboxes` (
|
-- 2 app
|
||||||
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
CREATE TABLE app_account (
|
||||||
`name` text NOT NULL,
|
account_id INT AUTO_INCREMENT,
|
||||||
`uidnext` int NOT NULL DEFAULT 1,
|
user_id INT NOT NULL,
|
||||||
`nextmodseq` bigint NOT NULL DEFAULT 1,
|
account_pwd BINARY(22),
|
||||||
`first_recent` int NOT NULL DEFAULT 1,
|
xoauth VARCHAR(116),
|
||||||
`uidvalidity` int NOT NULL DEFAULT 1
|
xoauth2 VARCHAR(116),
|
||||||
|
imap_host VARCHAR(255) NOT NULL DEFAULT 'localhost',
|
||||||
|
imap_port INT(5) NOT NULL DEFAULT 993,
|
||||||
|
smtp_host VARCHAR(255) NOT NULL DEFAULT 'localhost',
|
||||||
|
smtp_port INT(5) NOT NULL DEFAULT 465,
|
||||||
|
tls BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
PRIMARY KEY (account_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES address(address_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE `messages` (
|
-- 3
|
||||||
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
CREATE TABLE mailbox (
|
||||||
`idate` timestamp NOT NULL,
|
mailbox_id INT AUTO_INCREMENT,
|
||||||
`rfc822size` int
|
account_id INT NOT NULL,
|
||||||
|
mailbox_name TEXT NOT NULL,
|
||||||
|
uidnext INT NOT NULL DEFAULT 1,
|
||||||
|
nextmodseq BIGINT NOT NULL DEFAULT 1,
|
||||||
|
first_recent INT NOT NULL DEFAULT 1,
|
||||||
|
uidvalidity INT NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (mailbox_id),
|
||||||
|
FOREIGN KEY (account_id) REFERENCES app_account(account_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE `mailbox_messages` (
|
-- 4
|
||||||
`mailbox` int NOT NULL,
|
CREATE TABLE message (
|
||||||
`uid` int NOT NULL,
|
message_id INT AUTO_INCREMENT,
|
||||||
`message` int NOT NULL,
|
messageID TEXT NOT NULL,
|
||||||
`modseq` bigint NOT NULL,
|
idate TIMESTAMP NOT NULL,
|
||||||
`seen` boolean NOT NULL DEFAULT false,
|
rfc822size INT NOT NULL,
|
||||||
`deleted` boolean NOT NULL DEFAULT false,
|
PRIMARY KEY (message_id),
|
||||||
PRIMARY KEY (`uid`, `message`)
|
UNIQUE KEY (messageID)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE `bodyparts` (
|
-- 5
|
||||||
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
-- if mailbox_message deleted message is not deleted
|
||||||
`bytes` int NOT NULL,
|
CREATE TABLE mailbox_message (
|
||||||
`hash` text NOT NULL,
|
mailbox_id INT NOT NULL,
|
||||||
`text` text,
|
uid INT,
|
||||||
`data` binary
|
message_id INT,
|
||||||
|
modseq BIGINT NOT NULL,
|
||||||
|
seen BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
PRIMARY KEY (uid, message_id),
|
||||||
|
FOREIGN KEY (mailbox_id) REFERENCES mailbox(mailbox_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE `part_numbers` (
|
-- 6
|
||||||
`message` int PRIMARY KEY NOT NULL,
|
CREATE TABLE bodypart (
|
||||||
`part` text NOT NULL,
|
bodypart_id INT AUTO_INCREMENT,
|
||||||
`bodypart` int NOT NULL,
|
bytes INT NOT NULL,
|
||||||
`bytes` int,
|
hash TEXT NOT NULL,
|
||||||
`nb_lines` int
|
text TEXT,
|
||||||
|
data BINARY,
|
||||||
|
PRIMARY KEY (bodypart_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE `field_names` (
|
-- 7
|
||||||
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
CREATE TABLE source (
|
||||||
`name` text NOT NULL
|
message_id INT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (message_id),
|
||||||
|
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE `header_fields` (
|
-- 8
|
||||||
`id` int NOT NULL AUTO_INCREMENT,
|
CREATE TABLE field_name (
|
||||||
`message` int NOT NULL,
|
field_id INT AUTO_INCREMENT,
|
||||||
`part` text NOT NULL,
|
field_name VARCHAR (255),
|
||||||
`position` int NOT NULL,
|
PRIMARY KEY (field_id),
|
||||||
`field` int NOT NULL,
|
UNIQUE KEY (field_name)
|
||||||
`value` text,
|
|
||||||
PRIMARY KEY (`id`, `message`)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE `address_fields` (
|
-- 9
|
||||||
`message` int NOT NULL,
|
CREATE TABLE header_field (
|
||||||
`part` text NOT NULL,
|
message_id INT NOT NULL,
|
||||||
`position` int NOT NULL,
|
field_id INT NOT NULL,
|
||||||
`field` int NOT NULL,
|
bodypart_id INT,
|
||||||
`number` int,
|
part VARCHAR(128),
|
||||||
`address` int NOT NULL
|
value TEXT,
|
||||||
|
UNIQUE KEY (message_id, field_id, bodypart_id),
|
||||||
|
UNIQUE KEY (message_id, field_id, part),
|
||||||
|
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (field_id) REFERENCES field_name(field_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (bodypart_id) REFERENCES bodypart(bodypart_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX `addresses_index_0` ON `addresses` (`email`);
|
-- 10
|
||||||
|
CREATE TABLE address_field (
|
||||||
|
message_id INT NOT NULL,
|
||||||
|
field_id INT NOT NULL,
|
||||||
|
address_id INT NOT NULL,
|
||||||
|
number INT,
|
||||||
|
UNIQUE KEY (message_id, field_id, address_id),
|
||||||
|
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (field_id) REFERENCES field_name(field_id),
|
||||||
|
FOREIGN KEY (address_id) REFERENCES address(address_id)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX `mailboxes_index_1` ON `mailboxes` (`name`);
|
-- App table
|
||||||
|
|
||||||
CREATE UNIQUE INDEX `field_names_index_2` ON `field_names` (`name`);
|
-- 11
|
||||||
|
CREATE TABLE app_room (
|
||||||
|
room_id INT AUTO_INCREMENT,
|
||||||
|
room_name VARCHAR(255) NOT NULL,
|
||||||
|
owner_id INT NOT NULL,
|
||||||
|
message_id INT,
|
||||||
|
room_type INT NOT NULL DEFAULT 0,
|
||||||
|
lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
|
||||||
|
PRIMARY KEY (room_id),
|
||||||
|
UNIQUE KEY (owner_id, message_id, room_type),
|
||||||
|
FOREIGN KEY (owner_id) REFERENCES address(address_id),
|
||||||
|
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX `header_fields_index_3` ON `header_fields` (`message`);
|
-- 12
|
||||||
|
CREATE TABLE app_thread (
|
||||||
|
room_id INT NOT NULL,
|
||||||
|
parent_id INT,
|
||||||
|
root_id INT,
|
||||||
|
PRIMARY KEY (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_id) REFERENCES app_room(room_id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (root_id) REFERENCES app_room(room_id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX `header_fields_index_4` ON `header_fields` (`part`);
|
-- 13
|
||||||
|
CREATE TABLE app_room_message (
|
||||||
|
message_id INT NOT NULL,
|
||||||
|
room_id INT,
|
||||||
|
UNIQUE KEY (message_id, room_id),
|
||||||
|
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX `header_fields_index_5` ON `header_fields` (`position`);
|
-- 14
|
||||||
|
-- todo needed ?
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX `header_fields_index_6` ON `header_fields` (`field`);
|
-- 15
|
||||||
|
create table flag_name (
|
||||||
|
flag_id INT AUTO_INCREMENT,
|
||||||
|
flag_name VARCHAR(255) NOT NULL,
|
||||||
|
PRIMARY KEY (flag_id),
|
||||||
|
UNIQUE KEY (flag_name)
|
||||||
|
);
|
||||||
|
|
||||||
ALTER TABLE `mailbox_messages` ADD FOREIGN KEY (`mailbox`) REFERENCES `mailboxes` (`id`);
|
-- 16
|
||||||
|
create table flag (
|
||||||
ALTER TABLE `messages` ADD FOREIGN KEY (`id`) REFERENCES `mailbox_messages` (`message`);
|
message_id INT NOT NULL,
|
||||||
|
flag_id INT NOT NULL,
|
||||||
ALTER TABLE `part_numbers` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
|
UNIQUE KEY (message_id, flag_id),
|
||||||
|
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
||||||
ALTER TABLE `bodyparts` ADD FOREIGN KEY (`id`) REFERENCES `part_numbers` (`bodypart`);
|
FOREIGN KEY (flag_id) REFERENCES flag_name(flag_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
ALTER TABLE `header_fields` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
|
|
||||||
|
|
||||||
ALTER TABLE `header_fields` ADD FOREIGN KEY (`part`) REFERENCES `part_numbers` (`part`);
|
|
||||||
|
|
||||||
ALTER TABLE `header_fields` ADD FOREIGN KEY (`field`) REFERENCES `field_names` (`id`);
|
|
||||||
|
|
||||||
ALTER TABLE `address_fields` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
|
|
||||||
|
|
||||||
ALTER TABLE `address_fields` ADD FOREIGN KEY (`part`) REFERENCES `part_numbers` (`part`);
|
|
||||||
|
|
||||||
ALTER TABLE `address_fields` ADD FOREIGN KEY (`field`) REFERENCES `field_names` (`id`);
|
|
||||||
|
|
||||||
ALTER TABLE `address_fields` ADD FOREIGN KEY (`address`) REFERENCES `addresses` (`id`);
|
|
||||||
@@ -1,24 +1,23 @@
|
|||||||
const mysql = require("mysql");
|
import mysql from "mysql";
|
||||||
const MYSQL = require("./config.json").mysql;
|
import logger from "../system/Logger";
|
||||||
const DEBUG = require("../utils/debug.js").DEBUG;
|
require("dotenv").config();
|
||||||
|
|
||||||
|
|
||||||
const db = mysql.createConnection({
|
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.NODE_ENV === "test") ? process.env.NAME_DB_TEST : process.env.NAME_DB,
|
||||||
});
|
});
|
||||||
|
|
||||||
db.connect(function (err) {
|
db.connect(function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
DEBUG.log("Impossible de se connecter", err.code);
|
logger.err(`Unable to connect database ${err.code}`);
|
||||||
} else {
|
} else {
|
||||||
DEBUG.log("Database successfully connected");
|
logger.log("Database successfully connected");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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 +29,7 @@ function execQueryAsync(query, values) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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,19 +41,12 @@ function execQueryAsyncWithId(query, values) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
DEBUG.log(err);
|
logger.err(err);
|
||||||
throw (err);
|
throw err;
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
db, // todo remove this
|
|
||||||
execQuery,
|
|
||||||
execQueryAsync,
|
|
||||||
execQueryAsyncWithId
|
|
||||||
};
|
|
||||||
55
back/db/imap/imap-db.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { execQueryAsyncWithId, execQueryAsync, execQuery } from "../db";
|
||||||
|
|
||||||
|
export async function getAllAccounts() {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
app_account.account_id AS id,
|
||||||
|
address.email AS user,
|
||||||
|
app_account.account_pwd AS password,
|
||||||
|
app_account.imap_host,
|
||||||
|
app_account.imap_port,
|
||||||
|
app_account.smtp_host,
|
||||||
|
app_account.smtp_port,
|
||||||
|
app_account.tls
|
||||||
|
FROM app_account INNER JOIN address
|
||||||
|
WHERE address.address_id = app_account.user_id
|
||||||
|
`;
|
||||||
|
const values = [];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllMailboxes(accountId: number) {
|
||||||
|
const query = "SELECT * FROM mailbox WHERE mailbox.account_id = ?";
|
||||||
|
const values = [accountId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerMailbox(accountId: number, mailboxName: string) {
|
||||||
|
const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`;
|
||||||
|
const values = [accountId, mailboxName];
|
||||||
|
return await execQueryAsyncWithId(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMailbox(mailboxId: number) {
|
||||||
|
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
|
||||||
|
const values = [mailboxId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateMailbox(mailboxId: number, uidnext: number) {
|
||||||
|
const query = `UPDATE mailbox SET uidnext = ? WHERE mailbox_id = ?`;
|
||||||
|
const values = [uidnext, mailboxId];
|
||||||
|
execQuery(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateMailboxModseq(mailboxId: number, modseq: number) {
|
||||||
|
const query = `UPDATE mailbox SET nextmodseq = ? WHERE mailbox_id = ?`;
|
||||||
|
const values = [modseq, mailboxId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMailboxModseq(mailboxId: number): Promise<{ modseq: number }[]> {
|
||||||
|
const query = `SELECT nextmodseq AS modseq FROM mailbox WHERE mailbox_id = ?`;
|
||||||
|
const values = [mailboxId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const { execQueryAsyncWithId, execQueryAsync, execQuery } = require("../db");
|
|
||||||
|
|
||||||
async function getAllAccounts() {
|
|
||||||
const query = `
|
|
||||||
SELECT
|
|
||||||
app_account.account_id AS id,
|
|
||||||
address.email AS user,
|
|
||||||
app_account.account_pwd AS password,
|
|
||||||
app_account.host AS host,
|
|
||||||
app_account.port AS port,
|
|
||||||
app_account.tls AS tls
|
|
||||||
FROM app_account INNER JOIN address
|
|
||||||
WHERE address.address_id = app_account.user_id
|
|
||||||
`;
|
|
||||||
const values = [];
|
|
||||||
return await execQueryAsync(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`;
|
|
||||||
const values = [accountId, mailboxName];
|
|
||||||
return await execQueryAsyncWithId(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMailbox(mailboxId) {
|
|
||||||
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
|
|
||||||
const values = [mailboxId];
|
|
||||||
return await execQueryAsync(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
const { execQueryAsync, execQueryAsyncWithId } = require("./db.js");
|
|
||||||
const DEBUG = require("../utils/debug").DEBUG;
|
|
||||||
|
|
||||||
|
|
||||||
async function getAddresseId(email, name) {
|
|
||||||
const localpart = email.split("@")[0];
|
|
||||||
const domain = email.split("@")[1];
|
|
||||||
const query = `INSERT INTO address
|
|
||||||
(address_name, localpart, domain, email) VALUES (?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE address_name = ?, address_id = LAST_INSERT_ID(address_id)`;
|
|
||||||
const values = [name, localpart, domain, email, name];
|
|
||||||
return await execQueryAsyncWithId(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getFieldId(field) {
|
|
||||||
const query = `INSERT INTO field_name (field_name) VALUES (?) ON DUPLICATE KEY UPDATE field_id=LAST_INSERT_ID(field_id)`;
|
|
||||||
const values = [field]
|
|
||||||
return await execQueryAsyncWithId(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findRoomByOwner(ownerId) {
|
|
||||||
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
|
|
||||||
const values = [ownerId];
|
|
||||||
return await execQueryAsync(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getAddresseId,
|
|
||||||
getFieldId,
|
|
||||||
findRoomByOwner,
|
|
||||||
};
|
|
||||||
23
back/db/message/message-db.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { execQueryAsync } from "../db";
|
||||||
|
|
||||||
|
export async function getFlagsOnUid(uid: number): Promise<{ flag_id: number; flag_name: string }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT flag_name FROM flag_name
|
||||||
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id
|
||||||
|
INNER JOIN mailbox_message ON mailbox_message.message_id = flag.message_id
|
||||||
|
WHERE mailbox_message.uid = ?
|
||||||
|
`;
|
||||||
|
const values = [uid];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFlagsOnId(messageId: number): Promise<{ flag_id: number; flag_name: string }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT flag_name FROM flag_name
|
||||||
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id
|
||||||
|
INNER JOIN mailbox_message ON mailbox_message.message_id = flag.message_id
|
||||||
|
WHERE mailbox_message.message_id = ?
|
||||||
|
`;
|
||||||
|
const values = [messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
144
back/db/message/saveMessage-db.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { RoomType } from "../../mails/message/saveMessage";
|
||||||
|
import { hasSameElements } from "../../utils/array";
|
||||||
|
import { transformEmojis } from "../../utils/string";
|
||||||
|
import { execQueryAsync, execQueryAsyncWithId, execQuery } from "../db";
|
||||||
|
import { queryFromId, queryToId, queryCcId } from "../utils/addressQueries";
|
||||||
|
|
||||||
|
export async function getAllMembers(messageId: number) {
|
||||||
|
const query = `
|
||||||
|
SELECT GROUP_CONCAT(address.address_id) AS id
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: RoomType,
|
||||||
|
): Promise<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerMessageInRoom(messageId: number, roomId: number, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateLastUpdateRoom(roomId: number, idate: string) {
|
||||||
|
const query = `UPDATE app_room SET lastUpdate = ? WHERE room_id = ?`;
|
||||||
|
const values = [idate, roomId];
|
||||||
|
execQuery(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getThreadInfo(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getThreadInfoOnId(threadId: number): 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_room.room_id = app_thread.room_id
|
||||||
|
WHERE app_room.room_id = ?
|
||||||
|
`;
|
||||||
|
const values = [threadId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRoomType(roomId: number): Promise<{ room_type: number }[]> {
|
||||||
|
const query = `SELECT room_type FROM app_room WHERE room_id = ?`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findRoomsFromMessage(messageID: string): Promise<{ room_id: number }[]> {
|
||||||
|
// todo find message in room not started
|
||||||
|
const query = `
|
||||||
|
SELECT room_id FROM app_room_message
|
||||||
|
INNER JOIN message ON message.message_id = app_room_message.message_id
|
||||||
|
WHERE message.messageID = ? ORDER BY room_id
|
||||||
|
`;
|
||||||
|
const values = [messageID];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasSameMembersAsParent(messageId: number, messageID: string): Promise<boolean> {
|
||||||
|
const query1 = `
|
||||||
|
SELECT
|
||||||
|
GROUP_CONCAT(fromT.address_id) AS fromA,
|
||||||
|
GROUP_CONCAT(toT.address_id) AS toA,
|
||||||
|
GROUP_CONCAT(ccT.address_id) AS ccA
|
||||||
|
FROM message msg
|
||||||
|
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||||
|
${queryToId} toT ON msg.message_id = toT.message_id
|
||||||
|
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
||||||
|
WHERE msg.message_id = ?
|
||||||
|
`;
|
||||||
|
const values1 = [messageId];
|
||||||
|
let addressesMsg1 = await execQueryAsync(query1, values1);
|
||||||
|
|
||||||
|
const query2 = `
|
||||||
|
SELECT
|
||||||
|
GROUP_CONCAT(fromT.address_id) AS fromA,
|
||||||
|
GROUP_CONCAT(toT.address_id) AS toA,
|
||||||
|
GROUP_CONCAT(ccT.address_id) AS ccA
|
||||||
|
FROM message msg
|
||||||
|
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||||
|
${queryToId} toT ON msg.message_id = toT.message_id
|
||||||
|
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
||||||
|
WHERE msg.messageID = ?
|
||||||
|
`;
|
||||||
|
const values2 = [messageID];
|
||||||
|
let addressesMsg2 = await execQueryAsync(query2, values2);
|
||||||
|
|
||||||
|
addressesMsg1 = addressesMsg1[0]?.fromA
|
||||||
|
?.split(",")
|
||||||
|
.concat(addressesMsg1[0]?.toA?.split(","))
|
||||||
|
.concat(addressesMsg1[0]?.ccA?.split(","));
|
||||||
|
addressesMsg2 = addressesMsg2[0]?.fromA
|
||||||
|
?.split(",")
|
||||||
|
.concat(addressesMsg2[0]?.toA?.split(","))
|
||||||
|
.concat(addressesMsg2[0]?.ccA?.split(","));
|
||||||
|
|
||||||
|
return hasSameElements(addressesMsg1, addressesMsg2);
|
||||||
|
}
|
||||||
@@ -1,18 +1,24 @@
|
|||||||
const { transformEmojis } = require("../utils/string.js");
|
import { transformEmojis } from "../../utils/string";
|
||||||
const { db, execQuery, execQueryAsync, execQueryAsyncWithId } = require("./db.js");
|
import { execQuery, execQueryAsync, execQueryAsyncWithId } from "../db";
|
||||||
const DEBUG = require("../utils/debug").DEBUG;
|
|
||||||
|
|
||||||
async function registerMessage(timestamp, rfc822size, messageId) {
|
export async function registerMessage(timestamp: string, rfc822size: number, messageID: string) {
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO message
|
INSERT INTO message
|
||||||
(idate, messageID, rfc822size) VALUES (?, ?, ?)
|
(idate, messageID, rfc822size) VALUES (?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE message_id = LAST_INSERT_ID(message_id)
|
ON DUPLICATE KEY UPDATE message_id = LAST_INSERT_ID(message_id)
|
||||||
`;
|
`;
|
||||||
const values = [timestamp, messageId, rfc822size];
|
const values = [timestamp, messageID, rfc822size];
|
||||||
return await execQueryAsyncWithId(query, values);
|
return await execQueryAsyncWithId(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerMailbox_message(mailboxId, uid, messageId, modseq, seen, deleted) {
|
export function registerMailbox_message(
|
||||||
|
mailboxId: number,
|
||||||
|
uid: number,
|
||||||
|
messageId: number,
|
||||||
|
modseq: number,
|
||||||
|
seen: boolean,
|
||||||
|
deleted: boolean,
|
||||||
|
) {
|
||||||
const query = `
|
const query = `
|
||||||
INSERT IGNORE INTO mailbox_message
|
INSERT IGNORE INTO mailbox_message
|
||||||
(mailbox_id, uid, message_id, modseq, seen, deleted) VALUES (?, ?, ?, ?, ?, ?)
|
(mailbox_id, uid, message_id, modseq, seen, deleted) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
@@ -21,7 +27,13 @@ function registerMailbox_message(mailboxId, uid, messageId, modseq, seen, delete
|
|||||||
execQuery(query, values);
|
execQuery(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerBodypart(messageId, part, bodypartId, bytes, nbLines) {
|
export async function registerFlag(messageId: number, flagId: number) {
|
||||||
|
const query = `INSERT IGNORE INTO flag (message_id, flag_id) VALUES (?, ?)`;
|
||||||
|
const values = [messageId, flagId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerBodypart(messageId: number, part: string, bodypartId: number, bytes: number, nbLines: null) {
|
||||||
const query = `
|
const query = `
|
||||||
INSERT IGNORE INTO part_number
|
INSERT IGNORE INTO part_number
|
||||||
(message_id, part, bodypart_id, bytes, nb_lines) VALUES (?, ?, ?, ?, ?)
|
(message_id, part, bodypart_id, bytes, nb_lines) VALUES (?, ?, ?, ?, ?)
|
||||||
@@ -30,14 +42,20 @@ function registerBodypart(messageId, part, bodypartId, bytes, nbLines) {
|
|||||||
execQuery(query, values);
|
execQuery(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveBodypart(bytes, hash, text, data) {
|
export async function saveBodypart(bytes, hash, text, data) {
|
||||||
text = transformEmojis(text);
|
text = transformEmojis(text);
|
||||||
const query = `INSERT IGNORE INTO bodypart (bytes, hash, text, data) VALUES (?, ?, ?, ?)`;
|
const query = `INSERT IGNORE INTO bodypart (bytes, hash, text, data) VALUES (?, ?, ?, ?)`;
|
||||||
const values = [bytes, hash, text, data];
|
const values = [bytes, hash, text, data];
|
||||||
return await execQueryAsyncWithId(query, values);
|
return await execQueryAsyncWithId(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveHeader_fields(messageId, fieldId, bodypartId, part, value) {
|
export async function saveHeader_fields(
|
||||||
|
messageId: number,
|
||||||
|
fieldId: number,
|
||||||
|
bodypartId: number,
|
||||||
|
part: string,
|
||||||
|
value: string,
|
||||||
|
) {
|
||||||
value = transformEmojis(value);
|
value = transformEmojis(value);
|
||||||
const query = `
|
const query = `
|
||||||
INSERT IGNORE INTO header_field
|
INSERT IGNORE INTO header_field
|
||||||
@@ -47,7 +65,7 @@ async function saveHeader_fields(messageId, fieldId, bodypartId, part, value) {
|
|||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAddress_fields(messageId, fieldId, addressId, number) {
|
export async function saveAddress_fields(messageId: number, fieldId: number, addressId: number, number: number) {
|
||||||
const query = `
|
const query = `
|
||||||
INSERT IGNORE INTO address_field
|
INSERT IGNORE INTO address_field
|
||||||
(message_id , field_id, address_id, number) VALUES (?, ?, ?, ?)
|
(message_id , field_id, address_id, number) VALUES (?, ?, ?, ?)
|
||||||
@@ -56,7 +74,7 @@ async function saveAddress_fields(messageId, fieldId, addressId, number) {
|
|||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSource(messageId, content) {
|
export function saveSource(messageId: number, content: string) {
|
||||||
content = transformEmojis(content);
|
content = transformEmojis(content);
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO source (message_id, content) VALUES (?, ?)
|
INSERT INTO source (message_id, content) VALUES (?, ?)
|
||||||
@@ -65,13 +83,3 @@ function saveSource(messageId, content) {
|
|||||||
const values = [messageId, content, content];
|
const values = [messageId, content, content];
|
||||||
execQuery(query, values);
|
execQuery(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
registerMessage,
|
|
||||||
registerMailbox_message,
|
|
||||||
saveHeader_fields,
|
|
||||||
saveAddress_fields,
|
|
||||||
registerBodypart,
|
|
||||||
saveBodypart,
|
|
||||||
saveSource
|
|
||||||
}
|
|
||||||
36
back/db/message/updateMessage-db.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { execQuery, execQueryAsync, execQueryAsyncWithId } from "../db";
|
||||||
|
|
||||||
|
export async function getFlags(uid: number): Promise<{ flag_id: number; flag_name: string }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT * FROM flag_name
|
||||||
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id
|
||||||
|
INNER JOIN mailbox_message ON mailbox_message.message_id = flag.message_id
|
||||||
|
WHERE mailbox_message.uid = ?
|
||||||
|
`;
|
||||||
|
const values = [uid];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFlag(messageId: number, flagId: number) {
|
||||||
|
const query = `DELETE FROM flag WHERE message_id = ? AND flag_id = ?`;
|
||||||
|
const values = [messageId, flagId];
|
||||||
|
execQuery(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateMailboxSeen(messageId: number, isSeen: boolean) {
|
||||||
|
const query = `UPDATE mailbox_message SET seen = ? WHERE message_id = ?`;
|
||||||
|
const values = [isSeen, messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateMailboxDeleted(messageId: number, isDeleted: boolean) {
|
||||||
|
const query = `UPDATE mailbox_message SET deleted = ? WHERE message_id = ?`;
|
||||||
|
const values = [isDeleted, messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteMessage(messageId: number) {
|
||||||
|
const query = `DELETE FROM message WHERE message_id = ?`;
|
||||||
|
const values = [messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
const { transformEmojis } = require("../utils/string.js");
|
|
||||||
const { db, execQueryAsync, execQueryAsyncWithId, execQuery } = require("./db.js");
|
|
||||||
const { queryFromId, queryToId, queryCcId } = require("./utils/addressQueries.js");
|
|
||||||
const DEBUG = require("../utils/debug").DEBUG;
|
|
||||||
|
|
||||||
async function createRoom(roomName, ownerId, messageId) {
|
|
||||||
roomName = transformEmojis(roomName);
|
|
||||||
const query = `INSERT INTO app_room (room_name, owner_id, message_id) VALUES (?, ?, ?)`;
|
|
||||||
const values = [roomName.substring(0, 255), ownerId, messageId];
|
|
||||||
return await execQueryAsyncWithId(query, values);
|
|
||||||
// todo add members
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerMessageInRoom(messageId, roomId, isSeen, idate) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLastUpdateRoom(roomId, idate) {
|
|
||||||
const query = `UPDATE app_room SET lastUpdate = ? WHERE room_id = ?`;
|
|
||||||
const values = [idate, roomId];
|
|
||||||
execQuery(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementNotSeenRoom(roomId) {
|
|
||||||
// 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];
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLastUpdateThread(threadId) {
|
|
||||||
// todo
|
|
||||||
// check for parent
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementNotSeenThread(threadId) {
|
|
||||||
// todo
|
|
||||||
// also increment parent room
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isRoomGroup(roomId) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const query = `SELECT isGroup FROM app_room WHERE room_id = '${roomId}'`;
|
|
||||||
db.query(query, (err, results, fields) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
resolve(results[0].isGroup);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findRoomsFromMessage(messageId) {
|
|
||||||
const query = `SELECT room_id FROM app_room_message WHERE message_id = ? ORDER BY room_id`;
|
|
||||||
const values = [messageId];
|
|
||||||
return await execQueryAsync(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hasSameMembersAsParent(messageId, messageID) {
|
|
||||||
const query1 = `
|
|
||||||
SELECT
|
|
||||||
GROUP_CONCAT(fromT.address_id) AS fromA,
|
|
||||||
GROUP_CONCAT(toT.address_id) AS toA,
|
|
||||||
GROUP_CONCAT(ccT.address_id) AS ccA
|
|
||||||
FROM message msg
|
|
||||||
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
|
||||||
${queryToId} toT ON msg.message_id = toT.message_id
|
|
||||||
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
|
||||||
WHERE msg.message_id = ?
|
|
||||||
`;
|
|
||||||
const values1 = [messageId];
|
|
||||||
let addressesMsg1 = await execQueryAsync(query1, values1);
|
|
||||||
|
|
||||||
const query2 = `
|
|
||||||
SELECT
|
|
||||||
GROUP_CONCAT(fromT.address_id) AS fromA,
|
|
||||||
GROUP_CONCAT(toT.address_id) AS toA,
|
|
||||||
GROUP_CONCAT(ccT.address_id) AS ccA
|
|
||||||
FROM message msg
|
|
||||||
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
|
||||||
${queryToId} toT ON msg.message_id = toT.message_id
|
|
||||||
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
|
||||||
WHERE msg.messageID = ?
|
|
||||||
`;
|
|
||||||
const values2 = [messageID];
|
|
||||||
let addressesMsg2 = await execQueryAsync(query2, values2);
|
|
||||||
|
|
||||||
addressesMsg1 = addressesMsg1[0]?.fromA
|
|
||||||
?.split(",")
|
|
||||||
.concat(addressesMsg1[0]?.toA?.split(","))
|
|
||||||
.concat(addressesMsg1[0]?.ccA?.split(","));
|
|
||||||
addressesMsg2 = addressesMsg2[0]?.fromA
|
|
||||||
?.split(",")
|
|
||||||
.concat(addressesMsg2[0]?.toA?.split(","))
|
|
||||||
.concat(addressesMsg2[0]?.ccA?.split(","));
|
|
||||||
|
|
||||||
return (
|
|
||||||
addressesMsg1.length == addressesMsg2.length &&
|
|
||||||
addressesMsg1.reduce((a, b) => a && addressesMsg2.includes(b), true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createRoom,
|
|
||||||
registerMessageInRoom,
|
|
||||||
createThread,
|
|
||||||
registerMessageInThread,
|
|
||||||
isRoomGroup,
|
|
||||||
findRoomsFromMessage,
|
|
||||||
hasSameMembersAsParent,
|
|
||||||
};
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
create table addresses (
|
|
||||||
id int not null auto_increment,
|
|
||||||
name text,
|
|
||||||
localpart text not null,
|
|
||||||
domain text not null,
|
|
||||||
email text not null,
|
|
||||||
primary key (id),
|
|
||||||
unique key (email)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table mailboxes (
|
|
||||||
-- Grant: select, insert, update
|
|
||||||
id serial primary key,
|
|
||||||
name text not null unique,
|
|
||||||
-- owner int references users(id), todo
|
|
||||||
|
|
||||||
-- The UID that will be assigned to the next delivered message.
|
|
||||||
-- Incremented after each successful delivery.
|
|
||||||
uidnext int not null default 1,
|
|
||||||
|
|
||||||
-- The next modsequence value for this mailbox.
|
|
||||||
nextmodseq bigint not null default 1,
|
|
||||||
|
|
||||||
-- The UID of the first message that should be marked \Recent.
|
|
||||||
-- Set to uidnext when each new IMAP session is created.
|
|
||||||
first_recent int not null default 1,
|
|
||||||
|
|
||||||
-- The IMAP mailbox UIDVALIDITY value, which, along with a message UID,
|
|
||||||
-- is forever guaranteed to uniquely refer to a single message.
|
|
||||||
uidvalidity int not null default 1,
|
|
||||||
|
|
||||||
-- When a mailbox is deleted, its entry is marked (not removed), so
|
|
||||||
-- that its UIDVALIDITY can be incremented if it is ever re-created.
|
|
||||||
deleted boolean not null default false
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store message
|
|
||||||
*/
|
|
||||||
|
|
||||||
create table messages (
|
|
||||||
-- Grant: select, insert
|
|
||||||
id int primary key auto_increment,
|
|
||||||
-- the time the message was added to the mailbox, as a unix time_t
|
|
||||||
idate timestamp not null,
|
|
||||||
-- the size of the message in RFC 822 format, in bytes
|
|
||||||
rfc822size int
|
|
||||||
);
|
|
||||||
|
|
||||||
-- todo make this work
|
|
||||||
-- foreign key (mailbox) references mailboxes(id),
|
|
||||||
-- foreign key (uid) references messages(id),
|
|
||||||
create table mailbox_messages (
|
|
||||||
-- Grant: select, insert, update
|
|
||||||
mailbox int not null,
|
|
||||||
-- the message's number in the mailbox
|
|
||||||
uid int not null,
|
|
||||||
message int not null,
|
|
||||||
modseq bigint not null,
|
|
||||||
seen boolean not null default false,
|
|
||||||
deleted boolean not null default false,
|
|
||||||
primary key (mailbox, uid)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- tood should not be auto increment but nextval('bodypart_ids')
|
|
||||||
create table bodyparts (
|
|
||||||
-- Grant: select, insert
|
|
||||||
id int primary key auto_increment,
|
|
||||||
-- the size of either text or data, whichever is used
|
|
||||||
bytes int not null,
|
|
||||||
-- MD5 hash of either text or data
|
|
||||||
hash text not null,
|
|
||||||
text text,
|
|
||||||
data binary
|
|
||||||
);
|
|
||||||
|
|
||||||
create table part_numbers (
|
|
||||||
-- Grant: select, insert
|
|
||||||
message int references messages(id) on delete cascade,
|
|
||||||
part text not null,
|
|
||||||
bodypart int references bodyparts(id),
|
|
||||||
bytes int,
|
|
||||||
nbLines int,
|
|
||||||
primary key (message)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table field_names (
|
|
||||||
-- Grant: select, insert
|
|
||||||
id serial primary key,
|
|
||||||
name text unique
|
|
||||||
);
|
|
||||||
|
|
||||||
-- add foreign key (message, part)
|
|
||||||
-- references part_numbers(message, part)
|
|
||||||
--on delete cascade at the end
|
|
||||||
-- references field_names(id) to field
|
|
||||||
create table header_fields (
|
|
||||||
-- Grant: select, insert
|
|
||||||
id serial primary key,
|
|
||||||
message int not null,
|
|
||||||
part text not null,
|
|
||||||
position int not null,
|
|
||||||
field int not null,
|
|
||||||
value text,
|
|
||||||
unique (message, part, position, field)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table address_fields (
|
|
||||||
-- Grant: select, insert
|
|
||||||
message int not null,
|
|
||||||
part text not null,
|
|
||||||
position int not null,
|
|
||||||
field int not null,
|
|
||||||
number int,
|
|
||||||
address int not null-- references addresses(id),
|
|
||||||
-- foreign key (message, part)
|
|
||||||
-- references part_numbers(message, part)
|
|
||||||
-- on delete cascade
|
|
||||||
);
|
|
||||||
|
|
||||||
create table date_fields (
|
|
||||||
-- Grant: select, insert
|
|
||||||
message int not null references messages(id)
|
|
||||||
on delete cascade,
|
|
||||||
value timestamp --with time zone
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APP tables
|
|
||||||
*/
|
|
||||||
create table app_accounts (
|
|
||||||
id int not null auto_increment,
|
|
||||||
user varchar(255) not null,
|
|
||||||
password binary(20),
|
|
||||||
xoauth varchar(116),
|
|
||||||
xoauth2 varchar(116),
|
|
||||||
host varchar(255) not null default 'localhost',
|
|
||||||
port int(5) not null default 143,
|
|
||||||
tls int(1) not null default 0,
|
|
||||||
primary key (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table app_rooms (
|
|
||||||
id int not null auto_increment,
|
|
||||||
name text not null,
|
|
||||||
owner int not null,
|
|
||||||
isGroup BIT(1) not null default 0,
|
|
||||||
notSeen int not null default 0,
|
|
||||||
lastUpdate timestamp not null,
|
|
||||||
primary key (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table app_room_messages (
|
|
||||||
message int [not null]
|
|
||||||
room int,
|
|
||||||
primary key()
|
|
||||||
)
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
-- Mail storage
|
|
||||||
|
|
||||||
-- 1
|
|
||||||
CREATE TABLE address (
|
|
||||||
address_id INT AUTO_INCREMENT,
|
|
||||||
address_name TEXT,
|
|
||||||
localpart TEXT NOT NULL,
|
|
||||||
domain TEXT NOT NULL,
|
|
||||||
email TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (address_id),
|
|
||||||
UNIQUE KEY (email)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 2 app
|
|
||||||
CREATE TABLE app_account (
|
|
||||||
account_id INT AUTO_INCREMENT,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
account_pwd BINARY(22),
|
|
||||||
xoauth VARCHAR(116),
|
|
||||||
xoauth2 VARCHAR(116),
|
|
||||||
host VARCHAR(255) NOT NULL DEFAULT 'localhost',
|
|
||||||
port INT(5) NOT NULL DEFAULT 143,
|
|
||||||
tls BOOLEAN NOT NULL DEFAULT true,
|
|
||||||
PRIMARY KEY (account_id),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES address(address_id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 3
|
|
||||||
CREATE TABLE mailbox (
|
|
||||||
mailbox_id INT AUTO_INCREMENT,
|
|
||||||
account_id INT NOT NULL,
|
|
||||||
mailbox_name TEXT NOT NULL,
|
|
||||||
uidnext INT NOT NULL DEFAULT 1,
|
|
||||||
nextmodseq BIGINT NOT NULL DEFAULT 1,
|
|
||||||
first_recent INT NOT NULL DEFAULT 1,
|
|
||||||
uidvalidity INT NOT NULL DEFAULT 1,
|
|
||||||
PRIMARY KEY (mailbox_id),
|
|
||||||
FOREIGN KEY (account_id) REFERENCES app_account(account_id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 4
|
|
||||||
CREATE TABLE message (
|
|
||||||
message_id INT AUTO_INCREMENT,
|
|
||||||
messageID TEXT NOT NULL,
|
|
||||||
idate TIMESTAMP NOT NULL,
|
|
||||||
rfc822size INT NOT NULL,
|
|
||||||
PRIMARY KEY (message_id),
|
|
||||||
UNIQUE KEY (messageID)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5
|
|
||||||
-- if mailbox_message deleted message is not deleted
|
|
||||||
CREATE TABLE mailbox_message (
|
|
||||||
mailbox_id INT NOT NULL,
|
|
||||||
uid INT,
|
|
||||||
message_id INT,
|
|
||||||
modseq BIGINT NOT NULL,
|
|
||||||
seen BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
PRIMARY KEY (uid, message_id),
|
|
||||||
FOREIGN KEY (mailbox_id) REFERENCES mailbox(mailbox_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 6
|
|
||||||
CREATE TABLE bodypart (
|
|
||||||
bodypart_id INT AUTO_INCREMENT,
|
|
||||||
bytes INT NOT NULL,
|
|
||||||
hash TEXT NOT NULL,
|
|
||||||
text TEXT,
|
|
||||||
data BINARY,
|
|
||||||
PRIMARY KEY (bodypart_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 7
|
|
||||||
CREATE TABLE source (
|
|
||||||
message_id INT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (message_id),
|
|
||||||
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 8
|
|
||||||
CREATE TABLE field_name (
|
|
||||||
field_id INT AUTO_INCREMENT,
|
|
||||||
field_name VARCHAR (255),
|
|
||||||
PRIMARY KEY (field_id),
|
|
||||||
UNIQUE KEY (field_name)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 9
|
|
||||||
CREATE TABLE header_field (
|
|
||||||
message_id INT NOT NULL,
|
|
||||||
field_id INT NOT NULL,
|
|
||||||
bodypart_id INT,
|
|
||||||
part VARCHAR(128),
|
|
||||||
value TEXT,
|
|
||||||
UNIQUE KEY (message_id, field_id, bodypart_id),
|
|
||||||
UNIQUE KEY (message_id, field_id, part),
|
|
||||||
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (field_id) REFERENCES field_name(field_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (bodypart_id) REFERENCES bodypart(bodypart_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 10
|
|
||||||
CREATE TABLE address_field (
|
|
||||||
message_id INT NOT NULL,
|
|
||||||
field_id INT NOT NULL,
|
|
||||||
address_id INT NOT NULL,
|
|
||||||
number INT,
|
|
||||||
UNIQUE KEY (message_id, field_id, address_id),
|
|
||||||
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (field_id) REFERENCES field_name(field_id),
|
|
||||||
FOREIGN KEY (address_id) REFERENCES address(address_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- App table
|
|
||||||
|
|
||||||
-- 11
|
|
||||||
CREATE TABLE app_room (
|
|
||||||
room_id INT AUTO_INCREMENT,
|
|
||||||
room_name VARCHAR(255) NOT NULL,
|
|
||||||
owner_id INT NOT NULL,
|
|
||||||
message_id INT NOT NULL,
|
|
||||||
isGroup BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
notSeen INT NOT NULL DEFAULT 0,
|
|
||||||
lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
|
|
||||||
PRIMARY KEY (room_id),
|
|
||||||
FOREIGN KEY (owner_id) REFERENCES address(address_id),
|
|
||||||
FOREIGN KEY (message_id) REFERENCES message(message_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 12
|
|
||||||
CREATE TABLE app_thread (
|
|
||||||
room_id INT NOT NULL,
|
|
||||||
parent_room_id INT,
|
|
||||||
root_room_id INT,
|
|
||||||
isDm BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
PRIMARY KEY (room_id),
|
|
||||||
UNIQUE KEY (room_id, parent_room_id, root_room_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
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 13
|
|
||||||
CREATE TABLE app_room_message (
|
|
||||||
message_id INT NOT NULL,
|
|
||||||
room_id INT,
|
|
||||||
UNIQUE KEY (message_id, room_id),
|
|
||||||
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 14
|
|
||||||
CREATE TABLE app_room_member (
|
|
||||||
room_id INT NOT NULL,
|
|
||||||
member_id INT NOT NULL,
|
|
||||||
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (member_id) REFERENCES address(address_id)
|
|
||||||
);
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
const queryAddress = (type) => `
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT address_field.address_id, address_field.message_id
|
|
||||||
FROM address_field
|
|
||||||
INNER JOIN field_name
|
|
||||||
WHERE
|
|
||||||
field_name.field_id = address_field.field_id AND
|
|
||||||
field_name.field_name = '${type}'
|
|
||||||
)
|
|
||||||
`;
|
|
||||||
|
|
||||||
const queryFromId = queryAddress("from");
|
|
||||||
const queryToId = queryAddress("to");
|
|
||||||
const queryCcId = queryAddress("cc");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
queryFromId,
|
|
||||||
queryToId,
|
|
||||||
queryCcId
|
|
||||||
}
|
|
||||||
13
back/db/utils/addressQueries.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const queryAddress = (type: string): string => `
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT address_field.address_id, address_field.message_id
|
||||||
|
FROM address_field
|
||||||
|
INNER JOIN field_name ON field_name.field_id = address_field.field_id
|
||||||
|
WHERE
|
||||||
|
field_name.field_name = '${type}'
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const queryFromId = queryAddress("from");
|
||||||
|
export const queryToId = queryAddress("to");
|
||||||
|
export const queryCcId = queryAddress("cc");
|
||||||
76
back/db/utils/mail.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { execQueryAsync, execQueryAsyncWithId } from "../db";
|
||||||
|
|
||||||
|
export async function getAddressId(email: string, name?: string): Promise<number> {
|
||||||
|
const localpart = email.split("@")[0];
|
||||||
|
const domain = email.split("@")[1];
|
||||||
|
const query = `INSERT INTO address
|
||||||
|
(address_name, localpart, domain, email) VALUES (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE address_name = ?, address_id = LAST_INSERT_ID(address_id)`;
|
||||||
|
const values = [name, localpart, domain, email, name];
|
||||||
|
return await execQueryAsyncWithId(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAddresses(ids: number | number[]): Promise<{ id: number, email: string }[]> {
|
||||||
|
const query = `SELECT address_id AS id, email FROM address WHERE address_id IN (?)`;
|
||||||
|
const values = [ids];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
return await execQueryAsyncWithId(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFlagId(flag: string): Promise<number> {
|
||||||
|
const query = `INSERT INTO flag_name (flag_name) VALUES (?) ON DUPLICATE KEY UPDATE flag_id=LAST_INSERT_ID(flag_id)`;
|
||||||
|
const values = [flag];
|
||||||
|
return await execQueryAsyncWithId(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessageIdOnUid(uid: number): Promise<{ message_id: number }[]> {
|
||||||
|
const query = `SELECT message_id FROM mailbox_message WHERE uid = ?`;
|
||||||
|
const values = [uid];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessageUid(messageId: number): Promise<{uid: number}[]> {
|
||||||
|
const query = `SELECT uid FROM mailbox_message WHERE message_id = ?`;
|
||||||
|
const values = [messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessageIdOnID(messageID: string): Promise<{message_id: number}[]> {
|
||||||
|
const query = `SELECT message_id FROM message WHERE messageID = ?`;
|
||||||
|
const values = [messageID];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findRoomByOwner(ownerId: number): Promise<{ room_id: number }[]> {
|
||||||
|
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
|
||||||
|
const values = [ownerId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserIdOfMailbox(boxId: number): Promise<{ user_id: number }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT app_account.user_id
|
||||||
|
FROM mailbox
|
||||||
|
INNER JOIN app_account ON app_account.account_id = mailbox.account_id
|
||||||
|
WHERE mailbox.mailbox_id = ?
|
||||||
|
`;
|
||||||
|
const values = [boxId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserOfMailbox(mailboxId: number): Promise<{ user: string }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT address.email AS user
|
||||||
|
FROM mailbox
|
||||||
|
INNER JOIN app_account ON app_account.account_id = mailbox.account_id
|
||||||
|
INNER JOIN address on address.address_id = app_account.user_id
|
||||||
|
WHERE mailbox.mailbox_id = ?
|
||||||
|
`;
|
||||||
|
const values = [mailboxId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
47
back/interfaces/mail/attrs.interface.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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;
|
||||||
|
modseq?: number;
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
54
back/mails/EmailManager.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { ImapInstance } from "./imap/ImapInstance";
|
||||||
|
import { SmtpInstance } from "./smtp/SmtpInstance";
|
||||||
|
import logger from "../system/Logger";
|
||||||
|
import { getAllAccounts } from "../db/imap/imap-db";
|
||||||
|
|
||||||
|
export interface Account {
|
||||||
|
id: number;
|
||||||
|
user: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailManager {
|
||||||
|
imapInstances: ImapInstance[];
|
||||||
|
smtpInstances: SmtpInstance[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.imapInstances = [];
|
||||||
|
this.smtpInstances = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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.addImapInstance(accounts[i]);
|
||||||
|
this.addSmtpInstance(accounts[i]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.err(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addImapInstance(config) {
|
||||||
|
this.imapInstances.push(new ImapInstance(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
addSmtpInstance(config) {
|
||||||
|
this.smtpInstances.push(new SmtpInstance(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSmtp(email: string): SmtpInstance | undefined {
|
||||||
|
return this.smtpInstances.find((instance) => instance.user === email);
|
||||||
|
}
|
||||||
|
|
||||||
|
getImap(email: string): ImapInstance | undefined {
|
||||||
|
return this.imapInstances.find((instance) => instance.account.user === email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailManager = new EmailManager();
|
||||||
|
export default emailManager;
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
const { getMailbox, updateMailbox } = require("../../db/imap/imap");
|
|
||||||
const { DEBUG } = require("../../utils/debug");
|
|
||||||
const { registerMessageInApp } = require("../saveMessage");
|
|
||||||
const { saveMessage } = require("../storeMessage");
|
|
||||||
|
|
||||||
class Box {
|
|
||||||
constructor(_imap, boxId, _boxName) {
|
|
||||||
this.imap = _imap;
|
|
||||||
this.boxName = _boxName;
|
|
||||||
this.id = boxId;
|
|
||||||
this.box;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
this.box = (await getMailbox(this.id))[0];
|
|
||||||
|
|
||||||
const readOnly = true;
|
|
||||||
this.imap.openBox(this.boxName, readOnly, (err, box) => {
|
|
||||||
if (err) DEBUG.log(err);
|
|
||||||
this.sync(this.box.uidnext, box.uidnext);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sync(savedUid, currentUid) {
|
|
||||||
const promises = [];
|
|
||||||
const mails = [];
|
|
||||||
const f = this.imap.seq.fetch(`${savedUid}:${currentUid}`, {
|
|
||||||
size: true,
|
|
||||||
envelope: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
f.on("message", (msg, seqno) => {
|
|
||||||
msg.once("attributes", (attrs) => {
|
|
||||||
mails.push(attrs);
|
|
||||||
promises.push(saveMessage(attrs, this.id, this.imap));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
f.once("error", (err) => {
|
|
||||||
DEBUG.log("Fetch error: " + err);
|
|
||||||
});
|
|
||||||
|
|
||||||
f.once("end", async () => {
|
|
||||||
await Promise.all(promises).then(async (res) => {
|
|
||||||
for (let i = 0; i < mails.length; i++) {
|
|
||||||
console.log(i, mails[i].uid)
|
|
||||||
await registerMessageInApp(res[i], mails[i]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
updateMailbox(this.id, currentUid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Box,
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
const Imap = require("imap");
|
|
||||||
const { getAllMailboxes, registerMailbox } = require("../../db/imap/imap");
|
|
||||||
const { DEBUG } = require("../../utils/debug");
|
|
||||||
const { Box } = require("./Box");
|
|
||||||
|
|
||||||
class ImapInstance {
|
|
||||||
constructor(account) {
|
|
||||||
this.imap = new Imap({
|
|
||||||
user: account.user,
|
|
||||||
password: account.password,
|
|
||||||
tlsOptions: { servername: account.host },
|
|
||||||
host: account.host,
|
|
||||||
port: account.port,
|
|
||||||
tls: account.tls,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.account = account;
|
|
||||||
this.boxes = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IMAP
|
|
||||||
*/
|
|
||||||
this.imap.once("ready", () => {
|
|
||||||
DEBUG.log("imap connected")
|
|
||||||
this.imapReady();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.imap.once("error", function (err) {
|
|
||||||
DEBUG.log(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.imap.once("end", function () {
|
|
||||||
DEBUG.log("Connection ended");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.imap.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
imapReady() {
|
|
||||||
getAllMailboxes(this.account.id).then((mailboxes) => {
|
|
||||||
if (mailboxes.length > 0) {
|
|
||||||
this.boxes.push(new Box(this.imap, mailboxes[0].mailbox_id, mailboxes[0].mailbox_name));
|
|
||||||
} else {
|
|
||||||
this.imap.getBoxes('', (err, boxes) => {
|
|
||||||
if (err) DEBUG.log(err);
|
|
||||||
const allBoxName = this.getAllBox(boxes);
|
|
||||||
registerMailbox(this.account.id, allBoxName).then((mailboxId) => {
|
|
||||||
this.boxes.push(new Box(this.imap, mailboxId, allBoxName));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllBox(boxes) {
|
|
||||||
// ideally we should get the all box to get all messages
|
|
||||||
let allBox;
|
|
||||||
Object.keys(boxes).forEach(key => {
|
|
||||||
if (key === 'INBOX') return;
|
|
||||||
allBox = key;
|
|
||||||
Object.keys(boxes[key].children).forEach((childBoxes) => {
|
|
||||||
if (boxes[key].children[childBoxes].attribs.includes('\\All')) {
|
|
||||||
allBox += '/' + childBoxes;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return allBox;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
ImapInstance
|
|
||||||
}
|
|
||||||
88
back/mails/imap/ImapInstance.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Account } from "../EmailManager";
|
||||||
|
|
||||||
|
import Imap from "imap";
|
||||||
|
import { getAllMailboxes, registerMailbox } from "../../db/imap/imap-db";
|
||||||
|
import logger from "../../system/Logger";
|
||||||
|
import Mailbox from "./Mailbox";
|
||||||
|
|
||||||
|
export class ImapInstance {
|
||||||
|
imap: Imap;
|
||||||
|
account: Account;
|
||||||
|
boxes: Mailbox[];
|
||||||
|
|
||||||
|
constructor(account) {
|
||||||
|
this.imap = new Imap({
|
||||||
|
user: account.user,
|
||||||
|
password: account.password,
|
||||||
|
tlsOptions: { servername: account.imap_host },
|
||||||
|
host: account.imap_host,
|
||||||
|
port: account.imap_port,
|
||||||
|
tls: account.tls,
|
||||||
|
});
|
||||||
|
this.account = account;
|
||||||
|
this.boxes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IMAP init
|
||||||
|
*/
|
||||||
|
this.imap.once("ready", () => {
|
||||||
|
logger.log("Imap connected for " + this.account.user);
|
||||||
|
this.imapReady();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.imap.once("error", (err) => {
|
||||||
|
logger.err("Imap error for " + this.account.user + ": " + err);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.imap.once("end", () => {
|
||||||
|
logger.log("Connection ended for " + this.account.user);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.imap.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
imapReady = () => {
|
||||||
|
getAllMailboxes(this.account.id).then((mailboxes) => {
|
||||||
|
if (mailboxes.length > 0) {
|
||||||
|
this.boxes.push(new Mailbox(mailboxes[0].mailbox_id, mailboxes[0].mailbox_name, this));
|
||||||
|
} else {
|
||||||
|
this.getMailboxName("All").then((allBoxName) => {
|
||||||
|
registerMailbox(this.account.id, allBoxName).then((mailboxId) => {
|
||||||
|
this.boxes.push(new Mailbox(mailboxId, allBoxName, this));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getMailboxName(boxToFound: string): Promise<string> {
|
||||||
|
return new Promise((resolve, rejects) => {
|
||||||
|
let matchBox = "";
|
||||||
|
this.imap.getBoxes("", (err, boxes) => {
|
||||||
|
Object.keys(boxes).forEach((key) => {
|
||||||
|
if (matchBox.includes("/")) return; // already found
|
||||||
|
if (!boxes[key].children) return; // no children
|
||||||
|
matchBox = key;
|
||||||
|
Object.keys(boxes[key].children).forEach((childBox) => {
|
||||||
|
let attribs = boxes[key].children[childBox].attribs;
|
||||||
|
for (let i = 0; i < attribs.length; i++) {
|
||||||
|
if (attribs[i].includes(boxToFound)) {
|
||||||
|
matchBox += "/" + childBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!matchBox.includes("/")) {
|
||||||
|
logger.warn(`Did not find "${boxToFound}" mailbox`);
|
||||||
|
rejects();
|
||||||
|
} else {
|
||||||
|
resolve(matchBox);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getMailbox(mailboxId: number): Mailbox {
|
||||||
|
return this.boxes.find((box) => box.id === mailboxId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const { getAllAccounts } = require("../../db/imap/imap");
|
|
||||||
const { DEBUG } = require("../../utils/debug");
|
|
||||||
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) => {
|
|
||||||
DEBUG.log(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addInstance(config) {
|
|
||||||
this.instances.push(new ImapInstance(config));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
ImapSync
|
|
||||||
}
|
|
||||||
211
back/mails/imap/Mailbox.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import Imap, { Box } from "imap";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import { getMailbox, getMailboxModseq, updateMailbox, updateMailboxModseq } from "../../db/imap/imap-db";
|
||||||
|
import { Attrs, AttrsWithEnvelope } from "../../interfaces/mail/attrs.interface";
|
||||||
|
import logger from "../../system/Logger";
|
||||||
|
import RegisterMessageInApp from "../message/saveMessage";
|
||||||
|
import { saveMessage } from "../message/storeMessage";
|
||||||
|
import updateMessage from "../message/updateMessage";
|
||||||
|
import { ImapInstance } from "./ImapInstance";
|
||||||
|
|
||||||
|
export interface ImapInfo {
|
||||||
|
uid: number;
|
||||||
|
modseq: string;
|
||||||
|
flags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Mailbox {
|
||||||
|
imap: Imap;
|
||||||
|
boxName: string;
|
||||||
|
id: number;
|
||||||
|
box: Box;
|
||||||
|
msgToSync: number;
|
||||||
|
syncing: boolean;
|
||||||
|
imapInstance: ImapInstance;
|
||||||
|
|
||||||
|
constructor(_boxId: number, _boxName: string, _imapInstance: ImapInstance) {
|
||||||
|
this.imap = _imapInstance.imap;
|
||||||
|
this.boxName = _boxName;
|
||||||
|
this.id = _boxId;
|
||||||
|
this.box;
|
||||||
|
this.msgToSync = 0;
|
||||||
|
this.syncing = false;
|
||||||
|
this.imapInstance = _imapInstance;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// get mailbox from the database
|
||||||
|
this.box = (await getMailbox(this.id))[0];
|
||||||
|
const isReadOnly = false;
|
||||||
|
this.imap.openBox(this.boxName, isReadOnly, (err, box) => {
|
||||||
|
if (err) logger.err(err);
|
||||||
|
|
||||||
|
// sync messages and flags
|
||||||
|
this.initSync(box);
|
||||||
|
|
||||||
|
// wait for new mails
|
||||||
|
this.imap.on("mail", (numNewMsgs: number) => {
|
||||||
|
if (!this.syncing) {
|
||||||
|
// if not syncing restart a sync
|
||||||
|
this.syncManager(this.box.uidnext - 1, this.box.uidnext + numNewMsgs - 1);
|
||||||
|
} else {
|
||||||
|
// else save number of message to sync latter
|
||||||
|
this.msgToSync += numNewMsgs;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for flags update
|
||||||
|
this.imap.on("update", (seqno: number, info: ImapInfo) => {
|
||||||
|
logger.log(`Update message ${info.uid} with ${info.flags}`);
|
||||||
|
const updateMsg = new updateMessage(info.uid, info.flags);
|
||||||
|
updateMsg.updateFlags();
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for deletion
|
||||||
|
this.imap.on("expunge", (seqno: number) => {
|
||||||
|
// const updateMsg = new updateMessage(info.)
|
||||||
|
console.log("Message with sequence number " + seqno + " has been deleted from the server.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateModseq(newModseq: number) {
|
||||||
|
updateMailboxModseq(this.id, newModseq).then(() => {
|
||||||
|
this.box.highestmodseq = newModseq.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async initSync(box: Box) {
|
||||||
|
// sync mail only if has new messages
|
||||||
|
if (this.box.uidnext < box.uidnext) {
|
||||||
|
this.syncManager(this.box.uidnext, box.uidnext);
|
||||||
|
} else {
|
||||||
|
logger.log("Mail already up to date");
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync flags
|
||||||
|
const lastModseq = (await getMailboxModseq(this.id))[0]?.modseq ?? 0;
|
||||||
|
if (parseInt(box.highestmodseq) > lastModseq) {
|
||||||
|
const fetchStream = this.imap.fetch("1:*", { bodies: "", modifiers: { changedsince: lastModseq } });
|
||||||
|
fetchStream.on("message", (message) => {
|
||||||
|
message.once("attributes", (attrs) => {
|
||||||
|
const updateMsg = new updateMessage(attrs.uid, attrs.flags);
|
||||||
|
updateMsg.updateFlags();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fetchStream.once("error", function (err) {
|
||||||
|
logger.err("Fetch error when syncing flags: " + err);
|
||||||
|
});
|
||||||
|
fetchStream.once("end", function () {
|
||||||
|
logger.log("Done fetching new flags");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.log("Flags already up to date");
|
||||||
|
}
|
||||||
|
this.updateModseq(parseInt(box.highestmodseq));
|
||||||
|
}
|
||||||
|
|
||||||
|
syncManager = async (savedUid: number, currentUid: number) => {
|
||||||
|
this.syncing = true;
|
||||||
|
logger.log(`Fetching from ${savedUid} to ${currentUid} uid`);
|
||||||
|
const nbMessageToSync = currentUid - savedUid;
|
||||||
|
let STEP = nbMessageToSync > 200 ? Math.floor(nbMessageToSync / 7) : nbMessageToSync;
|
||||||
|
let mails: AttrsWithEnvelope[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < nbMessageToSync; i += STEP) {
|
||||||
|
mails = [];
|
||||||
|
try {
|
||||||
|
// fetch mails
|
||||||
|
let secondUid = savedUid + STEP < currentUid ? savedUid + STEP : currentUid;
|
||||||
|
await this.mailFetcher(savedUid, secondUid, mails);
|
||||||
|
logger.log(`Fetched ${STEP} uids (${mails.length} messages)`);
|
||||||
|
// save same in the database
|
||||||
|
for (let k = 0; k < mails.length; k++) {
|
||||||
|
try {
|
||||||
|
const messageId = await saveMessage(mails[k], this.id, this.imap);
|
||||||
|
const register = new RegisterMessageInApp(messageId, mails[k], this.id);
|
||||||
|
await register.save();
|
||||||
|
} catch (error) {
|
||||||
|
logger.err("Failed to save a message: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
savedUid = secondUid;
|
||||||
|
this.box.uidnext += savedUid;
|
||||||
|
|
||||||
|
updateMailbox(this.id, savedUid);
|
||||||
|
} catch (error) {
|
||||||
|
logger.err("Failed to sync message " + error);
|
||||||
|
}
|
||||||
|
logger.log(`Saved messages in uids ${i + STEP > nbMessageToSync ? nbMessageToSync : i + STEP}/${nbMessageToSync}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if has receive new msg during last sync then start a new sync
|
||||||
|
if (this.msgToSync > 0) {
|
||||||
|
const currentUid = this.box.uidnext;
|
||||||
|
this.box.uidnext += this.msgToSync;
|
||||||
|
// reset value to allow to detect new incoming message while syncing
|
||||||
|
this.msgToSync = 0;
|
||||||
|
await this.syncManager(currentUid, this.box.uidnext);
|
||||||
|
}
|
||||||
|
this.syncing = false;
|
||||||
|
logger.log(`Finished syncing messages`);
|
||||||
|
};
|
||||||
|
|
||||||
|
async mailFetcher(startUid: number, endUid: number, mails: Attrs[]): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const f = this.imap.fetch(`${startUid}:${endUid}`, {
|
||||||
|
size: true,
|
||||||
|
envelope: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
f.on("message", (msg, seqno) => {
|
||||||
|
msg.once("attributes", (attrs: AttrsWithEnvelope) => {
|
||||||
|
mails.push(attrs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
f.once("error", (err) => {
|
||||||
|
logger.err("Fetch error when fetching in uid range: " + err);
|
||||||
|
reject(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
f.once("end", async () => {
|
||||||
|
resolve(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addFlag(source: string, flags: string[]): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.imap.addFlags(source, flags, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFlag(source: string, flags: string[]): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.imap.delFlags(source, flags, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
move(source: string, mailboxName: string, callback: (error: Error) => void) {
|
||||||
|
this.imap.move(source, mailboxName, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToTrash(source: string, callback: (error: Error) => void) {
|
||||||
|
const trashName = await this.imapInstance.getMailboxName("Trash");
|
||||||
|
this.move(source, trashName, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
back/mails/message/Message.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { getFlagsOnId, getFlagsOnUid } from "../../db/message/message-db";
|
||||||
|
import { deleteMessage } from "../../db/message/updateMessage-db";
|
||||||
|
import { getMessageUid, getUserOfMailbox } from "../../db/utils/mail";
|
||||||
|
import emailManager from "../EmailManager";
|
||||||
|
import Mailbox from "../imap/Mailbox";
|
||||||
|
import Room from "../room/Room";
|
||||||
|
|
||||||
|
export default class Message {
|
||||||
|
messageId: number;
|
||||||
|
uid: number;
|
||||||
|
private _flags: string[] | undefined;
|
||||||
|
private _mailbox: Mailbox;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
setMessageId(messageId: number): Message {
|
||||||
|
this.messageId = messageId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async useUid(): Promise<Message> {
|
||||||
|
if (!this.messageId) {
|
||||||
|
throw "Define message id before trying to find uid";
|
||||||
|
}
|
||||||
|
this.uid = (await getMessageUid(this.messageId))[0]?.uid;
|
||||||
|
if (!this.uid) {
|
||||||
|
throw "Uid not found";
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async useFlags(): Promise<Message> {
|
||||||
|
let flags;
|
||||||
|
if (!this._flags) {
|
||||||
|
if (this.messageId) {
|
||||||
|
flags = await getFlagsOnId(this.messageId);
|
||||||
|
} else if (this.uid) {
|
||||||
|
flags = await getFlagsOnUid(this.uid);
|
||||||
|
} else {
|
||||||
|
throw "Neither message id or uid are set, please do so before attempting to load flags";
|
||||||
|
}
|
||||||
|
this._flags = flags;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async useMailbox(mailboxId: number, user?: string): Promise<Message> {
|
||||||
|
if (!user) {
|
||||||
|
user = (await getUserOfMailbox(mailboxId))[0]?.user;
|
||||||
|
if (!user) {
|
||||||
|
throw "Cannot find user of this mailbox";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._mailbox = emailManager.getImap(user).getMailbox(mailboxId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get mailbox() {
|
||||||
|
if (!this._mailbox) {
|
||||||
|
throw "Call useMailbox before calling functions related to mailbox";
|
||||||
|
}
|
||||||
|
return this._mailbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
get flags(): string[] {
|
||||||
|
console.log("called");
|
||||||
|
console.log(this._flags);
|
||||||
|
if (!this._flags) {
|
||||||
|
throw "Flags not loaded, call useFlags before calling functions related to flags";
|
||||||
|
} else {
|
||||||
|
return this._flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDeleted(): boolean {
|
||||||
|
return this.flags.includes("\\Deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async (messageId?: number): Promise<Message> => {
|
||||||
|
if (!(this.messageId ?? messageId)) {
|
||||||
|
throw "Delete need to have the message id";
|
||||||
|
}
|
||||||
|
await deleteMessage(this.messageId ?? messageId);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
}
|
||||||
204
back/mails/message/saveMessage.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import {
|
||||||
|
createRoom,
|
||||||
|
registerMessageInRoom,
|
||||||
|
getRoomType,
|
||||||
|
findRoomsFromMessage,
|
||||||
|
hasSameMembersAsParent,
|
||||||
|
registerThread,
|
||||||
|
registerMember,
|
||||||
|
getAllMembers,
|
||||||
|
getThreadInfo,
|
||||||
|
getThreadInfoOnId,
|
||||||
|
} from "../../db/message/saveMessage-db";
|
||||||
|
|
||||||
|
import { findRoomByOwner, getAddressId, getMessageIdOnID, getUserIdOfMailbox } from "../../db/utils/mail";
|
||||||
|
import { nbMembers } from "../utils/envelopeUtils";
|
||||||
|
import logger from "../../system/Logger";
|
||||||
|
import { Attrs, Envelope, User } from "../../interfaces/mail/attrs.interface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* take object address and join mailbox and host to return mailbox@host
|
||||||
|
*/
|
||||||
|
function createAddress(elt: User): string {
|
||||||
|
return `${elt.mailbox}@${elt.host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RoomType {
|
||||||
|
ROOM = 0,
|
||||||
|
CHANNEL = 1,
|
||||||
|
GROUP = 2,
|
||||||
|
DM = 3,
|
||||||
|
THREAD = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RegisterMessageInApp {
|
||||||
|
messageId: number;
|
||||||
|
attrs: Attrs;
|
||||||
|
envelope: Envelope;
|
||||||
|
messageID?: string;
|
||||||
|
boxId: number;
|
||||||
|
isSeen: boolean;
|
||||||
|
ownerId: number;
|
||||||
|
userId: number;
|
||||||
|
inReplyTo: string;
|
||||||
|
|
||||||
|
constructor(_messageId: number, _attrs: Attrs, _boxId: number) {
|
||||||
|
this.messageId = _messageId;
|
||||||
|
this.attrs = _attrs;
|
||||||
|
if (!this.attrs.envelope) throw new Error("Envelope must exist in attributes");
|
||||||
|
this.envelope = this.attrs.envelope;
|
||||||
|
this.messageID = this.envelope?.messageId;
|
||||||
|
this.boxId = _boxId;
|
||||||
|
this.isSeen = this.attrs.flags.includes("\\Seen");
|
||||||
|
this.ownerId = -1;
|
||||||
|
this.userId = -1;
|
||||||
|
this.inReplyTo = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
await this.init();
|
||||||
|
if (this.envelope.inReplyTo) {
|
||||||
|
this.inReplyTo = this.envelope.inReplyTo;
|
||||||
|
await this.saveReply();
|
||||||
|
} else {
|
||||||
|
if (await this.isFromUs()) {
|
||||||
|
if (this.isDm()) {
|
||||||
|
// create or add new message to DM
|
||||||
|
if (!this.envelope.to) throw new Error("Who send a DM and put the recipient in cc ?");
|
||||||
|
const userTo = await getAddressId(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 version is considered to be for personnal use
|
||||||
|
// so by default it will be a group
|
||||||
|
await this.initiateRoom(this.ownerId, RoomType.GROUP);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// todo if contains reply in recipent then is channel
|
||||||
|
await this.createOrRegisterOnExistence(this.ownerId, RoomType.ROOM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveReply() {
|
||||||
|
await findRoomsFromMessage(this.inReplyTo).then(async (rooms) => {
|
||||||
|
if (rooms.length < 1) {
|
||||||
|
// no rooms, so is a transfer
|
||||||
|
// todo test if members of transferred message are included
|
||||||
|
} else if (rooms.length === 1) {
|
||||||
|
// only one room so message is only in a room and not in a thread
|
||||||
|
// as a thread is associated to a room
|
||||||
|
const roomType = (await getRoomType(rooms[0].room_id))[0].room_type;
|
||||||
|
if (roomType == RoomType.GROUP || roomType == RoomType.THREAD) {
|
||||||
|
await this.createOrRegisterOnMembers(rooms[0].room_id, roomType == RoomType.THREAD);
|
||||||
|
} else {
|
||||||
|
// reply from CHANNEL or DM or ROOM
|
||||||
|
await this.initiateThread();
|
||||||
|
// todo
|
||||||
|
// if (sender == owner) { // correction from the original sender
|
||||||
|
// // leave in the same channel
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
} else if (rooms.length > 1) {
|
||||||
|
// get the lowest thread (order by room_id)
|
||||||
|
const roomId = rooms[rooms.length - 1].room_id;
|
||||||
|
await this.createOrRegisterOnMembers(roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.envelope.from) {
|
||||||
|
this.ownerId = await getAddressId(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add all members of the message to the room
|
||||||
|
*/
|
||||||
|
async registerMembers(roomId: number) {
|
||||||
|
getAllMembers(this.messageId).then((res) => {
|
||||||
|
if (res.lenght == 0) return;
|
||||||
|
const data = res[0].id.split(",");
|
||||||
|
data.forEach(async (memberId: number) => {
|
||||||
|
await registerMember(roomId, memberId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async initiateRoom(owner: number, roomType: RoomType) {
|
||||||
|
try {
|
||||||
|
const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType);
|
||||||
|
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
||||||
|
await this.registerMembers(roomId);
|
||||||
|
return roomId;
|
||||||
|
} catch (err) {
|
||||||
|
logger.err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrRegisterOnExistence(owner: number, roomType: RoomType) {
|
||||||
|
await findRoomByOwner(owner).then(async (res) => {
|
||||||
|
if (res.length == 0) {
|
||||||
|
// first message with this sender
|
||||||
|
await this.initiateRoom(owner, roomType);
|
||||||
|
} else {
|
||||||
|
// not a reply, add to the list of message if this sender
|
||||||
|
await registerMessageInRoom(this.messageId, res[0].room_id, this.envelope.date);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async initiateThread() {
|
||||||
|
const inReplyToId = (await getMessageIdOnID(this.inReplyTo))[0]?.message_id;
|
||||||
|
await createRoom(this.envelope.subject, this.ownerId, inReplyToId, RoomType.THREAD).then(
|
||||||
|
async (threadId: number) => {
|
||||||
|
// find parent room infos
|
||||||
|
let roomId: number;
|
||||||
|
let root_id: number;
|
||||||
|
await getThreadInfo(this.inReplyTo).then(async (room) => {
|
||||||
|
// todo room not lenght, reply to transfer ?
|
||||||
|
roomId = room[0].room_id;
|
||||||
|
root_id = room[0].root_id;
|
||||||
|
if (root_id === undefined) root_id = roomId;
|
||||||
|
await registerThread(threadId, roomId, root_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add original message
|
||||||
|
await registerMessageInRoom(inReplyToId, threadId, this.envelope.date);
|
||||||
|
// add reply message
|
||||||
|
await registerMessageInRoom(this.messageId, threadId, this.envelope.date);
|
||||||
|
await this.registerMembers(threadId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrRegisterOnMembers(roomId: number, isThread?: boolean) {
|
||||||
|
const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo);
|
||||||
|
if (hasSameMembers) {
|
||||||
|
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
||||||
|
if (isThread) {
|
||||||
|
await getThreadInfoOnId(roomId).then(async (res) => {
|
||||||
|
let root_id = res[0].root_id;
|
||||||
|
if (root_id == undefined) root_id = res[0].room_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.initiateThread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
const { getAddresseId } = require("../db/mail");
|
import { getAddressId, getFlagId } from "../../db/utils/mail";
|
||||||
const { DEBUG } = require("../utils/debug");
|
import { EmailAddress, ParsedMail, simpleParser } from "mailparser";
|
||||||
const { simpleParser } = require("mailparser");
|
import moment from "moment";
|
||||||
const moment = require("moment");
|
import Imap from "imap";
|
||||||
const {
|
import {
|
||||||
registerMessage,
|
registerMessage,
|
||||||
registerMailbox_message,
|
registerMailbox_message,
|
||||||
saveHeader_fields,
|
saveHeader_fields,
|
||||||
@@ -10,11 +10,14 @@ const {
|
|||||||
registerBodypart,
|
registerBodypart,
|
||||||
saveBodypart,
|
saveBodypart,
|
||||||
saveSource,
|
saveSource,
|
||||||
} = require("../db/saveMessage");
|
registerFlag,
|
||||||
|
} from "../../db/message/storeMessage-db";
|
||||||
|
|
||||||
const { getFieldId } = require("../db/mail");
|
import { getFieldId } from "../../db/utils/mail";
|
||||||
|
import logger from "../../system/Logger";
|
||||||
|
import { AttrsWithEnvelope } from "../../interfaces/mail/attrs.interface";
|
||||||
|
|
||||||
function saveMessage(attrs, mailboxId, imap) {
|
export function saveMessage(attrs: AttrsWithEnvelope, mailboxId: number, imap: Imap): Promise<number> {
|
||||||
const envelope = attrs.envelope;
|
const envelope = attrs.envelope;
|
||||||
const ts = moment(new Date(envelope.date).getTime()).format("YYYY-MM-DD HH:mm:ss");
|
const ts = moment(new Date(envelope.date).getTime()).format("YYYY-MM-DD HH:mm:ss");
|
||||||
const rfc822size = attrs.size;
|
const rfc822size = attrs.size;
|
||||||
@@ -23,10 +26,13 @@ function saveMessage(attrs, mailboxId, imap) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
registerMessage(ts, rfc822size, messageID)
|
registerMessage(ts, rfc822size, messageID)
|
||||||
.then((messageId) => {
|
.then((messageId) => {
|
||||||
const isSeen = attrs.flags.includes("Seen") ? 1 : 0; // todo verify
|
const isSeen: boolean = attrs.flags.includes("\\Seen");
|
||||||
const deleted = attrs.flags.includes("Deleted") ? 1 : 0; // todo verify
|
const deleted: boolean = attrs.flags.includes("\\Deleted");
|
||||||
|
|
||||||
registerMailbox_message(mailboxId, attrs.uid, messageId, attrs.modseq, isSeen, deleted);
|
registerMailbox_message(mailboxId, attrs.uid, messageId, attrs?.modseq || 0, isSeen, deleted);
|
||||||
|
registerFlags(messageId, attrs.flags);
|
||||||
|
|
||||||
|
// fetch message to save everything
|
||||||
const f = imap.fetch(attrs.uid, { bodies: "" });
|
const f = imap.fetch(attrs.uid, { bodies: "" });
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
|
|
||||||
@@ -54,35 +60,46 @@ function saveMessage(attrs, mailboxId, imap) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
f.once("error", function (err) {
|
f.once("error", function (err) {
|
||||||
console.log("Fetch error: " + err);
|
logger.warn("Fetch error: " + err);
|
||||||
});
|
});
|
||||||
f.once("end", function () {
|
f.once("end", function () {
|
||||||
DEBUG.log("Done fetching data of "+messageID);
|
// logger.log("Done fetching data of " + messageID); // todo
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
DEBUG.log("Unable to register message: " + err);
|
logger.warn("Unable to register message: " + err);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveFromParsedData(parsed, messageId) {
|
function registerFlags(messageId: number, flags: string[]) {
|
||||||
const promises = [];
|
flags.forEach((flag) => {
|
||||||
|
getFlagId(flag).then((flagId) => {
|
||||||
|
registerFlag(messageId, flagId);
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
logger.err(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFromParsedData(parsed: ParsedMail, messageId: number) {
|
||||||
|
const promises: Promise<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(
|
||||||
// save address field
|
// save address field
|
||||||
getFieldId(key).then((fieldId) => {
|
getFieldId(key).then((fieldId) => {
|
||||||
parsed[key].value.forEach((addr, nb) => {
|
parsed[key].value.forEach((addr: EmailAddress, nb: number) => {
|
||||||
getAddresseId(addr.address, addr.name).then(async (addressId) => {
|
getAddressId(addr.address, addr.name).then(async (addressId) => {
|
||||||
await saveAddress_fields(messageId, fieldId, addressId, nb);
|
await saveAddress_fields(messageId, fieldId, addressId, nb);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (["subject", "inReplyTo"].includes(key)) {
|
} else if (["subject", "inReplyTo", "references"].includes(key)) {
|
||||||
// todo : "references" (array)
|
// todo : "references" (array)
|
||||||
|
if (key == "references") return;
|
||||||
promises.push(
|
promises.push(
|
||||||
getFieldId(key).then(async (fieldId) => {
|
getFieldId(key).then(async (fieldId) => {
|
||||||
await saveHeader_fields(messageId, fieldId, undefined, undefined, parsed[key]);
|
await saveHeader_fields(messageId, fieldId, undefined, undefined, parsed[key]);
|
||||||
@@ -91,25 +108,25 @@ async function saveFromParsedData(parsed, messageId) {
|
|||||||
} else if (["html", "text", "textAsHtml"].includes(key)) {
|
} else if (["html", "text", "textAsHtml"].includes(key)) {
|
||||||
const hash = "0";
|
const hash = "0";
|
||||||
const size = "0";
|
const size = "0";
|
||||||
|
let partType = "text/plain";
|
||||||
|
if (key == "html") {
|
||||||
|
partType = "text/html";
|
||||||
|
} else if (key == "textAsHtml") {
|
||||||
|
partType = "text/TexAsHtml";
|
||||||
|
}
|
||||||
saveBodypart(size, hash, parsed[key], "").then((bodypartId) => {
|
saveBodypart(size, hash, parsed[key], "").then((bodypartId) => {
|
||||||
getFieldId(key).then((fieldId) => {
|
getFieldId(key).then((fieldId) => {
|
||||||
saveHeader_fields(
|
saveHeader_fields(messageId, fieldId, bodypartId, partType, undefined);
|
||||||
messageId,
|
|
||||||
fieldId,
|
|
||||||
bodypartId,
|
|
||||||
undefined, // todo ?
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (key == "attachments") {
|
} else if (key == "attachments") {
|
||||||
// todo
|
// todo attachments
|
||||||
} else if (["date", "messageId", "headers", "headerLines"].includes(key)) {
|
} else if (["date", "messageId", "headers", "headerLines"].includes(key)) {
|
||||||
// messageId and date are already saved
|
// messageId and date are already saved
|
||||||
// other field are not important and can be retrieved in source
|
// other field are not important and can be retrieved in source
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
DEBUG.log("doesn't know key: " + key);
|
logger.warn("doesn't know key: " + key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -117,12 +134,8 @@ async function saveFromParsedData(parsed, messageId) {
|
|||||||
// todo when transfered
|
// todo when transfered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env["NODE_DEV"] == "TEST") {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
saveMessage,
|
saveFromParsedData,
|
||||||
};
|
|
||||||
|
|
||||||
if (process.env['NODE_DEV'] == 'TEST') {
|
|
||||||
module.exports = {
|
|
||||||
saveFromParsedData
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
43
back/mails/message/updateMessage.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { registerFlag } from "../../db/message/storeMessage-db";
|
||||||
|
import { deleteFlag, getFlags, updateMailboxDeleted, updateMailboxSeen } from "../../db/message/updateMessage-db";
|
||||||
|
import { getFlagId, getMessageIdOnUid } from "../../db/utils/mail";
|
||||||
|
|
||||||
|
export default class updateMessage {
|
||||||
|
uid: number;
|
||||||
|
flags: string[];
|
||||||
|
|
||||||
|
constructor(_uid: number, _flags: string[]) {
|
||||||
|
this.uid = _uid;
|
||||||
|
this.flags = _flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFlags() {
|
||||||
|
const messageId = (await getMessageIdOnUid(this.uid))[0]?.message_id;
|
||||||
|
if (!messageId) return;
|
||||||
|
const currentFlags = await getFlags(this.uid);
|
||||||
|
|
||||||
|
const flagsToAdd = this.flags.filter((flag) => !currentFlags.find((f) => flag == f.flag_name));
|
||||||
|
const flagToRm = currentFlags.filter((f) => !this.flags.includes(f.flag_name));
|
||||||
|
|
||||||
|
flagsToAdd.forEach(async (flag) => {
|
||||||
|
const flagId = await getFlagId(flag);
|
||||||
|
registerFlag(messageId, flagId);
|
||||||
|
});
|
||||||
|
|
||||||
|
flagToRm.forEach(async (flag) => {
|
||||||
|
deleteFlag(messageId, flag.flag_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (flagsToAdd.includes("\\Seen")) {
|
||||||
|
updateMailboxSeen(messageId, true);
|
||||||
|
} else if (flagToRm.find((f) => f.flag_name == "\\Seen")) {
|
||||||
|
updateMailboxSeen(messageId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flagsToAdd.includes("\\Deleted")) {
|
||||||
|
updateMailboxDeleted(messageId, true);
|
||||||
|
} else if (flagToRm.find((f) => f.flag_name == "\\Deleted")) {
|
||||||
|
updateMailboxDeleted(messageId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
back/mails/room/Room.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { deleteRoom, getRoomNbMessageAndThread, getRoomOnMessageId } from "../../db/Room-db";
|
||||||
|
|
||||||
|
export default class Room {
|
||||||
|
private _roomId: number;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
setRoomId(roomId: number): Room {
|
||||||
|
this._roomId = roomId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomId(): number {
|
||||||
|
return this._roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRoomIdOnMessageId(messageId: number): Promise<Room> {
|
||||||
|
const res = await getRoomOnMessageId(messageId);
|
||||||
|
if (res.length == 0) {
|
||||||
|
throw "Message has no room";
|
||||||
|
}
|
||||||
|
this._roomId = res[0].room_id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the room have threads or messages
|
||||||
|
async shouldDelete(): Promise<boolean> {
|
||||||
|
if (!this._roomId) {
|
||||||
|
throw "shouldDelete needs to have a roomId set.";
|
||||||
|
}
|
||||||
|
const res = await getRoomNbMessageAndThread(this._roomId);
|
||||||
|
if (res.length === 0) return true;
|
||||||
|
if (res[0].nbMessage === 0 && res[0].nbThread === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<Room> {
|
||||||
|
if (!this._roomId) {
|
||||||
|
throw "shouldDelete needs to have a roomId set.";
|
||||||
|
}
|
||||||
|
await deleteRoom(this._roomId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
const {
|
|
||||||
createRoom,
|
|
||||||
registerMessageInRoom,
|
|
||||||
createThread,
|
|
||||||
registerMessageInThread,
|
|
||||||
isRoomGroup,
|
|
||||||
findRoomsFromMessage,
|
|
||||||
hasSameMembersAsParent,
|
|
||||||
} = require("../db/saveMessageApp");
|
|
||||||
|
|
||||||
const { findRoomByOwner, getAddresseId } = require("../db/mail");
|
|
||||||
const { isDmOnEnvelope } = 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 registerMessageInApp(messageId, attrs) {
|
|
||||||
const isSeen = attrs.flags.includes("Seen") ? 1 : 0; // todo verify
|
|
||||||
const envelope = attrs.envelope;
|
|
||||||
|
|
||||||
await getAddresseId(createAddress(envelope.sender[0])).then(async (ownerId) => {
|
|
||||||
if (envelope.inReplyTo) {
|
|
||||||
await registerReplyMessage(envelope, messageId, isSeen, ownerId);
|
|
||||||
} else {
|
|
||||||
await findRoomByOwner(ownerId).then(async (res) => {
|
|
||||||
if (res.length == 0) {
|
|
||||||
// first message of this sender
|
|
||||||
await createRoom(envelope.subject, ownerId, messageId).then(async (roomId) => {
|
|
||||||
await registerMessageInRoom(messageId, roomId, isSeen, envelope.date);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// not a reply, add to the list of message if this sender
|
|
||||||
await registerMessageInRoom(messageId, res[0].room_id, isSeen, envelope.date);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerReplyMessage(envelope, messageId, isSeen, ownerId) {
|
|
||||||
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 to begin
|
|
||||||
await isRoomGroup(rooms[0].room_id).then(async (isGroup) => {
|
|
||||||
if (isGroup) {
|
|
||||||
const hasSameMembers = await hasSameMembersAsParent(messageID, envelope.inReplyTo);
|
|
||||||
if (hasSameMembers) {
|
|
||||||
await registerMessageInRoom(messageId, rooms[0].room_id, isSeen, envelope.date);
|
|
||||||
} else {
|
|
||||||
// is a group and has not the same member as the previous message
|
|
||||||
// some recipient has been removed create a thread
|
|
||||||
const isDm = isDmOnEnvelope(envelope);
|
|
||||||
await createThread(envelope.subject, ownerId, messageId, rooms[0].room_id, isDm).then(async (threadId) => {
|
|
||||||
await registerMessageInThread(messageId, threadId, isSeen);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} 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 room = rooms[rooms.length-1];
|
|
||||||
const hasSameMembers = await hasSameMembersAsParent(messageID, envelope.inReplyTo);
|
|
||||||
if (hasSameMembers) {
|
|
||||||
await registerMessageInThread(messageId, room.room_id, isSeen);
|
|
||||||
} else {
|
|
||||||
// has not the same members so it is a derivation of this thread
|
|
||||||
// todo put this in a function and add default message in the reply chain
|
|
||||||
const isDm = isDmOnEnvelope(envelope);
|
|
||||||
await createThread(envelope.subject, ownerId, messageId, room.room_id, isDm).then(async (threadId) => {
|
|
||||||
await registerMessageInThread(messageId, threadId, isSeen);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
registerMessageInApp
|
|
||||||
};
|
|
||||||
|
|
||||||
39
back/mails/smtp/SmtpInstance.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import logger from "../../system/Logger";
|
||||||
|
import nodemailer, { Transporter } from "nodemailer";
|
||||||
|
|
||||||
|
export class SmtpInstance {
|
||||||
|
transporter: Transporter;
|
||||||
|
user: string;
|
||||||
|
|
||||||
|
constructor(account: { user: string; password: string, smtp_host: string, smtp_port: number }) {
|
||||||
|
this.user = account.user;
|
||||||
|
this.transporter = nodemailer.createTransport({
|
||||||
|
host: account.smtp_host,
|
||||||
|
port: account.smtp_port,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user: account.user,
|
||||||
|
pass: account.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMail(message: any) {
|
||||||
|
console.log(this.user)
|
||||||
|
console.log(message)
|
||||||
|
// const msg = {
|
||||||
|
// from: "",
|
||||||
|
// to: "",
|
||||||
|
// subject: "Hello ✔",
|
||||||
|
// text: "Hello world?",
|
||||||
|
// html: "<b>Hello world?</b>",
|
||||||
|
// };
|
||||||
|
// this.transporter.sendMail(msg, (err, message) => {
|
||||||
|
// if (err) {
|
||||||
|
// logger.err(err);
|
||||||
|
// throw err;
|
||||||
|
// }
|
||||||
|
// logger.log(message);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
||||||
17
back/mails/utils/envelopeUtils.ts
Normal 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;
|
||||||
|
}
|
||||||
53
back/mails/utils/mailBuilder.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export default class MailBuilder {
|
||||||
|
message: any;
|
||||||
|
constructor(message = {}) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
from(addresses: string[] | string): MailBuilder {
|
||||||
|
this.message.from = addresses;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
to(addresses: string[] | string): MailBuilder {
|
||||||
|
this.message.to = addresses;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
cc(addresses: string[] | string): MailBuilder {
|
||||||
|
this.message.cc = addresses;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bcc(addresses: string[] | string): MailBuilder {
|
||||||
|
this.message.bcc = addresses;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
subject(subject: string): MailBuilder {
|
||||||
|
this.message.subject = subject;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
text(textContent: string): MailBuilder {
|
||||||
|
this.message.text = textContent;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
html(htmlContent: string): MailBuilder {
|
||||||
|
this.message.html = htmlContent;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inReplyTo(messageID: string): MailBuilder {
|
||||||
|
this.message.inReplyTo = messageID;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inReplySubject(originSubject: string): MailBuilder {
|
||||||
|
// todo concate if multiple ?
|
||||||
|
this.message.subject = "RE: " + originSubject;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
// https://cr.yp.to/immhf/thread.html
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
function isDmOnEnvelope(envelope) {
|
|
||||||
const members =
|
|
||||||
envelope.bcc?.length +
|
|
||||||
envelope.cc?.length +
|
|
||||||
envelope.to?.length +
|
|
||||||
envelope.sender?.length +
|
|
||||||
envelope.from?.length;
|
|
||||||
return members === 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
isDmOnEnvelope,
|
|
||||||
};
|
|
||||||
4456
back/package-lock.json
generated
@@ -1,8 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build": "npx tsc",
|
||||||
|
"start": "node build/server.js",
|
||||||
|
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q build/server.js\"",
|
||||||
|
"clean": "rm -rf build"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@databases/mysql-test": "^4.0.2",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"ajv-formats": "^2.1.1",
|
"ajv-formats": "^2.1.1",
|
||||||
|
"colors": "^1.4.0",
|
||||||
"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",
|
||||||
@@ -12,11 +21,29 @@
|
|||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@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",
|
||||||
|
"@types/nodemailer": "^6.4.7",
|
||||||
|
"concurrently": "^8.0.1",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"jest-mysql": "^2.0.0"
|
"sql-mysql": "^1.2.0",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
|
"sqlparser": "^0.1.7",
|
||||||
|
"ts-jest": "^29.0.5",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^4.9.5"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "jest-mysql",
|
"preset": "ts-jest",
|
||||||
|
"setupFiles": [
|
||||||
|
"<rootDir>/test/.jest/setEnvVars.js"
|
||||||
|
],
|
||||||
|
"testEnvironment": "node",
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
"<rootDir>/test//**/*-test.[jt]s?(x)"
|
"<rootDir>/test//**/*-test.[jt]s?(x)"
|
||||||
]
|
]
|
||||||
|
|||||||
28
back/routes/account.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import express from "express";
|
||||||
|
import Account from "../abl/Account-abl";
|
||||||
|
import validator from "../validator/validator";
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all mailboxes and folders for an user
|
||||||
|
*/
|
||||||
|
router.get("/getAll", async (req, res) => {
|
||||||
|
await validator.validate("getAccounts", req.params, res, Account.getAll);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new mailbox inside the app
|
||||||
|
*/
|
||||||
|
router.post("/register", async (req, res) => {
|
||||||
|
await validator.validate("createAccount", req.body, res, Account.register);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all rooms from a mailbox
|
||||||
|
*/
|
||||||
|
router.get("/:mailboxId/rooms", async (req, res) => {
|
||||||
|
// todo offet limit
|
||||||
|
await validator.validate("getRooms", req.params, res, Account.getRooms);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
const statusCodes = require("../utils/statusCodes.js").statusCodes;
|
|
||||||
const express = require("express");
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
const Ajv = require("ajv");
|
|
||||||
const addFormats = require("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");
|
|
||||||
|
|
||||||
const validate_account = ajv.compile(schema_account);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all mailboxes and folders for an user
|
|
||||||
*/
|
|
||||||
router.get("/accounts", (req, res) => {
|
|
||||||
getAccounts().then((data) => {
|
|
||||||
res.status(statusCodes.OK).json(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/:mailboxId/rooms", async (req, res) => {
|
|
||||||
// todo use offset limit
|
|
||||||
await rooms(req.params, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/:roomId/messages", async (req, res) => {
|
|
||||||
const { roomId } = req.params;
|
|
||||||
await messages(req.params, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a new mailbox inside the app
|
|
||||||
*/
|
|
||||||
router.post("/account", async (req, res) => {
|
|
||||||
const valid = validate_account(req.body);
|
|
||||||
if (!valid) {
|
|
||||||
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validate_account.errors });
|
|
||||||
} else {
|
|
||||||
await addAccount(req.body, res);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
23
back/routes/message.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import express from "express";
|
||||||
|
import MessageAbl from "../abl/Message-abl";
|
||||||
|
import validator from "../validator/validator";
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/addFlag", async (req, res) => {
|
||||||
|
await validator.validate("addFlag", req.body, res, MessageAbl.addFlag);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/removeFlag", async (req, res) => {
|
||||||
|
await validator.validate("removeFlag", req.body, res, MessageAbl.removeFlag);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/deleteRemote", async(req, res) => {
|
||||||
|
await validator.validate("delete", req.body, res, MessageAbl.deleteRemoteOnly);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/delete", async(req, res) => {
|
||||||
|
await validator.validate("delete", req.body, res, MessageAbl.deleteEverywhere);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default router;
|
||||||
28
back/routes/room.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import express from "express";
|
||||||
|
import RoomAbl from "../abl/Room-abl";
|
||||||
|
import validator from "../validator/validator";
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all messages from a room
|
||||||
|
*/
|
||||||
|
router.get("/:roomId/messages", async (req, res) => {
|
||||||
|
await validator.validate("getMessages", req.params, res, RoomAbl.getMessages);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all members from a room
|
||||||
|
*/
|
||||||
|
router.get("/:roomId/members", async (req, res) => {
|
||||||
|
await validator.validate("getMembers", req.params, res, RoomAbl.getMembers);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/response", async (req, res) => {
|
||||||
|
await validator.validate("response", req.body, res, RoomAbl.response);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/delete", async (req, res) => {
|
||||||
|
await validator.validate("deleteRoom", req.body, res, RoomAbl.delete);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
42
back/saveLogic.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
if (message.isReply()) {
|
||||||
|
// todo transfer
|
||||||
|
if (message.replyToThread()) {
|
||||||
|
if (message.hasSameMember(parent)) {
|
||||||
|
addToThread();
|
||||||
|
} else {
|
||||||
|
createThread();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (message.isInGroup()) {
|
||||||
|
if (message.hasSameMember(parent)) {
|
||||||
|
addToGroup();
|
||||||
|
} else {
|
||||||
|
createThread();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (message.isFromChannelOwner()) {
|
||||||
|
addToChannel(); // todo not sure (with references)
|
||||||
|
} else {
|
||||||
|
createThread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (message.isFromUser()) {
|
||||||
|
if (message.isDm()) {
|
||||||
|
if (firstMessage()) {
|
||||||
|
createDM();
|
||||||
|
} else {
|
||||||
|
addToDm();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createRoom();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (firstMessage()) {
|
||||||
|
createRoom();
|
||||||
|
} else {
|
||||||
|
addToRoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"email": { "type": "string", "format": "email" },
|
|
||||||
"pwd": { "type": "string" },
|
|
||||||
"xoauth": { "type": "string" },
|
|
||||||
"xoauth2": { "type": "string" },
|
|
||||||
"host": { "type": "string", "format": "hostname" },
|
|
||||||
"port": { "type": "number", "maximum": 65535 },
|
|
||||||
"tls": { "type": "boolean" }
|
|
||||||
},
|
|
||||||
"required": ["email", "host", "port", "tls"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
const express = require("express");
|
|
||||||
const cors = require("cors");
|
|
||||||
const app = express();
|
|
||||||
const { ImapSync } = require("./mails/imap/ImapSync");
|
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(
|
|
||||||
express.urlencoded({
|
|
||||||
extended: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
app.use(cors());
|
|
||||||
app.listen(process.env.PORT || 5500);
|
|
||||||
|
|
||||||
const mailRouter = require("./routes/mail");
|
|
||||||
app.use("/api/mail", mailRouter);
|
|
||||||
|
|
||||||
const imapSync = new ImapSync();
|
|
||||||
imapSync.init()
|
|
||||||
41
back/server.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import express from "express";
|
||||||
|
import cors from "cors";
|
||||||
|
const app = express();
|
||||||
|
import { execQueryAsync, execQuery } from "./db/db";
|
||||||
|
import emailManager from "./mails/EmailManager";
|
||||||
|
import accountRouter from "./routes/account";
|
||||||
|
import roomRouter from "./routes/room";
|
||||||
|
import messageRouter from "./routes/message";
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(
|
||||||
|
express.urlencoded({
|
||||||
|
extended: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.use(cors());
|
||||||
|
app.listen(process.env.PORT || 5500);
|
||||||
|
|
||||||
|
app.use("/api/account", accountRouter);
|
||||||
|
app.use("/api/room", roomRouter);
|
||||||
|
app.use("/api/message", messageRouter);
|
||||||
|
|
||||||
|
// create imap and smtp instances for each account
|
||||||
|
emailManager.init();
|
||||||
|
|
||||||
|
// debug reset all tables
|
||||||
|
const shouldReset = false;
|
||||||
|
if (shouldReset) {
|
||||||
|
const query = "SELECT table_name FROM INFORMATION_SCHEMA.tables WHERE table_schema = 'mail'";
|
||||||
|
execQueryAsync(query, []).then((results) => {
|
||||||
|
execQuery("SET FOREIGN_KEY_CHECKS=0", []);
|
||||||
|
results.map((table) => {
|
||||||
|
if (table.table_name == "app_account") return;
|
||||||
|
if (table.table_name == "address") return;
|
||||||
|
if (table.table_name == "mailbox") return;
|
||||||
|
console.log(table.table_name);
|
||||||
|
execQuery("DELETE FROM " + table.table_name, []);
|
||||||
|
// execQuery("DROP TABLE " + table.table_name, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
52
back/system/Logger.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import color from "colors";
|
||||||
|
|
||||||
|
const logType = {
|
||||||
|
LOG: 0,
|
||||||
|
DEBUG: 1,
|
||||||
|
WARN: 2,
|
||||||
|
ERR: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
print(header: string, message: string, type: number) {
|
||||||
|
const content = `[${this._timestamp}] - [${header}] -- ${message}`;
|
||||||
|
switch (type) {
|
||||||
|
case logType.LOG:
|
||||||
|
console.log(content);
|
||||||
|
break;
|
||||||
|
case logType.DEBUG:
|
||||||
|
console.debug(content);
|
||||||
|
break;
|
||||||
|
case logType.WARN:
|
||||||
|
console.warn(content);
|
||||||
|
break;
|
||||||
|
case logType.ERR:
|
||||||
|
console.error(content);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log = (...message: any[]) => this.print("LOG", `${message}`, logType.LOG);
|
||||||
|
err = (...message: any[]) => this.print("ERR", `${message}`, logType.ERR);
|
||||||
|
warn = (...message: any[]) => this.print("WARN", `${message}`, logType.WARN);
|
||||||
|
debug = (...message: any[]) => this.print("DEBUG", `${message}`, logType.DEBUG);
|
||||||
|
success = (...message: any[]) => this.print("SUCCESS".green, `${message}`, logType.LOG);
|
||||||
|
|
||||||
|
get = (url: string, state: string, ...message: string[]) =>
|
||||||
|
this.print("GET".green, `[${state} - ${url}]: ${message}`, logType.LOG);
|
||||||
|
post = (url: string, state: string, ...message: string[]) =>
|
||||||
|
this.print("POST".blue, `[${state} - ${url}]: ${message}`, logType.LOG);
|
||||||
|
put = (url: string, state: string, ...message: string[]) =>
|
||||||
|
this.print("PUT".yellow, `[${state} - ${url}]: ${message}`, logType.LOG);
|
||||||
|
del = (url: string, state: string, ...message: string[]) =>
|
||||||
|
this.print("DEL".red, `[${state} - ${url}]: ${message}`, logType.LOG);
|
||||||
|
|
||||||
|
get _timestamp() {
|
||||||
|
return new Date().toLocaleString("en-GB", { hour12: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = new Logger();
|
||||||
|
export default logger;
|
||||||
1
back/test/.jest/setEnvVars.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
process.env.NODE_ENV = "test";
|
||||||
48
back/test/db/api-tes.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
process.env.NODE_ENV = "test";
|
||||||
|
import { jest, describe, it, expect } from "@jest/globals";
|
||||||
|
import { execQueryAsync, execQuery } from "../../db/db";
|
||||||
|
import { createRoom, registerMessageInRoom } from "../../db/message/saveMessage-db";
|
||||||
|
import { registerFlag, registerMessage } from "../../db/message/storeMessage-db";
|
||||||
|
import { getFlagId } from "../../db/utils/mail";
|
||||||
|
import { RoomType } from "../../mails/message/saveMessage";
|
||||||
|
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
console.log(await execQueryAsync(`SHOW TABLES`, []));
|
||||||
|
// mocked(incrementNotSeenRoom).mockImplementation(db.incrementNotSeenRoom);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const query = "SELECT table_name FROM INFORMATION_SCHEMA.tables WHERE table_schema = 'mail_test'";
|
||||||
|
execQueryAsync(query, []).then((results) => {
|
||||||
|
execQuery("SET FOREIGN_KEY_CHECKS=0", []);
|
||||||
|
results.map((table) => {
|
||||||
|
execQuery("DELETE FROM " + table.table_name, []);
|
||||||
|
// execQuery("DROP TABLE " + table.table_name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const insertMessageWithFlag = async (flags: string[]): Promise<number> => {
|
||||||
|
const messageId = await registerMessage("", 0, "");
|
||||||
|
flags.forEach(async (flag) => {
|
||||||
|
const flagId = await getFlagId(flag);
|
||||||
|
await registerFlag(messageId, flagId);
|
||||||
|
});
|
||||||
|
return messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("api-db", () => {
|
||||||
|
it.only("should count the number of unseen message in a room", async () => {
|
||||||
|
const msgIdSeen = await insertMessageWithFlag(["\\\\Seen"]);
|
||||||
|
const msgIdNotSeen = await insertMessageWithFlag([]);
|
||||||
|
const msgIdNotSeen2 = await insertMessageWithFlag([]);
|
||||||
|
const roomId = await createRoom("roomName", 0, msgIdSeen, RoomType.ROOM);
|
||||||
|
await registerMessageInRoom(msgIdSeen, roomId, "");
|
||||||
|
await registerMessageInRoom(msgIdNotSeen, roomId, "");
|
||||||
|
await registerMessageInRoom(msgIdNotSeen2, roomId, "");
|
||||||
|
|
||||||
|
const res =
|
||||||
|
expect()
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
process.env["NODE_DEV"] = "TEST";
|
|
||||||
const { saveFromParsedData } = require("../../mails/storeMessage");
|
|
||||||
const { TestDb } = require("../sql/test-utilsDb");
|
|
||||||
const MYSQL = require("../../db/config.json").MYSQL;
|
|
||||||
|
|
||||||
let db;
|
|
||||||
beforeAll(async () => {
|
|
||||||
const options = {
|
|
||||||
databaseOptions: {
|
|
||||||
host: "localhost",
|
|
||||||
port: 3306,
|
|
||||||
user: MYSQL.user,
|
|
||||||
password: MYSQL.pwd,
|
|
||||||
database: "mail_test",
|
|
||||||
},
|
|
||||||
dbSchema: "../../sql/structureV2.sql",
|
|
||||||
};
|
|
||||||
db = new TestDb(options);
|
|
||||||
await db.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
db.cleanTables();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("saveMessage", async () => {
|
|
||||||
describe("rooms", async () => {
|
|
||||||
it("messages not related and from same sender should be in the same room", async () => {
|
|
||||||
await saveFromParsedData(
|
|
||||||
{
|
|
||||||
to: { value: [{ address: "address1@email.com" }] },
|
|
||||||
from: { value: [{ address: "address2@email.com" }] },
|
|
||||||
messageId: "<messageId1>",
|
|
||||||
},
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
await saveFromParsedData(
|
|
||||||
{
|
|
||||||
to: { value: [{ address: "address1@email.com" }] },
|
|
||||||
from: { value: [{ address: "address2@email.com" }] },
|
|
||||||
messageId: "<messageId2>",
|
|
||||||
},
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
// todo call parser
|
|
||||||
const query = ""
|
|
||||||
db.execQueryAsync().then((res) => {
|
|
||||||
expect(res.length).toBe(2);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
229
back/test/mail/saveMessage-test.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
jest.mock("mysql");
|
||||||
|
import mysql from "mysql";
|
||||||
|
mysql.createConnection = jest.fn();
|
||||||
|
mysql.createConnection.mockImplementation(() => {
|
||||||
|
return { connect: () => new Promise((resolve, rejects) => resolve(true)) };
|
||||||
|
});
|
||||||
|
|
||||||
|
import saveMessageDatabase from "../test-utils/db/test-saveMessage";
|
||||||
|
import { generateAttrs, generateUsers, randomInt } from "../test-utils/test-attrsUtils";
|
||||||
|
import { jest, describe, it, expect } from "@jest/globals";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import registerMessageInApp, { RoomType } from "../../mails/message/saveMessage";
|
||||||
|
|
||||||
|
const db = new saveMessageDatabase(generateUsers(5));
|
||||||
|
const ownUser = db.users[0];
|
||||||
|
const messageId = 1;
|
||||||
|
const boxId = 1;
|
||||||
|
|
||||||
|
jest.mock("../../db/utils/mail", () => {
|
||||||
|
return {
|
||||||
|
findRoomByOwner: jest.fn(),
|
||||||
|
getAddressId: jest.fn(),
|
||||||
|
getUserIdOfMailbox: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
jest.mock("../../db/message/saveMessage-db", () => {
|
||||||
|
return {
|
||||||
|
createRoom: jest.fn(),
|
||||||
|
registerMessageInRoom: jest.fn(),
|
||||||
|
getRoomType: jest.fn(),
|
||||||
|
findRoomsFromMessage: jest.fn(),
|
||||||
|
hasSameMembersAsParent: jest.fn(),
|
||||||
|
registerThread: jest.fn(),
|
||||||
|
registerMember: jest.fn(),
|
||||||
|
getAllMembers: jest.fn(),
|
||||||
|
getThreadInfo: jest.fn(),
|
||||||
|
getThreadInfoOnId: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
import { getAddressId, getUserIdOfMailbox, findRoomByOwner } from "../../db/utils/mail";
|
||||||
|
import {
|
||||||
|
createRoom,
|
||||||
|
registerMessageInRoom,
|
||||||
|
getRoomType,
|
||||||
|
findRoomsFromMessage,
|
||||||
|
hasSameMembersAsParent,
|
||||||
|
registerThread,
|
||||||
|
registerMember,
|
||||||
|
getAllMembers,
|
||||||
|
getThreadInfo,
|
||||||
|
getThreadInfoOnId,
|
||||||
|
} from "../../db/message/saveMessage-db";
|
||||||
|
import { AttrsWithEnvelopeTest, createReplyWithSameMembers } from "../test-utils/test-messageUtils";
|
||||||
|
// todo esbuild
|
||||||
|
// 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
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
mocked(getAddressId).mockImplementation(db.getAddressId);
|
||||||
|
mocked(getUserIdOfMailbox).mockImplementation(db.getUserIdOfMailbox);
|
||||||
|
mocked(findRoomByOwner).mockImplementation(db.findRoomByOwner);
|
||||||
|
|
||||||
|
mocked(createRoom).mockImplementation(db.createRoom);
|
||||||
|
mocked(registerMessageInRoom).mockImplementation(db.registerMessageInRoom);
|
||||||
|
mocked(getRoomType).mockImplementation(db.getRoomType);
|
||||||
|
mocked(findRoomsFromMessage).mockImplementation(db.findRoomsFromMessage);
|
||||||
|
mocked(hasSameMembersAsParent).mockImplementation(db.hasSameMembersAsParent);
|
||||||
|
mocked(registerThread).mockImplementation(db.registerThread);
|
||||||
|
mocked(registerMember).mockImplementation(db.registerMember);
|
||||||
|
mocked(getAllMembers).mockImplementation(db.getAllMembers);
|
||||||
|
mocked(getThreadInfo).mockImplementation(db.getThreadInfo);
|
||||||
|
mocked(getThreadInfoOnId).mockImplementation(db.getThreadInfoOnId);
|
||||||
|
});
|
||||||
|
|
||||||
|
let msgFromUs_1: AttrsWithEnvelopeTest;
|
||||||
|
let replyTo1_2: AttrsWithEnvelopeTest;
|
||||||
|
let replyTo2_3: AttrsWithEnvelopeTest;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
msgFromUs_1 = {
|
||||||
|
attrs: generateAttrs({ from: [ownUser.user], to: [db.users[1].user], messageId: "1" }),
|
||||||
|
message_id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
replyTo1_2 = {
|
||||||
|
attrs: generateAttrs({ from: [ownUser.user], to: [db.users[1].user], messageId: "2", inReplyTo: "1" }),
|
||||||
|
message_id: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
replyTo2_3 = {
|
||||||
|
attrs: generateAttrs({
|
||||||
|
from: [ownUser.user],
|
||||||
|
to: [db.users[1].user, db.users[2].user],
|
||||||
|
messageId: "3",
|
||||||
|
inReplyTo: "2",
|
||||||
|
}),
|
||||||
|
message_id: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.clear();
|
||||||
|
db.messages.push(msgFromUs_1);
|
||||||
|
db.messages.push(replyTo1_2);
|
||||||
|
db.messages.push(replyTo2_3);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("saveMessage", () => {
|
||||||
|
describe("functions", () => {
|
||||||
|
it("isFromUs", async () => {
|
||||||
|
const attrs = generateAttrs({ from: [ownUser.user], to: [db.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: [db.users[2].user], to: [db.users[1].user] });
|
||||||
|
const register2 = new registerMessageInApp(messageId, attrs2, boxId);
|
||||||
|
await register2.init();
|
||||||
|
const res2 = await register2.isFromUs();
|
||||||
|
|
||||||
|
expect(res2).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("room creation", () => {
|
||||||
|
it("should create a DM when there is a new first message from us to one recipient", async () => {
|
||||||
|
const attrs = generateAttrs({ from: [ownUser.user], to: [db.users[1].user] });
|
||||||
|
|
||||||
|
const register = new registerMessageInApp(messageId, attrs, boxId);
|
||||||
|
|
||||||
|
const createOrRegisterOnExistence = jest
|
||||||
|
.spyOn(register, "createOrRegisterOnExistence")
|
||||||
|
.mockImplementation((owner: number, roomType: RoomType) => new Promise((resolve, reject) => resolve()));
|
||||||
|
|
||||||
|
await register.save();
|
||||||
|
|
||||||
|
// the owner of the room will be the recipient (not us)
|
||||||
|
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(db.users[1].id, RoomType.DM);
|
||||||
|
});
|
||||||
|
it("should create a GROUP when there is a new first message from us to multiple recipients", async () => {
|
||||||
|
const attrs = generateAttrs({ from: [ownUser.user], to: [db.users[1].user, db.users[2].user] });
|
||||||
|
|
||||||
|
const register = new registerMessageInApp(messageId, attrs, boxId);
|
||||||
|
|
||||||
|
const initiateRoom = jest
|
||||||
|
.spyOn(register, "initiateRoom")
|
||||||
|
.mockImplementation((owner: number, roomType: RoomType) => Promise.resolve(1));
|
||||||
|
|
||||||
|
await register.save();
|
||||||
|
|
||||||
|
expect(initiateRoom).toHaveBeenCalledWith(ownUser.id, RoomType.GROUP);
|
||||||
|
});
|
||||||
|
// 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", () => {
|
||||||
|
|
||||||
|
// });
|
||||||
|
it("should create a ROOM when there is a new first message from other to me only", async () => {
|
||||||
|
const attrs = generateAttrs({ from: [db.users[1].user], to: [ownUser.user] });
|
||||||
|
|
||||||
|
const register = new registerMessageInApp(messageId, attrs, boxId);
|
||||||
|
|
||||||
|
const createOrRegisterOnExistence = jest
|
||||||
|
.spyOn(register, "createOrRegisterOnExistence")
|
||||||
|
.mockImplementation((owner: number, roomType: RoomType) => {
|
||||||
|
return new Promise((resolve, reject) => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
await register.save();
|
||||||
|
|
||||||
|
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(db.users[1].id, RoomType.ROOM);
|
||||||
|
});
|
||||||
|
it("should create THREAD when reply to a message in a DM", async () => {
|
||||||
|
let register = new registerMessageInApp(msgFromUs_1.message_id, msgFromUs_1.attrs, boxId);
|
||||||
|
await register.save();
|
||||||
|
register = new registerMessageInApp(replyTo1_2.message_id, replyTo1_2.attrs, boxId);
|
||||||
|
await register.save();
|
||||||
|
|
||||||
|
const thread = db.rooms.find((room) => room.is_thread);
|
||||||
|
expect(thread.room_type).toBe(RoomType.THREAD);
|
||||||
|
expect(thread.root_id).toBe(0);
|
||||||
|
expect(thread.parent_id).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create THREAD when reply in THREAD with different members", async () => {
|
||||||
|
let register = new registerMessageInApp(msgFromUs_1.message_id, msgFromUs_1.attrs, boxId);
|
||||||
|
await register.save();
|
||||||
|
register = new registerMessageInApp(replyTo1_2.message_id, replyTo1_2.attrs, boxId);
|
||||||
|
await register.save();
|
||||||
|
register = new registerMessageInApp(replyTo2_3.message_id, replyTo2_3.attrs, boxId);
|
||||||
|
await register.save();
|
||||||
|
|
||||||
|
const threads = db.rooms.filter((room) => room.is_thread);
|
||||||
|
expect(threads).toHaveLength(2);
|
||||||
|
const thread = threads[1];
|
||||||
|
expect(thread.room_type).toBe(RoomType.THREAD);
|
||||||
|
expect(thread.root_id).toBe(0);
|
||||||
|
expect(thread.parent_id).toBe(1);
|
||||||
|
expect(thread.members).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("joins room", () => {
|
||||||
|
it("should add message to THREAD when reply to a message in it with same members", async () => {
|
||||||
|
let register = new registerMessageInApp(msgFromUs_1.message_id, msgFromUs_1.attrs, boxId);
|
||||||
|
await register.save();
|
||||||
|
register = new registerMessageInApp(replyTo1_2.message_id, replyTo1_2.attrs, boxId);
|
||||||
|
await register.save();
|
||||||
|
let newReplyInThread = createReplyWithSameMembers(replyTo1_2, db);
|
||||||
|
register = new registerMessageInApp(newReplyInThread.message_id, newReplyInThread.attrs, boxId);
|
||||||
|
await register.save();
|
||||||
|
|
||||||
|
expect(db.rooms).toHaveLength(2);
|
||||||
|
expect(db.room_message.filter((message) => message.room_id === db.rooms[1].room_id)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
back/test/mail/utils/envelopeUtils-test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
19
back/test/sql/saveMessageApp-tes.js
Normal 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("", () => {
|
||||||
|
|
||||||
|
// });
|
||||||
|
// });
|
||||||
@@ -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("", () => {
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const mysql = require("mysql");
|
const mysql from ";
|
||||||
|
|
||||||
export class TestDb {
|
export class TestDb {
|
||||||
constructor (options) {
|
constructor (options) {
|
||||||
|
|||||||
35
back/test/test-utils/db/test-db.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const sqlite3 = require("sqlite3").verbose();
|
||||||
|
const sqlMysql = require("sql-mysql");
|
||||||
|
export const db = new sqlite3.Database(":memory:");
|
||||||
|
var Parser = require("sqlparser");
|
||||||
|
import fs from "fs";
|
||||||
|
// const mysqlQuery = 'SELECT * FROM users WHERE age > 18';
|
||||||
|
// const sqliteQuery = sqlMysql.toSqlite(mysqlQuery);
|
||||||
|
// console.log(sqliteQuery)
|
||||||
|
export function initDatabase() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const pathSQL = __dirname + "/../../../db/structureV2.sql";
|
||||||
|
// fs.readdir(__dirname + "/../../../db/structureV2.sql", (err, files) => {
|
||||||
|
// if (err) console.log(err);
|
||||||
|
// else {
|
||||||
|
// console.log("\nCurrent directory filenames:");
|
||||||
|
// files.forEach((file) => {
|
||||||
|
// console.log(file);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
const sqlTables = fs.readFileSync(pathSQL, "utf8");
|
||||||
|
// const sqliteTables = sqlMysql.toSqlite(sqlTables);
|
||||||
|
var sqliteTables = Parser.parse(sqlTables);
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run(sqliteTables, (error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
157
back/test/test-utils/db/test-saveMessage.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { AttrsWithEnvelope, User } from "../../../interfaces/mail/attrs.interface";
|
||||||
|
import { RoomType } from "../../../mails/message/saveMessage";
|
||||||
|
import { getMembers } from "../../../mails/utils/envelopeUtils";
|
||||||
|
import { hasSameElements } from "../../../utils/array";
|
||||||
|
import { generateUsers, UserTest } from "../test-attrsUtils";
|
||||||
|
|
||||||
|
interface Room {
|
||||||
|
room_id: number;
|
||||||
|
room_name: string;
|
||||||
|
owner_id: number;
|
||||||
|
message_id: number;
|
||||||
|
room_type: RoomType;
|
||||||
|
notSeen: number;
|
||||||
|
lastUpdate: string;
|
||||||
|
members?: UserTest[];
|
||||||
|
is_thread?: boolean;
|
||||||
|
parent_id?: number;
|
||||||
|
root_id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class saveMessageDatabase {
|
||||||
|
rooms: Room[];
|
||||||
|
roomId: number;
|
||||||
|
messages: { attrs: AttrsWithEnvelope; message_id: number }[];
|
||||||
|
room_message: { room_id: number; message_id: number }[];
|
||||||
|
users: UserTest[];
|
||||||
|
|
||||||
|
constructor(_users) {
|
||||||
|
this.rooms = [];
|
||||||
|
this.messages = [];
|
||||||
|
this.room_message = [];
|
||||||
|
this.users = _users;
|
||||||
|
this.roomId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.rooms = [];
|
||||||
|
this.messages = [];
|
||||||
|
this.room_message = [];
|
||||||
|
this.roomId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findRoomById = (roomId: number): Room => {
|
||||||
|
return this.rooms.find((room) => room.room_id === roomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
_findUserByMailbox = (mailbox: string): UserTest => {
|
||||||
|
return this.users.find((user) => user.user.mailbox === mailbox);
|
||||||
|
};
|
||||||
|
|
||||||
|
createRoom = (
|
||||||
|
roomName: string | null | undefined,
|
||||||
|
ownerId: number,
|
||||||
|
messageId: number,
|
||||||
|
roomType: RoomType,
|
||||||
|
): Promise<number> => {
|
||||||
|
this.rooms.push({
|
||||||
|
room_id: this.roomId,
|
||||||
|
room_name: roomName,
|
||||||
|
owner_id: ownerId,
|
||||||
|
message_id: messageId,
|
||||||
|
room_type: roomType,
|
||||||
|
notSeen: 0,
|
||||||
|
lastUpdate: "0",
|
||||||
|
});
|
||||||
|
this.roomId++;
|
||||||
|
return Promise.resolve(this.roomId - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerMessageInRoom = (messageId: number, roomId: number, idate: string | undefined | null): Promise<void> => {
|
||||||
|
this.room_message.push({ message_id: messageId, room_id: roomId });
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
getRoomType = (roomId: number): Promise<{ room_type: number }[]> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resolve([{ room_type: this.rooms.find((room) => room.room_id == roomId).room_type }]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
findRoomsFromMessage = (messageID: string): Promise<{ room_id: number }[]> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const rooms = this.rooms.filter((room) => room.message_id.toString() === messageID);
|
||||||
|
const res: { room_id: number }[] = [];
|
||||||
|
rooms.forEach((room) => {
|
||||||
|
res.push({ room_id: room.room_id });
|
||||||
|
});
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
hasSameMembersAsParent = (messageId: number, messageID: string): Promise<boolean> => {
|
||||||
|
const msg1 = this.messages.find((message) => message.attrs.envelope.messageId === messageID);
|
||||||
|
const msg2 = this.messages.find((message) => message.message_id === messageId);
|
||||||
|
const members1 = getMembers(msg1.attrs.envelope);
|
||||||
|
const members2 = getMembers(msg2.attrs.envelope);
|
||||||
|
let ids1 = [];
|
||||||
|
let ids2 = [];
|
||||||
|
members1.forEach((member) => ids1.push(this._findUserByMailbox(member.mailbox).id));
|
||||||
|
members2.forEach((member) => ids2.push(this._findUserByMailbox(member.mailbox).id));
|
||||||
|
return Promise.resolve(hasSameElements(ids1, ids2));
|
||||||
|
};
|
||||||
|
|
||||||
|
registerThread = async (roomId: number, parentId: number, rootId: number) => {
|
||||||
|
const room = this._findRoomById(roomId);
|
||||||
|
room.is_thread = true;
|
||||||
|
room.parent_id = parentId;
|
||||||
|
room.root_id = rootId;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerMember = (roomId: number, memberId: number): Promise<any> => {
|
||||||
|
const room = this._findRoomById(roomId);
|
||||||
|
if (!room.members) room.members = [];
|
||||||
|
room.members.push(this.users.find((user) => user.id == memberId));
|
||||||
|
return Promise.resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
getAllMembers = (messageId: number): Promise<any> => {
|
||||||
|
const message = this.messages.find((message) => message.message_id === messageId);
|
||||||
|
let res = "";
|
||||||
|
getMembers(message.attrs.envelope).forEach((member) => {
|
||||||
|
res += this.users.find((user) => user.user.mailbox === member.mailbox).id + ",";
|
||||||
|
});
|
||||||
|
res = res.substring(0, res.length - 1);
|
||||||
|
return Promise.resolve([{ id: res }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
getThreadInfo = (messageID: string): Promise<{ room_id: number; root_id: number }[]> => {
|
||||||
|
const room = this.rooms.find((room) => room.message_id.toString() === messageID);
|
||||||
|
return Promise.resolve([{ room_id: room.room_id, root_id: room.root_id }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
getThreadInfoOnId = (threadId: number): Promise<{ room_id: number; root_id: number }[]> => {
|
||||||
|
const room = this._findRoomById(threadId);
|
||||||
|
return Promise.resolve([{ room_id: room.root_id, root_id: room.root_id }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
findRoomByOwner = (ownerId: number): Promise<{ room_id: number }[]> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const rooms = this.rooms.filter((room) => room.owner_id === ownerId);
|
||||||
|
const res = [];
|
||||||
|
rooms.forEach((room) => {
|
||||||
|
res.push({ room_id: room.room_id });
|
||||||
|
});
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getAddressId = (email: string, name?: string): Promise<number> => {
|
||||||
|
const match = this.users.find((user) => user.user.mailbox + "@" + user.user.host == email);
|
||||||
|
return new Promise((resolve, reject) => resolve(match?.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
getUserIdOfMailbox = (boxId: number): Promise<{ user_id: number }[]> => {
|
||||||
|
return new Promise((resolve, rejects) => resolve([{ user_id: this.users[0].id }]));
|
||||||
|
};
|
||||||
|
}
|
||||||
204
back/test/test-utils/names.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
const names = [
|
||||||
|
"James",
|
||||||
|
"Mary",
|
||||||
|
"Robert",
|
||||||
|
"Patricia",
|
||||||
|
"John",
|
||||||
|
"Jennifer",
|
||||||
|
"Michael",
|
||||||
|
"Linda",
|
||||||
|
"David",
|
||||||
|
"Elizabeth",
|
||||||
|
"William",
|
||||||
|
"Barbara",
|
||||||
|
"Richard",
|
||||||
|
"Susan",
|
||||||
|
"Joseph",
|
||||||
|
"Jessica",
|
||||||
|
"Thomas",
|
||||||
|
"Sarah",
|
||||||
|
"Charles",
|
||||||
|
"Karen",
|
||||||
|
"Christopher",
|
||||||
|
"Lisa",
|
||||||
|
"Daniel",
|
||||||
|
"Nancy",
|
||||||
|
"Matthew",
|
||||||
|
"Betty",
|
||||||
|
"Anthony",
|
||||||
|
"Margaret",
|
||||||
|
"Mark",
|
||||||
|
"Sandra",
|
||||||
|
"Donald",
|
||||||
|
"Ashley",
|
||||||
|
"Steven",
|
||||||
|
"Kimberly",
|
||||||
|
"Paul",
|
||||||
|
"Emily",
|
||||||
|
"Andrew",
|
||||||
|
"Donna",
|
||||||
|
"Joshua",
|
||||||
|
"Michelle",
|
||||||
|
"Kenneth",
|
||||||
|
"Carol",
|
||||||
|
"Kevin",
|
||||||
|
"Amanda",
|
||||||
|
"Brian",
|
||||||
|
"Dorothy",
|
||||||
|
"George",
|
||||||
|
"Melissa",
|
||||||
|
"Timothy",
|
||||||
|
"Deborah",
|
||||||
|
"Ronald",
|
||||||
|
"Stephanie",
|
||||||
|
"Edward",
|
||||||
|
"Rebecca",
|
||||||
|
"Jason",
|
||||||
|
"Sharon",
|
||||||
|
"Jeffrey",
|
||||||
|
"Laura",
|
||||||
|
"Ryan",
|
||||||
|
"Cynthia",
|
||||||
|
"Jacob",
|
||||||
|
"Kathleen",
|
||||||
|
"Gary",
|
||||||
|
"Amy",
|
||||||
|
"Nicholas",
|
||||||
|
"Angela",
|
||||||
|
"Eric",
|
||||||
|
"Shirley",
|
||||||
|
"Jonathan",
|
||||||
|
"Anna",
|
||||||
|
"Stephen",
|
||||||
|
"Brenda",
|
||||||
|
"Larry",
|
||||||
|
"Pamela",
|
||||||
|
"Justin",
|
||||||
|
"Emma",
|
||||||
|
"Scott",
|
||||||
|
"Nicole",
|
||||||
|
"Brandon",
|
||||||
|
"Helen",
|
||||||
|
"Benjamin",
|
||||||
|
"Samantha",
|
||||||
|
"Samuel",
|
||||||
|
"Katherine",
|
||||||
|
"Gregory",
|
||||||
|
"Christine",
|
||||||
|
"Alexander",
|
||||||
|
"Debra",
|
||||||
|
"Frank",
|
||||||
|
"Rachel",
|
||||||
|
"Patrick",
|
||||||
|
"Carolyn",
|
||||||
|
"Raymond",
|
||||||
|
"Janet",
|
||||||
|
"Jack",
|
||||||
|
"Catherine",
|
||||||
|
"Dennis",
|
||||||
|
"Maria",
|
||||||
|
"Jerry",
|
||||||
|
"Heather",
|
||||||
|
"Tyler",
|
||||||
|
"Diane",
|
||||||
|
"Aaron",
|
||||||
|
"Ruth",
|
||||||
|
"Jose",
|
||||||
|
"Julie",
|
||||||
|
"Adam",
|
||||||
|
"Olivia",
|
||||||
|
"Nathan",
|
||||||
|
"Joyce",
|
||||||
|
"Henry",
|
||||||
|
"Virginia",
|
||||||
|
"Douglas",
|
||||||
|
"Victoria",
|
||||||
|
"Zachary",
|
||||||
|
"Kelly",
|
||||||
|
"Peter",
|
||||||
|
"Lauren",
|
||||||
|
"Kyle",
|
||||||
|
"Christina",
|
||||||
|
"Ethan",
|
||||||
|
"Joan",
|
||||||
|
"Walter",
|
||||||
|
"Evelyn",
|
||||||
|
"Noah",
|
||||||
|
"Judith",
|
||||||
|
"Jeremy",
|
||||||
|
"Megan",
|
||||||
|
"Christian",
|
||||||
|
"Andrea",
|
||||||
|
"Keith",
|
||||||
|
"Cheryl",
|
||||||
|
"Roger",
|
||||||
|
"Hannah",
|
||||||
|
"Terry",
|
||||||
|
"Jacqueline",
|
||||||
|
"Gerald",
|
||||||
|
"Martha",
|
||||||
|
"Harold",
|
||||||
|
"Gloria",
|
||||||
|
"Sean",
|
||||||
|
"Teresa",
|
||||||
|
"Austin",
|
||||||
|
"Ann",
|
||||||
|
"Carl",
|
||||||
|
"Sara",
|
||||||
|
"Arthur",
|
||||||
|
"Madison",
|
||||||
|
"Lawrence",
|
||||||
|
"Frances",
|
||||||
|
"Dylan",
|
||||||
|
"Kathryn",
|
||||||
|
"Jesse",
|
||||||
|
"Janice",
|
||||||
|
"Jordan",
|
||||||
|
"Jean",
|
||||||
|
"Bryan",
|
||||||
|
"Abigail",
|
||||||
|
"Billy",
|
||||||
|
"Alice",
|
||||||
|
"Joe",
|
||||||
|
"Julia",
|
||||||
|
"Bruce",
|
||||||
|
"Judy",
|
||||||
|
"Gabriel",
|
||||||
|
"Sophia",
|
||||||
|
"Logan",
|
||||||
|
"Grace",
|
||||||
|
"Albert",
|
||||||
|
"Denise",
|
||||||
|
"Willie",
|
||||||
|
"Amber",
|
||||||
|
"Alan",
|
||||||
|
"Doris",
|
||||||
|
"Juan",
|
||||||
|
"Marilyn",
|
||||||
|
"Wayne",
|
||||||
|
"Danielle",
|
||||||
|
"Elijah",
|
||||||
|
"Beverly",
|
||||||
|
"Randy",
|
||||||
|
"Isabella",
|
||||||
|
"Roy",
|
||||||
|
"Theresa",
|
||||||
|
"Vincent",
|
||||||
|
"Diana",
|
||||||
|
"Ralph",
|
||||||
|
"Natalie",
|
||||||
|
"Eugene",
|
||||||
|
"Brittany",
|
||||||
|
"Russell",
|
||||||
|
"Charlotte",
|
||||||
|
"Bobby",
|
||||||
|
"Marie",
|
||||||
|
"Mason",
|
||||||
|
"Kayla",
|
||||||
|
"Philip",
|
||||||
|
"Alexis",
|
||||||
|
"Louis",
|
||||||
|
"Lori",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default names;
|
||||||
75
back/test/test-utils/test-attrsUtils.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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),
|
||||||
|
};
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserTest {
|
||||||
|
user: User;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateUsers(nb: number): UserTest[] {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomInt(length: number): number {
|
||||||
|
return parseInt((Math.random() * Math.pow(10, length)).toFixed());
|
||||||
|
}
|
||||||
17
back/test/test-utils/test-messageUtils.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { randomInt } from "crypto";
|
||||||
|
import { AttrsWithEnvelope } from "../../interfaces/mail/attrs.interface";
|
||||||
|
import saveMessageDatabase from "./db/test-saveMessage";
|
||||||
|
|
||||||
|
export interface AttrsWithEnvelopeTest {
|
||||||
|
attrs: AttrsWithEnvelope;
|
||||||
|
message_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createReplyWithSameMembers(origin: AttrsWithEnvelopeTest, db?: saveMessageDatabase): AttrsWithEnvelopeTest {
|
||||||
|
const reply = JSON.parse(JSON.stringify(origin));
|
||||||
|
reply.attrs.envelope.inReplyTo = origin.attrs.envelope.messageId;
|
||||||
|
reply.message_id = randomInt(5);
|
||||||
|
reply.attrs.envelope.messageId = reply.message_id.toString();
|
||||||
|
db.messages.push(reply);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
12
back/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./build",
|
||||||
|
"target": "es6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node", "jest"],
|
||||||
|
}
|
||||||
|
}
|
||||||
16
back/utils/array.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export function removeDuplicates(array: []) {
|
||||||
|
const unique = [];
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
if (!unique.includes(array[i])) {
|
||||||
|
unique.push(array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasSameElements(a1: any[], a2: any[]) {
|
||||||
|
return (
|
||||||
|
a1.length == a2.length &&
|
||||||
|
a1.reduce((a, b) => a && a2.includes(b), true)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
const DEBUG = (function () {
|
|
||||||
const timestamp = function () {};
|
|
||||||
timestamp.toString = () => "[" + new Date().toLocaleString() + "]";
|
|
||||||
return { log: console.log.bind(console, "%s", timestamp) };
|
|
||||||
})();
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
DEBUG,
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
// from https://github.com/prettymuchbryce/http-status-codes/blob/master/src/utils.ts
|
// from https://github.com/prettymuchbryce/http-status-codes/blob/master/src/utils.ts
|
||||||
|
|
||||||
const statusCodes = {
|
const statusCodes = {
|
||||||
"CONTINUE": 100,
|
"CONTINUE": 100,
|
||||||
"SWITCHING_PROTOCOLS": 101,
|
"SWITCHING_PROTOCOLS": 101,
|
||||||
@@ -59,6 +58,4 @@ const statusCodes = {
|
|||||||
"NETWORK_AUTHENTICATION_REQUIRED": 511,
|
"NETWORK_AUTHENTICATION_REQUIRED": 511,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export default statusCodes;
|
||||||
statusCodes: statusCodes
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
16
back/validator/schemas/createAccount-schema.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": { "type": "string", "format": "email" },
|
||||||
|
"pwd": { "type": "string" },
|
||||||
|
"xoauth": { "type": "string" },
|
||||||
|
"xoauth2": { "type": "string" },
|
||||||
|
"imapHost": { "type": "string", "format": "hostname" },
|
||||||
|
"smtpHost": { "type": "string", "format": "hostname" },
|
||||||
|
"imapPort": { "type": "number", "maximum": 65535 },
|
||||||
|
"smtpPort": { "type": "number", "maximum": 65535 },
|
||||||
|
"tls": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["email", "imapHost", "smtpHost", "imapPort", "smtpPort", "tls"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
16
back/validator/schemas/delete-schema.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mailboxId": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"mailboxId",
|
||||||
|
"messageId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
12
back/validator/schemas/deleteRoom-schema.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"roomId": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"roomId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
6
back/validator/schemas/getAccounts-schema.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": [],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
12
back/validator/schemas/getMembers-schema.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"roomId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"roomId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
12
back/validator/schemas/getMessages-schema.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"roomId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"roomId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
12
back/validator/schemas/getRooms-schema.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mailboxId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"mailboxId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
21
back/validator/schemas/response-schema.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"roomId": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"html": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"user", "roomId", "text", "html"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
20
back/validator/schemas/setFlag-schema.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mailboxId": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"flag": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"mailboxId",
|
||||||
|
"messageId",
|
||||||
|
"flag"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
86
back/validator/validator.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import Ajv from "ajv";
|
||||||
|
import addFormats from "ajv-formats";
|
||||||
|
const ajv = new Ajv({ allErrors: true });
|
||||||
|
addFormats(ajv);
|
||||||
|
|
||||||
|
import createAccountSchema from "./schemas/createAccount-schema.json";
|
||||||
|
import getAccountSchema from "./schemas/getAccounts-schema.json";
|
||||||
|
import getRoomSchema from "./schemas/getRooms-schema.json";
|
||||||
|
import getMessagesSchema from "./schemas/getMessages-schema.json";
|
||||||
|
import getMembersSchema from "./schemas/getMembers-schema.json";
|
||||||
|
import setFlagSchema from "./schemas/setFlag-schema.json";
|
||||||
|
import responseSchema from "./schemas/response-schema.json";
|
||||||
|
import deleteSchema from "./schemas/delete-schema.json";
|
||||||
|
import deleteRoomSchema from "./schemas/deleteRoom-schema.json";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import statusCodes from "../utils/statusCodes";
|
||||||
|
import logger from "../system/Logger";
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
validateCreateAccount: any;
|
||||||
|
validateGetAccounts: any;
|
||||||
|
validateGetRooms: any;
|
||||||
|
validateGetMessages: any;
|
||||||
|
validateGetMembers: any;
|
||||||
|
validateSetFlag: any;
|
||||||
|
validateResponse: any;
|
||||||
|
delete: any;
|
||||||
|
deleteRoom: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.validateCreateAccount = ajv.compile(createAccountSchema);
|
||||||
|
this.validateGetAccounts = ajv.compile(getAccountSchema);
|
||||||
|
this.validateGetRooms = ajv.compile(getRoomSchema);
|
||||||
|
this.validateGetMessages = ajv.compile(getMessagesSchema);
|
||||||
|
this.validateGetMembers = ajv.compile(getMembersSchema);
|
||||||
|
this.validateSetFlag = ajv.compile(setFlagSchema);
|
||||||
|
this.validateResponse = ajv.compile(responseSchema);
|
||||||
|
this.delete = ajv.compile(deleteSchema);
|
||||||
|
this.deleteRoom = ajv.compile(deleteRoomSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSchema(name: string): any {
|
||||||
|
switch (name) {
|
||||||
|
case "createAccount":
|
||||||
|
return this.validateCreateAccount;
|
||||||
|
case "getAccounts":
|
||||||
|
return this.validateGetAccounts;
|
||||||
|
case "getRooms":
|
||||||
|
return this.validateGetRooms;
|
||||||
|
case "getMessages":
|
||||||
|
return this.validateGetMessages;
|
||||||
|
case "getMembers":
|
||||||
|
return this.validateGetMembers;
|
||||||
|
case "addFlag":
|
||||||
|
case "removeFlag":
|
||||||
|
return this.validateSetFlag;
|
||||||
|
case "response":
|
||||||
|
return this.validateResponse;
|
||||||
|
case "delete":
|
||||||
|
return this.delete;
|
||||||
|
case "deleteRoom":
|
||||||
|
return this.deleteRoom;
|
||||||
|
default:
|
||||||
|
logger.err(`Schema ${name} not found`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(
|
||||||
|
schemaName: string,
|
||||||
|
args: any,
|
||||||
|
res: Response,
|
||||||
|
callback: (body: any, res: Response) => Promise<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
const validator = this._getSchema(schemaName);
|
||||||
|
const valid = validator(args);
|
||||||
|
if (!valid) {
|
||||||
|
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validator.errors });
|
||||||
|
} else {
|
||||||
|
await callback(args, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = new Validator();
|
||||||
|
export default validator;
|
||||||
3173
back/yarn.lock
@@ -1,24 +0,0 @@
|
|||||||
# mail
|
|
||||||
|
|
||||||
## Project setup
|
|
||||||
```
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
|
||||||
```
|
|
||||||
yarn serve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and minifies for production
|
|
||||||
```
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lints and fixes files
|
|
||||||
```
|
|
||||||
yarn lint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Customize configuration
|
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
['@babel/preset-env', {targets: {node: 'current'}}],
|
||||||
|
'@babel/preset-typescript',
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"module": "esnext",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11262
front/package-lock.json
generated
@@ -5,26 +5,69 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/components": "^9.13.0",
|
"@popperjs/core": "^2.11.7",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@tiptap/extension-bold": "^2.0.3",
|
||||||
"axios": "^1.3.4",
|
"@tiptap/extension-bullet-list": "^2.0.3",
|
||||||
"core-js": "^3.8.3",
|
"@tiptap/extension-hard-break": "^2.0.3",
|
||||||
"dompurify": "^3.0.1",
|
"@tiptap/extension-heading": "^2.0.3",
|
||||||
|
"@tiptap/extension-highlight": "^2.0.3",
|
||||||
|
"@tiptap/extension-history": "^2.0.3",
|
||||||
|
"@tiptap/extension-image": "^2.0.3",
|
||||||
|
"@tiptap/extension-italic": "^2.0.3",
|
||||||
|
"@tiptap/extension-link": "^2.0.3",
|
||||||
|
"@tiptap/extension-list-item": "^2.0.3",
|
||||||
|
"@tiptap/extension-ordered-list": "^2.0.3",
|
||||||
|
"@tiptap/extension-task-item": "^2.0.3",
|
||||||
|
"@tiptap/extension-task-list": "^2.0.3",
|
||||||
|
"@tiptap/extension-text-align": "^2.0.3",
|
||||||
|
"@tiptap/extension-underline": "^2.0.3",
|
||||||
|
"@tiptap/pm": "^2.0.3",
|
||||||
|
"@tiptap/starter-kit": "^2.0.3",
|
||||||
|
"@tiptap/vue-3": "^2.0.3",
|
||||||
|
"popper.js": "^1.16.1",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.0.3",
|
||||||
"vuex": "^4.0.2"
|
"vue-svg-loader": "^0.16.0",
|
||||||
|
"vuex": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.16",
|
"@babel/core": "^7.12.16",
|
||||||
"@babel/eslint-parser": "^7.12.16",
|
"@babel/eslint-parser": "^7.12.16",
|
||||||
|
"@babel/preset-typescript": "^7.21.4",
|
||||||
|
"@types/dompurify": "^3.0.1",
|
||||||
|
"@types/jest": "^27.0.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
|
"@typescript-eslint/parser": "^5.4.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.0",
|
"@vue/cli-plugin-babel": "~5.0.0",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||||
|
"@vue/cli-plugin-router": "~5.0.0",
|
||||||
|
"@vue/cli-plugin-typescript": "~5.0.0",
|
||||||
|
"@vue/cli-plugin-unit-jest": "~5.0.0",
|
||||||
|
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||||
"@vue/cli-service": "~5.0.0",
|
"@vue/cli-service": "~5.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^9.1.0",
|
||||||
|
"@vue/test-utils": "^2.0.0-0",
|
||||||
|
"@vue/vue3-jest": "^27.0.0-alpha.1",
|
||||||
|
"@vueuse/components": "^9.13.0",
|
||||||
|
"@vueuse/core": "^9.13.0",
|
||||||
|
"axios": "^1.3.4",
|
||||||
|
"babel-jest": "^27.0.6",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"dompurify": "^3.0.1",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-vue": "^8.0.3"
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
|
"jest": "^27.0.5",
|
||||||
|
"prettier": "^2.4.1",
|
||||||
|
"sass": "^1.62.0",
|
||||||
|
"sass-loader": "^13.2.2",
|
||||||
|
"ts-jest": "^27.0.4",
|
||||||
|
"typescript": "~4.5.5"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
@@ -33,14 +76,27 @@
|
|||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:vue/vue3-essential",
|
"plugin:vue/vue3-essential",
|
||||||
"eslint:recommended"
|
"eslint:recommended",
|
||||||
|
"@vue/typescript/recommended",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"parser": "@babel/eslint-parser"
|
"ecmaVersion": 2020
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"vue/multi-word-component-names": "off"
|
"vue/multi-word-component-names": "off"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/__tests__/*.{j,t}s?(x)",
|
||||||
|
"**/tests/unit/**/*.spec.{j,t}s?(x)"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
|
import Sidebar from "./views/sidebar/Sidebar";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -9,22 +10,10 @@ import { RouterView } from "vue-router";
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import Sidebar from './views/sidebar/Sidebar'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'App',
|
|
||||||
components: {
|
|
||||||
Sidebar,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#app {
|
#app {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
33
front/src/assets/css/main.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
:root {
|
||||||
|
--primary-text: #ffffff;
|
||||||
|
--secondary-text: #a9b2bc;
|
||||||
|
/* 9fa9ba */
|
||||||
|
--primary-background: #1d1d23;
|
||||||
|
--secondary-background: #24242b;
|
||||||
|
/* 1d1d23 */
|
||||||
|
--tertiary-background: #2a2a33;
|
||||||
|
--quaternary-background: #303a46;
|
||||||
|
|
||||||
|
--selected: #41474f;
|
||||||
|
--warn: #e4b31d;
|
||||||
|
--danger: #d74453;
|
||||||
|
|
||||||
|
--border-color: #505050;
|
||||||
|
|
||||||
|
--svg-primary-text: brightness(0) saturate(100%) invert(100%) sepia(4%) saturate(1934%) hue-rotate(130deg)
|
||||||
|
brightness(114%) contrast(100%);
|
||||||
|
|
||||||
|
--svg-selected: brightness(0) saturate(100%) invert(22%) sepia(1%) saturate(7429%) hue-rotate(175deg)
|
||||||
|
brightness(79%) contrast(69%);
|
||||||
|
--svg-warn: brightness(0) saturate(100%) invert(77%) sepia(81%) saturate(1010%) hue-rotate(347deg) brightness(95%)
|
||||||
|
contrast(88%);
|
||||||
|
--svg-danger: brightness(0) saturate(100%) invert(53%) sepia(83%) saturate(4662%) hue-rotate(327deg) brightness(87%)
|
||||||
|
contrast(92%);
|
||||||
|
/* 343a46 */
|
||||||
|
}
|
||||||
|
/* .badge-primary { */
|
||||||
|
/* https://angel-rs.github.io/css-color-filter-generator/ */
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: var(--selected);
|
||||||
|
}
|
||||||
1
front/src/assets/svg/add-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 169 B |
1
front/src/assets/svg/align-center.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM5 19H19V21H5V19ZM3 14H21V16H3V14ZM5 9H19V11H5V9Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
1
front/src/assets/svg/align-justify.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM3 14H21V16H3V14ZM3 9H21V11H3V9Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
1
front/src/assets/svg/align-left.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM3 19H17V21H3V19ZM3 14H21V16H3V14ZM3 9H17V11H3V9Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
1
front/src/assets/svg/align-right.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM7 19H21V21H7V19ZM3 14H21V16H3V14ZM7 9H21V11H7V9Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
1
front/src/assets/svg/attachment-2.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M14.8287 7.7574L9.1718 13.4143C8.78127 13.8048 8.78127 14.4379 9.1718 14.8285C9.56232 15.219 10.1955 15.219 10.586 14.8285L16.2429 9.17161C17.4144 8.00004 17.4144 6.10055 16.2429 4.92897C15.0713 3.7574 13.1718 3.7574 12.0002 4.92897L6.34337 10.5858C4.39075 12.5384 4.39075 15.7043 6.34337 17.6569C8.29599 19.6095 11.4618 19.6095 13.4144 17.6569L19.0713 12L20.4855 13.4143L14.8287 19.0711C12.095 21.8048 7.66283 21.8048 4.92916 19.0711C2.19549 16.3374 2.19549 11.9053 4.92916 9.17161L10.586 3.51476C12.5386 1.56214 15.7045 1.56214 17.6571 3.51476C19.6097 5.46738 19.6097 8.63321 17.6571 10.5858L12.0002 16.2427C10.8287 17.4143 8.92916 17.4143 7.75759 16.2427C6.58601 15.0711 6.58601 13.1716 7.75759 12L13.4144 6.34319L14.8287 7.7574Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 860 B |
1
front/src/assets/svg/attachment-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M14 13.5V8C14 5.79086 12.2091 4 10 4C7.79086 4 6 5.79086 6 8V13.5C6 17.0899 8.91015 20 12.5 20C16.0899 20 19 17.0899 19 13.5V4H21V13.5C21 18.1944 17.1944 22 12.5 22C7.80558 22 4 18.1944 4 13.5V8C4 4.68629 6.68629 2 10 2C13.3137 2 16 4.68629 16 8V13.5C16 15.433 14.433 17 12.5 17C10.567 17 9 15.433 9 13.5V8H11V13.5C11 14.3284 11.6716 15 12.5 15C13.3284 15 14 14.3284 14 13.5Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 503 B |
1
front/src/assets/svg/bold.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M8 11H12.5C13.8807 11 15 9.88071 15 8.5C15 7.11929 13.8807 6 12.5 6H8V11ZM18 15.5C18 17.9853 15.9853 20 13.5 20H6V4H12.5C14.9853 4 17 6.01472 17 8.5C17 9.70431 16.5269 10.7981 15.7564 11.6058C17.0979 12.3847 18 13.837 18 15.5ZM8 13V18H13.5C14.8807 18 16 16.8807 16 15.5C16 14.1193 14.8807 13 13.5 13H8Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 430 B |