Compare commits
53 Commits
b48c834d36
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adb7a1161a | ||
|
|
3c069ed2f8 | ||
|
|
ae73326820 | ||
|
|
f40b6758de | ||
|
|
43e8cc7d3e | ||
|
|
edddf9afbf | ||
|
|
3bffd88108 | ||
|
|
0094783a4e | ||
|
|
f42d819e45 | ||
|
|
467b0eebe9 | ||
|
|
e8e8555c2f | ||
|
|
af4cc2f6a0 | ||
|
|
2cae8f12a7 | ||
|
|
7be2e84691 | ||
|
|
737a22e1f8 | ||
|
|
4ecd723cec | ||
|
|
53c79aebc4 | ||
|
|
b2b0949353 | ||
|
|
ffcfc57bbe | ||
|
|
f7c95b3a36 | ||
|
|
843659b495 | ||
|
|
1a7828b281 | ||
|
|
b821c89e20 | ||
|
|
c3374a612e | ||
|
|
686e6a4911 | ||
|
|
b137263bef | ||
|
|
2c7b4f1c78 | ||
|
|
3dab9c8db1 | ||
|
|
5aef5ab7b0 | ||
|
|
8f980748b5 | ||
|
|
22fb12e6d6 | ||
|
|
12e508c7cb | ||
|
|
29b4b7bfeb | ||
|
|
91a52a29ce | ||
|
|
6e8e3bf1f4 | ||
|
|
dd8f9210a9 | ||
|
|
10fc3fd628 | ||
|
|
dc20ccfec0 | ||
|
|
ca096dac89 | ||
|
|
cd996d851a | ||
|
|
0f063deff9 | ||
|
|
614f7d9802 | ||
|
|
318748c984 | ||
|
|
4d3fbe292e | ||
|
|
2594f6042b | ||
|
|
8c7cc1f316 | ||
|
|
85b63f0ea3 | ||
|
|
e44584df0a | ||
|
|
956bc35158 | ||
|
|
79e17ad24f | ||
|
|
4799e477be | ||
|
|
7ad22e55c1 | ||
|
|
5b62fce48a |
3
.gitignore
vendored
@@ -31,4 +31,5 @@ log*
|
|||||||
tmp
|
tmp
|
||||||
test.*
|
test.*
|
||||||
*.png
|
*.png
|
||||||
!*/schemas/*
|
!*/schemas/*
|
||||||
|
todo
|
||||||
31
README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# TRIFORM
|
||||||
|
|
||||||
|
_WORK IN PROGRESS_
|
||||||
|
|
||||||
|
TRIFORM (Threaded Rooms Interface For Organised Relational Mails) is a mail client which sorts mails into conversations based on the members present in each mails.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [x] Multi account (tested only with gmail)
|
||||||
|
- [x] Live syncing of mails
|
||||||
|
- [x] Live syncing of flags
|
||||||
|
- [x] Mark as read/unread
|
||||||
|
- [x] Flag important
|
||||||
|
- [ ] Attachments
|
||||||
|
- [ ] Send new mails
|
||||||
|
- [ ] Respond to mails
|
||||||
|
- [x] Delete mails
|
||||||
|
- [ ] Delete rooms
|
||||||
|
- [ ] Live sync with the ui
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
- Room: General space where messages are grouped.
|
||||||
|
- Channel: Space use for newsletters or other conversations when there is a low need for reply.
|
||||||
|
- Group: Space contening several users that are part of the conversation.
|
||||||
|
- DM: Space defining a conversation with only one other member.
|
||||||
|
- Thread: Sub-Space generally created when changing the number of member in a space.
|
||||||
|
|
||||||
|
## Examples
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { getAccounts, registerAccount } from "../db/api-db";
|
import { getAccounts, getRooms, registerAccount } from "../db/api-db";
|
||||||
import { getAddresseId } from "../db/utils/mail";
|
import { getAddressId } from "../db/utils/mail";
|
||||||
|
import logger from "../system/Logger";
|
||||||
import statusCodes from "../utils/statusCodes";
|
import statusCodes from "../utils/statusCodes";
|
||||||
|
|
||||||
export default class Account {
|
export default class Account {
|
||||||
@@ -11,10 +12,10 @@ export default class Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async register(body, res: Response) {
|
static async register(body, res: Response) {
|
||||||
const { email, pwd, xoauth, xoauth2, host, port, tls } = body;
|
const { email, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls } = body;
|
||||||
getAddresseId(email).then((addressId) => {
|
getAddressId(email).then((addressId) => {
|
||||||
registerAccount(addressId, pwd, xoauth, xoauth2, host, port, tls)
|
registerAccount(addressId, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls)
|
||||||
.then((mailboxId) => {
|
.then((mailboxId) => {0
|
||||||
res.status(statusCodes.OK).json({ id: mailboxId });
|
res.status(statusCodes.OK).json({ id: mailboxId });
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -23,4 +24,16 @@ export default class Account {
|
|||||||
});
|
});
|
||||||
// todo change mailbox to account
|
// todo change mailbox to account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getRooms(body, res: Response) {
|
||||||
|
const { mailboxId, offset, limit } = body;
|
||||||
|
getRooms(mailboxId)
|
||||||
|
.then((rooms) => {
|
||||||
|
res.status(statusCodes.OK).json(rooms);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.err(err);
|
||||||
|
res.status(statusCodes.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
120
back/abl/Message-abl.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import statusCode from "../utils/statusCodes";
|
||||||
|
import { Response } from "express";
|
||||||
|
import logger from "../system/Logger";
|
||||||
|
import Message from "../mails/message/Message";
|
||||||
|
import Room from "../mails/room/Room";
|
||||||
|
|
||||||
|
export default class MessageAbl {
|
||||||
|
static async changeFlag(body, res: Response, isDelete: boolean) {
|
||||||
|
const { mailboxId, messageId, flag } = body;
|
||||||
|
const message = new Message().setMessageId(messageId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.useUid();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.useMailbox(mailboxId);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isDelete) {
|
||||||
|
await message.mailbox.removeFlag(message.uid.toString(), flag);
|
||||||
|
} else {
|
||||||
|
await message.mailbox.addFlag(message.uid.toString(), flag);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
}
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addFlag(body, res: Response) {
|
||||||
|
await MessageAbl.changeFlag(body, res, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeFlag(body, res: Response) {
|
||||||
|
await MessageAbl.changeFlag(body, res, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteRemoteUtil(message: Message, mailboxId: number, res, isFull: boolean): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await message.useUid();
|
||||||
|
} catch (error) {
|
||||||
|
logger.err(error);
|
||||||
|
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.useMailbox(mailboxId);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.mailbox.addFlag(message.uid.toString(), ["\\Deleted"]);
|
||||||
|
await message.mailbox.moveToTrash(message.uid.toString(), (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.err(err);
|
||||||
|
}
|
||||||
|
// throw err;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.log(err);
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFull) {
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static deleteRemoteOnly = async (body, res: Response) => {
|
||||||
|
const { mailboxId, messageId } = body;
|
||||||
|
const message = new Message().setMessageId(messageId);
|
||||||
|
await MessageAbl.deleteRemoteUtil(message, mailboxId, res, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
static deleteEverywhere = async (body, res: Response) => {
|
||||||
|
const { mailboxId, messageId } = body;
|
||||||
|
const message = new Message().setMessageId(messageId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.useFlags();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
logger.err(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if message not deleted remotly, delete it
|
||||||
|
if (!message.isDeleted) {
|
||||||
|
const success = await MessageAbl.deleteRemoteUtil(message, mailboxId, res, false);
|
||||||
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await new Room().setRoomIdOnMessageId(messageId);
|
||||||
|
try {
|
||||||
|
await message.delete();
|
||||||
|
if (room.roomId && await room.shouldDelete()) {
|
||||||
|
await room.delete();
|
||||||
|
res.status(statusCode.OK).json({ deleteRoom: true }).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import statusCode from "../utils/statusCodes";
|
|
||||||
import { Response } from "express";
|
|
||||||
|
|
||||||
export default class Message {
|
|
||||||
|
|
||||||
static async addFlag(body, res: Response) {
|
|
||||||
console.log("hit")
|
|
||||||
res.status(statusCode.OK).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async removeFlag(body, res: Response) {
|
|
||||||
console.log("hit")
|
|
||||||
res.status(statusCode.OK).send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
93
back/abl/Room-abl.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import statusCode from "../utils/statusCodes";
|
||||||
|
import { Response } from "express";
|
||||||
|
import { RoomType } from "../mails/message/saveMessage";
|
||||||
|
import { getRoomType } from "../db/message/saveMessage-db";
|
||||||
|
import { getLastMsgData, getRoomOwner } from "../db/Room-db";
|
||||||
|
import emailManager from "../mails/EmailManager";
|
||||||
|
import MailBuilder from "../mails/utils/mailBuilder";
|
||||||
|
import { getAddresses } from "../db/utils/mail";
|
||||||
|
import { getMembers, getMessages, getRooms } from "../db/api-db";
|
||||||
|
import logger from "../system/Logger";
|
||||||
|
import Room from "../mails/room/Room";
|
||||||
|
|
||||||
|
function rmUserFromAddrs(addresses: { email: string }[], user: string) {
|
||||||
|
let index = addresses.findIndex((a) => a.email == user);
|
||||||
|
if (index != -1) {
|
||||||
|
addresses.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default class RoomAbl {
|
||||||
|
// todo change name of reponse
|
||||||
|
static async response(body, res: Response) {
|
||||||
|
const { user, roomId, text, html } = body;
|
||||||
|
const roomType = (await getRoomType(roomId))[0].room_type;
|
||||||
|
|
||||||
|
if (roomType === RoomType.DM) {
|
||||||
|
const ownerEmail = (await getRoomOwner(roomId))[0].email;
|
||||||
|
|
||||||
|
const mailBuilder = new MailBuilder();
|
||||||
|
mailBuilder.from(user).to(ownerEmail).text(text).html(html);
|
||||||
|
|
||||||
|
emailManager.getSmtp(user)?.sendMail(mailBuilder.message);
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
} else if (roomType === RoomType.GROUP || roomType === RoomType.THREAD) {
|
||||||
|
const lastMsgData = (await getLastMsgData(roomId))[0];
|
||||||
|
const mailBuilder = new MailBuilder();
|
||||||
|
mailBuilder.inReplySubject(lastMsgData.subject).inReplyTo(lastMsgData.messageID).text(text).html(html);
|
||||||
|
|
||||||
|
const from = await getAddresses(lastMsgData.fromA);
|
||||||
|
let to = lastMsgData.toA ? await getAddresses(lastMsgData.toA) : [];
|
||||||
|
let cc = lastMsgData.ccA ? await getAddresses(lastMsgData.ccA) : [];
|
||||||
|
|
||||||
|
// remove us from recipients
|
||||||
|
rmUserFromAddrs(to, user);
|
||||||
|
rmUserFromAddrs(from, user);
|
||||||
|
|
||||||
|
// add sender of previous as recipient if it is not us
|
||||||
|
if (from.findIndex((a) => a.email == user) == -1) {
|
||||||
|
to = to.concat(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
mailBuilder
|
||||||
|
.from(user)
|
||||||
|
.to(to.map((a) => a.email))
|
||||||
|
.cc(cc.map((a) => a.email));
|
||||||
|
|
||||||
|
emailManager.getSmtp(user)?.sendMail(mailBuilder.message);
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
} else {
|
||||||
|
res.status(statusCode.FORBIDDEN).send({ error: "Cannot add a new message in a room or a channel." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getMessages(body, res: Response) {
|
||||||
|
const { roomId } = body;
|
||||||
|
getMessages(roomId)
|
||||||
|
.then((messages) => {
|
||||||
|
res.status(statusCode.OK).json(messages);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.err(err);
|
||||||
|
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getMembers(body, res: Response) {
|
||||||
|
const { roomId } = body;
|
||||||
|
getMembers(roomId)
|
||||||
|
.then((addresses) => {
|
||||||
|
res.status(statusCode.OK).json(addresses);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.err(err);
|
||||||
|
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(body, res: Response) {
|
||||||
|
const { roomId } = body;
|
||||||
|
console.log("delete", roomId);
|
||||||
|
const room = new Room().setRoomId(roomId);
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import statusCode from "../utils/statusCodes";
|
|
||||||
import { getMembers } from "../db/api-db";
|
|
||||||
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.err(err)
|
|
||||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import statusCode from "../utils/statusCodes";
|
|
||||||
import { getMessages } from "../db/api-db";
|
|
||||||
import logger from "../system/Logger";
|
|
||||||
import { Response } from "express";
|
|
||||||
|
|
||||||
export async function messages(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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import statusCode from "../utils/statusCodes";
|
|
||||||
import { getRooms } from "../db/api-db";
|
|
||||||
import logger from "../system/Logger";
|
|
||||||
import { Response } from "express";
|
|
||||||
|
|
||||||
export async function rooms(body, res: Response) {
|
|
||||||
const { mailboxId, offset, limit } = body;
|
|
||||||
getRooms(mailboxId).then((rooms) => {
|
|
||||||
res.status(statusCode.OK).json(rooms);
|
|
||||||
}).catch((err) => {
|
|
||||||
logger.err(err)
|
|
||||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
85
back/db/Room-db.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { execQueryAsync } from "./db";
|
||||||
|
import { queryCcId, queryFromId, queryToId } from "./utils/addressQueries";
|
||||||
|
|
||||||
|
export async function getRoomOwner(roomId: number) {
|
||||||
|
const query = `
|
||||||
|
SELECT address.email
|
||||||
|
FROM app_room
|
||||||
|
INNER JOIN address ON address.address_id = app_room.owner_id
|
||||||
|
WHERE app_room.room_id = ?
|
||||||
|
`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get all the data needed to reply to a message in a room
|
||||||
|
*/
|
||||||
|
export async function getLastMsgData(roomId: number) {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
msg.message_id AS id,
|
||||||
|
GROUP_CONCAT(fromT.address_id) AS fromA,
|
||||||
|
GROUP_CONCAT(toT.address_id) AS toA,
|
||||||
|
GROUP_CONCAT(ccT.address_id) AS ccA,
|
||||||
|
subjectT.value AS subject,
|
||||||
|
content.text AS content,
|
||||||
|
message.idate AS date,
|
||||||
|
message.messageID AS messageID
|
||||||
|
FROM app_room_message msg
|
||||||
|
|
||||||
|
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||||
|
${queryToId} toT ON msg.message_id = toT.message_id
|
||||||
|
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT header_field.message_id, header_field.value
|
||||||
|
FROM header_field
|
||||||
|
INNER JOIN field_name
|
||||||
|
WHERE
|
||||||
|
field_name.field_id = header_field.field_id AND
|
||||||
|
field_name.field_name = 'subject'
|
||||||
|
) subjectT ON msg.message_id = subjectT.message_id
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT bodypart.text, header_field.message_id FROM bodypart
|
||||||
|
INNER JOIN header_field
|
||||||
|
INNER JOIN field_name
|
||||||
|
WHERE
|
||||||
|
field_name.field_id = header_field.field_id AND
|
||||||
|
field_name.field_name = 'html' AND
|
||||||
|
bodypart.bodypart_id = header_field.bodypart_id
|
||||||
|
) content ON msg.message_id = content.message_id
|
||||||
|
|
||||||
|
INNER JOIN message ON message.message_id = msg.message_id
|
||||||
|
|
||||||
|
WHERE msg.room_id = ?
|
||||||
|
GROUP BY msg.message_id
|
||||||
|
ORDER BY message.idate DESC
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRoomOnMessageId(messageId: number) {
|
||||||
|
const query = `SELECT room_id FROM app_room_message WHERE message_id = ?`;
|
||||||
|
const values = [messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRoomNbMessageAndThread(roomId: number): Promise<{ nbMessage: number; nbThread: number }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT COUNT(arm.room_id) AS nbMessage, COUNT(app_thread.room_id) AS nbThread
|
||||||
|
FROM app_room_message arm
|
||||||
|
INNER JOIN app_thread ON (app_thread.root_id = arm.room_id OR app_thread.parent_id = arm.room_id)
|
||||||
|
WHERE arm.room_id = ?`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRoom(roomId: number) {
|
||||||
|
const query = `DELETE FROM app_room WHERE room_id = ?`;
|
||||||
|
const values = [roomId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { execQueryAsync, execQueryAsyncWithId } from "./db";
|
import { execQueryAsync, execQueryAsyncWithId } from "./db";
|
||||||
import { queryCcId, queryToId, queryFromId } from "./utils/addressQueries";
|
import { queryCcId, queryToId, queryFromId } from "./utils/addressQueries";
|
||||||
|
|
||||||
export async function registerAccount(userId, pwd, xoauth, xoauth2, host, port, tls) {
|
export async function registerAccount(userId, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls) {
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO app_account
|
INSERT INTO app_account
|
||||||
(user_id, account_pwd, xoauth, xoauth2, host, port, tls) VALUES (?, ?, ?, ?, ?, ?, ?)
|
(user_id, account_pwd, xoauth, xoauth2, imap_host, smtp_host, imap_port, smtp_port, tls) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`;
|
`;
|
||||||
const values = [userId, pwd, xoauth, xoauth2, host, port, tls];
|
const values = [userId, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls];
|
||||||
return await execQueryAsyncWithId(query, values);
|
return await execQueryAsyncWithId(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export async function getAccounts() {
|
|||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRooms(mailboxId) {
|
export async function getRooms(mailboxId: number) {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
room.room_id AS id,
|
room.room_id AS id,
|
||||||
@@ -38,6 +38,7 @@ export async function getRooms(mailboxId) {
|
|||||||
room.room_type AS roomType,
|
room.room_type AS roomType,
|
||||||
mailbox_message.mailbox_id AS mailboxId,
|
mailbox_message.mailbox_id AS mailboxId,
|
||||||
app_thread.parent_id
|
app_thread.parent_id
|
||||||
|
|
||||||
FROM app_room room
|
FROM app_room room
|
||||||
INNER JOIN message ON message.message_id = room.message_id
|
INNER JOIN message ON message.message_id = room.message_id
|
||||||
INNER JOIN mailbox_message ON mailbox_message.message_id = message.message_id
|
INNER JOIN mailbox_message ON mailbox_message.message_id = message.message_id
|
||||||
@@ -55,6 +56,7 @@ export async function getRooms(mailboxId) {
|
|||||||
)
|
)
|
||||||
) notSeenRoom ON notSeenRoom.room_id = room.room_id
|
) notSeenRoom ON notSeenRoom.room_id = room.room_id
|
||||||
|
|
||||||
|
-- get not seen in thread
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT app_room_message.message_id, app_thread.parent_id
|
SELECT app_room_message.message_id, app_thread.parent_id
|
||||||
FROM app_room
|
FROM app_room
|
||||||
@@ -89,7 +91,8 @@ export async function getMessages(roomId: number) {
|
|||||||
subjectT.value AS subject,
|
subjectT.value AS subject,
|
||||||
content.text AS content,
|
content.text AS content,
|
||||||
message.idate AS date,
|
message.idate AS date,
|
||||||
GROUP_CONCAT(flagT.flag_name) AS flags
|
GROUP_CONCAT(flagT.flag_name) AS flags,
|
||||||
|
thread.room_id AS thread
|
||||||
FROM app_room_message msg
|
FROM app_room_message msg
|
||||||
|
|
||||||
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||||
@@ -104,9 +107,10 @@ export async function getMessages(roomId: number) {
|
|||||||
field_name.field_id = header_field.field_id AND
|
field_name.field_id = header_field.field_id AND
|
||||||
field_name.field_name = 'subject'
|
field_name.field_name = 'subject'
|
||||||
) subjectT ON msg.message_id = subjectT.message_id
|
) subjectT ON msg.message_id = subjectT.message_id
|
||||||
|
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT bodypart.text, header_field.message_id FROM bodypart
|
SELECT bodypart.text, header_field.message_id
|
||||||
|
FROM bodypart
|
||||||
INNER JOIN header_field
|
INNER JOIN header_field
|
||||||
INNER JOIN field_name
|
INNER JOIN field_name
|
||||||
WHERE
|
WHERE
|
||||||
@@ -120,15 +124,22 @@ export async function getMessages(roomId: number) {
|
|||||||
|
|
||||||
INNER JOIN message ON message.message_id = msg.message_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 = ?
|
WHERE msg.room_id = ?
|
||||||
GROUP BY msg.message_id
|
GROUP BY msg.message_id
|
||||||
ORDER BY message.idate DESC;
|
ORDER BY message.idate ASC;
|
||||||
`;
|
`;
|
||||||
const values = [roomId];
|
const values = [roomId];
|
||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMembers(roomId) {
|
export async function getMembers(roomId: number) {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
address.address_id AS id,
|
address.address_id AS id,
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ CREATE TABLE app_account (
|
|||||||
account_pwd BINARY(22),
|
account_pwd BINARY(22),
|
||||||
xoauth VARCHAR(116),
|
xoauth VARCHAR(116),
|
||||||
xoauth2 VARCHAR(116),
|
xoauth2 VARCHAR(116),
|
||||||
host VARCHAR(255) NOT NULL DEFAULT 'localhost',
|
imap_host VARCHAR(255) NOT NULL DEFAULT 'localhost',
|
||||||
port INT(5) NOT NULL DEFAULT 143,
|
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,
|
tls BOOLEAN NOT NULL DEFAULT true,
|
||||||
PRIMARY KEY (account_id),
|
PRIMARY KEY (account_id),
|
||||||
FOREIGN KEY (user_id) REFERENCES address(address_id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES address(address_id) ON DELETE CASCADE
|
||||||
@@ -121,13 +123,13 @@ CREATE TABLE app_room (
|
|||||||
room_id INT AUTO_INCREMENT,
|
room_id INT AUTO_INCREMENT,
|
||||||
room_name VARCHAR(255) NOT NULL,
|
room_name VARCHAR(255) NOT NULL,
|
||||||
owner_id INT NOT NULL,
|
owner_id INT NOT NULL,
|
||||||
message_id INT NOT NULL,
|
message_id INT,
|
||||||
room_type INT NOT NULL DEFAULT 0,
|
room_type INT NOT NULL DEFAULT 0,
|
||||||
lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
|
lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
|
||||||
PRIMARY KEY (room_id),
|
PRIMARY KEY (room_id),
|
||||||
UNIQUE KEY (owner_id, message_id, room_type),
|
UNIQUE KEY (owner_id, message_id, room_type),
|
||||||
FOREIGN KEY (owner_id) REFERENCES address(address_id),
|
FOREIGN KEY (owner_id) REFERENCES address(address_id),
|
||||||
FOREIGN KEY (message_id) REFERENCES message(message_id)
|
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 12
|
-- 12
|
||||||
@@ -175,6 +177,5 @@ create table flag (
|
|||||||
flag_id INT NOT NULL,
|
flag_id INT NOT NULL,
|
||||||
UNIQUE KEY (message_id, flag_id),
|
UNIQUE KEY (message_id, flag_id),
|
||||||
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (flag_id) REFERENCES flag_name(flag_id) ON DELETE CASCADE
|
FOREIGN KEY (flag_id) REFERENCES flag_name(flag_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
@@ -6,9 +6,11 @@ export async function getAllAccounts() {
|
|||||||
app_account.account_id AS id,
|
app_account.account_id AS id,
|
||||||
address.email AS user,
|
address.email AS user,
|
||||||
app_account.account_pwd AS password,
|
app_account.account_pwd AS password,
|
||||||
app_account.host AS host,
|
app_account.imap_host,
|
||||||
app_account.port AS port,
|
app_account.imap_port,
|
||||||
app_account.tls AS tls
|
app_account.smtp_host,
|
||||||
|
app_account.smtp_port,
|
||||||
|
app_account.tls
|
||||||
FROM app_account INNER JOIN address
|
FROM app_account INNER JOIN address
|
||||||
WHERE address.address_id = app_account.user_id
|
WHERE address.address_id = app_account.user_id
|
||||||
`;
|
`;
|
||||||
@@ -16,26 +18,38 @@ export async function getAllAccounts() {
|
|||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllMailboxes(accountId) {
|
export async function getAllMailboxes(accountId: number) {
|
||||||
const query = 'SELECT * FROM mailbox WHERE mailbox.account_id = ?';
|
const query = "SELECT * FROM mailbox WHERE mailbox.account_id = ?";
|
||||||
const values = [accountId];
|
const values = [accountId];
|
||||||
return await execQueryAsync(query, values)
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerMailbox(accountId, mailboxName) {
|
export async function registerMailbox(accountId: number, mailboxName: string) {
|
||||||
const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`;
|
const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`;
|
||||||
const values = [accountId, mailboxName];
|
const values = [accountId, mailboxName];
|
||||||
return await execQueryAsyncWithId(query, values);
|
return await execQueryAsyncWithId(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMailbox(mailboxId) {
|
export async function getMailbox(mailboxId: number) {
|
||||||
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
|
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
|
||||||
const values = [mailboxId];
|
const values = [mailboxId];
|
||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateMailbox(mailboxId, uidnext) {
|
export function updateMailbox(mailboxId: number, uidnext: number) {
|
||||||
const query = `UPDATE mailbox SET uidnext = ? WHERE mailbox_id = ?`;
|
const query = `UPDATE mailbox SET uidnext = ? WHERE mailbox_id = ?`;
|
||||||
const values = [uidnext, mailboxId];
|
const values = [uidnext, mailboxId];
|
||||||
execQuery(query, values);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
23
back/db/message/message-db.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { execQueryAsync } from "../db";
|
||||||
|
|
||||||
|
export async function getFlagsOnUid(uid: number): Promise<{ flag_id: number; flag_name: string }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT flag_name FROM flag_name
|
||||||
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id
|
||||||
|
INNER JOIN mailbox_message ON mailbox_message.message_id = flag.message_id
|
||||||
|
WHERE mailbox_message.uid = ?
|
||||||
|
`;
|
||||||
|
const values = [uid];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFlagsOnId(messageId: number): Promise<{ flag_id: number; flag_name: string }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT flag_name FROM flag_name
|
||||||
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id
|
||||||
|
INNER JOIN mailbox_message ON mailbox_message.message_id = flag.message_id
|
||||||
|
WHERE mailbox_message.message_id = ?
|
||||||
|
`;
|
||||||
|
const values = [messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { execQuery, execQueryAsync, execQueryAsyncWithId } from "../db";
|
import { execQuery, execQueryAsync, execQueryAsyncWithId } from "../db";
|
||||||
|
|
||||||
export async function getFlags(uid: number): Promise<{flag_id: number, flag_name: string}[]> {
|
export async function getFlags(uid: number): Promise<{ flag_id: number; flag_name: string }[]> {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT * FROM flag_name
|
SELECT * FROM flag_name
|
||||||
INNER JOIN flag ON flag.flag_id = flag_name.flag_id
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id
|
||||||
@@ -19,12 +19,18 @@ export async function deleteFlag(messageId: number, flagId: number) {
|
|||||||
|
|
||||||
export async function updateMailboxSeen(messageId: number, isSeen: boolean) {
|
export async function updateMailboxSeen(messageId: number, isSeen: boolean) {
|
||||||
const query = `UPDATE mailbox_message SET seen = ? WHERE message_id = ?`;
|
const query = `UPDATE mailbox_message SET seen = ? WHERE message_id = ?`;
|
||||||
const values = [messageId, isSeen];
|
const values = [isSeen, messageId];
|
||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMailboxDeleted(messageId: number, isDeleted: boolean) {
|
export async function updateMailboxDeleted(messageId: number, isDeleted: boolean) {
|
||||||
const query = `UPDATE mailbox_message SET deleted = ? WHERE message_id = ?`;
|
const query = `UPDATE mailbox_message SET deleted = ? WHERE message_id = ?`;
|
||||||
const values = [messageId, isDeleted];
|
const values = [isDeleted, messageId];
|
||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteMessage(messageId: number) {
|
||||||
|
const query = `DELETE FROM message WHERE message_id = ?`;
|
||||||
|
const values = [messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { execQueryAsync, execQueryAsyncWithId } from "../db";
|
import { execQueryAsync, execQueryAsyncWithId } from "../db";
|
||||||
|
|
||||||
export async function getAddresseId(email: string, name?: string): Promise<number> {
|
export async function getAddressId(email: string, name?: string): Promise<number> {
|
||||||
const localpart = email.split("@")[0];
|
const localpart = email.split("@")[0];
|
||||||
const domain = email.split("@")[1];
|
const domain = email.split("@")[1];
|
||||||
const query = `INSERT INTO address
|
const query = `INSERT INTO address
|
||||||
@@ -10,6 +10,12 @@ export async function getAddresseId(email: string, name?: string): Promise<numbe
|
|||||||
return await execQueryAsyncWithId(query, values);
|
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> {
|
export async function getFieldId(field: string): Promise<number> {
|
||||||
const query = `INSERT INTO field_name (field_name) VALUES (?) ON DUPLICATE KEY UPDATE field_id=LAST_INSERT_ID(field_id)`;
|
const query = `INSERT INTO field_name (field_name) VALUES (?) ON DUPLICATE KEY UPDATE field_id=LAST_INSERT_ID(field_id)`;
|
||||||
const values = [field];
|
const values = [field];
|
||||||
@@ -22,12 +28,24 @@ export async function getFlagId(flag: string): Promise<number> {
|
|||||||
return await execQueryAsyncWithId(query, values);
|
return await execQueryAsyncWithId(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMessageIdOnUid(uid: number): Promise<{message_id: number}[]> {
|
export async function getMessageIdOnUid(uid: number): Promise<{ message_id: number }[]> {
|
||||||
const query = `SELECT message_id FROM mailbox_message WHERE uid = ?`;
|
const query = `SELECT message_id FROM mailbox_message WHERE uid = ?`;
|
||||||
const values = [uid];
|
const values = [uid];
|
||||||
return await execQueryAsync(query, values);
|
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 }[]> {
|
export async function findRoomByOwner(ownerId: number): Promise<{ room_id: number }[]> {
|
||||||
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
|
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
|
||||||
const values = [ownerId];
|
const values = [ownerId];
|
||||||
@@ -44,3 +62,15 @@ export async function getUserIdOfMailbox(boxId: number): Promise<{ user_id: numb
|
|||||||
const values = [boxId];
|
const values = [boxId];
|
||||||
return await execQueryAsync(query, values);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,30 +5,32 @@ import { getAllAccounts } from "../db/imap/imap-db";
|
|||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
user: string
|
user: string;
|
||||||
password?: string
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmailManager {
|
class EmailManager {
|
||||||
imapInstances: ImapInstance[]
|
imapInstances: ImapInstance[];
|
||||||
smtpInstances: SmtpInstance[]
|
smtpInstances: SmtpInstance[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.imapInstances = [];
|
this.imapInstances = [];
|
||||||
this.smtpInstances = [];
|
this.smtpInstances = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
getAllAccounts().then((accounts: Account[]) => {
|
getAllAccounts()
|
||||||
for (let i = 0; i < accounts.length; i++) {
|
.then((accounts: Account[]) => {
|
||||||
accounts[i].password = accounts[i]?.password?.toString().replace(/[\u{0080}-\u{FFFF}]/gu,"");
|
for (let i = 0; i < accounts.length; i++) {
|
||||||
if (accounts[i].id == 2) continue; //debug_todo
|
accounts[i].password = accounts[i]?.password?.toString().replace(/[\u{0080}-\u{FFFF}]/gu, "");
|
||||||
this.addImapInstance(accounts[i]);
|
if (accounts[i].id == 2) continue; //debug_todo
|
||||||
this.addSmtpInstance(accounts[i]);
|
this.addImapInstance(accounts[i]);
|
||||||
}
|
this.addSmtpInstance(accounts[i]);
|
||||||
}).catch((err) => {
|
}
|
||||||
logger.err(err);
|
})
|
||||||
});
|
.catch((err) => {
|
||||||
|
logger.err(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addImapInstance(config) {
|
addImapInstance(config) {
|
||||||
@@ -38,7 +40,15 @@ class EmailManager {
|
|||||||
addSmtpInstance(config) {
|
addSmtpInstance(config) {
|
||||||
this.smtpInstances.push(new SmtpInstance(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();
|
const emailManager = new EmailManager();
|
||||||
export default emailManager;
|
export default emailManager;
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ export class ImapInstance {
|
|||||||
this.imap = new Imap({
|
this.imap = new Imap({
|
||||||
user: account.user,
|
user: account.user,
|
||||||
password: account.password,
|
password: account.password,
|
||||||
tlsOptions: { servername: account.host },
|
tlsOptions: { servername: account.imap_host },
|
||||||
host: account.host,
|
host: account.imap_host,
|
||||||
port: account.port,
|
port: account.imap_port,
|
||||||
tls: account.tls,
|
tls: account.tls,
|
||||||
});
|
});
|
||||||
this.account = account;
|
this.account = account;
|
||||||
this.boxes = [];
|
this.boxes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IMAP
|
* IMAP init
|
||||||
*/
|
*/
|
||||||
this.imap.once("ready", () => {
|
this.imap.once("ready", () => {
|
||||||
logger.log("Imap connected for " + this.account.user);
|
logger.log("Imap connected for " + this.account.user);
|
||||||
@@ -41,37 +41,48 @@ export class ImapInstance {
|
|||||||
this.imap.connect();
|
this.imap.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
imapReady() {
|
imapReady = () => {
|
||||||
getAllMailboxes(this.account.id).then((mailboxes) => {
|
getAllMailboxes(this.account.id).then((mailboxes) => {
|
||||||
if (mailboxes.length > 0) {
|
if (mailboxes.length > 0) {
|
||||||
this.boxes.push(new Mailbox(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 {
|
} else {
|
||||||
this.imap.getBoxes("", (err, boxes) => {
|
this.getMailboxName("All").then((allBoxName) => {
|
||||||
if (err) logger.err(err);
|
|
||||||
const allBoxName = this.getAllBox(boxes);
|
|
||||||
registerMailbox(this.account.id, allBoxName).then((mailboxId) => {
|
registerMailbox(this.account.id, allBoxName).then((mailboxId) => {
|
||||||
this.boxes.push(new Mailbox(this.imap, mailboxId, allBoxName));
|
this.boxes.push(new Mailbox(mailboxId, allBoxName, this));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
getAllBox(boxes) {
|
getMailboxName(boxToFound: string): Promise<string> {
|
||||||
// ideally we should get the all box to get all messages
|
return new Promise((resolve, rejects) => {
|
||||||
let allBox = '';
|
let matchBox = "";
|
||||||
Object.keys(boxes).forEach((key) => {
|
this.imap.getBoxes("", (err, boxes) => {
|
||||||
if (key === "INBOX") return;
|
Object.keys(boxes).forEach((key) => {
|
||||||
if (allBox.includes("/")) return; // already found
|
if (matchBox.includes("/")) return; // already found
|
||||||
if (!boxes[key].children) return; // no children
|
if (!boxes[key].children) return; // no children
|
||||||
allBox = key;
|
matchBox = key;
|
||||||
Object.keys(boxes[key].children).forEach((childBoxes) => {
|
Object.keys(boxes[key].children).forEach((childBox) => {
|
||||||
if (boxes[key].children[childBoxes].attribs.includes("\\All")) {
|
let attribs = boxes[key].children[childBox].attribs;
|
||||||
allBox += "/" + childBoxes;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import Imap, { ImapMessageAttributes, Box } from "imap";
|
import Imap, { Box } from "imap";
|
||||||
import { getMailbox, updateMailbox } from "../../db/imap/imap-db";
|
import { resolve } from "path";
|
||||||
|
import { getMailbox, getMailboxModseq, updateMailbox, updateMailboxModseq } from "../../db/imap/imap-db";
|
||||||
import { Attrs, AttrsWithEnvelope } from "../../interfaces/mail/attrs.interface";
|
import { Attrs, AttrsWithEnvelope } from "../../interfaces/mail/attrs.interface";
|
||||||
import logger from "../../system/Logger";
|
import logger from "../../system/Logger";
|
||||||
import RegisterMessageInApp from "../message/saveMessage";
|
import RegisterMessageInApp from "../message/saveMessage";
|
||||||
import { saveMessage } from "../message/storeMessage";
|
import { saveMessage } from "../message/storeMessage";
|
||||||
import updateMessage from "../message/updateMessage";
|
import updateMessage from "../message/updateMessage";
|
||||||
|
import { ImapInstance } from "./ImapInstance";
|
||||||
|
|
||||||
export interface ImapInfo {
|
export interface ImapInfo {
|
||||||
uid: number;
|
uid: number;
|
||||||
@@ -19,102 +21,191 @@ export default class Mailbox {
|
|||||||
box: Box;
|
box: Box;
|
||||||
msgToSync: number;
|
msgToSync: number;
|
||||||
syncing: boolean;
|
syncing: boolean;
|
||||||
|
imapInstance: ImapInstance;
|
||||||
|
|
||||||
constructor(_imap, _boxId, _boxName) {
|
constructor(_boxId: number, _boxName: string, _imapInstance: ImapInstance) {
|
||||||
this.imap = _imap;
|
this.imap = _imapInstance.imap;
|
||||||
this.boxName = _boxName;
|
this.boxName = _boxName;
|
||||||
this.id = _boxId;
|
this.id = _boxId;
|
||||||
this.box;
|
this.box;
|
||||||
this.msgToSync = 0;
|
this.msgToSync = 0;
|
||||||
this.syncing = false;
|
this.syncing = false;
|
||||||
|
this.imapInstance = _imapInstance;
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// get mailbox from the database
|
// get mailbox from the database
|
||||||
this.box = (await getMailbox(this.id))[0];
|
this.box = (await getMailbox(this.id))[0];
|
||||||
|
const isReadOnly = false;
|
||||||
const readOnly = true;
|
this.imap.openBox(this.boxName, isReadOnly, (err, box) => {
|
||||||
this.imap.openBox(this.boxName, readOnly, (err, box) => {
|
|
||||||
if (err) logger.err(err);
|
if (err) logger.err(err);
|
||||||
|
|
||||||
// sync only if has new messages
|
// sync messages and flags
|
||||||
if (this.box.uidnext < box.uidnext) {
|
this.initSync(box);
|
||||||
this.sync(this.box.uidnext, box.uidnext);
|
|
||||||
} else {
|
|
||||||
logger.log("Already up to date")
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for new mails
|
// wait for new mails
|
||||||
this.imap.on("mail", (numNewMsgs: number) => {
|
this.imap.on("mail", (numNewMsgs: number) => {
|
||||||
if (!this.syncing) {
|
if (!this.syncing) {
|
||||||
// if not syncing restart a sync
|
// if not syncing restart a sync
|
||||||
this.sync(this.box.uidnext, this.box.uidnext + numNewMsgs);
|
this.syncManager(this.box.uidnext - 1, this.box.uidnext + numNewMsgs - 1);
|
||||||
} else {
|
} else {
|
||||||
// else save number of message to sync latter
|
// else save number of message to sync latter
|
||||||
this.msgToSync += numNewMsgs;
|
this.msgToSync += numNewMsgs;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// wait for flags update
|
||||||
this.imap.on("update", (seqno: number, info: ImapInfo) => {
|
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);
|
const updateMsg = new updateMessage(info.uid, info.flags);
|
||||||
updateMsg.updateFlags();
|
updateMsg.updateFlags();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async sync(savedUid: number, currentUid: number) {
|
// wait for deletion
|
||||||
this.syncing = true;
|
this.imap.on("expunge", (seqno: number) => {
|
||||||
const promises: Promise<unknown>[] = [];
|
// const updateMsg = new updateMessage(info.)
|
||||||
const mails: Attrs[] = [];
|
console.log("Message with sequence number " + seqno + " has been deleted from the server.");
|
||||||
logger.log(`Syncing from ${savedUid} to ${currentUid} uid`);
|
|
||||||
const f = this.imap.seq.fetch(`${savedUid}:${currentUid}`, {
|
|
||||||
size: true,
|
|
||||||
envelope: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
f.on("message", (msg, seqno) => {
|
|
||||||
msg.once("attributes", (attrs: AttrsWithEnvelope) => {
|
|
||||||
console.log(attrs.envelope);
|
|
||||||
mails.push(attrs);
|
|
||||||
promises.push(saveMessage(attrs, this.id, this.imap));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
f.once("error", (err) => {
|
async updateModseq(newModseq: number) {
|
||||||
logger.err("Fetch error: " + err);
|
updateMailboxModseq(this.id, newModseq).then(() => {
|
||||||
});
|
this.box.highestmodseq = newModseq.toString();
|
||||||
|
|
||||||
f.once("end", async () => {
|
|
||||||
let step = 20;
|
|
||||||
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: number) => {
|
|
||||||
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);
|
|
||||||
this.syncing = false;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
this.sync(currentUid, this.box.uidnext);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initSync(box: Box) {
|
||||||
|
// sync mail only if has new messages
|
||||||
|
if (this.box.uidnext < box.uidnext) {
|
||||||
|
this.syncManager(this.box.uidnext, box.uidnext);
|
||||||
|
} else {
|
||||||
|
logger.log("Mail already up to date");
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync flags
|
||||||
|
const lastModseq = (await getMailboxModseq(this.id))[0]?.modseq ?? 0;
|
||||||
|
if (parseInt(box.highestmodseq) > lastModseq) {
|
||||||
|
const fetchStream = this.imap.fetch("1:*", { bodies: "", modifiers: { changedsince: lastModseq } });
|
||||||
|
fetchStream.on("message", (message) => {
|
||||||
|
message.once("attributes", (attrs) => {
|
||||||
|
const updateMsg = new updateMessage(attrs.uid, attrs.flags);
|
||||||
|
updateMsg.updateFlags();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fetchStream.once("error", function (err) {
|
||||||
|
logger.err("Fetch error when syncing flags: " + err);
|
||||||
|
});
|
||||||
|
fetchStream.once("end", function () {
|
||||||
|
logger.log("Done fetching new flags");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.log("Flags already up to date");
|
||||||
|
}
|
||||||
|
this.updateModseq(parseInt(box.highestmodseq));
|
||||||
|
}
|
||||||
|
|
||||||
|
syncManager = async (savedUid: number, currentUid: number) => {
|
||||||
|
this.syncing = true;
|
||||||
|
logger.log(`Fetching from ${savedUid} to ${currentUid} uid`);
|
||||||
|
const nbMessageToSync = currentUid - savedUid;
|
||||||
|
let STEP = nbMessageToSync > 200 ? Math.floor(nbMessageToSync / 7) : nbMessageToSync;
|
||||||
|
let mails: AttrsWithEnvelope[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < nbMessageToSync; i += STEP) {
|
||||||
|
mails = [];
|
||||||
|
try {
|
||||||
|
// fetch mails
|
||||||
|
let secondUid = savedUid + STEP < currentUid ? savedUid + STEP : currentUid;
|
||||||
|
await this.mailFetcher(savedUid, secondUid, mails);
|
||||||
|
logger.log(`Fetched ${STEP} uids (${mails.length} messages)`);
|
||||||
|
// save same in the database
|
||||||
|
for (let k = 0; k < mails.length; k++) {
|
||||||
|
try {
|
||||||
|
const messageId = await saveMessage(mails[k], this.id, this.imap);
|
||||||
|
const register = new RegisterMessageInApp(messageId, mails[k], this.id);
|
||||||
|
await register.save();
|
||||||
|
} catch (error) {
|
||||||
|
logger.err("Failed to save a message: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
savedUid = secondUid;
|
||||||
|
this.box.uidnext += savedUid;
|
||||||
|
|
||||||
|
updateMailbox(this.id, savedUid);
|
||||||
|
} catch (error) {
|
||||||
|
logger.err("Failed to sync message " + error);
|
||||||
|
}
|
||||||
|
logger.log(`Saved messages in uids ${i + STEP > nbMessageToSync ? nbMessageToSync : i + STEP}/${nbMessageToSync}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if has receive new msg during last sync then start a new sync
|
||||||
|
if (this.msgToSync > 0) {
|
||||||
|
const currentUid = this.box.uidnext;
|
||||||
|
this.box.uidnext += this.msgToSync;
|
||||||
|
// reset value to allow to detect new incoming message while syncing
|
||||||
|
this.msgToSync = 0;
|
||||||
|
await this.syncManager(currentUid, this.box.uidnext);
|
||||||
|
}
|
||||||
|
this.syncing = false;
|
||||||
|
logger.log(`Finished syncing messages`);
|
||||||
|
};
|
||||||
|
|
||||||
|
async mailFetcher(startUid: number, endUid: number, mails: Attrs[]): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const f = this.imap.fetch(`${startUid}:${endUid}`, {
|
||||||
|
size: true,
|
||||||
|
envelope: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
f.on("message", (msg, seqno) => {
|
||||||
|
msg.once("attributes", (attrs: AttrsWithEnvelope) => {
|
||||||
|
mails.push(attrs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
f.once("error", (err) => {
|
||||||
|
logger.err("Fetch error when fetching in uid range: " + err);
|
||||||
|
reject(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
f.once("end", async () => {
|
||||||
|
resolve(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addFlag(source: string, flags: string[]): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.imap.addFlags(source, flags, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFlag(source: string, flags: string[]): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.imap.delFlags(source, flags, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
move(source: string, mailboxName: string, callback: (error: Error) => void) {
|
||||||
|
this.imap.move(source, mailboxName, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToTrash(source: string, callback: (error: Error) => void) {
|
||||||
|
const trashName = await this.imapInstance.getMailboxName("Trash");
|
||||||
|
this.move(source, trashName, callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
86
back/mails/message/Message.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { getFlagsOnId, getFlagsOnUid } from "../../db/message/message-db";
|
||||||
|
import { deleteMessage } from "../../db/message/updateMessage-db";
|
||||||
|
import { getMessageUid, getUserOfMailbox } from "../../db/utils/mail";
|
||||||
|
import emailManager from "../EmailManager";
|
||||||
|
import Mailbox from "../imap/Mailbox";
|
||||||
|
import Room from "../room/Room";
|
||||||
|
|
||||||
|
export default class Message {
|
||||||
|
messageId: number;
|
||||||
|
uid: number;
|
||||||
|
private _flags: string[] | undefined;
|
||||||
|
private _mailbox: Mailbox;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
setMessageId(messageId: number): Message {
|
||||||
|
this.messageId = messageId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async useUid(): Promise<Message> {
|
||||||
|
if (!this.messageId) {
|
||||||
|
throw "Define message id before trying to find uid";
|
||||||
|
}
|
||||||
|
this.uid = (await getMessageUid(this.messageId))[0]?.uid;
|
||||||
|
if (!this.uid) {
|
||||||
|
throw "Uid not found";
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async useFlags(): Promise<Message> {
|
||||||
|
let flags;
|
||||||
|
if (!this._flags) {
|
||||||
|
if (this.messageId) {
|
||||||
|
flags = await getFlagsOnId(this.messageId);
|
||||||
|
} else if (this.uid) {
|
||||||
|
flags = await getFlagsOnUid(this.uid);
|
||||||
|
} else {
|
||||||
|
throw "Neither message id or uid are set, please do so before attempting to load flags";
|
||||||
|
}
|
||||||
|
this._flags = flags;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async useMailbox(mailboxId: number, user?: string): Promise<Message> {
|
||||||
|
if (!user) {
|
||||||
|
user = (await getUserOfMailbox(mailboxId))[0]?.user;
|
||||||
|
if (!user) {
|
||||||
|
throw "Cannot find user of this mailbox";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._mailbox = emailManager.getImap(user).getMailbox(mailboxId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get mailbox() {
|
||||||
|
if (!this._mailbox) {
|
||||||
|
throw "Call useMailbox before calling functions related to mailbox";
|
||||||
|
}
|
||||||
|
return this._mailbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
get flags(): string[] {
|
||||||
|
console.log("called");
|
||||||
|
console.log(this._flags);
|
||||||
|
if (!this._flags) {
|
||||||
|
throw "Flags not loaded, call useFlags before calling functions related to flags";
|
||||||
|
} else {
|
||||||
|
return this._flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDeleted(): boolean {
|
||||||
|
return this.flags.includes("\\Deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async (messageId?: number): Promise<Message> => {
|
||||||
|
if (!(this.messageId ?? messageId)) {
|
||||||
|
throw "Delete need to have the message id";
|
||||||
|
}
|
||||||
|
await deleteMessage(this.messageId ?? messageId);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
getThreadInfoOnId,
|
getThreadInfoOnId,
|
||||||
} from "../../db/message/saveMessage-db";
|
} from "../../db/message/saveMessage-db";
|
||||||
|
|
||||||
import { findRoomByOwner, getAddresseId, getUserIdOfMailbox } from "../../db/utils/mail";
|
import { findRoomByOwner, getAddressId, getMessageIdOnID, getUserIdOfMailbox } from "../../db/utils/mail";
|
||||||
import { nbMembers } from "../utils/envelopeUtils";
|
import { nbMembers } from "../utils/envelopeUtils";
|
||||||
import logger from "../../system/Logger";
|
import logger from "../../system/Logger";
|
||||||
import { Attrs, Envelope, User } from "../../interfaces/mail/attrs.interface";
|
import { Attrs, Envelope, User } from "../../interfaces/mail/attrs.interface";
|
||||||
@@ -55,93 +55,6 @@ export default class RegisterMessageInApp {
|
|||||||
this.inReplyTo = "";
|
this.inReplyTo = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
|
||||||
if (this.envelope.from) {
|
|
||||||
this.ownerId = await getAddresseId(createAddress(this.envelope.from[0])); // todo use sender or from ?
|
|
||||||
} else {
|
|
||||||
throw new Error("Envelope must have a 'from' field");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isDm = () => nbMembers(this.envelope) == 2;
|
|
||||||
|
|
||||||
async isFromUs() {
|
|
||||||
if (this.userId == -1) {
|
|
||||||
await getUserIdOfMailbox(this.boxId).then((res) => {
|
|
||||||
this.userId = res[0]?.user_id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.ownerId == this.userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async registerMembers(roomId: number) {
|
|
||||||
getAllMembers(this.messageId).then((res) => {
|
|
||||||
if (res.lenght == 0) return;
|
|
||||||
const data = res[0].id.split(",");
|
|
||||||
data.forEach(async (memberId: number) => {
|
|
||||||
await registerMember(roomId, memberId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async initiateRoom(owner: number, roomType: RoomType) {
|
|
||||||
try {
|
|
||||||
const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType);
|
|
||||||
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
|
||||||
await this.registerMembers(roomId);
|
|
||||||
return roomId;
|
|
||||||
} catch (err) {
|
|
||||||
logger.err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOrRegisterOnExistence(owner: number, roomType: RoomType) {
|
|
||||||
await findRoomByOwner(owner).then(async (res) => {
|
|
||||||
if (res.length == 0) {
|
|
||||||
// first message with this sender
|
|
||||||
await this.initiateRoom(owner, roomType);
|
|
||||||
} else {
|
|
||||||
// not a reply, add to the list of message if this sender
|
|
||||||
await registerMessageInRoom(this.messageId, res[0].room_id, this.envelope.date);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async initiateThread() {
|
|
||||||
await createRoom(this.envelope.subject, this.ownerId, this.messageId, RoomType.THREAD).then(
|
|
||||||
async (threadId: number) => {
|
|
||||||
// find parent room infos
|
|
||||||
let roomId: number;
|
|
||||||
let root_id: number;
|
|
||||||
await getThreadInfo(this.inReplyTo).then(async (room) => {
|
|
||||||
// todo room not lenght, reply to transfer ?
|
|
||||||
roomId = room[0].room_id;
|
|
||||||
root_id = room[0].root_id;
|
|
||||||
if (root_id === undefined) root_id = roomId;
|
|
||||||
await registerThread(threadId, roomId, root_id);
|
|
||||||
});
|
|
||||||
// impl register previous message or go back
|
|
||||||
await registerMessageInRoom(this.messageId, threadId, this.envelope.date);
|
|
||||||
await this.registerMembers(threadId);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOrRegisterOnMembers(roomId: number, isThread?: boolean) {
|
|
||||||
const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo);
|
|
||||||
if (hasSameMembers) {
|
|
||||||
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
|
||||||
if (isThread) {
|
|
||||||
await getThreadInfoOnId(roomId).then(async (res) => {
|
|
||||||
let root_id = res[0].root_id;
|
|
||||||
if (root_id == undefined) root_id = res[0].room_id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.initiateThread();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
await this.init();
|
await this.init();
|
||||||
if (this.envelope.inReplyTo) {
|
if (this.envelope.inReplyTo) {
|
||||||
@@ -152,7 +65,7 @@ export default class RegisterMessageInApp {
|
|||||||
if (this.isDm()) {
|
if (this.isDm()) {
|
||||||
// create or add new message to DM
|
// create or add new message to DM
|
||||||
if (!this.envelope.to) throw new Error("Who send a DM and put the recipient in cc ?");
|
if (!this.envelope.to) throw new Error("Who send a DM and put the recipient in cc ?");
|
||||||
const userTo = await getAddresseId(createAddress(this.envelope.to[0]));
|
const userTo = await getAddressId(createAddress(this.envelope.to[0]));
|
||||||
await this.createOrRegisterOnExistence(userTo, RoomType.DM);
|
await this.createOrRegisterOnExistence(userTo, RoomType.DM);
|
||||||
} else {
|
} else {
|
||||||
// it is not a reply and not a dm
|
// it is not a reply and not a dm
|
||||||
@@ -194,4 +107,98 @@ export default class RegisterMessageInApp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.envelope.from) {
|
||||||
|
this.ownerId = await getAddressId(createAddress(this.envelope.from[0])); // todo use sender or from ?
|
||||||
|
} else {
|
||||||
|
throw new Error("Envelope must have a 'from' field");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDm = () => nbMembers(this.envelope) == 2;
|
||||||
|
|
||||||
|
async isFromUs() {
|
||||||
|
if (this.userId == -1) {
|
||||||
|
await getUserIdOfMailbox(this.boxId).then((res) => {
|
||||||
|
this.userId = res[0]?.user_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.ownerId == this.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add all members of the message to the room
|
||||||
|
*/
|
||||||
|
async registerMembers(roomId: number) {
|
||||||
|
getAllMembers(this.messageId).then((res) => {
|
||||||
|
if (res.lenght == 0) return;
|
||||||
|
const data = res[0].id.split(",");
|
||||||
|
data.forEach(async (memberId: number) => {
|
||||||
|
await registerMember(roomId, memberId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async initiateRoom(owner: number, roomType: RoomType) {
|
||||||
|
try {
|
||||||
|
const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType);
|
||||||
|
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
||||||
|
await this.registerMembers(roomId);
|
||||||
|
return roomId;
|
||||||
|
} catch (err) {
|
||||||
|
logger.err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrRegisterOnExistence(owner: number, roomType: RoomType) {
|
||||||
|
await findRoomByOwner(owner).then(async (res) => {
|
||||||
|
if (res.length == 0) {
|
||||||
|
// first message with this sender
|
||||||
|
await this.initiateRoom(owner, roomType);
|
||||||
|
} else {
|
||||||
|
// not a reply, add to the list of message if this sender
|
||||||
|
await registerMessageInRoom(this.messageId, res[0].room_id, this.envelope.date);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async initiateThread() {
|
||||||
|
const inReplyToId = (await getMessageIdOnID(this.inReplyTo))[0]?.message_id;
|
||||||
|
await createRoom(this.envelope.subject, this.ownerId, inReplyToId, RoomType.THREAD).then(
|
||||||
|
async (threadId: number) => {
|
||||||
|
// find parent room infos
|
||||||
|
let roomId: number;
|
||||||
|
let root_id: number;
|
||||||
|
await getThreadInfo(this.inReplyTo).then(async (room) => {
|
||||||
|
// todo room not lenght, reply to transfer ?
|
||||||
|
roomId = room[0].room_id;
|
||||||
|
root_id = room[0].root_id;
|
||||||
|
if (root_id === undefined) root_id = roomId;
|
||||||
|
await registerThread(threadId, roomId, root_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add original message
|
||||||
|
await registerMessageInRoom(inReplyToId, threadId, this.envelope.date);
|
||||||
|
// add reply message
|
||||||
|
await registerMessageInRoom(this.messageId, threadId, this.envelope.date);
|
||||||
|
await this.registerMembers(threadId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrRegisterOnMembers(roomId: number, isThread?: boolean) {
|
||||||
|
const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo);
|
||||||
|
if (hasSameMembers) {
|
||||||
|
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
||||||
|
if (isThread) {
|
||||||
|
await getThreadInfoOnId(roomId).then(async (res) => {
|
||||||
|
let root_id = res[0].root_id;
|
||||||
|
if (root_id == undefined) root_id = res[0].room_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.initiateThread();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAddresseId, getFlagId } from "../../db/utils/mail";
|
import { getAddressId, getFlagId } from "../../db/utils/mail";
|
||||||
import { EmailAddress, ParsedMail, simpleParser } from "mailparser";
|
import { EmailAddress, ParsedMail, simpleParser } from "mailparser";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Imap from "imap";
|
import Imap from "imap";
|
||||||
@@ -91,7 +91,7 @@ async function saveFromParsedData(parsed: ParsedMail, messageId: number) {
|
|||||||
// save address field
|
// save address field
|
||||||
getFieldId(key).then((fieldId) => {
|
getFieldId(key).then((fieldId) => {
|
||||||
parsed[key].value.forEach((addr: EmailAddress, nb: number) => {
|
parsed[key].value.forEach((addr: EmailAddress, nb: number) => {
|
||||||
getAddresseId(addr.address, addr.name).then(async (addressId) => {
|
getAddressId(addr.address, addr.name).then(async (addressId) => {
|
||||||
await saveAddress_fields(messageId, fieldId, addressId, nb);
|
await saveAddress_fields(messageId, fieldId, addressId, nb);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -120,7 +120,7 @@ async function saveFromParsedData(parsed: ParsedMail, messageId: number) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (key == "attachments") {
|
} else if (key == "attachments") {
|
||||||
// todo
|
// todo attachments
|
||||||
} else if (["date", "messageId", "headers", "headerLines"].includes(key)) {
|
} else if (["date", "messageId", "headers", "headerLines"].includes(key)) {
|
||||||
// messageId and date are already saved
|
// messageId and date are already saved
|
||||||
// other field are not important and can be retrieved in source
|
// other field are not important and can be retrieved in source
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export default class updateMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateFlags() {
|
async updateFlags() {
|
||||||
const messageId = (await getMessageIdOnUid(this.uid))[0].message_id;
|
const messageId = (await getMessageIdOnUid(this.uid))[0]?.message_id;
|
||||||
|
if (!messageId) return;
|
||||||
const currentFlags = await getFlags(this.uid);
|
const currentFlags = await getFlags(this.uid);
|
||||||
|
|
||||||
const flagsToAdd = this.flags.filter((flag) => !currentFlags.find((f) => flag == f.flag_name));
|
const flagsToAdd = this.flags.filter((flag) => !currentFlags.find((f) => flag == f.flag_name));
|
||||||
|
|||||||
46
back/mails/room/Room.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { deleteRoom, getRoomNbMessageAndThread, getRoomOnMessageId } from "../../db/Room-db";
|
||||||
|
|
||||||
|
export default class Room {
|
||||||
|
private _roomId: number;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
setRoomId(roomId: number): Room {
|
||||||
|
this._roomId = roomId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomId(): number {
|
||||||
|
return this._roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRoomIdOnMessageId(messageId: number): Promise<Room> {
|
||||||
|
const res = await getRoomOnMessageId(messageId);
|
||||||
|
if (res.length == 0) {
|
||||||
|
throw "Message has no room";
|
||||||
|
}
|
||||||
|
this._roomId = res[0].room_id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the room have threads or messages
|
||||||
|
async shouldDelete(): Promise<boolean> {
|
||||||
|
if (!this._roomId) {
|
||||||
|
throw "shouldDelete needs to have a roomId set.";
|
||||||
|
}
|
||||||
|
const res = await getRoomNbMessageAndThread(this._roomId);
|
||||||
|
if (res.length === 0) return true;
|
||||||
|
if (res[0].nbMessage === 0 && res[0].nbThread === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<Room> {
|
||||||
|
if (!this._roomId) {
|
||||||
|
throw "shouldDelete needs to have a roomId set.";
|
||||||
|
}
|
||||||
|
await deleteRoom(this._roomId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@ import nodemailer, { Transporter } from "nodemailer";
|
|||||||
|
|
||||||
export class SmtpInstance {
|
export class SmtpInstance {
|
||||||
transporter: Transporter;
|
transporter: Transporter;
|
||||||
|
user: string;
|
||||||
|
|
||||||
constructor(account: {user: string, password: string}) {
|
constructor(account: { user: string; password: string, smtp_host: string, smtp_port: number }) {
|
||||||
// todo store other data
|
this.user = account.user;
|
||||||
this.transporter = nodemailer.createTransport({
|
this.transporter = nodemailer.createTransport({
|
||||||
host: "smtp.gmail.com",
|
host: account.smtp_host,
|
||||||
port: 465,
|
port: account.smtp_port,
|
||||||
secure: true,
|
secure: true,
|
||||||
auth: {
|
auth: {
|
||||||
user: account.user,
|
user: account.user,
|
||||||
@@ -17,20 +18,22 @@ export class SmtpInstance {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMail() {
|
sendMail(message: any) {
|
||||||
const msg = {
|
console.log(this.user)
|
||||||
from: "",
|
console.log(message)
|
||||||
to: "",
|
// const msg = {
|
||||||
subject: "Hello ✔",
|
// from: "",
|
||||||
text: "Hello world?",
|
// to: "",
|
||||||
html: "<b>Hello world?</b>",
|
// subject: "Hello ✔",
|
||||||
};
|
// text: "Hello world?",
|
||||||
this.transporter.sendMail(msg, (err, message) => {
|
// html: "<b>Hello world?</b>",
|
||||||
if (err) {
|
// };
|
||||||
logger.err(err);
|
// this.transporter.sendMail(msg, (err, message) => {
|
||||||
throw err;
|
// if (err) {
|
||||||
}
|
// logger.err(err);
|
||||||
logger.log(message);
|
// throw err;
|
||||||
});
|
// }
|
||||||
|
// logger.log(message);
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
back/mails/utils/mailBuilder.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export default class MailBuilder {
|
||||||
|
message: any;
|
||||||
|
constructor(message = {}) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
from(addresses: string[] | string): MailBuilder {
|
||||||
|
this.message.from = addresses;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
to(addresses: string[] | string): MailBuilder {
|
||||||
|
this.message.to = addresses;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
cc(addresses: string[] | string): MailBuilder {
|
||||||
|
this.message.cc = addresses;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bcc(addresses: string[] | string): MailBuilder {
|
||||||
|
this.message.bcc = addresses;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
subject(subject: string): MailBuilder {
|
||||||
|
this.message.subject = subject;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
text(textContent: string): MailBuilder {
|
||||||
|
this.message.text = textContent;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
html(htmlContent: string): MailBuilder {
|
||||||
|
this.message.html = htmlContent;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inReplyTo(messageID: string): MailBuilder {
|
||||||
|
this.message.inReplyTo = messageID;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inReplySubject(originSubject: string): MailBuilder {
|
||||||
|
// todo concate if multiple ?
|
||||||
|
this.message.subject = "RE: " + originSubject;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
// https://cr.yp.to/immhf/thread.html
|
||||||
|
}
|
||||||
28
back/routes/account.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import express from "express";
|
||||||
|
import Account from "../abl/Account-abl";
|
||||||
|
import validator from "../validator/validator";
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all mailboxes and folders for an user
|
||||||
|
*/
|
||||||
|
router.get("/getAll", async (req, res) => {
|
||||||
|
await validator.validate("getAccounts", req.params, res, Account.getAll);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new mailbox inside the app
|
||||||
|
*/
|
||||||
|
router.post("/register", async (req, res) => {
|
||||||
|
await validator.validate("createAccount", req.body, res, Account.register);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all rooms from a mailbox
|
||||||
|
*/
|
||||||
|
router.get("/:mailboxId/rooms", async (req, res) => {
|
||||||
|
// todo offet limit
|
||||||
|
await validator.validate("getRooms", req.params, res, Account.getRooms);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
import { rooms } from "../abl/rooms";
|
|
||||||
import Message from "../abl/Messages-abl";
|
|
||||||
import { messages } from "../abl/messages";
|
|
||||||
import { members } from "../abl/members";
|
|
||||||
import Account from "../abl/Account-abl";
|
|
||||||
import validator from "../validator/validator";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all mailboxes and folders for an user
|
|
||||||
*/
|
|
||||||
router.get("/accounts", async (req, res) => {
|
|
||||||
await validator.validate("getAccounts", req.params, res, Account.getAll);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all rooms from a mailbox
|
|
||||||
*/
|
|
||||||
router.get("/:mailboxId/rooms", async (req, res) => {
|
|
||||||
// todo offet limit
|
|
||||||
await validator.validate("getRooms", req.params, res, rooms);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all messages from a room
|
|
||||||
*/
|
|
||||||
router.get("/:roomId/messages", async (req, res) => {
|
|
||||||
await validator.validate("getMessages", req.params, res, messages);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all members from a room
|
|
||||||
*/
|
|
||||||
router.get("/:roomId/members", async (req, res) => {
|
|
||||||
await validator.validate("getMembers", req.params, res, members);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a new mailbox inside the app
|
|
||||||
*/
|
|
||||||
router.post("/account", async (req, res) => {
|
|
||||||
await validator.validate("createAccount", req.body, res, Account.register);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/addFlag", async (req, res) => {
|
|
||||||
await validator.validate("addFlag", req.body, res, Message.addFlag);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/removeFlag", async (req, res) => {
|
|
||||||
await validator.validate("removeFlag", req.body, res, Message.removeFlag);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
23
back/routes/message.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import express from "express";
|
||||||
|
import MessageAbl from "../abl/Message-abl";
|
||||||
|
import validator from "../validator/validator";
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/addFlag", async (req, res) => {
|
||||||
|
await validator.validate("addFlag", req.body, res, MessageAbl.addFlag);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/removeFlag", async (req, res) => {
|
||||||
|
await validator.validate("removeFlag", req.body, res, MessageAbl.removeFlag);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/deleteRemote", async(req, res) => {
|
||||||
|
await validator.validate("delete", req.body, res, MessageAbl.deleteRemoteOnly);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/delete", async(req, res) => {
|
||||||
|
await validator.validate("delete", req.body, res, MessageAbl.deleteEverywhere);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default router;
|
||||||
28
back/routes/room.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import express from "express";
|
||||||
|
import RoomAbl from "../abl/Room-abl";
|
||||||
|
import validator from "../validator/validator";
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all messages from a room
|
||||||
|
*/
|
||||||
|
router.get("/:roomId/messages", async (req, res) => {
|
||||||
|
await validator.validate("getMessages", req.params, res, RoomAbl.getMessages);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all members from a room
|
||||||
|
*/
|
||||||
|
router.get("/:roomId/members", async (req, res) => {
|
||||||
|
await validator.validate("getMembers", req.params, res, RoomAbl.getMembers);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/response", async (req, res) => {
|
||||||
|
await validator.validate("response", req.body, res, RoomAbl.response);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/delete", async (req, res) => {
|
||||||
|
await validator.validate("deleteRoom", req.body, res, RoomAbl.delete);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
const app = express();
|
const app = express();
|
||||||
import ImapSync from "./mails/EmailManager";
|
|
||||||
import { execQueryAsync, execQuery } from "./db/db";
|
import { execQueryAsync, execQuery } from "./db/db";
|
||||||
|
import emailManager from "./mails/EmailManager";
|
||||||
|
import accountRouter from "./routes/account";
|
||||||
|
import roomRouter from "./routes/room";
|
||||||
|
import messageRouter from "./routes/message";
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(
|
app.use(
|
||||||
@@ -13,10 +16,11 @@ app.use(
|
|||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.listen(process.env.PORT || 5500);
|
app.listen(process.env.PORT || 5500);
|
||||||
|
|
||||||
import mailRouter from "./routes/mail";
|
app.use("/api/account", accountRouter);
|
||||||
import emailManager from "./mails/EmailManager";
|
app.use("/api/room", roomRouter);
|
||||||
app.use("/api/mail", mailRouter);
|
app.use("/api/message", messageRouter);
|
||||||
|
|
||||||
|
// create imap and smtp instances for each account
|
||||||
emailManager.init();
|
emailManager.init();
|
||||||
|
|
||||||
// debug reset all tables
|
// debug reset all tables
|
||||||
@@ -31,7 +35,7 @@ if (shouldReset) {
|
|||||||
if (table.table_name == "mailbox") return;
|
if (table.table_name == "mailbox") return;
|
||||||
console.log(table.table_name);
|
console.log(table.table_name);
|
||||||
execQuery("DELETE FROM " + table.table_name, []);
|
execQuery("DELETE FROM " + table.table_name, []);
|
||||||
// execQuery("DROP TABLE " + table.table_name);
|
// execQuery("DROP TABLE " + table.table_name, []);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const boxId = 1;
|
|||||||
jest.mock("../../db/utils/mail", () => {
|
jest.mock("../../db/utils/mail", () => {
|
||||||
return {
|
return {
|
||||||
findRoomByOwner: jest.fn(),
|
findRoomByOwner: jest.fn(),
|
||||||
getAddresseId: jest.fn(),
|
getAddressId: jest.fn(),
|
||||||
getUserIdOfMailbox: jest.fn(),
|
getUserIdOfMailbox: jest.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -38,7 +38,7 @@ jest.mock("../../db/message/saveMessage-db", () => {
|
|||||||
getThreadInfoOnId: jest.fn(),
|
getThreadInfoOnId: jest.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
import { getAddresseId, getUserIdOfMailbox, findRoomByOwner } from "../../db/utils/mail";
|
import { getAddressId, getUserIdOfMailbox, findRoomByOwner } from "../../db/utils/mail";
|
||||||
import {
|
import {
|
||||||
createRoom,
|
createRoom,
|
||||||
registerMessageInRoom,
|
registerMessageInRoom,
|
||||||
@@ -69,7 +69,7 @@ import { AttrsWithEnvelopeTest, createReplyWithSameMembers } from "../test-utils
|
|||||||
// if only me reply -> channel
|
// if only me reply -> channel
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
mocked(getAddresseId).mockImplementation(db.getAddresseId);
|
mocked(getAddressId).mockImplementation(db.getAddressId);
|
||||||
mocked(getUserIdOfMailbox).mockImplementation(db.getUserIdOfMailbox);
|
mocked(getUserIdOfMailbox).mockImplementation(db.getUserIdOfMailbox);
|
||||||
mocked(findRoomByOwner).mockImplementation(db.findRoomByOwner);
|
mocked(findRoomByOwner).mockImplementation(db.findRoomByOwner);
|
||||||
|
|
||||||
@@ -146,6 +146,7 @@ describe("saveMessage", () => {
|
|||||||
|
|
||||||
await register.save();
|
await register.save();
|
||||||
|
|
||||||
|
// the owner of the room will be the recipient (not us)
|
||||||
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(db.users[1].id, RoomType.DM);
|
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(db.users[1].id, RoomType.DM);
|
||||||
});
|
});
|
||||||
it("should create a GROUP when there is a new first message from us to multiple recipients", async () => {
|
it("should create a GROUP when there is a new first message from us to multiple recipients", async () => {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default class saveMessageDatabase {
|
|||||||
this.rooms = [];
|
this.rooms = [];
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.room_message = [];
|
this.room_message = [];
|
||||||
this.users = generateUsers(5); // todo
|
this.users = _users;
|
||||||
this.roomId = 0;
|
this.roomId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ export default class saveMessageDatabase {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getAddresseId = (email: string, name?: string): Promise<number> => {
|
getAddressId = (email: string, name?: string): Promise<number> => {
|
||||||
const match = this.users.find((user) => user.user.mailbox + "@" + user.user.host == email);
|
const match = this.users.find((user) => user.user.mailbox + "@" + user.user.host == email);
|
||||||
return new Promise((resolve, reject) => resolve(match?.id));
|
return new Promise((resolve, reject) => resolve(match?.id));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
"pwd": { "type": "string" },
|
"pwd": { "type": "string" },
|
||||||
"xoauth": { "type": "string" },
|
"xoauth": { "type": "string" },
|
||||||
"xoauth2": { "type": "string" },
|
"xoauth2": { "type": "string" },
|
||||||
"host": { "type": "string", "format": "hostname" },
|
"imapHost": { "type": "string", "format": "hostname" },
|
||||||
"port": { "type": "number", "maximum": 65535 },
|
"smtpHost": { "type": "string", "format": "hostname" },
|
||||||
|
"imapPort": { "type": "number", "maximum": 65535 },
|
||||||
|
"smtpPort": { "type": "number", "maximum": 65535 },
|
||||||
"tls": { "type": "boolean" }
|
"tls": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": ["email", "host", "port", "tls"],
|
"required": ["email", "imapHost", "smtpHost", "imapPort", "smtpPort", "tls"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
16
back/validator/schemas/delete-schema.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mailboxId": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"mailboxId",
|
||||||
|
"messageId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
12
back/validator/schemas/deleteRoom-schema.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"roomId": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"roomId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
21
back/validator/schemas/response-schema.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"roomId": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"html": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"user", "roomId", "text", "html"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ import getRoomSchema from "./schemas/getRooms-schema.json";
|
|||||||
import getMessagesSchema from "./schemas/getMessages-schema.json";
|
import getMessagesSchema from "./schemas/getMessages-schema.json";
|
||||||
import getMembersSchema from "./schemas/getMembers-schema.json";
|
import getMembersSchema from "./schemas/getMembers-schema.json";
|
||||||
import setFlagSchema from "./schemas/setFlag-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 { Request, Response } from "express";
|
||||||
import statusCodes from "../utils/statusCodes";
|
import statusCodes from "../utils/statusCodes";
|
||||||
import logger from "../system/Logger";
|
import logger from "../system/Logger";
|
||||||
@@ -20,6 +23,9 @@ class Validator {
|
|||||||
validateGetMessages: any;
|
validateGetMessages: any;
|
||||||
validateGetMembers: any;
|
validateGetMembers: any;
|
||||||
validateSetFlag: any;
|
validateSetFlag: any;
|
||||||
|
validateResponse: any;
|
||||||
|
delete: any;
|
||||||
|
deleteRoom: any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.validateCreateAccount = ajv.compile(createAccountSchema);
|
this.validateCreateAccount = ajv.compile(createAccountSchema);
|
||||||
@@ -28,6 +34,9 @@ class Validator {
|
|||||||
this.validateGetMessages = ajv.compile(getMessagesSchema);
|
this.validateGetMessages = ajv.compile(getMessagesSchema);
|
||||||
this.validateGetMembers = ajv.compile(getMembersSchema);
|
this.validateGetMembers = ajv.compile(getMembersSchema);
|
||||||
this.validateSetFlag = ajv.compile(setFlagSchema);
|
this.validateSetFlag = ajv.compile(setFlagSchema);
|
||||||
|
this.validateResponse = ajv.compile(responseSchema);
|
||||||
|
this.delete = ajv.compile(deleteSchema);
|
||||||
|
this.deleteRoom = ajv.compile(deleteRoomSchema);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getSchema(name: string): any {
|
_getSchema(name: string): any {
|
||||||
@@ -45,6 +54,12 @@ class Validator {
|
|||||||
case "addFlag":
|
case "addFlag":
|
||||||
case "removeFlag":
|
case "removeFlag":
|
||||||
return this.validateSetFlag;
|
return this.validateSetFlag;
|
||||||
|
case "response":
|
||||||
|
return this.validateResponse;
|
||||||
|
case "delete":
|
||||||
|
return this.delete;
|
||||||
|
case "deleteRoom":
|
||||||
|
return this.deleteRoom;
|
||||||
default:
|
default:
|
||||||
logger.err(`Schema ${name} not found`);
|
logger.err(`Schema ${name} not found`);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -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/).
|
|
||||||
3527
front/package-lock.json
generated
@@ -9,8 +9,29 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@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": "^3.2.13",
|
||||||
"vue-router": "^4.0.3",
|
"vue-router": "^4.0.3",
|
||||||
|
"vue-svg-loader": "^0.16.0",
|
||||||
"vuex": "^4.0.0"
|
"vuex": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -43,6 +64,8 @@
|
|||||||
"eslint-plugin-vue": "^8.0.3",
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
"jest": "^27.0.5",
|
"jest": "^27.0.5",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
|
"sass": "^1.62.0",
|
||||||
|
"sass-loader": "^13.2.2",
|
||||||
"ts-jest": "^27.0.4",
|
"ts-jest": "^27.0.4",
|
||||||
"typescript": "~4.5.5"
|
"typescript": "~4.5.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
|
import Sidebar from "./views/sidebar/Sidebar";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -9,17 +10,6 @@ import { RouterView } from "vue-router";
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import Sidebar from "./views/sidebar/Sidebar";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "App",
|
|
||||||
components: {
|
|
||||||
Sidebar,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#app {
|
#app {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -7,7 +7,27 @@
|
|||||||
/* 1d1d23 */
|
/* 1d1d23 */
|
||||||
--tertiary-background: #2a2a33;
|
--tertiary-background: #2a2a33;
|
||||||
--quaternary-background: #303a46;
|
--quaternary-background: #303a46;
|
||||||
|
|
||||||
--selected: #41474f;
|
--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 */
|
/* 343a46 */
|
||||||
}
|
}
|
||||||
/* .badge-primary { */
|
/* .badge-primary { */
|
||||||
|
/* https://angel-rs.github.io/css-color-filter-generator/ */
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: var(--selected);
|
||||||
|
}
|
||||||
|
|||||||
1
front/src/assets/svg/add-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 169 B |
1
front/src/assets/svg/align-center.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM5 19H19V21H5V19ZM3 14H21V16H3V14ZM5 9H19V11H5V9Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
1
front/src/assets/svg/align-justify.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM3 14H21V16H3V14ZM3 9H21V11H3V9Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
1
front/src/assets/svg/align-left.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM3 19H17V21H3V19ZM3 14H21V16H3V14ZM3 9H17V11H3V9Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
1
front/src/assets/svg/align-right.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM7 19H21V21H7V19ZM3 14H21V16H3V14ZM7 9H21V11H7V9Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
1
front/src/assets/svg/attachment-2.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M14.8287 7.7574L9.1718 13.4143C8.78127 13.8048 8.78127 14.4379 9.1718 14.8285C9.56232 15.219 10.1955 15.219 10.586 14.8285L16.2429 9.17161C17.4144 8.00004 17.4144 6.10055 16.2429 4.92897C15.0713 3.7574 13.1718 3.7574 12.0002 4.92897L6.34337 10.5858C4.39075 12.5384 4.39075 15.7043 6.34337 17.6569C8.29599 19.6095 11.4618 19.6095 13.4144 17.6569L19.0713 12L20.4855 13.4143L14.8287 19.0711C12.095 21.8048 7.66283 21.8048 4.92916 19.0711C2.19549 16.3374 2.19549 11.9053 4.92916 9.17161L10.586 3.51476C12.5386 1.56214 15.7045 1.56214 17.6571 3.51476C19.6097 5.46738 19.6097 8.63321 17.6571 10.5858L12.0002 16.2427C10.8287 17.4143 8.92916 17.4143 7.75759 16.2427C6.58601 15.0711 6.58601 13.1716 7.75759 12L13.4144 6.34319L14.8287 7.7574Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 860 B |
1
front/src/assets/svg/attachment-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M14 13.5V8C14 5.79086 12.2091 4 10 4C7.79086 4 6 5.79086 6 8V13.5C6 17.0899 8.91015 20 12.5 20C16.0899 20 19 17.0899 19 13.5V4H21V13.5C21 18.1944 17.1944 22 12.5 22C7.80558 22 4 18.1944 4 13.5V8C4 4.68629 6.68629 2 10 2C13.3137 2 16 4.68629 16 8V13.5C16 15.433 14.433 17 12.5 17C10.567 17 9 15.433 9 13.5V8H11V13.5C11 14.3284 11.6716 15 12.5 15C13.3284 15 14 14.3284 14 13.5Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 503 B |
1
front/src/assets/svg/bold.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M8 11H12.5C13.8807 11 15 9.88071 15 8.5C15 7.11929 13.8807 6 12.5 6H8V11ZM18 15.5C18 17.9853 15.9853 20 13.5 20H6V4H12.5C14.9853 4 17 6.01472 17 8.5C17 9.70431 16.5269 10.7981 15.7564 11.6058C17.0979 12.3847 18 13.837 18 15.5ZM8 13V18H13.5C14.8807 18 16 16.8807 16 15.5C16 14.1193 14.8807 13 13.5 13H8Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 430 B |
1
front/src/assets/svg/contract-up-down-line.svg
Normal 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 |
1
front/src/assets/svg/delete-bin-4-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/delete-bin-6-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/expand-left-fill.svg
Normal 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 |
1
front/src/assets/svg/expand-up-down-line.svg
Normal 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 |
1
front/src/assets/svg/flag-2-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/flag-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/font-color.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/h-1.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M13 20H11V13H4V20H2V4H4V11H11V4H13V20ZM21.0005 8V20H19.0005L19 10.204L17 10.74V8.67L19.5005 8H21.0005Z" fill="#000"></path></svg>
|
||||||
|
After Width: | Height: | Size: 221 B |
1
front/src/assets/svg/h-2.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/h-3.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/h-4.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M13 20H11V13H4V20H2V4H4V11H11V4H13V20ZM22 8V16H23.5V18H22V20H20V18H14.5V16.66L19.5 8H22ZM20 11.133L17.19 16H20V11.133Z" fill="#000"></path></svg>
|
||||||
|
After Width: | Height: | Size: 237 B |
1
front/src/assets/svg/h-5.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/h-6.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/heading.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M17 11V4H19V21H17V13H7V21H5V4H7V11H17Z" fill="#000"></path></svg>
|
||||||
|
After Width: | Height: | Size: 157 B |
1
front/src/assets/svg/image-add-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="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 |
1
front/src/assets/svg/indent-decrease.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM11 14H21V16H11V14ZM11 9H21V11H11V9ZM3 12.5L7 9V16L3 12.5Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 217 B |
1
front/src/assets/svg/indent-increase.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM11 14H21V16H11V14ZM11 9H21V11H11V9ZM7 12.5L3 16V9L7 12.5Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 217 B |
1
front/src/assets/svg/italic.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M15 20H7V18H9.92661L12.0425 6H9V4H17V6H14.0734L11.9575 18H15V20Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 192 B |
1
front/src/assets/svg/link.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M18.3643 15.5353L16.95 14.1211L18.3643 12.7069C20.3169 10.7543 20.3169 7.58847 18.3643 5.63585C16.4116 3.68323 13.2458 3.68323 11.2932 5.63585L9.87898 7.05007L8.46477 5.63585L9.87898 4.22164C12.6127 1.48797 17.0448 1.48797 19.7785 4.22164C22.5121 6.95531 22.5121 11.3875 19.7785 14.1211L18.3643 15.5353ZM15.5358 18.3638L14.1216 19.778C11.388 22.5117 6.9558 22.5117 4.22213 19.778C1.48846 17.0443 1.48846 12.6122 4.22213 9.87849L5.63634 8.46428L7.05055 9.87849L5.63634 11.2927C3.68372 13.2453 3.68372 16.4112 5.63634 18.3638C7.58896 20.3164 10.7548 20.3164 12.7074 18.3638L14.1216 16.9496L15.5358 18.3638ZM14.8287 7.75717L16.2429 9.17139L9.17187 16.2425L7.75766 14.8282L14.8287 7.75717Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 813 B |
1
front/src/assets/svg/list-check-2.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M11 4H21V6H11V4ZM11 8H17V10H11V8ZM11 14H21V16H11V14ZM11 18H17V20H11V18ZM3 4H9V10H3V4ZM5 6V8H7V6H5ZM3 14H9V20H3V14ZM5 16V18H7V16H5Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
1
front/src/assets/svg/list-check.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M8 4H21V6H8V4ZM3 3.5H6V6.5H3V3.5ZM3 10.5H6V13.5H3V10.5ZM3 17.5H6V20.5H3V17.5ZM8 11H21V13H8V11ZM8 18H21V20H8V18Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 239 B |
1
front/src/assets/svg/list-ordered.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M8 4H21V6H8V4ZM5 3V6H6V7H3V6H4V4H3V3H5ZM3 14V11.5H5V11H3V10H6V12.5H4V13H6V14H3ZM5 19.5H3V18.5H5V18H3V17H6V21H3V20H5V19.5ZM8 11H21V13H8V11ZM8 18H21V20H8V18Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 283 B |
1
front/src/assets/svg/list-unordered.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M8 4H21V6H8V4ZM4.5 6.5C3.67157 6.5 3 5.82843 3 5C3 4.17157 3.67157 3.5 4.5 3.5C5.32843 3.5 6 4.17157 6 5C6 5.82843 5.32843 6.5 4.5 6.5ZM4.5 13.5C3.67157 13.5 3 12.8284 3 12C3 11.1716 3.67157 10.5 4.5 10.5C5.32843 10.5 6 11.1716 6 12C6 12.8284 5.32843 13.5 4.5 13.5ZM4.5 20.4C3.67157 20.4 3 19.7284 3 18.9C3 18.0716 3.67157 17.4 4.5 17.4C5.32843 17.4 6 18.0716 6 18.9C6 19.7284 5.32843 20.4 4.5 20.4ZM8 11H21V13H8V11ZM8 18H21V20H8V18Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 561 B |
1
front/src/assets/svg/mail-add-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M22 13H20V7.23792L12.0718 14.338L4 7.21594V19H14V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V13ZM4.51146 5L12.0619 11.662L19.501 5H4.51146ZM21 18H24V20H21V23H19V20H16V18H19V15H21V18Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 352 B |
1
front/src/assets/svg/mail-check-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M22 14H20V7.23792L12.0718 14.338L4 7.21594V19H14V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V14ZM4.51146 5L12.0619 11.662L19.501 5H4.51146ZM19 22L15.4645 18.4645L16.8787 17.0503L19 19.1716L22.5355 15.636L23.9497 17.0503L19 22Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 396 B |
1
front/src/assets/svg/mail-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM20 7.23792L12.0718 14.338L4 7.21594V19H20V7.23792ZM4.51146 5L12.0619 11.662L19.501 5H4.51146Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 341 B |
1
front/src/assets/svg/mail-open-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M2.24283 6.85419L11.4895 1.30843C11.8062 1.11848 12.2019 1.11855 12.5185 1.30862L21.7573 6.85416C21.9079 6.94453 22 7.10726 22 7.28286V19.9998C22 20.5521 21.5523 20.9998 21 20.9998H3C2.44772 20.9998 2 20.5521 2 19.9998V7.28298C2 7.10732 2.09218 6.94454 2.24283 6.85419ZM4 8.13244V18.9998H20V8.13197L12.0037 3.33221L4 8.13244ZM12.0597 13.6981L17.3556 9.23515L18.6444 10.7645L12.074 16.3016L5.36401 10.7715L6.63599 9.22813L12.0597 13.6981Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 565 B |
1
front/src/assets/svg/mail-send-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M21 3C21.5523 3 22 3.44772 22 4V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V19H20V7.3L12 14.5L2 5.5V4C2 3.44772 2.44772 3 3 3H21ZM8 15V17H0V15H8ZM5 10V12H0V10H5ZM19.5659 5H4.43414L12 11.8093L19.5659 5Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 364 B |
1
front/src/assets/svg/mail-star-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M22 13H20V7.23792L12.0718 14.338L4 7.21594V19H14V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V13ZM4.51146 5L12.0619 11.662L19.501 5H4.51146ZM19.5 21.75L16.855 23.1406L17.3601 20.1953L15.2202 18.1094L18.1775 17.6797L19.5 15L20.8225 17.6797L23.7798 18.1094L21.6399 20.1953L22.145 23.1406L19.5 21.75Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 466 B |
1
front/src/assets/svg/mail-unread-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M16.1 3C16.0344 3.32311 16 3.65753 16 4C16 4.34247 16.0344 4.67689 16.1 5H4.51146L12.0619 11.662L17.1098 7.14141C17.5363 7.66888 18.0679 8.10787 18.6728 8.42652L12.0718 14.338L4 7.21594V19H20V8.89998C20.3231 8.96557 20.6575 9 21 9C21.3425 9 21.6769 8.96557 22 8.89998V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H16.1ZM21 1C22.6569 1 24 2.34315 24 4C24 5.65685 22.6569 7 21 7C19.3431 7 18 5.65685 18 4C18 2.34315 19.3431 1 21 1Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 596 B |
1
front/src/assets/svg/menu-add-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M18 15L17.999 18H21V20H17.999L18 23H16L15.999 20H13V18H15.999L16 15H18ZM11 18V20H3V18H11ZM21 11V13H3V11H21ZM21 4V6H3V4H21Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 250 B |
1
front/src/assets/svg/menu-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 4H21V6H3V4ZM3 11H21V13H3V11ZM3 18H21V20H3V18Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 176 B |
1
front/src/assets/svg/paint-fill.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M19.2277 18.7321L20.9955 16.9644L22.7632 18.7321C23.7395 19.7084 23.7395 21.2914 22.7632 22.2677C21.7869 23.244 20.204 23.244 19.2277 22.2677C18.2514 21.2914 18.2514 19.7084 19.2277 18.7321ZM8.87861 1.07959L20.1923 12.3933C20.5828 12.7838 20.5828 13.417 20.1923 13.8075L11.707 22.2928C11.3165 22.6833 10.6833 22.6833 10.2928 22.2928L1.80754 13.8075C1.41702 13.417 1.41702 12.7838 1.80754 12.3933L9.58572 4.61512L7.4644 2.4938L8.87861 1.07959ZM10.9999 6.02934L3.92886 13.1004H18.071L10.9999 6.02934Z" fill="#000"></path></svg>
|
||||||
|
After Width: | Height: | Size: 617 B |
1
front/src/assets/svg/pushpin-2-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 3V5H17V11L19 14V16H13V23H11V16H5V14L7 11V5H6V3H18ZM9 5V11.6056L7.4037 14H16.5963L15 11.6056V5H9Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 184 B |
1
front/src/assets/svg/pushpin-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.8273 1.68994L22.3126 10.1752L20.8984 11.5894L20.1913 10.8823L15.9486 15.125L15.2415 18.6605L13.8273 20.0747L9.58466 15.8321L4.63492 20.7818L3.2207 19.3676L8.17045 14.4179L3.92781 10.1752L5.34202 8.76101L8.87756 8.0539L13.1202 3.81126L12.4131 3.10416L13.8273 1.68994ZM14.5344 5.22548L9.86358 9.89631L7.0417 10.4607L13.5418 16.9608L14.1062 14.1389L18.7771 9.46812L14.5344 5.22548Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 466 B |
1
front/src/assets/svg/reply-all-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M14 4.5V9C19.5228 9 24 13.4772 24 19C24 19.2727 23.9891 19.5428 23.9677 19.81C22.5055 17.0364 19.6381 15.119 16.313 15.0053L16 15H13.9999L14 19.5L6 12L14 4.5ZM8 4.5V7.237L2.92 12L7.999 16.761L8 19.5L0 12L8 4.5ZM12 9.11646L8.92423 12L11.9999 14.8834L11.9999 13L16.0341 13.0003L16.3814 13.0065C17.6657 13.0504 18.9053 13.3165 20.0568 13.7734C18.5898 12.0749 16.4204 11 14 11H12V9.11646Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 512 B |
1
front/src/assets/svg/reply-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M11 20L1 12L11 4V9C16.5228 9 21 13.4772 21 19C21 19.2727 20.9891 19.5428 20.9677 19.81C19.5055 17.0364 16.6381 15.119 13.313 15.0053L13 15H10.9999L11 20ZM8.99986 13H10.9999L13.0341 13.0003L13.3814 13.0065C14.6657 13.0504 15.9053 13.3165 17.0568 13.7734C15.5898 12.0749 13.4204 11 11 11H9V8.16125L4.20156 12L8.99992 15.8387L8.99986 13Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 462 B |
1
front/src/assets/svg/send-plane-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M21.7264 2.95706L16.2732 22.0433C16.1222 22.5718 15.7976 22.5958 15.5561 22.1127L10.9998 13.0002L1.92266 9.36931C1.41298 9.16544 1.41929 8.86034 1.9567 8.6812L21.0429 2.31913C21.5714 2.14297 21.8745 2.43878 21.7264 2.95706ZM19.0351 5.0966L6.81197 9.17097L12.4486 11.4256L15.4893 17.507L19.0351 5.0966Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 429 B |
1
front/src/assets/svg/share-forward-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M13 14H11C7.54202 14 4.53953 15.9502 3.03239 18.8107C3.01093 18.5433 3 18.2729 3 18C3 12.4772 7.47715 8 13 8V2.5L23.5 11L13 19.5V14ZM11 12H15V15.3078L20.3214 11L15 6.69224V10H13C10.5795 10 8.41011 11.0749 6.94312 12.7735C8.20873 12.2714 9.58041 12 11 12Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 382 B |
1
front/src/assets/svg/strikethrough.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M17.1538 14C17.3846 14.5161 17.5 15.0893 17.5 15.7196C17.5 17.0625 16.9762 18.1116 15.9286 18.867C14.8809 19.6223 13.4335 20 11.5862 20C9.94674 20 8.32335 19.6185 6.71592 18.8555V16.6009C8.23538 17.4783 9.7908 17.917 11.3822 17.917C13.9333 17.917 15.2128 17.1846 15.2208 15.7196C15.2208 15.0939 15.0049 14.5598 14.5731 14.1173C14.5339 14.0772 14.4939 14.0381 14.4531 14H3V12H21V14H17.1538ZM13.076 11H7.62908C7.4566 10.8433 7.29616 10.6692 7.14776 10.4778C6.71592 9.92084 6.5 9.24559 6.5 8.45207C6.5 7.21602 6.96583 6.165 7.89749 5.299C8.82916 4.43299 10.2706 4 12.2219 4C13.6934 4 15.1009 4.32808 16.4444 4.98426V7.13591C15.2448 6.44921 13.9293 6.10587 12.4978 6.10587C10.0187 6.10587 8.77917 6.88793 8.77917 8.45207C8.77917 8.87172 8.99709 9.23796 9.43293 9.55079C9.86878 9.86362 10.4066 10.1135 11.0463 10.3004C11.6665 10.4816 12.3431 10.7148 13.076 11H13.076Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 990 B |
1
front/src/assets/svg/task-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M19 4H5V20H19V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H19.9997C20.5519 2 20.9996 2.44772 20.9997 3L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11.2929 13.1213L15.5355 8.87868L16.9497 10.2929L11.2929 15.9497L7.40381 12.0607L8.81802 10.6464L11.2929 13.1213Z" fill="#000"></path></svg>
|
||||||
|
After Width: | Height: | Size: 420 B |
1
front/src/assets/svg/underline.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M8 3V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V3H18V12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12V3H8ZM4 20H20V22H4V20Z" fill="rgba(0,0,0,1)"></path></svg>
|
||||||
|
After Width: | Height: | Size: 274 B |
40
front/src/components/basic/Button.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
onClick: { type: Function },
|
||||||
|
text: { type: String },
|
||||||
|
class: { type: String },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<button :class="props.class" @click="props.onClick">{{ props.text }}</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
button {
|
||||||
|
padding: 5px;
|
||||||
|
padding: 7px 18px;
|
||||||
|
background-color: #09a35b;
|
||||||
|
color: var(--primary-text);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
background-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cancel {
|
||||||
|
background-color: var(--quaternary-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
70
front/src/components/basic/Input.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, defineEmits, withDefaults } from "vue";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
type: string;
|
||||||
|
required: boolean;
|
||||||
|
onChange: any;
|
||||||
|
placeholder: string;
|
||||||
|
label: string;
|
||||||
|
vModel: string;
|
||||||
|
modelValue: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
required: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["update:modelValue"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<label v-show="props.label">{{ props.label }}</label>
|
||||||
|
<input
|
||||||
|
:value="modelValue"
|
||||||
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
|
:type="props.type"
|
||||||
|
:required="props.required"
|
||||||
|
@change="props.onChange"
|
||||||
|
:placeholder="props.placeholder"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
label {
|
||||||
|
color: var(--secondary-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
background-color: var(--quaternary-background);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--primary-text);
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 9px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type="number"] {
|
||||||
|
appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import { ref, computed, watchEffect } from "vue";
|
import { ref, computed, watchEffect } from "vue";
|
||||||
import Modal from "./Modal";
|
import Modal from "./Modal";
|
||||||
import API from "../../services/imapAPI";
|
import API from "../../services/imapAPI";
|
||||||
|
import Input from "../basic/Input.vue";
|
||||||
|
import Button from "../basic/Button.vue";
|
||||||
|
|
||||||
const modal = ref(false);
|
const modal = ref(false);
|
||||||
|
|
||||||
@@ -9,35 +11,49 @@ const email = ref("");
|
|||||||
const pwd = ref("");
|
const pwd = ref("");
|
||||||
const xoauth = ref("");
|
const xoauth = ref("");
|
||||||
const xoauth2 = ref("");
|
const xoauth2 = ref("");
|
||||||
const host = ref("");
|
const imapHost = ref("");
|
||||||
const port = ref(993);
|
const imapPort = ref(993);
|
||||||
|
const smtpHost = ref("");
|
||||||
|
const smtpPort = ref(465);
|
||||||
|
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
|
|
||||||
const knownHosts = {
|
const knownHosts = {
|
||||||
"outlook.com": {
|
"outlook.com": {
|
||||||
host: "outlook.office365.com",
|
imap: "outlook.office365.com",
|
||||||
|
smtp: "outlook.office365.com",
|
||||||
},
|
},
|
||||||
"hotmail.com": {
|
"hotmail.com": {
|
||||||
host: "outlook.office365.com",
|
imap: "outlook.office365.com",
|
||||||
|
smtp: "outlook.office365.com",
|
||||||
},
|
},
|
||||||
"live.com": {
|
"live.com": {
|
||||||
host: "outlook.office365.com",
|
imap: "outlook.office365.com",
|
||||||
|
smtp: "outlook.office365.com",
|
||||||
},
|
},
|
||||||
"zoho.com": {
|
"zoho.com": {
|
||||||
host: "imap.zoho.eu",
|
imap: "imap.zoho.eu",
|
||||||
|
smtp: "smtp.zoho.eu",
|
||||||
},
|
},
|
||||||
"yahoo.com": {
|
"yahoo.com": {
|
||||||
host: "imap.mail.yahoo.com",
|
imap: "imap.mail.yahoo.com",
|
||||||
|
smtp: "smtp.mail.yahoo.com",
|
||||||
},
|
},
|
||||||
"icloud.com": {
|
"icloud.com": {
|
||||||
host: "imap.mail.me.com",
|
imap: "imap.mail.me.com",
|
||||||
|
smtp: "smtp.mail.me.com",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const refHost = computed({
|
const refSmtpHost = computed({
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
host.value = val;
|
smtpHost.value = val;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const refImapHost = computed({
|
||||||
|
set: (val) => {
|
||||||
|
imapHost.value = val;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +76,7 @@ function checkError() {
|
|||||||
err.value = "You need at least one authentification method.";
|
err.value = "You need at least one authentification method.";
|
||||||
} else if ([pwd.value, xoauth.value, xoauth2.value].filter((val) => val != "").length > 1) {
|
} else if ([pwd.value, xoauth.value, xoauth2.value].filter((val) => val != "").length > 1) {
|
||||||
err.value = "You need only one authentification method.";
|
err.value = "You need only one authentification method.";
|
||||||
} else if (host.value == "" || port.value == "") {
|
} else if (smtpHost.value == "" || smtpPort.value == "" || imapHost.value == "" || imapPort.value == "") {
|
||||||
err.value = "You need to complete the port and the host.";
|
err.value = "You need to complete the port and the host.";
|
||||||
} else {
|
} else {
|
||||||
err.value = "";
|
err.value = "";
|
||||||
@@ -75,8 +91,10 @@ function addAccountRequest() {
|
|||||||
pwd: pwd.value,
|
pwd: pwd.value,
|
||||||
xoauth: xoauth.value,
|
xoauth: xoauth.value,
|
||||||
xoauth2: xoauth2.value,
|
xoauth2: xoauth2.value,
|
||||||
host: host.value,
|
imapHost: imapHost.value,
|
||||||
port: port.value,
|
imapPort: imapPort.value,
|
||||||
|
smtpHost: smtpHost.value,
|
||||||
|
smtpPort: smtpPort.value,
|
||||||
tls: true,
|
tls: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,76 +116,75 @@ watchEffect(() => {
|
|||||||
function mailChange() {
|
function mailChange() {
|
||||||
if (email.value.includes("@")) {
|
if (email.value.includes("@")) {
|
||||||
const domain = email.value.split("@")[1];
|
const domain = email.value.split("@")[1];
|
||||||
if (!knownHosts[domain]) {
|
if (imapHost.value == "") {
|
||||||
refHost.value = "imap." + domain;
|
refImapHost.value = knownHosts[domain]?.imap ?? `imap.${domain}`;
|
||||||
} else {
|
}
|
||||||
refHost.value = knownHosts[domain].host;
|
if (smtpHost.value == "") {
|
||||||
|
refSmtpHost.value = knownHosts[domain]?.smtp ?? `smtp.${domain}`;
|
||||||
}
|
}
|
||||||
// todo check if manual
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button @click="modal = true">Add Account</button>
|
<Button :onClick="() => (modal = true)" text="Add Account" />
|
||||||
<Modal v-if="modal" title="Add new account" @close-modal="modal = false">
|
<Modal v-if="modal" title="Add new account" @close-modal="modal = false">
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Email: </label>
|
<Input
|
||||||
<input @change="mailChange" v-model="email" type="email" required />
|
label="Email:"
|
||||||
|
:onChange="mailChange"
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset>
|
<div class="field">
|
||||||
|
<Input label="Plain password:" v-model="pwd" type="password" />
|
||||||
|
</div>
|
||||||
|
<!-- <fieldset>
|
||||||
<legend>Authentification method</legend>
|
<legend>Authentification method</legend>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Plain password:</label>
|
<Input label="Xoauth:" v-model="xoauth" type="text" />
|
||||||
<input v-model="pwd" type="password" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Xoauth:</label>
|
<Input label="Xoauth2:" v-model="xoauth2" type="text" />
|
||||||
<input v-model="xoauth" type="text" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
</fieldset> -->
|
||||||
<label>Xoauth2:</label>
|
|
||||||
<input v-model="xoauth2" type="text" />
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div class="config">
|
<fieldset class="config">
|
||||||
<input v-model="host" id="host" type="text" placeholder="imap host" />
|
<legend>Imap</legend>
|
||||||
<input v-model="port" id="port" type="number" placeholder="port" />
|
<Input v-model="imapHost" class="host" type="text" placeholder="host" />
|
||||||
</div>
|
<Input v-model="imapPort" class="port" type="number" placeholder="port" />
|
||||||
<!-- tls -->
|
</fieldset>
|
||||||
<div>
|
<fieldset class="config">
|
||||||
<button :disabled="error != ''" @click="addAccountRequest">Add</button>
|
<legend>Smtp</legend>
|
||||||
{{ error }}
|
<Input v-model="smtpHost" class="host" type="text" placeholder="host" />
|
||||||
</div>
|
<Input v-model="smtpPort" class="port" type="number" placeholder="port" />
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
||||||
|
<!-- tls -->
|
||||||
|
<template v-slot:actions>
|
||||||
|
<Button :disabled="error != ''" :onClick="addAccountRequest" text="Add" />
|
||||||
|
{{ error }}
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
/* Chrome, Safari, Edge, Opera */
|
|
||||||
input::-webkit-outer-spin-button,
|
|
||||||
input::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Firefox */
|
|
||||||
input[type="number"] {
|
|
||||||
appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field > input {
|
.field {
|
||||||
margin-top: 2px;
|
input {
|
||||||
width: 95%;
|
margin-top: 2px;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
@@ -179,54 +196,16 @@ fieldset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.config {
|
.config {
|
||||||
|
display: block;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#host {
|
.host {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
width: calc(95% - 100px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#port {
|
.port {
|
||||||
|
display: inline-flex;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 5px;
|
|
||||||
padding: 7px 18px;
|
|
||||||
background-color: #09a35b;
|
|
||||||
color: var(--primary-text);
|
|
||||||
border-radius: 5px;
|
|
||||||
border: none;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
|
||||||
transition: opacity 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
-webkit-box-flex: 1;
|
|
||||||
background-color: var(--quaternary-background);
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--primary-text);
|
|
||||||
-ms-flex: 1;
|
|
||||||
flex: 1;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
font-weight: 400;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 8px 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
color: var(--secondary-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
54
front/src/components/modals/ConfirmationModal.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps, withDefaults } from "vue";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
import Button from "../basic/Button.vue";
|
||||||
|
|
||||||
|
const modal = ref(false);
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title: string;
|
||||||
|
isDanger: boolean;
|
||||||
|
onConfirmation: any;
|
||||||
|
onCancel?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: () => "Confirmation",
|
||||||
|
isDanger: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = ref("");
|
||||||
|
|
||||||
|
const handleContinue = async () => {
|
||||||
|
try {
|
||||||
|
if (props.onConfirmation) {
|
||||||
|
await props.onConfirmation();
|
||||||
|
}
|
||||||
|
modal.value = false;
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
if (props.onCancel) {
|
||||||
|
await props.onCancel();
|
||||||
|
}
|
||||||
|
modal.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Modal v-if="modal" :title="props.title" @close-modal="modal = false">
|
||||||
|
<template v-slot:body> Are you sure you want to do that ? </template>
|
||||||
|
<template v-slot:actions>
|
||||||
|
<Button :onClick="handleCancel" class="cancel" text="Cancel" />
|
||||||
|
<Button :onClick="handleContinue" :class="props.isDanger ? 'danger' : ''" text="Confirm" />
|
||||||
|
{{ error }}
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -1,26 +1,59 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Message } from "@/store/models/model";
|
import { Address, Message, Room } from "@/store/models/model";
|
||||||
import { ref, watch, defineProps, PropType } from "vue";
|
import { ref, Ref, watch, defineProps, PropType } from "vue";
|
||||||
import Content from "../structure/message/Content.vue";
|
import Content from "../structure/message/Content.vue";
|
||||||
import Modal from "./Modal.vue";
|
import Modal from "./Modal.vue";
|
||||||
|
import MemberList from "@/views/room/MemberList.vue";
|
||||||
|
import { removeDuplicates } from "@/utils/array";
|
||||||
|
import { decodeEmojis } from "@/utils/string";
|
||||||
|
|
||||||
const props = defineProps({ messageId: { type: Number, require: true }, message: Object as PropType<Message> });
|
const props = defineProps({
|
||||||
|
messageId: { type: Number, require: true },
|
||||||
|
message: Object as PropType<Message>,
|
||||||
|
room: Object as PropType<Room>,
|
||||||
|
});
|
||||||
|
|
||||||
const messageId = ref(-1);
|
const messageId = ref(-1);
|
||||||
|
const fromA = ref<Address[]>([]);
|
||||||
|
const toA = ref<Address[]>([]);
|
||||||
|
const ccA = ref<Address[]>([]);
|
||||||
|
|
||||||
|
const getAddr = (ids: string | undefined, addrs: Address[] | undefined, res: Ref<Address[]>) => {
|
||||||
|
res.value = [];
|
||||||
|
if (!ids || !addrs) return;
|
||||||
|
let idsClean = removeDuplicates(ids.split(","));
|
||||||
|
idsClean.forEach((id) => {
|
||||||
|
let addr = addrs.find((member) => member.id === parseInt(id));
|
||||||
|
if (addr) {
|
||||||
|
res.value.push(addr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.messageId,
|
() => props.messageId,
|
||||||
(newMessageId: number | undefined) => {
|
(newMessageId: number | undefined) => {
|
||||||
if (!newMessageId) return;
|
if (!newMessageId) return;
|
||||||
messageId.value = newMessageId;
|
messageId.value = newMessageId;
|
||||||
|
getAddr(props.message?.fromA, props.room?.members, fromA);
|
||||||
|
getAddr(props.message?.toA, props.room?.members, toA);
|
||||||
|
getAddr(props.message?.ccA, props.room?.members, ccA);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<Modal v-if="messageId != -1" @close-modal="() => $emit('close')">
|
<Modal v-if="messageId != -1" @close-modal="() => $emit('close')" id="modal">
|
||||||
|
<template v-slot:header>
|
||||||
|
<h2 id="header">
|
||||||
|
{{ decodeEmojis(props.message?.subject) ?? "No Object" }}
|
||||||
|
</h2>
|
||||||
|
</template>
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<div>{{ props.message?.subject ?? "No Object" }}</div>
|
<MemberList type="from" :members="fromA" />
|
||||||
|
<MemberList type="to" :members="toA" />
|
||||||
|
<MemberList v-if="ccA.length > 0" type="cc" :members="ccA" />
|
||||||
<Content type="large" :content="props.message?.content" class="content" />
|
<Content type="large" :content="props.message?.content" class="content" />
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -31,7 +64,12 @@ watch(
|
|||||||
.main {
|
.main {
|
||||||
min-width: 700px;
|
min-width: 700px;
|
||||||
}
|
}
|
||||||
/* todo */
|
|
||||||
|
#header {
|
||||||
|
width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* todo define size automatically */
|
||||||
.content {
|
.content {
|
||||||
width: 700px;
|
width: 700px;
|
||||||
height: 700px;
|
height: 700px;
|
||||||
|
|||||||
@@ -26,13 +26,18 @@ onUnmounted(() => {
|
|||||||
<div class="modal-wrapper">
|
<div class="modal-wrapper">
|
||||||
<div class="modal" v-on-click-outside="close">
|
<div class="modal" v-on-click-outside="close">
|
||||||
<header class="modal-header">
|
<header class="modal-header">
|
||||||
<h2>{{ props.title }}</h2>
|
<slot name="header">
|
||||||
|
<h2>{{ props.title }}</h2>
|
||||||
|
</slot>
|
||||||
<div class="close-button" @click="close"></div>
|
<div class="close-button" @click="close"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<slot name="body"> This is the default body! </slot>
|
<slot name="body"> This is the default body! </slot>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<slot name="actions"></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,13 +63,29 @@ onUnmounted(() => {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
color: var(--primary-text);
|
color: var(--primary-text);
|
||||||
background-color: var(--secondary-background);
|
background-color: var(--secondary-background);
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 2.4rem;
|
font-size: 2.4rem;
|
||||||
|
|||||||