Compare commits

..

134 Commits

Author SHA1 Message Date
grimhilt
adb7a1161a add implementation of colors for address in front 2023-07-18 16:16:40 +02:00
grimhilt
3c069ed2f8 better title in message view modal 2023-07-16 16:46:38 +02:00
grimhilt
ae73326820 improve add account flow 2023-07-16 16:24:30 +02:00
grimhilt
f40b6758de add members in message modal 2023-07-16 15:54:57 +02:00
grimhilt
43e8cc7d3e correct typo 2023-07-15 00:27:58 +02:00
grimhilt
edddf9afbf add link to the thread of a message 2023-07-15 00:19:39 +02:00
grimhilt
3bffd88108 add original message in thread 2023-07-14 19:05:37 +02:00
grimhilt
0094783a4e minor changes 2023-07-14 16:26:13 +02:00
grimhilt
f42d819e45 confirmation modal 2023-05-23 16:35:23 +02:00
grimhilt
467b0eebe9 setup accounts 2023-05-23 15:56:27 +02:00
grimhilt
e8e8555c2f add smtp port on front 2023-05-23 15:36:14 +02:00
grimhilt
af4cc2f6a0 create custom component 2023-05-23 15:19:52 +02:00
grimhilt
2cae8f12a7 add route room delete 2023-05-17 18:14:25 +02:00
grimhilt
7be2e84691 add actions header room 2023-05-17 18:00:15 +02:00
grimhilt
737a22e1f8 improve svgloader 2023-05-17 17:44:55 +02:00
grimhilt
4ecd723cec load message in page loading 2023-05-08 01:10:13 +02:00
grimhilt
53c79aebc4 fix deletion back 2023-05-08 00:35:18 +02:00
grimhilt
b2b0949353 delete thread front 2023-05-08 00:34:32 +02:00
grimhilt
ffcfc57bbe fix errors and beautify with use of class 2023-05-07 23:42:44 +02:00
grimhilt
f7c95b3a36 update delete behavior when have thread and beautify code 2023-05-07 23:27:08 +02:00
grimhilt
843659b495 delete room when empty 2023-05-06 15:58:33 +02:00
grimhilt
1a7828b281 minor fixes for deletion compatibility 2023-05-06 15:17:02 +02:00
grimhilt
b821c89e20 delete from server working 2023-05-06 14:57:55 +02:00
grimhilt
c3374a612e compact mode for messages 2023-05-06 14:14:32 +02:00
grimhilt
686e6a4911 set loading of deletions 2023-05-06 13:30:50 +02:00
grimhilt
b137263bef deletion of messages (failing on server) 2023-05-06 13:23:13 +02:00
grimhilt
2c7b4f1c78 fix live sync mail 2023-05-06 12:31:13 +02:00
grimhilt
3dab9c8db1 try using search with syncManager 2023-05-06 12:28:53 +02:00
grimhilt
5aef5ab7b0 improve initial sync behavior 2023-05-05 19:09:27 +02:00
grimhilt
8f980748b5 link api front 2023-05-02 13:51:13 +02:00
grimhilt
22fb12e6d6 change full archi api backend 2023-05-02 13:47:04 +02:00
grimhilt
12e508c7cb update backend api architecture 2023-05-02 12:25:50 +02:00
grimhilt
29b4b7bfeb fix types highestmodseq 2023-05-02 12:12:09 +02:00
grimhilt
91a52a29ce change color of flag 2023-04-26 17:45:48 +02:00
grimhilt
6e8e3bf1f4 fix svg errors on display and add flagged 2023-04-26 17:40:07 +02:00
grimhilt
dd8f9210a9 loading when click read or unread 2023-04-26 16:55:14 +02:00
grimhilt
10fc3fd628 add tooltip on composer 2023-04-26 12:56:58 +02:00
grimhilt
dc20ccfec0 prevent bubble when all options are shown 2023-04-25 19:41:10 +02:00
grimhilt
ca096dac89 add readme 2023-04-23 19:29:51 +02:00
grimhilt
cd996d851a sync flags on server start 2023-04-17 13:08:54 +02:00
grimhilt
0f063deff9 minor fixes 2023-04-16 15:22:49 +02:00
grimhilt
614f7d9802 routes of update flags with imap 2023-04-16 12:31:46 +02:00
grimhilt
318748c984 add more advance composer feature 2023-04-16 01:19:58 +02:00
grimhilt
4d3fbe292e more options on composer inside editor 2023-04-15 18:14:57 +02:00
grimhilt
2594f6042b add svg 2023-04-15 17:44:47 +02:00
grimhilt
8c7cc1f316 create tooltip 2023-04-15 17:44:08 +02:00
grimhilt
85b63f0ea3 import lib for composer 2023-04-15 15:55:31 +02:00
grimhilt
e44584df0a simplify and composer design 2023-04-15 00:39:24 +02:00
grimhilt
956bc35158 fix duplication of threads on account change 2023-04-14 23:04:09 +02:00
grimhilt
79e17ad24f fix mail view in modal 2023-04-14 21:00:23 +02:00
grimhilt
4799e477be reply message from thread and room 2023-04-14 20:54:44 +02:00
grimhilt
7ad22e55c1 start on repond to message (basic input and builder for dm) 2023-04-14 18:37:33 +02:00
grimhilt
5b62fce48a minor changes 2023-04-14 18:37:01 +02:00
grimhilt
b48c834d36 change notif when setting seen flag 2023-04-13 16:43:44 +02:00
grimhilt
8cb1271f9a store message outside of room object 2023-04-13 13:14:50 +02:00
grimhilt
0b950ba7a7 use provider instead of props in options 2023-04-13 12:15:04 +02:00
grimhilt
e43ab6cfe1 change validator api 2023-04-13 10:58:58 +02:00
grimhilt
4e79ab12dc add button to set seen flag on front 2023-04-12 19:01:40 +02:00
grimhilt
1ab74d67ca close on escape 2023-04-11 19:15:35 +02:00
grimhilt
9842ebeb12 show mail content in modal on double click 2023-04-11 18:59:10 +02:00
grimhilt
8cc738c9b2 tests to mysql (not working) 2023-04-11 18:19:47 +02:00
grimhilt
49e8ec64e0 remove unseen in database 2023-04-11 18:19:09 +02:00
grimhilt
dcb7075dca rename api to api-db 2023-04-11 18:15:55 +02:00
grimhilt
4cc53752d7 sum of notseen thread and room 2023-04-11 18:14:48 +02:00
grimhilt
a80873b617 not seen in rooms in api 2023-04-11 16:26:09 +02:00
grimhilt
4d7a919054 fix notseen badge on thread 2023-04-11 16:19:53 +02:00
grimhilt
3357009d6a count number of message read 2023-04-11 00:10:00 +02:00
grimhilt
4b21168547 add smtpInstance in emailManager 2023-04-09 23:46:33 +02:00
grimhilt
160bb0c605 add send mail 2023-04-09 23:29:05 +02:00
grimhilt
b9cc543b64 open viewMessageModal on double click 2023-04-08 01:01:10 +02:00
grimhilt
5b6995d6a6 change archi and use schema routes 2023-04-08 00:00:24 +02:00
grimhilt
65db4d8b7e show flags on front 2023-04-07 23:26:19 +02:00
grimhilt
9d12e81e07 should update flags 2023-04-07 18:28:43 +02:00
grimhilt
af76e8f2f9 register flags 2023-04-07 17:59:24 +02:00
grimhilt
46ed3a1f41 update architecture and types 2023-04-07 17:43:08 +02:00
grimhilt
d641b01758 should implement continuous syncing 2023-04-07 17:15:28 +02:00
grimhilt
20fe48974f style to implement flags and options on message 2023-04-07 16:52:13 +02:00
grimhilt
398d243eac show list of users in rooms 2023-04-07 16:10:23 +02:00
grimhilt
649bccb01e move message to typescript 2023-04-07 15:15:49 +02:00
grimhilt
7e0e27c2b6 show members in a room 2023-04-07 00:50:59 +02:00
grimhilt
7c98c1eb0c restructure members storage in rooms store 2023-04-06 20:24:11 +02:00
grimhilt
8c6a2bcfd7 show notifications front 2023-04-06 13:09:47 +02:00
grimhilt
b14a0ca586 global color and better message style 2023-04-05 18:17:09 +02:00
grimhilt
6d9d67905c fix thread hover and composer 2023-04-05 17:31:53 +02:00
grimhilt
97768e3695 add thread in api and front 2023-04-05 17:28:58 +02:00
grimhilt
51003b494b fix sql queries to sync mail 2023-04-05 16:10:10 +02:00
grimhilt
8b4210914b fix blocked iframe when change room 2023-04-05 15:46:17 +02:00
grimhilt
41aabb868f remove duplicate loading 2023-04-05 15:23:36 +02:00
grimhilt
16d0fafb1a create message test utils 2023-04-05 14:32:22 +02:00
grimhilt
86f321c0a1 test unseen behavior 2023-04-05 14:21:46 +02:00
grimhilt
65631f8e9a thread tests 2023-04-05 12:33:32 +02:00
grimhilt
9fc31f8686 advancements in tests 2023-04-04 18:00:27 +02:00
grimhilt
5a71e104cd implement database as a class for tests 2023-04-04 17:13:18 +02:00
grimhilt
9c16e06446 trying stuff to mock mysql 2023-04-04 15:13:38 +02:00
grimhilt
de94bd4bab change file archi 2023-04-04 13:53:57 +02:00
grimhilt
fd253197cc show addresses in front 2023-04-03 20:37:07 +02:00
grimhilt
6b4264fccc show description in room header 2023-04-03 20:11:07 +02:00
grimhilt
4e48c5d813 start message to bottom 2023-04-02 17:39:04 +02:00
grimhilt
4bff5be6c1 use strict front 2023-04-02 16:52:19 +02:00
grimhilt
e3d8d3cf9b switch front to typescript 2023-04-02 16:44:54 +02:00
grimhilt
7f535f2e95 fix some errors on front 2023-04-02 13:36:59 +02:00
grimhilt
948ec3c7b4 improve logger 2023-04-02 12:59:11 +02:00
grimhilt
3042ed972b run server as typescript 2023-04-02 12:35:55 +02:00
grimhilt
11ab6a6a21 tests in typescript 2023-04-01 22:36:51 +02:00
grimhilt
90dd16ee0d started to convert to typescript 2023-04-01 16:32:29 +02:00
grimhilt
aced3b8914 add logic and more test to saveMessage 2023-04-01 15:07:49 +02:00
grimhilt
68e1dfe7d8 implement save of thread and members 2023-03-31 16:07:02 +02:00
grimhilt
a82ff9b85b improve tests saveMessage 2023-03-30 09:16:10 +02:00
grimhilt
8306543ddd improve saveMessage (switch to class) and started to test 2023-03-29 21:00:43 +02:00
grimhilt
6507d466ad logic pseudo code 2023-03-29 17:43:46 +02:00
grimhilt
44125fc55d get members 2023-03-29 16:48:28 +02:00
grimhilt
14dd6b36f8 fix duplicate content header 2023-03-29 16:34:30 +02:00
grimhilt
91898e25a5 improve syncing and storing 2023-03-29 16:23:24 +02:00
grimhilt
185f051a63 change logger 2023-03-28 16:57:44 +02:00
grimhilt
838550b6cc display mail in iframe, add design for thread and unseen 2023-03-27 01:04:43 +02:00
grimhilt
5447557f91 apply difference between mailbox and account 2023-03-26 14:55:13 +02:00
grimhilt
62dd43c3d5 link imap sync to server and show email on front 2023-03-26 14:20:16 +02:00
grimhilt
0ea7f5865b advancements in tests and storing messages 2023-03-25 16:47:23 +01:00
grimhilt
4d4ef54bcb load message in front 2023-03-25 13:06:59 +01:00
grimhilt
926dc60920 fix modal background opacity 2023-03-23 23:59:49 +01:00
grimhilt
d6f06f3ca6 start to load messages from rooms 2023-03-20 21:28:13 +01:00
grimhilt
9b3ddd291e basic routing for roomview 2023-03-20 15:00:15 +01:00
grimhilt
d7029854b4 fetch rooms 2023-03-20 14:43:07 +01:00
grimhilt
095efb5440 fetching mailboxes from api 2023-03-17 13:31:27 +01:00
grimhilt
14e64c1fc3 save message working without reply 2023-03-16 16:14:25 +01:00
grimhilt
95f39cf53a save message sync 2023-03-15 14:48:15 +01:00
grimhilt
f9fbab3a21 advancement on logic of app 2023-03-13 19:12:57 +01:00
grimhilt
aa9a69e17f add queries for app functionnalities 2023-03-13 00:54:44 +01:00
grimhilt
3286a2e52b started some app structure 2023-03-13 00:13:17 +01:00
grimhilt
9046ccf137 update database structure 2023-03-11 14:52:08 +01:00
grimhilt
29bf4bbdbd gloablly save messages 2023-03-10 17:07:05 +01:00
grimhilt
df69a7dbd9 solution clean not working 2023-03-10 16:18:04 +01:00
grimhilt
427ffba725 save 2023-03-10 16:08:50 +01:00
grimhilt
d3893c682e remove password 2023-03-01 16:57:11 +01:00
180 changed files with 22488 additions and 2779 deletions

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ tmp
test.*
*.png
!*/schemas/*
todo

31
README.md Normal file
View 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
View 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
View 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
View 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
}
}

View File

@@ -1,18 +0,0 @@
import statusCode from "../utils/statusCodes";
import { registerAccount } from "../db/api";
import { getAddresseId } from "../db/mail";
export async function addAccount(body, res) {
const { email, pwd, xoauth, xoauth2, host, port, tls } = body;
getAddresseId(email).then((addressId) => {
registerAccount(addressId, pwd, xoauth, xoauth2, host, port, tls)
.then((mailboxId) => {
res.status(statusCode.OK).json({ id: mailboxId });
})
.catch(() => {
res.status(statusCode.INTERNAL_SERVER_ERROR);
});
});
}
// todo change mailbox to account

View File

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

View File

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

View File

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

85
back/db/Room-db.ts Normal file
View 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
View 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);
}

View File

@@ -1,112 +0,0 @@
import { execQueryAsync, execQueryAsyncWithId } from "./db";
import { queryCcId, queryToId, queryFromId } from "./utils/addressQueries";
export async function registerAccount(userId, pwd, xoauth, xoauth2, host, port, tls) {
const query = `
INSERT INTO app_account
(user_id, account_pwd, xoauth, xoauth2, host, port, tls) VALUES (?, ?, ?, ?, ?, ?, ?)
`;
const values = [userId, pwd, xoauth, xoauth2, host, port, 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) {
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);
}
export 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);
}
export async function getMembers(roomId) {
const query = `
SELECT
address.address_id,
address.address_name,
address.email
FROM app_room_member
INNER JOIN address ON address.address_id = app_room_member.member_id
WHERE app_room_member.room_id = ?
`;
const values = [roomId];
return await execQueryAsync(query, values);
}

View File

@@ -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

View File

@@ -1,108 +1,181 @@
CREATE TABLE `addresses` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` text,
`localpart` text NOT NULL,
`domain` text NOT NULL,
`email` text NOT NULL
-- 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)
);
CREATE TABLE `mailboxes` (
`id` int PRIMARY KEY NOT NULL AUTO_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
-- 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),
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` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`idate` timestamp NOT NULL,
`rfc822size` int
-- 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
);
CREATE TABLE `mailbox_messages` (
`mailbox` int NOT NULL,
`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 (`uid`, `message`)
-- 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)
);
CREATE TABLE `bodyparts` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`bytes` int NOT NULL,
`hash` text NOT NULL,
`text` text,
`data` binary
-- 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
);
CREATE TABLE `part_numbers` (
`message` int PRIMARY KEY NOT NULL,
`part` text NOT NULL,
`bodypart` int NOT NULL,
`bytes` int,
`nb_lines` int
-- 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)
);
CREATE TABLE `field_names` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` text NOT NULL
-- 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
);
CREATE TABLE `header_fields` (
`id` int NOT NULL AUTO_INCREMENT,
`message` int NOT NULL,
`part` text NOT NULL,
`position` int NOT NULL,
`field` int NOT NULL,
`value` text,
PRIMARY KEY (`id`, `message`)
-- 8
CREATE TABLE field_name (
field_id INT AUTO_INCREMENT,
field_name VARCHAR (255),
PRIMARY KEY (field_id),
UNIQUE KEY (field_name)
);
CREATE 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
-- 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)
);
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`);
ALTER TABLE `messages` ADD FOREIGN KEY (`id`) REFERENCES `mailbox_messages` (`message`);
ALTER TABLE `part_numbers` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
ALTER TABLE `bodyparts` ADD FOREIGN KEY (`id`) REFERENCES `part_numbers` (`bodypart`);
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`);
-- 16
create table flag (
message_id INT NOT NULL,
flag_id INT NOT NULL,
UNIQUE KEY (message_id, flag_id),
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
FOREIGN KEY (flag_id) REFERENCES flag_name(flag_id) ON DELETE CASCADE
);

View File

@@ -2,17 +2,16 @@ import mysql from "mysql";
import logger from "../system/Logger";
require("dotenv").config();
// todo remove export
export const db = mysql.createConnection({
const db = mysql.createConnection({
host: process.env.HOST_DB,
user: process.env.USER_DB,
password: process.env.PASSWORD_DB,
database: process.env.NAME_DB,
database: (process.env.NODE_ENV === "test") ? process.env.NAME_DB_TEST : process.env.NAME_DB,
});
db.connect(function (err) {
if (err) {
logger.error(`Unable to connect database ${err.code}`);
logger.err(`Unable to connect database ${err.code}`);
} else {
logger.log("Database successfully connected");
}
@@ -45,7 +44,7 @@ export function execQueryAsyncWithId(query: string, values: any[]): Promise<numb
export function execQuery(query: string, values: any[]) {
db.query(query, values, (err, results, fields) => {
if (err) {
logger.error(err);
logger.err(err);
throw err;
}
return results;

55
back/db/imap/imap-db.ts Normal file
View 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);
}

View File

@@ -1,41 +0,0 @@
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.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);
}
export async function getAllMailboxes(accountId) {
const query = 'SELECT * FROM mailbox WHERE mailbox.account_id = ?';
const values = [accountId];
return await execQueryAsync(query, values)
}
export async function registerMailbox(accountId, mailboxName) {
const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`;
const values = [accountId, mailboxName];
return await execQueryAsyncWithId(query, values);
}
export async function getMailbox(mailboxId) {
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
const values = [mailboxId];
return await execQueryAsync(query, values);
}
export function updateMailbox(mailboxId, uidnext) {
const query = `UPDATE mailbox SET uidnext = ? WHERE mailbox_id = ?`;
const values = [uidnext, mailboxId];
execQuery(query, values);
}

View File

@@ -1,35 +0,0 @@
import { execQueryAsync, execQueryAsyncWithId } from "./db";
export async function getAddresseId(email: string, name?: string): Promise<number> {
console.log("get address id")
const localpart = email.split("@")[0];
const domain = email.split("@")[1];
const query = `INSERT INTO address
(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 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 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);
}

View 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);
}

View File

@@ -1,10 +1,12 @@
import { transformEmojis } from "../utils/string";
import { db, execQueryAsync, execQueryAsyncWithId, execQuery } from "./db";
import { queryFromId, queryToId, queryCcId } from "./utils/addressQueries";
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 is
SELECT GROUP_CONCAT(address.address_id) AS id
FROM address
INNER JOIN address_field ON
address_field.address_id = address.address_id AND
@@ -25,8 +27,8 @@ export async function createRoom(
roomName: string | null | undefined,
ownerId: number,
messageId: number,
roomType: 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 (?, ?, ?, ?)`;
@@ -34,22 +36,13 @@ export async function createRoom(
return await execQueryAsyncWithId(query, values);
}
// todo date not good
export async function registerMessageInRoom(
messageId: number,
roomId: number,
isSeen: boolean,
idate: string | undefined | null,
) {
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);
// if (!isSeen) {
// incrementNotSeenRoom(roomId);
// }
}
export function updateLastUpdateRoom(roomId: number, idate: string) {
@@ -58,18 +51,14 @@ export function updateLastUpdateRoom(roomId: number, idate: string) {
execQuery(query, values);
}
export function incrementNotSeenRoom(roomId: number) {
// todo
}
export async function getRoomInfo(messageID: string): Promise<{ room_id: number; root_id: number }[]> {
export async function getThreadInfo(messageID: string): Promise<{ room_id: number; root_id: number }[]> {
const query = `
SELECT
app_room.room_id
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 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 = ?
`;
@@ -77,30 +66,43 @@ export async function getRoomInfo(messageID: string): Promise<{ room_id: number;
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 isRoomGroup(roomId: number): Promise<boolean> {
return new Promise((resolve, reject) => {
const query = `SELECT isGroup FROM app_room WHERE room_id = '${roomId}'`;
db.query(query, (err, results, fields) => {
if (err) reject(err);
resolve(results[0].isGroup);
});
});
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) {
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 WHERE message_id = ? ORDER BY room_id`;
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) {
export async function hasSameMembersAsParent(messageId: number, messageID: string): Promise<boolean> {
const query1 = `
SELECT
GROUP_CONCAT(fromT.address_id) AS fromA,
@@ -138,8 +140,5 @@ export async function hasSameMembersAsParent(messageId: number, messageID: strin
.concat(addressesMsg2[0]?.toA?.split(","))
.concat(addressesMsg2[0]?.ccA?.split(","));
return (
addressesMsg1.length == addressesMsg2.length &&
addressesMsg1.reduce((a, b) => a && addressesMsg2.includes(b), true)
);
return hasSameElements(addressesMsg1, addressesMsg2);
}

View File

@@ -1,17 +1,24 @@
import { transformEmojis } from "../utils/string";
import { execQuery, execQueryAsync, execQueryAsyncWithId } from "./db";
import { transformEmojis } from "../../utils/string";
import { execQuery, execQueryAsync, execQueryAsyncWithId } from "../db";
export async function registerMessage(timestamp, rfc822size, messageId) {
export async function registerMessage(timestamp: string, rfc822size: number, messageID: string) {
const query = `
INSERT INTO message
(idate, messageID, rfc822size) VALUES (?, ?, ?)
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);
}
export 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 = `
INSERT IGNORE INTO mailbox_message
(mailbox_id, uid, message_id, modseq, seen, deleted) VALUES (?, ?, ?, ?, ?, ?)
@@ -20,7 +27,13 @@ export function registerMailbox_message(mailboxId, uid, messageId, modseq, seen,
execQuery(query, values);
}
export 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 = `
INSERT IGNORE INTO part_number
(message_id, part, bodypart_id, bytes, nb_lines) VALUES (?, ?, ?, ?, ?)
@@ -36,7 +49,13 @@ export async function saveBodypart(bytes, hash, text, data) {
return await execQueryAsyncWithId(query, values);
}
export 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);
const query = `
INSERT IGNORE INTO header_field
@@ -46,7 +65,7 @@ export async function saveHeader_fields(messageId, fieldId, bodypartId, part, va
return await execQueryAsync(query, values);
}
export async function saveAddress_fields(messageId, fieldId, addressId, number) {
export async function saveAddress_fields(messageId: number, fieldId: number, addressId: number, number: number) {
const query = `
INSERT IGNORE INTO address_field
(message_id , field_id, address_id, number) VALUES (?, ?, ?, ?)
@@ -55,7 +74,7 @@ export async function saveAddress_fields(messageId, fieldId, addressId, number)
return await execQueryAsync(query, values);
}
export function saveSource(messageId, content) {
export function saveSource(messageId: number, content: string) {
content = transformEmojis(content);
const query = `
INSERT INTO source (message_id, content) VALUES (?, ?)

View 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);
}

View File

@@ -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()
)

View File

@@ -1,162 +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,
room_type INT NOT NULL DEFAULT 0,
notSeen INT NOT NULL DEFAULT 0,
lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
PRIMARY KEY (room_id),
UNIQUE KEY (owner_id, message_id, room_type),
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_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
);
-- 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,
UNIQUE KEY (room_id, member_id),
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE,
FOREIGN KEY (member_id) REFERENCES address(address_id)
);

View File

@@ -2,9 +2,8 @@ const queryAddress = (type: string): string => `
LEFT JOIN (
SELECT address_field.address_id, address_field.message_id
FROM address_field
INNER JOIN field_name
INNER JOIN field_name ON field_name.field_id = address_field.field_id
WHERE
field_name.field_id = address_field.field_id AND
field_name.field_name = '${type}'
)
`;

76
back/db/utils/mail.ts Normal file
View 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);
}

View File

@@ -43,4 +43,5 @@ export interface AttrsWithEnvelope {
envelope: Envelope;
/** The RFC822 message size (only set if requested with fetch()). */
size?: number | undefined;
modseq?: number;
}

View 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;

View File

@@ -1,77 +0,0 @@
import Imap, { ImapMessageAttributes, MailBoxes } from "imap";
import { getMailbox, updateMailbox } from "../../db/imap/imap";
import { Attrs } from "../../interfaces/mail/attrs.interface";
import logger from "../../system/Logger";
import RegisterMessageInApp from "../saveMessage";
import { saveMessage } from "../storeMessage";
export default class Box {
imap: Imap;
boxName: string;
id: number;
box: MailBoxes;
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) logger.error(err);
this.sync(this.box.uidnext, box.uidnext);
});
}
sync(savedUid, currentUid) {
const promises: Promise<unknown>[] = [];
const mails: ImapMessageAttributes[] = [];
logger.log(`Syncing from ${savedUid} to ${currentUid} uid`);
const f = this.imap.seq.fetch(`${savedUid}:${currentUid}`, {
// const f = this.imap.seq.fetch(`${savedUid}:${currentUid}`, {
size: true,
envelope: true,
});
f.on("message", (msg, seqno) => {
msg.once("attributes", (attrs: Attrs) => {
console.log(attrs.envelope)
mails.push(attrs);
promises.push(saveMessage(attrs, this.id, this.imap));
});
});
f.once("error", (err) => {
logger.error("Fetch error: " + err);
});
f.once("end", async () => {
let step = 20;
logger.log(promises.length)
for (let i = 0; i < promises.length; i += step) {
for (let j = i; j < (i + step && promises.length); j++) {
await new Promise((resolve, reject) => {
promises[j]
.then(async (res) => {
const register = new RegisterMessageInApp(res, mails[j], this.id);
await register.save();
resolve("");
})
.catch((err) => {
reject(err);
});
});
}
logger.log(`Saved messages ${i + step > promises.length ? promises.length : i + step}/${mails.length}`);
updateMailbox(this.id, mails[i].uid);
}
updateMailbox(this.id, currentUid);
});
}
}

View File

@@ -1,29 +1,29 @@
import { Account } from "./ImapSync";
import { Account } from "../EmailManager";
import Imap from "imap";
import { getAllMailboxes, registerMailbox } from "../../db/imap/imap";
import { getAllMailboxes, registerMailbox } from "../../db/imap/imap-db";
import logger from "../../system/Logger";
import Box from "./Box";
import Mailbox from "./Mailbox";
export class ImapInstance {
imap: Imap;
account: Account;
boxes: Box[];
boxes: Mailbox[];
constructor(account) {
this.imap = new Imap({
user: account.user,
password: account.password,
tlsOptions: { servername: account.host },
host: account.host,
port: account.port,
tlsOptions: { servername: account.imap_host },
host: account.imap_host,
port: account.imap_port,
tls: account.tls,
});
this.account = account;
this.boxes = [];
/**
* IMAP
* IMAP init
*/
this.imap.once("ready", () => {
logger.log("Imap connected for " + this.account.user);
@@ -31,7 +31,7 @@ export class ImapInstance {
});
this.imap.once("error", (err) => {
logger.error("Imap error for " + this.account.user + ": " + err);
logger.err("Imap error for " + this.account.user + ": " + err);
});
this.imap.once("end", () => {
@@ -41,37 +41,48 @@ export class ImapInstance {
this.imap.connect();
}
imapReady() {
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));
this.boxes.push(new Mailbox(mailboxes[0].mailbox_id, mailboxes[0].mailbox_name, this));
} else {
this.imap.getBoxes("", (err, boxes) => {
if (err) logger.error(err);
const allBoxName = this.getAllBox(boxes);
this.getMailboxName("All").then((allBoxName) => {
registerMailbox(this.account.id, allBoxName).then((mailboxId) => {
this.boxes.push(new Box(this.imap, mailboxId, allBoxName));
this.boxes.push(new Mailbox(mailboxId, allBoxName, this));
});
});
}
});
}
};
getAllBox(boxes) {
// ideally we should get the all box to get all messages
let allBox = '';
Object.keys(boxes).forEach((key) => {
if (key === "INBOX") return;
if (allBox.includes("/")) return; // already found
if (!boxes[key].children) return; // no children
allBox = key;
Object.keys(boxes[key].children).forEach((childBoxes) => {
if (boxes[key].children[childBoxes].attribs.includes("\\All")) {
allBox += "/" + childBoxes;
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);
}
});
});
if (!allBox.includes("/")) logger.warn("Did not find 'All' mailbox");
return allBox;
}
getMailbox(mailboxId: number): Mailbox {
return this.boxes.find((box) => box.id === mailboxId);
}
}

View File

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

211
back/mails/imap/Mailbox.ts Normal file
View 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);
}
}

View 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;
};
}

View File

@@ -1,20 +1,20 @@
import {
createRoom,
registerMessageInRoom,
isRoomGroup,
getRoomType,
findRoomsFromMessage,
hasSameMembersAsParent,
registerThread,
registerMember,
getAllMembers,
getRoomInfo,
} from "../db/saveMessageApp";
getThreadInfo,
getThreadInfoOnId,
} from "../../db/message/saveMessage-db";
import { findRoomByOwner, getAddresseId, getUserIdOfMailbox } from "../db/mail";
import { nbMembers } from "./utils/envelopeUtils";
import logger from "../system/Logger";
import { ImapMessageAttributes } from "imap";
import { Attrs, Envelope, User } from "../interfaces/mail/attrs.interface";
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
@@ -23,13 +23,13 @@ function createAddress(elt: User): string {
return `${elt.mailbox}@${elt.host}`;
}
export const roomType = {
ROOM: 0,
CHANNEL: 1,
GROUP: 2,
DM: 3,
THREAD: 4,
};
export enum RoomType {
ROOM = 0,
CHANNEL = 1,
GROUP = 2,
DM = 3,
THREAD = 4,
}
export default class RegisterMessageInApp {
messageId: number;
@@ -49,15 +49,68 @@ export default class RegisterMessageInApp {
this.envelope = this.attrs.envelope;
this.messageID = this.envelope?.messageId;
this.boxId = _boxId;
this.isSeen = this.attrs.flags.includes("\\Seen") ? true : false;
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 getAddresseId(createAddress(this.envelope.from[0])); // todo use sender or 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");
}
@@ -74,110 +127,78 @@ export default class RegisterMessageInApp {
return this.ownerId == this.userId;
}
/**
* add all members of the message to the room
*/
async registerMembers(roomId: number) {
getAllMembers(this.messageId).then((res) => {
res[0].id.split(",").foreach(async (memberId: number) => {
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: number) {
async initiateRoom(owner: number, roomType: RoomType) {
try {
const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType);
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
this.registerMembers(roomId);
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
await this.registerMembers(roomId);
return roomId;
} catch (err) {
logger.error(err);
logger.err(err);
}
}
async createOrRegisterOnExistence(owner: number, roomType: number) {
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.isSeen, this.envelope.date);
await registerMessageInRoom(this.messageId, res[0].room_id, this.envelope.date);
}
});
}
async initiateThread() {
await createRoom(this.envelope.subject, this.ownerId, this.messageId, roomType.THREAD).then(
async (roomId: number) => {
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
await getRoomInfo(this.inReplyTo).then(async (room) => {
let roomId: number;
let root_id: number;
await getThreadInfo(this.inReplyTo).then(async (room) => {
// todo room not lenght, reply to transfer ?
let root_id = room[0].root_id;
if (!root_id) root_id = room[0].room_id;
await registerThread(roomId, room[0].room_id, root_id);
roomId = room[0].room_id;
root_id = room[0].root_id;
if (root_id === undefined) root_id = roomId;
await registerThread(threadId, roomId, root_id);
});
// impl register previous message ?
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
await this.registerMembers(roomId);
// 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) {
async createOrRegisterOnMembers(roomId: number, isThread?: boolean) {
const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo);
if (hasSameMembers) {
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
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();
}
}
async save() {
await this.init();
if (this.envelope.inReplyTo) {
this.inReplyTo = this.envelope.inReplyTo;
this.saveReply();
} else {
if (await this.isFromUs()) {
if (this.isDm()) {
// create or add new message to DM
if (!this.envelope.to) throw new Error("Who send a DM and put the recipient in cc ?");
const userTo = await getAddresseId(createAddress(this.envelope.to[0]));
await this.createOrRegisterOnExistence(userTo, roomType.DM);
} else {
// it is not a reply and not a dm
// so it is a channel, which can be possibly a group
this.initiateRoom(this.ownerId, roomType.ROOM);
}
} else {
await this.createOrRegisterOnExistence(this.ownerId, roomType.ROOM);
}
}
}
async saveReply() {
await findRoomsFromMessage(this.inReplyTo).then(async (rooms) => {
if (rooms.length == 0) {
// no rooms, so is a transfer
// todo test if members of transferred message are included
} else if (rooms.length === 1) {
// only one room so message is only in a room and not in a thread
// as a thread is associated to a room
await isRoomGroup(rooms[0].room_id).then(async (isGroup: boolean) => {
if (isGroup) {
this.createOrRegisterOnMembers(rooms[0].room_id);
} else {
// reply from channel
// todo
// if (sender == owner) { // correction from the original sender
// // leave in the same channel
// }
}
});
} else if (rooms.length > 1) {
// get the lowest thread (order by room_id)
const roomId = rooms[rooms.length - 1].room_id;
this.createOrRegisterOnMembers(roomId);
}
});
}
}

View File

@@ -1,6 +1,7 @@
import { getAddresseId } from "../db/mail";
import {simpleParser} from "mailparser";
import { getAddressId, getFlagId } from "../../db/utils/mail";
import { EmailAddress, ParsedMail, simpleParser } from "mailparser";
import moment from "moment";
import Imap from "imap";
import {
registerMessage,
registerMailbox_message,
@@ -9,12 +10,14 @@ import {
registerBodypart,
saveBodypart,
saveSource,
} from "../db/saveMessage";
registerFlag,
} from "../../db/message/storeMessage-db";
import { getFieldId } from "../db/mail";
import logger from "../system/Logger";
import { getFieldId } from "../../db/utils/mail";
import logger from "../../system/Logger";
import { AttrsWithEnvelope } from "../../interfaces/mail/attrs.interface";
export function saveMessage(attrs, mailboxId, imap) {
export function saveMessage(attrs: AttrsWithEnvelope, mailboxId: number, imap: Imap): Promise<number> {
const envelope = attrs.envelope;
const ts = moment(new Date(envelope.date).getTime()).format("YYYY-MM-DD HH:mm:ss");
const rfc822size = attrs.size;
@@ -23,10 +26,13 @@ export function saveMessage(attrs, mailboxId, imap) {
return new Promise((resolve, reject) => {
registerMessage(ts, rfc822size, messageID)
.then((messageId) => {
const isSeen = attrs.flags.includes("\\Seen") ? 1 : 0; // todo verify
const deleted = attrs.flags.includes("\\Deleted") ? 1 : 0; // todo verify
const isSeen: boolean = attrs.flags.includes("\\Seen");
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: "" });
let buffer = "";
@@ -67,15 +73,25 @@ export function saveMessage(attrs, mailboxId, imap) {
});
}
async function saveFromParsedData(parsed, messageId) {
function registerFlags(messageId: number, flags: string[]) {
flags.forEach((flag) => {
getFlagId(flag).then((flagId) => {
registerFlag(messageId, flagId);
}).catch((err: Error) => {
logger.err(err);
});
});
}
async function saveFromParsedData(parsed: ParsedMail, messageId: number) {
const promises: Promise<any>[] = [];
Object.keys(parsed).forEach((key) => {
if (["from", "to", "cc", "bcc", "replyTo"].includes(key)) {
promises.push(
// save address field
getFieldId(key).then((fieldId) => {
parsed[key].value.forEach((addr, nb) => {
getAddresseId(addr.address, addr.name).then(async (addressId) => {
parsed[key].value.forEach((addr: EmailAddress, nb: number) => {
getAddressId(addr.address, addr.name).then(async (addressId) => {
await saveAddress_fields(messageId, fieldId, addressId, nb);
});
});
@@ -104,7 +120,7 @@ async function saveFromParsedData(parsed, messageId) {
});
});
} else if (key == "attachments") {
// todo
// todo attachments
} else if (["date", "messageId", "headers", "headerLines"].includes(key)) {
// messageId and date are already saved
// other field are not important and can be retrieved in source
@@ -118,7 +134,6 @@ async function saveFromParsedData(parsed, messageId) {
// todo when transfered
}
if (process.env["NODE_DEV"] == "TEST") {
module.exports = {
saveFromParsedData,

View 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
View 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;
}
}

View 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);
// });
}
}

View 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
}

2855
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,10 @@
"clean": "rm -rf build"
},
"dependencies": {
"@databases/mysql-test": "^4.0.2",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"colors": "^1.4.0",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
@@ -26,14 +28,21 @@
"@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",
"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": {
"preset": "ts-jest",
"setupFiles": [
"<rootDir>/test/.jest/setEnvVars.js"
],
"testEnvironment": "node",
"testMatch": [
"<rootDir>/test//**/*-test.[jt]s?(x)"

28
back/routes/account.ts Normal file
View 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;

View File

@@ -1,54 +0,0 @@
import statusCodes from "../utils/statusCodes";
import express from "express";
const router = express.Router();
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
import schema_account from "../schemas/account_schema.json";
import { addAccount } from "../controllers/addAccount";
import { getAccounts } from "../db/api";
import { rooms } from "../controllers/rooms";
import { messages } from "../controllers/messages";
import { members } from "../controllers/members";
const validate_account = ajv.compile(schema_account);
/**
* 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);
});
router.get("/:roomId/members", async (req, res) => {
const { roomId } = req.params;
await members(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);
}
});
export default router;

23
back/routes/message.ts Normal file
View 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
View 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;

View File

@@ -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
}

View File

@@ -1,7 +1,11 @@
import express from "express";
import cors from "cors";
const app = express();
import ImapSync from "./mails/imap/ImapSync";
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(
@@ -12,8 +16,26 @@ app.use(
app.use(cors());
app.listen(process.env.PORT || 5500);
import mailRouter from "./routes/mail";
app.use("/api/mail", mailRouter);
app.use("/api/account", accountRouter);
app.use("/api/room", roomRouter);
app.use("/api/message", messageRouter);
const imapSync = new ImapSync();
imapSync.init();
// 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, []);
});
});
}

View File

@@ -1,40 +1,50 @@
import color from "colors";
const logType = {
LOG: 0,
DEBUG: 1,
WARN: 2,
ERR: 3,
};
class Logger {
constructor() {
constructor() {}
}
log(...content): void {
// console.log(this._prefix("log"), content);
}
warn(...content): void {
// console.warn(this._prefix("warn"), content);
}
error(...content): void {
// console.error(this._prefix("err"), content);
}
_prefix(type: string): string {
let typeStr = "";
print(header: string, message: string, type: number) {
const content = `[${this._timestamp}] - [${header}] -- ${message}`;
switch (type) {
case "log":
typeStr = "LOG"
case logType.LOG:
console.log(content);
break;
case "warn":
typeStr = "WARN"
case logType.DEBUG:
console.debug(content);
break;
case "err":
typeStr = "ERR"
case logType.WARN:
console.warn(content);
break;
default:
case logType.ERR:
console.error(content);
break;
}
return `[${typeStr}: ${this._timestamp()}]`;
}
_timestamp() {
return new Date().toLocaleString();
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 });
}
}

View File

@@ -0,0 +1 @@
process.env.NODE_ENV = "test";

48
back/test/db/api-tes.ts Normal file
View 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()
})
});

View File

@@ -1,11 +1,58 @@
import { generateAttrs, generateUsers } from "../test-utils/test-attrsUtils";
import registerMessageInApp, { roomType } from "../../mails/saveMessage";
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 { getAddresseId, getUserIdOfMailbox } from "../../db/mail";
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
// todo mock db
// new message from us
// to multiple people -> room
// if response has same member => group
@@ -21,32 +68,65 @@ import { getAddresseId, getUserIdOfMailbox } from "../../db/mail";
// if multiple members reply -> group
// if only me reply -> channel
const users = generateUsers(5);
const ownUser = users[0];
const messageId = 1;
const boxId = 1;
beforeAll(async () => {
mocked(getAddressId).mockImplementation(db.getAddressId);
mocked(getUserIdOfMailbox).mockImplementation(db.getUserIdOfMailbox);
mocked(findRoomByOwner).mockImplementation(db.findRoomByOwner);
jest.mock("../../db/mail", () => ({
getAddresseId: jest.fn().mockImplementation((email) => {
const match = users.find((user) => user.user.mailbox + "@" + user.user.host == email);
return new Promise((resolve, reject) => resolve(match?.id));
}),
getUserIdOfMailbox: jest.fn().mockImplementation((boxId) => {
return new Promise((resolve, reject) => resolve([{ user_id: ownUser.id }]));
}),
}));
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: [users[1].user] });
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: [users[2].user], to: [users[1].user] });
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();
@@ -54,59 +134,96 @@ describe("saveMessage", () => {
expect(res2).toBe(false);
});
});
describe("implementation", () => {
describe("new first message from us", () => {
it("new first message from us to one recipient should create a DM", async () => {
const attrs = generateAttrs({ from: [ownUser.user], to: [users[1].user] });
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 register = new registerMessageInApp(messageId, attrs, boxId);
const createOrRegisterOnExistence = jest
.spyOn(register, "createOrRegisterOnExistence")
.mockImplementation(
(owner: number, roomType: number) => new Promise((resolve, reject) => resolve()),
);
const createOrRegisterOnExistence = jest
.spyOn(register, "createOrRegisterOnExistence")
.mockImplementation((owner: number, roomType: RoomType) => new Promise((resolve, reject) => resolve()));
await register.save();
await register.save();
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(users[1].id, roomType.DM);
});
it("new first message from us to multiple recipients should create a ROOM", async () => {
const attrs = generateAttrs({ from: [ownUser.user], to: [users[1].user, users[2].user] });
const register = new registerMessageInApp(messageId, attrs, boxId);
const initiateRoom = jest
.spyOn(register, "initiateRoom")
.mockImplementation((owner: number, roomType: number) => Promise.resolve(1));
await register.save();
expect(initiateRoom).toHaveBeenCalledWith(ownUser.id, roomType.ROOM);
});
// it("response to new first message to multiple recipients with same members should change room type to GROUP", () => {});
// it("response to new first message to multiple recipients with different members should change room type to CHANNEL", () => {});
// the owner of the room will be the recipient (not us)
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(db.users[1].id, RoomType.DM);
});
describe("new first message from other", () => {
it("new first message from other to me only should create a room", async () => {
const attrs = generateAttrs({ from: [users[1].user], to: [ownUser.user] });
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 register = new registerMessageInApp(messageId, attrs, boxId);
const createOrRegisterOnExistence = jest
.spyOn(register, "createOrRegisterOnExistence")
.mockImplementation((owner: number, roomType: number) => {
return new Promise((resolve, reject) => resolve());
});
const initiateRoom = jest
.spyOn(register, "initiateRoom")
.mockImplementation((owner: number, roomType: RoomType) => Promise.resolve(1));
await register.save();
await register.save();
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(users[1].id, roomType.ROOM);
});
expect(initiateRoom).toHaveBeenCalledWith(ownUser.id, RoomType.GROUP);
});
// describe("replies", () => {
// it("", () => {});
// it("response to new first message to multiple recipients with same members should change room type to GROUP", () => {
// });
// describe("", () => {});
// 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);
});
});
});

View 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);
}
});
});
});
}

View 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 }]));
};
}

View File

@@ -36,14 +36,16 @@ export function generateAttrs(options: Options): AttrsWithEnvelope {
"flags": options.flags ?? [],
"uid": options.uid ?? randomInt(3),
"modseq": options.modseq ?? randomInt(7),
"x-gm-labels": ["\\Inbox"],
"x-gm-msgid": "1760991478422670209",
"x-gm-thrid": "1760991478422670209",
};
return attrs;
}
export function generateUsers(nb: number) {
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({
@@ -68,6 +70,6 @@ function randomString(length: number): string {
return result;
}
function randomInt(length: number): number {
export function randomInt(length: number): number {
return parseInt((Math.random() * Math.pow(10, length)).toFixed());
}

View 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;
}

View File

@@ -8,10 +8,5 @@
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node", "jest"],
"paths": {
"*": ["node_modules/*", "src/types/*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
}

View File

@@ -1,5 +1,5 @@
export function removeDuplicates(array: []) {
let unique = [];
const unique = [];
for (let i = 0; i < array.length; i++) {
if (!unique.includes(array[i])) {
unique.push(array[i]);
@@ -7,3 +7,10 @@ export function removeDuplicates(array: []) {
}
return unique;
}
export function hasSameElements(a1: any[], a2: any[]) {
return (
a1.length == a2.length &&
a1.reduce((a, b) => a && a2.includes(b), true)
);
}

View 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
}

View File

@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"mailboxId": {
"type": "number"
},
"messageId": {
"type": "number"
}
},
"required": [
"mailboxId",
"messageId"
],
"additionalProperties": false
}

View File

@@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"roomId": {
"type": "number"
}
},
"required": [
"roomId"
],
"additionalProperties": false
}

View File

@@ -0,0 +1,6 @@
{
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
}

View File

@@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"roomId": {
"type": "string"
}
},
"required": [
"roomId"
],
"additionalProperties": false
}

View File

@@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"roomId": {
"type": "string"
}
},
"required": [
"roomId"
],
"additionalProperties": false
}

View File

@@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"mailboxId": {
"type": "string"
}
},
"required": [
"mailboxId"
],
"additionalProperties": false
}

View 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
}

View File

@@ -0,0 +1,20 @@
{
"type": "object",
"properties": {
"mailboxId": {
"type": "number"
},
"messageId": {
"type": "number"
},
"flag": {
"type": "string"
}
},
"required": [
"mailboxId",
"messageId",
"flag"
],
"additionalProperties": false
}

View 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;

View File

@@ -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/).

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

11270
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,26 +5,69 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@vueuse/components": "^9.13.0",
"@vueuse/core": "^9.13.0",
"axios": "^1.3.4",
"core-js": "^3.8.3",
"dompurify": "^3.0.1",
"@popperjs/core": "^2.11.7",
"@tiptap/extension-bold": "^2.0.3",
"@tiptap/extension-bullet-list": "^2.0.3",
"@tiptap/extension-hard-break": "^2.0.3",
"@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-router": "^4.1.6",
"vuex": "^4.0.2"
"vue-router": "^4.0.3",
"vue-svg-loader": "^0.16.0",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^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-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/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-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": {
"root": true,
@@ -33,14 +76,27 @@
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
"ecmaVersion": 2020
},
"rules": {
"vue/multi-word-component-names": "off"
}
},
"overrides": [
{
"files": [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)"
],
"env": {
"jest": true
}
}
]
},
"browserslist": [
"> 1%",

View File

@@ -1,30 +1,19 @@
<script setup>
import { RouterView } from "vue-router";
import Sidebar from "./views/sidebar/Sidebar";
</script>
<template>
<div id="app">
<Sidebar />
<RouterView/>
</div>
<div id="app">
<Sidebar />
<RouterView />
</div>
</template>
<script>
import Sidebar from './views/sidebar/Sidebar'
export default {
name: 'App',
components: {
Sidebar,
},
}
</script>
<style>
#app {
display: flex;
height: 100%;
width: 100%;
display: flex;
height: 100%;
width: 100%;
}
</style>

View 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);
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.79297 5.20718L12.0001 11.4143L18.2072 5.20718L16.793 3.79297L12.0001 8.58586L7.20718 3.79297L5.79297 5.20718Z M18.2073 18.7928L12.0002 12.5857L5.79312 18.7928L7.20733 20.207L12.0002 15.4141L16.7931 20.207L18.2073 18.7928Z"></path></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M20 7V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V7H2V5H22V7H20ZM6 7V20H18V7H6ZM7 2H17V4H7V2ZM11 10H13V17H11V10Z" fill="rgba(0,0,0,1)"></path></svg>

After

Width:  |  Height:  |  Size: 255 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z" fill="rgba(0,0,0,1)"></path></svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.4143 4.58594L10.4142 11.0003L16.0003 11.0004L16.0003 13.0004L10.4142 13.0003L10.4141 19.4144L3 12.0002L10.4143 4.58594ZM18.0002 19.0002V5.00018H20.0002V19.0002H18.0002Z"></path></svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.2073 9.04304L12.0002 2.83594L5.79312 9.04304L7.20733 10.4573L12.0002 5.66436L16.7931 10.4573L18.2073 9.04304Z M5.79297 14.9574L12.0001 21.1646L18.2072 14.9574L16.793 13.5432L12.0001 18.3361L7.20718 13.5432L5.79297 14.9574Z"></path></svg>

After

Width:  |  Height:  |  Size: 310 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M21.1384 3C21.4146 3 21.6385 3.22386 21.6385 3.5C21.6385 3.58701 21.6157 3.67252 21.5725 3.74807L18 10L21.5725 16.2519C21.7095 16.4917 21.6262 16.7971 21.3865 16.9341C21.3109 16.9773 21.2254 17 21.1384 17H4V22H2V3H21.1384ZM18.5536 5H4V15H18.5536L15.6965 10L18.5536 5Z" fill="rgba(0,0,0,1)"></path></svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M12.382 3C12.7607 3 13.107 3.214 13.2764 3.55279L14 5H20C20.5523 5 21 5.44772 21 6V17C21 17.5523 20.5523 18 20 18H13.618C13.2393 18 12.893 17.786 12.7236 17.4472L12 16H5V22H3V3H12.382ZM11.7639 5H5V14H13.2361L14.2361 16H19V7H12.7639L11.7639 5Z" fill="rgba(0,0,0,1)"></path></svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M15.2459 14H8.75407L7.15407 18H5L11 3H13L19 18H16.8459L15.2459 14ZM14.4459 12L12 5.88516L9.55407 12H14.4459ZM3 20H21V22H3V20Z" fill="rgba(0,0,0,1)"></path></svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M13 20H11V13H4V20H2V4H4V11H11V4H13V20ZM21.0005 8V20H19.0005L19 10.204L17 10.74V8.67L19.5005 8H21.0005Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 221 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M4 4V11H11V4H13V20H11V13H4V20H2V4H4ZM18.5 8C20.5711 8 22.25 9.67893 22.25 11.75C22.25 12.6074 21.9623 13.3976 21.4781 14.0292L21.3302 14.2102L18.0343 18H22V20H15L14.9993 18.444L19.8207 12.8981C20.0881 12.5908 20.25 12.1893 20.25 11.75C20.25 10.7835 19.4665 10 18.5 10C17.5818 10 16.8288 10.7071 16.7558 11.6065L16.75 11.75H14.75C14.75 9.67893 16.4289 8 18.5 8Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 479 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M22 8L21.9984 10L19.4934 12.883C21.0823 13.3184 22.25 14.7728 22.25 16.5C22.25 18.5711 20.5711 20.25 18.5 20.25C16.674 20.25 15.1528 18.9449 14.8184 17.2166L16.7821 16.8352C16.9384 17.6413 17.6481 18.25 18.5 18.25C19.4665 18.25 20.25 17.4665 20.25 16.5C20.25 15.5335 19.4665 14.75 18.5 14.75C18.214 14.75 17.944 14.8186 17.7056 14.9403L16.3992 13.3932L19.3484 10H15V8H22ZM4 4V11H11V4H13V20H11V13H4V20H2V4H4Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M13 20H11V13H4V20H2V4H4V11H11V4H13V20ZM22 8V16H23.5V18H22V20H20V18H14.5V16.66L19.5 8H22ZM20 11.133L17.19 16H20V11.133Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 237 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M22 8V10H17.6769L17.2126 12.6358C17.5435 12.5472 17.8912 12.5 18.25 12.5C20.4591 12.5 22.25 14.2909 22.25 16.5C22.25 18.7091 20.4591 20.5 18.25 20.5C16.4233 20.5 14.8827 19.2756 14.4039 17.6027L16.3271 17.0519C16.5667 17.8881 17.3369 18.5 18.25 18.5C19.3546 18.5 20.25 17.6046 20.25 16.5C20.25 15.3954 19.3546 14.5 18.25 14.5C17.6194 14.5 17.057 14.7918 16.6904 15.2478L14.8803 14.3439L16 8H22ZM4 4V11H11V4H13V20H11V13H4V20H2V4H4Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 549 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M21.097 8L18.499 12.5C20.7091 12.5 22.5 14.2909 22.5 16.5C22.5 18.7091 20.7091 20.5 18.5 20.5C16.2909 20.5 14.5 18.7091 14.5 16.5C14.5 15.7636 14.699 15.0737 15.0461 14.4811L18.788 8H21.097ZM4 4V11H11V4H13V20H11V13H4V20H2V4H4ZM18.5 14.5C17.3954 14.5 16.5 15.3954 16.5 16.5C16.5 17.6046 17.3954 18.5 18.5 18.5C19.6046 18.5 20.5 17.6046 20.5 16.5C20.5 15.3954 19.6046 14.5 18.5 14.5Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M17 11V4H19V21H17V13H7V21H5V4H7V11H17Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 157 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M21 15V18H24V20H21V23H19V20H16V18H19V15H21ZM21.0082 3C21.556 3 22 3.44495 22 3.9934V13H20V5H4V18.999L14 9L17 12V14.829L14 11.8284L6.827 19H14V21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082ZM8 7C9.10457 7 10 7.89543 10 9C10 10.1046 9.10457 11 8 11C6.89543 11 6 10.1046 6 9C6 7.89543 6.89543 7 8 7Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 453 B

View 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 19H21V21H3V19ZM11 14H21V16H11V14ZM11 9H21V11H11V9ZM3 12.5L7 9V16L3 12.5Z" fill="rgba(0,0,0,1)"></path></svg>

After

Width:  |  Height:  |  Size: 217 B

View 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 19H21V21H3V19ZM11 14H21V16H11V14ZM11 9H21V11H11V9ZM7 12.5L3 16V9L7 12.5Z" fill="rgba(0,0,0,1)"></path></svg>

After

Width:  |  Height:  |  Size: 217 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M15 20H7V18H9.92661L12.0425 6H9V4H17V6H14.0734L11.9575 18H15V20Z" fill="rgba(0,0,0,1)"></path></svg>

After

Width:  |  Height:  |  Size: 192 B

Some files were not shown because too many files have changed in this diff Show More