Compare commits

...

39 Commits

Author SHA1 Message Date
grimhilt
fd253197cc show addresses in front 2023-04-03 20:37:07 +02:00
grimhilt
6b4264fccc show description in room header 2023-04-03 20:11:07 +02:00
grimhilt
4e48c5d813 start message to bottom 2023-04-02 17:39:04 +02:00
grimhilt
4bff5be6c1 use strict front 2023-04-02 16:52:19 +02:00
grimhilt
e3d8d3cf9b switch front to typescript 2023-04-02 16:44:54 +02:00
grimhilt
7f535f2e95 fix some errors on front 2023-04-02 13:36:59 +02:00
grimhilt
948ec3c7b4 improve logger 2023-04-02 12:59:11 +02:00
grimhilt
3042ed972b run server as typescript 2023-04-02 12:35:55 +02:00
grimhilt
11ab6a6a21 tests in typescript 2023-04-01 22:36:51 +02:00
grimhilt
90dd16ee0d started to convert to typescript 2023-04-01 16:32:29 +02:00
grimhilt
aced3b8914 add logic and more test to saveMessage 2023-04-01 15:07:49 +02:00
grimhilt
68e1dfe7d8 implement save of thread and members 2023-03-31 16:07:02 +02:00
grimhilt
a82ff9b85b improve tests saveMessage 2023-03-30 09:16:10 +02:00
grimhilt
8306543ddd improve saveMessage (switch to class) and started to test 2023-03-29 21:00:43 +02:00
grimhilt
6507d466ad logic pseudo code 2023-03-29 17:43:46 +02:00
grimhilt
44125fc55d get members 2023-03-29 16:48:28 +02:00
grimhilt
14dd6b36f8 fix duplicate content header 2023-03-29 16:34:30 +02:00
grimhilt
91898e25a5 improve syncing and storing 2023-03-29 16:23:24 +02:00
grimhilt
185f051a63 change logger 2023-03-28 16:57:44 +02:00
grimhilt
838550b6cc display mail in iframe, add design for thread and unseen 2023-03-27 01:04:43 +02:00
grimhilt
5447557f91 apply difference between mailbox and account 2023-03-26 14:55:13 +02:00
grimhilt
62dd43c3d5 link imap sync to server and show email on front 2023-03-26 14:20:16 +02:00
grimhilt
0ea7f5865b advancements in tests and storing messages 2023-03-25 16:47:23 +01:00
grimhilt
4d4ef54bcb load message in front 2023-03-25 13:06:59 +01:00
grimhilt
926dc60920 fix modal background opacity 2023-03-23 23:59:49 +01:00
grimhilt
d6f06f3ca6 start to load messages from rooms 2023-03-20 21:28:13 +01:00
grimhilt
9b3ddd291e basic routing for roomview 2023-03-20 15:00:15 +01:00
grimhilt
d7029854b4 fetch rooms 2023-03-20 14:43:07 +01:00
grimhilt
095efb5440 fetching mailboxes from api 2023-03-17 13:31:27 +01:00
grimhilt
14e64c1fc3 save message working without reply 2023-03-16 16:14:25 +01:00
grimhilt
95f39cf53a save message sync 2023-03-15 14:48:15 +01:00
grimhilt
f9fbab3a21 advancement on logic of app 2023-03-13 19:12:57 +01:00
grimhilt
aa9a69e17f add queries for app functionnalities 2023-03-13 00:54:44 +01:00
grimhilt
3286a2e52b started some app structure 2023-03-13 00:13:17 +01:00
grimhilt
9046ccf137 update database structure 2023-03-11 14:52:08 +01:00
grimhilt
29bf4bbdbd gloablly save messages 2023-03-10 17:07:05 +01:00
grimhilt
df69a7dbd9 solution clean not working 2023-03-10 16:18:04 +01:00
grimhilt
427ffba725 save 2023-03-10 16:08:50 +01:00
grimhilt
d3893c682e remove password 2023-03-01 16:57:11 +01:00
108 changed files with 26465 additions and 12030 deletions

15
.gitignore vendored
View File

@ -1,11 +1,10 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
.env
# Log files
npm-debug.log*
@ -21,3 +20,15 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
build
.tmp
.s.*
log*
.direnv
.envrc
*.txt
tmp
test.*
*.png
!*/schemas/*

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"printWidth": 120,
"tabWidth": 4,
"quoteProps": "consistent",
"trailingComma": "all",
"useTabs": false
}

View File

@ -1,24 +0,0 @@
# mail
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@ -1,13 +0,0 @@
const statusCodes = require("../utils/statusCodes.js").statusCodes;
/**
* Return all mailboxes and folders for an user
*/
function getMailboxes(req, res) {
const {token} = req.params;
const query = ``;
}
module.exports = {
getFolders,
}

View File

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

View File

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

View File

@ -0,0 +1,14 @@
import statusCode from "../utils/statusCodes";
import { getMessages } from "../db/api";
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);
});
}

13
back/controllers/rooms.ts Normal file
View File

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

113
back/db/api.ts Normal file
View File

@ -0,0 +1,113 @@
import { execQueryAsync, execQueryAsyncWithId } from "./db";
import { queryCcId, queryToId, queryFromId } from "./utils/addressQueries";
export async function registerAccount(userId, pwd, xoauth, xoauth2, host, port, tls) {
const query = `
INSERT INTO app_account
(user_id, account_pwd, xoauth, xoauth2, host, port, tls) VALUES (?, ?, ?, ?, ?, ?, ?)
`;
const values = [userId, pwd, xoauth, xoauth2, host, port, tls];
return await execQueryAsyncWithId(query, values);
}
export async function getAccounts() {
// todo mailbox or account id ?
const query = `
SELECT
mailbox.mailbox_id AS id,
address.email
FROM app_account
INNER JOIN address
INNER JOIN mailbox
WHERE
address.address_id = app_account.user_id AND
mailbox.account_id = app_account.account_id
`;
const values = [];
return await execQueryAsync(query, values);
}
export async function getRooms(mailboxId) {
const query = `
SELECT
app_room.room_id AS id,
app_room.room_name AS roomName,
address.email AS user,
app_room.owner_id AS userId,
app_room.notSeen,
app_room.room_type AS roomType,
mailbox_message.mailbox_id AS mailboxId
FROM app_room
INNER JOIN message
INNER JOIN mailbox_message
INNER JOIN address
WHERE
message.message_id = app_room.message_id AND
mailbox_message.mailbox_id = ? AND
mailbox_message.message_id = message.message_id AND
address.address_id = app_room.owner_id
ORDER BY app_room.lastUpdate DESC
`;
const values = [mailboxId];
return await execQueryAsync(query, values);
}
export async function getMessages(roomId) {
// todo attachements name
const query = `
SELECT
msg.message_id AS id,
GROUP_CONCAT(fromT.address_id) AS fromA,
GROUP_CONCAT(toT.address_id) AS toA,
GROUP_CONCAT(ccT.address_id) AS ccA,
subjectT.value AS subject,
content.text AS content,
message.idate AS date
FROM app_room_message msg
${queryFromId} fromT ON msg.message_id = fromT.message_id
${queryToId} toT ON msg.message_id = toT.message_id
${queryCcId} ccT ON msg.message_id = ccT.message_id
LEFT JOIN (
SELECT header_field.message_id, header_field.value
FROM header_field
INNER JOIN field_name
WHERE
field_name.field_id = header_field.field_id AND
field_name.field_name = 'subject'
) subjectT ON msg.message_id = subjectT.message_id
LEFT JOIN (
SELECT bodypart.text, header_field.message_id FROM bodypart
INNER JOIN header_field
INNER JOIN field_name
WHERE
field_name.field_id = header_field.field_id AND
field_name.field_name = 'html' AND
bodypart.bodypart_id = header_field.bodypart_id
) content ON msg.message_id = content.message_id
INNER JOIN message ON message.message_id = msg.message_id
WHERE msg.room_id = ?
GROUP BY msg.message_id
ORDER BY message.idate DESC;
`;
const values = [roomId];
return await execQueryAsync(query, values);
}
export async function getMembers(roomId) {
const query = `
SELECT
address.address_id AS id,
address.address_name AS name,
address.email AS email
FROM app_room_member
INNER JOIN address ON address.address_id = app_room_member.member_id
WHERE app_room_member.room_id = ?
`;
const values = [roomId];
return await execQueryAsync(query, values);
}

140
back/db/database.dart Normal file
View File

@ -0,0 +1,140 @@
Table "addresses" {
"id" int [pk, not null, increment]
"name" text
"localpart" text [not null]
"domain" text [not null]
"email" text [not null]
Indexes {
email [unique]
}
}
Table "mailboxes" {
"id" int [pk, not null, increment]
"name" text [not null]
"uidnext" int [not null, default: 1]
"nextmodseq" bigint [not null, default: 1]
"first_recent" int [not null, default: 1]
"uidvalidity" int [not null, default: 1]
Indexes {
name [unique]
}
}
Table "messages" {
"id" int [pk, not null, increment]
"messageID" text [pk, not null]
"idate" timestamp [not null]
"rfc822size" int
}
Table "mailbox_messages" {
"mailbox" int [not null]
"uid" int [pk, not null]
"message" int [pk, not null]
"modseq" bigint [not null]
"seen" boolean [not null, default: false]
"deleted" boolean [not null, default: false]
}
Ref: mailbox_messages.mailbox > mailboxes.id
Ref: mailbox_messages.message - messages.id
Table "bodyparts" {
"id" int [pk, not null, increment]
"bytes" int [not null]
"hash" text [not null]
"text" text
"data" binary
}
Table "part_numbers" {
"message" int [pk, not null]
"part" text [not null]
"bodypart" int [not null]
"bytes" int
"nb_lines" int
}
// todo on delete cascade
Ref: part_numbers.message > messages.id
Ref: part_numbers.bodypart - bodyparts.id
Table "field_names" {
"id" int [pk, not null, increment]
"name" text [not null]
Indexes {
name [unique]
}
}
Table "header_fields" {
"id" int [pk, not null, increment]
"message" int [pk, not null]
"part" text [not null]
"position" int [not null]
"field" int [not null]
"value" text
Indexes {
message [unique]
part [unique]
position [unique]
field [unique]
}
}
Ref: header_fields.message > messages.id
Ref: header_fields.part > part_numbers.part
Ref: header_fields.field > field_names.id
Table "address_fields" {
"message" int [not null]
"part" text [not null]
"position" int [not null]
"field" int [not null]
"number" int
"address" int [not null]
}
Ref: address_fields.message > messages.id
Ref: address_fields.part > part_numbers.part
Ref: address_fields.field > field_names.id
Ref: address_fields.address > addresses.id
// app table
Table "front_threads" {
"id" int [pk, not null, increment]
"room" int [not null]
"name" text
"notSeen" int [not null, default: true]
"lastUpdate" timestamp [not null]
"isDm" bool [not null, default: true]
}
Ref: front_threads.room > front_rooms.id
Table "front_rooms" {
"id" int [pk, not null, increment]
"name" text
"isGroup" bool [not null, default: false]
"notSeen" int [not null]
"lastUpdate" timestamp [not null]
}
Table "front_room_messages" {
"room" int [not null]
"thread" int [not null]
"message" int [not null]
}
Ref: front_room_messages.room > front_rooms.id
Ref: front_room_messages.message - messages.id
Ref: front_room_messages.thread > front_threads.id

108
back/db/database.sql Normal file
View File

@ -0,0 +1,108 @@
CREATE TABLE `addresses` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` text,
`localpart` text NOT NULL,
`domain` text NOT NULL,
`email` text NOT NULL
);
CREATE TABLE `mailboxes` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` text NOT NULL,
`uidnext` int NOT NULL DEFAULT 1,
`nextmodseq` bigint NOT NULL DEFAULT 1,
`first_recent` int NOT NULL DEFAULT 1,
`uidvalidity` int NOT NULL DEFAULT 1
);
CREATE TABLE `messages` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`idate` timestamp NOT NULL,
`rfc822size` int
);
CREATE TABLE `mailbox_messages` (
`mailbox` int NOT NULL,
`uid` int NOT NULL,
`message` int NOT NULL,
`modseq` bigint NOT NULL,
`seen` boolean NOT NULL DEFAULT false,
`deleted` boolean NOT NULL DEFAULT false,
PRIMARY KEY (`uid`, `message`)
);
CREATE TABLE `bodyparts` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`bytes` int NOT NULL,
`hash` text NOT NULL,
`text` text,
`data` binary
);
CREATE TABLE `part_numbers` (
`message` int PRIMARY KEY NOT NULL,
`part` text NOT NULL,
`bodypart` int NOT NULL,
`bytes` int,
`nb_lines` int
);
CREATE TABLE `field_names` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` text NOT NULL
);
CREATE TABLE `header_fields` (
`id` int NOT NULL AUTO_INCREMENT,
`message` int NOT NULL,
`part` text NOT NULL,
`position` int NOT NULL,
`field` int NOT NULL,
`value` text,
PRIMARY KEY (`id`, `message`)
);
CREATE TABLE `address_fields` (
`message` int NOT NULL,
`part` text NOT NULL,
`position` int NOT NULL,
`field` int NOT NULL,
`number` int,
`address` int NOT NULL
);
CREATE UNIQUE INDEX `addresses_index_0` ON `addresses` (`email`);
CREATE UNIQUE INDEX `mailboxes_index_1` ON `mailboxes` (`name`);
CREATE UNIQUE INDEX `field_names_index_2` ON `field_names` (`name`);
CREATE UNIQUE INDEX `header_fields_index_3` ON `header_fields` (`message`);
CREATE UNIQUE INDEX `header_fields_index_4` ON `header_fields` (`part`);
CREATE UNIQUE INDEX `header_fields_index_5` ON `header_fields` (`position`);
CREATE UNIQUE INDEX `header_fields_index_6` ON `header_fields` (`field`);
ALTER TABLE `mailbox_messages` ADD FOREIGN KEY (`mailbox`) REFERENCES `mailboxes` (`id`);
ALTER TABLE `messages` ADD FOREIGN KEY (`id`) REFERENCES `mailbox_messages` (`message`);
ALTER TABLE `part_numbers` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
ALTER TABLE `bodyparts` ADD FOREIGN KEY (`id`) REFERENCES `part_numbers` (`bodypart`);
ALTER TABLE `header_fields` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
ALTER TABLE `header_fields` ADD FOREIGN KEY (`part`) REFERENCES `part_numbers` (`part`);
ALTER TABLE `header_fields` ADD FOREIGN KEY (`field`) REFERENCES `field_names` (`id`);
ALTER TABLE `address_fields` ADD FOREIGN KEY (`message`) REFERENCES `messages` (`id`);
ALTER TABLE `address_fields` ADD FOREIGN KEY (`part`) REFERENCES `part_numbers` (`part`);
ALTER TABLE `address_fields` ADD FOREIGN KEY (`field`) REFERENCES `field_names` (`id`);
ALTER TABLE `address_fields` ADD FOREIGN KEY (`address`) REFERENCES `addresses` (`id`);

53
back/db/db.ts Normal file
View File

@ -0,0 +1,53 @@
import mysql from "mysql";
import logger from "../system/Logger";
require("dotenv").config();
// todo remove export
export const db = mysql.createConnection({
host: process.env.HOST_DB,
user: process.env.USER_DB,
password: process.env.PASSWORD_DB,
database: process.env.NAME_DB,
});
db.connect(function (err) {
if (err) {
logger.err(`Unable to connect database ${err.code}`);
} else {
logger.log("Database successfully connected");
}
});
export function execQueryAsync(query: string, values: any[]): Promise<any> {
return new Promise((resolve, reject) => {
db.query(query, values, (err, results, fields) => {
if (err) {
reject(err);
} else {
resolve(results);
}
});
});
}
export function execQueryAsyncWithId(query: string, values: any[]): Promise<number> {
return new Promise((resolve, reject) => {
db.query(query, values, (err, results, fields) => {
if (err) {
reject(err);
} else {
resolve(results.insertId);
}
});
});
}
export function execQuery(query: string, values: any[]) {
db.query(query, values, (err, results, fields) => {
if (err) {
logger.err(err);
throw err;
}
return results;
});
}

41
back/db/imap/imap.ts Normal file
View File

@ -0,0 +1,41 @@
import { execQueryAsyncWithId, execQueryAsync, execQuery } from "../db";
export async function getAllAccounts() {
const query = `
SELECT
app_account.account_id AS id,
address.email AS user,
app_account.account_pwd AS password,
app_account.host AS host,
app_account.port AS port,
app_account.tls AS tls
FROM app_account INNER JOIN address
WHERE address.address_id = app_account.user_id
`;
const values = [];
return await execQueryAsync(query, values);
}
export async function getAllMailboxes(accountId) {
const query = 'SELECT * FROM mailbox WHERE mailbox.account_id = ?';
const values = [accountId];
return await execQueryAsync(query, values)
}
export async function registerMailbox(accountId, mailboxName) {
const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`;
const values = [accountId, mailboxName];
return await execQueryAsyncWithId(query, values);
}
export async function getMailbox(mailboxId) {
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
const values = [mailboxId];
return await execQueryAsync(query, values);
}
export function updateMailbox(mailboxId, uidnext) {
const query = `UPDATE mailbox SET uidnext = ? WHERE mailbox_id = ?`;
const values = [uidnext, mailboxId];
execQuery(query, values);
}

34
back/db/mail.ts Normal file
View File

@ -0,0 +1,34 @@
import { execQueryAsync, execQueryAsyncWithId } from "./db";
export async function getAddresseId(email: string, name?: string): Promise<number> {
const localpart = email.split("@")[0];
const domain = email.split("@")[1];
const query = `INSERT INTO address
(address_name, localpart, domain, email) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE address_name = ?, address_id = LAST_INSERT_ID(address_id)`;
const values = [name, localpart, domain, email, name];
return await execQueryAsyncWithId(query, values);
}
export async function getFieldId(field: string): Promise<number> {
const query = `INSERT INTO field_name (field_name) VALUES (?) ON DUPLICATE KEY UPDATE field_id=LAST_INSERT_ID(field_id)`;
const values = [field];
return await execQueryAsyncWithId(query, values);
}
export async function findRoomByOwner(ownerId: number): Promise<{ room_id: number }[]> {
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
const values = [ownerId];
return await execQueryAsync(query, values);
}
export async function getUserIdOfMailbox(boxId: number): Promise<{ user_id: number }[]> {
const query = `
SELECT app_account.user_id
FROM mailbox
INNER JOIN app_account ON app_account.account_id = mailbox.account_id
WHERE mailbox.mailbox_id = ?
`;
const values = [boxId];
return await execQueryAsync(query, values);
}

66
back/db/saveMessage.ts Normal file
View File

@ -0,0 +1,66 @@
import { transformEmojis } from "../utils/string";
import { execQuery, execQueryAsync, execQueryAsyncWithId } from "./db";
export async function registerMessage(timestamp, rfc822size, messageId) {
const query = `
INSERT INTO message
(idate, messageID, rfc822size) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE message_id = LAST_INSERT_ID(message_id)
`;
const values = [timestamp, messageId, rfc822size];
return await execQueryAsyncWithId(query, values);
}
export function registerMailbox_message(mailboxId, uid, messageId, modseq, seen, deleted) {
const query = `
INSERT IGNORE INTO mailbox_message
(mailbox_id, uid, message_id, modseq, seen, deleted) VALUES (?, ?, ?, ?, ?, ?)
`;
const values = [mailboxId, uid, messageId, modseq, seen, deleted];
execQuery(query, values);
}
export function registerBodypart(messageId, part, bodypartId, bytes, nbLines) {
const query = `
INSERT IGNORE INTO part_number
(message_id, part, bodypart_id, bytes, nb_lines) VALUES (?, ?, ?, ?, ?)
`;
const values = [messageId, part, bodypartId, bytes, nbLines];
execQuery(query, values);
}
export async function saveBodypart(bytes, hash, text, data) {
text = transformEmojis(text);
const query = `INSERT IGNORE INTO bodypart (bytes, hash, text, data) VALUES (?, ?, ?, ?)`;
const values = [bytes, hash, text, data];
return await execQueryAsyncWithId(query, values);
}
export async function saveHeader_fields(messageId, fieldId, bodypartId, part, value) {
value = transformEmojis(value);
const query = `
INSERT IGNORE INTO header_field
(message_id, field_id, bodypart_id, part, value) VALUES (?, ?, ?, ?, ?)
`;
const values = [messageId, fieldId, bodypartId, part, value];
return await execQueryAsync(query, values);
}
export async function saveAddress_fields(messageId, fieldId, addressId, number) {
const query = `
INSERT IGNORE INTO address_field
(message_id , field_id, address_id, number) VALUES (?, ?, ?, ?)
`;
const values = [messageId, fieldId, addressId, number];
return await execQueryAsync(query, values);
}
export function saveSource(messageId, content) {
content = transformEmojis(content);
const query = `
INSERT INTO source (message_id, content) VALUES (?, ?)
ON DUPLICATE KEY UPDATE content = ?
`;
const values = [messageId, content, content];
execQuery(query, values);
}

145
back/db/saveMessageApp.ts Normal file
View File

@ -0,0 +1,145 @@
import { transformEmojis } from "../utils/string";
import { db, execQueryAsync, execQueryAsyncWithId, execQuery } from "./db";
import { queryFromId, queryToId, queryCcId } from "./utils/addressQueries";
export async function getAllMembers(messageId: number) {
const query = `
SELECT GROUP_CONCAT(address.address_id) AS id
FROM address
INNER JOIN address_field ON
address_field.address_id = address.address_id AND
address_field.message_id = ?
GROUP BY address_field.message_id
`;
const values = [messageId];
return await execQueryAsync(query, values);
}
export async function registerMember(roomId: number, memberId: number) {
const query = `INSERT IGNORE INTO app_room_member (room_id, member_id) VALUES (?, ?)`;
const values = [roomId, memberId];
return await execQueryAsync(query, values);
}
export async function createRoom(
roomName: string | null | undefined,
ownerId: number,
messageId: number,
roomType: number,
) {
if (!roomName) roomName = "No room name";
roomName = transformEmojis(roomName);
const query = `INSERT IGNORE INTO app_room (room_name, owner_id, message_id, room_type) VALUES (?, ?, ?, ?)`;
const values = [roomName.substring(0, 255), ownerId, messageId, roomType];
return await execQueryAsyncWithId(query, values);
}
// todo date not good
export async function registerMessageInRoom(
messageId: number,
roomId: number,
isSeen: boolean,
idate: string | undefined | null,
) {
if (!idate) idate = new Date().toString();
const query = `INSERT IGNORE INTO app_room_message (message_id, room_id) VALUES (?, ?)`;
const values = [messageId, roomId];
await execQueryAsync(query, values);
updateLastUpdateRoom(roomId, idate);
// if (!isSeen) {
// incrementNotSeenRoom(roomId);
// }
}
export function updateLastUpdateRoom(roomId: number, idate: string) {
const query = `UPDATE app_room SET lastUpdate = ? WHERE room_id = ?`;
const values = [idate, roomId];
execQuery(query, values);
}
export function incrementNotSeenRoom(roomId: number) {
// todo
}
export async function getRoomInfo(messageID: string): Promise<{ room_id: number; root_id: number }[]> {
const query = `
SELECT
app_room.room_id
app_thread.root_id
FROM app_room
LEFT JOIN app_thread ON app_thread.room_id = app_room.room_id
INNER JOIN app_room_message ON app_room_message.room_id = app_room.room_id
INNER JOIN message ON message.message_id = app_room_message.message_id
WHERE message.messageID = ?
`;
const values = [messageID];
return await execQueryAsync(query, values);
}
export async function registerThread(roomId: number, parentId: number, rootId: number) {
const query = `INSERT IGNORE INTO app_thread (room_id, parent_id, root_id) VALUES (?, ?, ?)`;
const values = [roomId, parentId, rootId];
return await execQueryAsync(query, values);
}
export async function isRoomGroup(roomId: number): Promise<boolean> {
return new Promise((resolve, reject) => {
const query = `SELECT isGroup FROM app_room WHERE room_id = '${roomId}'`;
db.query(query, (err, results, fields) => {
if (err) reject(err);
resolve(results[0].isGroup);
});
});
}
export async function findRoomsFromMessage(messageID: string) {
// todo find message in room not started
const query = `SELECT room_id FROM app_room_message WHERE message_id = ? ORDER BY room_id`;
const values = [messageID];
return await execQueryAsync(query, values);
}
export async function hasSameMembersAsParent(messageId: number, messageID: string) {
const query1 = `
SELECT
GROUP_CONCAT(fromT.address_id) AS fromA,
GROUP_CONCAT(toT.address_id) AS toA,
GROUP_CONCAT(ccT.address_id) AS ccA
FROM message msg
${queryFromId} fromT ON msg.message_id = fromT.message_id
${queryToId} toT ON msg.message_id = toT.message_id
${queryCcId} ccT ON msg.message_id = ccT.message_id
WHERE msg.message_id = ?
`;
const values1 = [messageId];
let addressesMsg1 = await execQueryAsync(query1, values1);
const query2 = `
SELECT
GROUP_CONCAT(fromT.address_id) AS fromA,
GROUP_CONCAT(toT.address_id) AS toA,
GROUP_CONCAT(ccT.address_id) AS ccA
FROM message msg
${queryFromId} fromT ON msg.message_id = fromT.message_id
${queryToId} toT ON msg.message_id = toT.message_id
${queryCcId} ccT ON msg.message_id = ccT.message_id
WHERE msg.messageID = ?
`;
const values2 = [messageID];
let addressesMsg2 = await execQueryAsync(query2, values2);
addressesMsg1 = addressesMsg1[0]?.fromA
?.split(",")
.concat(addressesMsg1[0]?.toA?.split(","))
.concat(addressesMsg1[0]?.ccA?.split(","));
addressesMsg2 = addressesMsg2[0]?.fromA
?.split(",")
.concat(addressesMsg2[0]?.toA?.split(","))
.concat(addressesMsg2[0]?.ccA?.split(","));
return (
addressesMsg1.length == addressesMsg2.length &&
addressesMsg1.reduce((a, b) => a && addressesMsg2.includes(b), true)
);
}

View File

@ -139,3 +139,19 @@ create table app_accounts (
tls int(1) not null default 0,
primary key (id)
);
create table app_rooms (
id int not null auto_increment,
name text not null,
owner int not null,
isGroup BIT(1) not null default 0,
notSeen int not null default 0,
lastUpdate timestamp not null,
primary key (id)
);
create table app_room_messages (
message int [not null]
room int,
primary key()
)

162
back/db/structureV2.sql Normal file
View File

@ -0,0 +1,162 @@
-- Mail storage
-- 1
CREATE TABLE address (
address_id INT AUTO_INCREMENT,
address_name TEXT,
localpart TEXT NOT NULL,
domain TEXT NOT NULL,
email TEXT NOT NULL,
PRIMARY KEY (address_id),
UNIQUE KEY (email)
);
-- 2 app
CREATE TABLE app_account (
account_id INT AUTO_INCREMENT,
user_id INT NOT NULL,
account_pwd BINARY(22),
xoauth VARCHAR(116),
xoauth2 VARCHAR(116),
host VARCHAR(255) NOT NULL DEFAULT 'localhost',
port INT(5) NOT NULL DEFAULT 143,
tls BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (account_id),
FOREIGN KEY (user_id) REFERENCES address(address_id) ON DELETE CASCADE
);
-- 3
CREATE TABLE mailbox (
mailbox_id INT AUTO_INCREMENT,
account_id INT NOT NULL,
mailbox_name TEXT NOT NULL,
uidnext INT NOT NULL DEFAULT 1,
nextmodseq BIGINT NOT NULL DEFAULT 1,
first_recent INT NOT NULL DEFAULT 1,
uidvalidity INT NOT NULL DEFAULT 1,
PRIMARY KEY (mailbox_id),
FOREIGN KEY (account_id) REFERENCES app_account(account_id) ON DELETE CASCADE
);
-- 4
CREATE TABLE message (
message_id INT AUTO_INCREMENT,
messageID TEXT NOT NULL,
idate TIMESTAMP NOT NULL,
rfc822size INT NOT NULL,
PRIMARY KEY (message_id),
UNIQUE KEY (messageID)
);
-- 5
-- if mailbox_message deleted message is not deleted
CREATE TABLE mailbox_message (
mailbox_id INT NOT NULL,
uid INT,
message_id INT,
modseq BIGINT NOT NULL,
seen BOOLEAN NOT NULL DEFAULT false,
deleted BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (uid, message_id),
FOREIGN KEY (mailbox_id) REFERENCES mailbox(mailbox_id) ON DELETE CASCADE,
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE
);
-- 6
CREATE TABLE bodypart (
bodypart_id INT AUTO_INCREMENT,
bytes INT NOT NULL,
hash TEXT NOT NULL,
text TEXT,
data BINARY,
PRIMARY KEY (bodypart_id)
);
-- 7
CREATE TABLE source (
message_id INT NOT NULL,
content TEXT NOT NULL,
PRIMARY KEY (message_id),
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE
);
-- 8
CREATE TABLE field_name (
field_id INT AUTO_INCREMENT,
field_name VARCHAR (255),
PRIMARY KEY (field_id),
UNIQUE KEY (field_name)
);
-- 9
CREATE TABLE header_field (
message_id INT NOT NULL,
field_id INT NOT NULL,
bodypart_id INT,
part VARCHAR(128),
value TEXT,
UNIQUE KEY (message_id, field_id, bodypart_id),
UNIQUE KEY (message_id, field_id, part),
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
FOREIGN KEY (field_id) REFERENCES field_name(field_id) ON DELETE CASCADE,
FOREIGN KEY (bodypart_id) REFERENCES bodypart(bodypart_id)
);
-- 10
CREATE TABLE address_field (
message_id INT NOT NULL,
field_id INT NOT NULL,
address_id INT NOT NULL,
number INT,
UNIQUE KEY (message_id, field_id, address_id),
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
FOREIGN KEY (field_id) REFERENCES field_name(field_id),
FOREIGN KEY (address_id) REFERENCES address(address_id)
);
-- App table
-- 11
CREATE TABLE app_room (
room_id INT AUTO_INCREMENT,
room_name VARCHAR(255) NOT NULL,
owner_id INT NOT NULL,
message_id INT NOT NULL,
room_type INT NOT NULL DEFAULT 0,
notSeen INT NOT NULL DEFAULT 0,
lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
PRIMARY KEY (room_id),
UNIQUE KEY (owner_id, message_id, room_type),
FOREIGN KEY (owner_id) REFERENCES address(address_id),
FOREIGN KEY (message_id) REFERENCES message(message_id)
);
-- 12
CREATE TABLE app_thread (
room_id INT NOT NULL,
parent_id INT,
root_id INT,
PRIMARY KEY (room_id),
UNIQUE KEY (room_id, parent_id, root_id),
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES app_room(room_id) ON DELETE SET NULL,
FOREIGN KEY (root_id) REFERENCES app_room(room_id) ON DELETE SET NULL
);
-- 13
CREATE TABLE app_room_message (
message_id INT NOT NULL,
room_id INT,
UNIQUE KEY (message_id, room_id),
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE SET NULL
);
-- 14
CREATE TABLE app_room_member (
room_id INT NOT NULL,
member_id INT NOT NULL,
UNIQUE KEY (room_id, member_id),
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE,
FOREIGN KEY (member_id) REFERENCES address(address_id)
);

View File

@ -0,0 +1,14 @@
const queryAddress = (type: string): string => `
LEFT JOIN (
SELECT address_field.address_id, address_field.message_id
FROM address_field
INNER JOIN field_name
WHERE
field_name.field_id = address_field.field_id AND
field_name.field_name = '${type}'
)
`;
export const queryFromId = queryAddress("from");
export const queryToId = queryAddress("to");
export const queryCcId = queryAddress("cc");

View File

@ -1,162 +0,0 @@
const Imap = require('imap');
const {simpleParser} = require('mailparser');
const inspect = require('util').inspect;
const saveMessage = require('./storeMessage').saveMessage;
const imap = new Imap({
user: '***REMOVED***',
password: '***REMOVED***',
tlsOptions: {servername: "imap.gmail.com"},
host: 'imap.gmail.com',
port: 993,
tls: true
});
imap.once('ready', function() {
const readOnly = true;
imap.openBox('INBOX', readOnly, (err, box) => {
// console.log(box); // uidvalidty uidnext, messages total and new
if (err) throw err;
const f = imap.seq.fetch('2:2', {
bodies: ['HEADER.FIELDS (FROM)','TEXT'],
struct: true,
envelope: true,
extensions: true
});
f.on('message', function(msg, seqno) {
// console.log('Message #%d', seqno);
var prefix = '(#' + seqno + ') ';
let attributes = undefined;
let body = undefined;
msg.on('body', function(stream, info) {
simpleParser(stream, async (err, parsed) => {
body = parsed;
if (attributes) {
saveMessage(body, attributes);
};
// console.log(parsed.headers)
// const {from, subject, textAsHtml, text} = parsed;
// console.log(parsed.attachments)
// console.log(prefix + parsed.text)
// console.log(parsed.from.value);
// console.log(parsed.subject);
// console.log(parsed.date)
// console.log(parsed.replyTo.value);
// console.log(parsed.messageId);
// console.log(parsed.html);
// console.log(parsed.text);
// console.log(parsed.textAsHtml);
});
});
msg.once('attributes', attrs => {
attributes = attrs;
if (body) {
saveMessage(body, attributes);
};
// console.log(prefix + 'Attributes: %s', inspect(attrs, false, 8));
});
msg.once('end', function() {
console.log(prefix + 'Finished');
});
});
f.once('error', function(err) {
console.log('Fetch error: ' + err);
});
f.once('end', function() {
console.log('Done fetching all messages!');
imap.end();
});
});
});
imap.once('error', function(err) {
console.log(err);
});
imap.once('end', function() {
console.log('Connection ended');
});
imap.connect();
// const getEmails = () => {
// imap.once('ready', () => {
// imap.openBox('INBOX', false, () => {
// imap.search(['UNSEEN'], (err, results) => {
// const f = imap.fetch(results, {bodies: ''});
// f.on('message', msg => {
// msg.on('body', stream => {
// simpleParser(stream, async (err, parsed) => {
// // const {from, subject, textAsHtml, text} = parsed;
// // console.log(parsed.from.value);
// // console.log(parsed.subject);
// // console.log(parsed.date)
// // console.log(parsed.replyTo.value);
// // console.log(parsed.messageId);
// // console.log(parsed.html);
// // console.log(parsed.text);
// // console.log(parsed.textAsHtml);
// // 'x-emsg-mtaselection' => 'prod_5_emailing.carrefour.fr',
// // 'message-id' => '<emsg.6584.7d09.15dd1ec64c1@ukmme02.em.unica.net>',
// // 'feedback-id' => 'emailing.carrefour.fr:10070-6584:emsg-x',
// // 'list' => { unsubscribe: [Object] },
// // 'mime-version' => '1.0',
// // 'content-type' => { value: 'text/html', params: [Object] },
// // 'content-transfer-encoding' => 'quoted-printable'
// });
// });
// msg.once('attributes', attrs => {
// const {uid} = attrs;
// console.log(uid)
// // imap.addFlags(uid, ['\\Seen'], () => {
// // // Mark the email as read after reading it
// // console.log('Marked as read!');
// // });
// });
// });
// f.once('error', ex => {
// return Promise.reject(ex);
// });
// f.once('end', () => {
// console.log('Done fetching all messages!');
// imap.end();
// });
// });
// });
// });
// imap.once('error', err => {
// console.log(err);
// });
// imap.once('end', () => {
// console.log('Connection ended');
// });
// imap.connect();
// };
// getEmails();
function isValidEmail(email) {
// todo
return true;
}
module.exports = {
isValidEmail
}

View File

@ -1,72 +0,0 @@
const { getAddresseId } = require("../sql/mail");
const { DEBUG } = require("../utils/debug");
const { registerMessage, registerMailbox_message, saveHeader_fields, saveAddress_fields } = require('../sql/saveMessage');
const { getMailboxId, getField } = require('../sql/mail');
function saveMessage(message, attributes, mailbox) {
const timestamp = new Date(attributes.envelope.date).getTime();
const rfc822size = 0; // todo
registerMessage(timestamp, rfc822size).then((messageId) => {
getMailboxId(mailbox).then((mailboxId) => {
const seen = attributes.flags.includes('Seen') ? 1 : 0; // todo verify
const deleted = attributes.flags.includes('Deleted') ? 1 : 0; // todo verify
registerMailbox_message(mailboxId, attributes.uid, messageId, attributes.modseq, seen, deleted).then(() => {
attributes.struct.forEach(part => {
// saveBodyparts().then((bodypartId) => {
// const partText = undefined;
// savePart_numbers(messageId, partText, bodypartId, part.size, part.lines)
// });
});
const part = ''; // todo
Object.keys(attributes.envelope).forEach(key => {
const newKey = keyNormalizer(key);
if (isHeader(newKey)) {
getField(newKey).then((fieldId) => {
saveHeader_fields(messageId, part, 2, fieldId, attributes.envelope[key]);
});
} else {
getField(newKey).then((fieldId) => {
if (attributes.envelope[key]) {
attributes.envelope[key].forEach((elt, index) => {
saveAddress_fields(messageId, part, fieldId, index, getAddresseId(`${elt.mailbox}@${elt.host}`, elt.name));
});
}
});
}
});
// todo add date field
});
});
});
}
function isHeader(key) {
switch (key) {
case 'date':
case 'subject':
case 'messageId':
return true;
case 'from':
case 'sender':
case 'replyTo':
case 'to':
case 'cc':
case 'bcc':
case 'inReplyTo':
return false;
default:
DEBUG.log("Unknown header key: "+key);
return true;
}
}
function keyNormalizer(key) {
// todo
return key;
}
module.exports = {
saveMessage
}

View File

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

77
back/mails/imap/Box.ts Normal file
View File

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

View File

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

View File

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

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

@ -0,0 +1,184 @@
import {
createRoom,
registerMessageInRoom,
isRoomGroup,
findRoomsFromMessage,
hasSameMembersAsParent,
registerThread,
registerMember,
getAllMembers,
getRoomInfo,
} from "../db/saveMessageApp";
import { findRoomByOwner, getAddresseId, getUserIdOfMailbox } from "../db/mail";
import { nbMembers } from "./utils/envelopeUtils";
import logger from "../system/Logger";
import { ImapMessageAttributes } from "imap";
import { Attrs, Envelope, User } from "../interfaces/mail/attrs.interface";
/**
* take object address and join mailbox and host to return mailbox@host
*/
function createAddress(elt: User): string {
return `${elt.mailbox}@${elt.host}`;
}
export const roomType = {
ROOM: 0,
CHANNEL: 1,
GROUP: 2,
DM: 3,
THREAD: 4,
};
export default class RegisterMessageInApp {
messageId: number;
attrs: Attrs;
envelope: Envelope;
messageID?: string;
boxId: number;
isSeen: boolean;
ownerId: number;
userId: number;
inReplyTo: string;
constructor(_messageId: number, _attrs: Attrs, _boxId: number) {
this.messageId = _messageId;
this.attrs = _attrs;
if (!this.attrs.envelope) throw new Error("Envelope must exist in attributes");
this.envelope = this.attrs.envelope;
this.messageID = this.envelope?.messageId;
this.boxId = _boxId;
this.isSeen = this.attrs.flags.includes("\\Seen") ? true : false;
this.ownerId = -1;
this.userId = -1;
this.inReplyTo = "";
}
async init() {
if (this.envelope.from) {
this.ownerId = await getAddresseId(createAddress(this.envelope.from[0])); // todo use sender or from ?
} else {
throw new Error("Envelope must have a 'from' field");
}
}
isDm = () => nbMembers(this.envelope) == 2;
async isFromUs() {
if (this.userId == -1) {
await getUserIdOfMailbox(this.boxId).then((res) => {
this.userId = res[0]?.user_id;
});
}
return this.ownerId == this.userId;
}
async registerMembers(roomId: number) {
getAllMembers(this.messageId).then((res) => {
const data = res[0].id.split(",");
data.forEach(async (memberId: number) => {
await registerMember(roomId, memberId);
});
});
}
async initiateRoom(owner: number, roomType: number) {
try {
const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType);
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
this.registerMembers(roomId);
return roomId;
} catch (err) {
logger.err(err);
}
}
async createOrRegisterOnExistence(owner: number, roomType: number) {
await findRoomByOwner(owner).then(async (res) => {
if (res.length == 0) {
// first message with this sender
await this.initiateRoom(owner, roomType);
} else {
// not a reply, add to the list of message if this sender
await registerMessageInRoom(this.messageId, res[0].room_id, this.isSeen, this.envelope.date);
}
});
}
async initiateThread() {
await createRoom(this.envelope.subject, this.ownerId, this.messageId, roomType.THREAD).then(
async (roomId: number) => {
// find parent room infos
await getRoomInfo(this.inReplyTo).then(async (room) => {
// todo room not lenght, reply to transfer ?
let root_id = room[0].root_id;
if (!root_id) root_id = room[0].room_id;
await registerThread(roomId, room[0].room_id, root_id);
});
// impl register previous message ?
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
await this.registerMembers(roomId);
},
);
}
async createOrRegisterOnMembers(roomId: number) {
const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo);
if (hasSameMembers) {
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
} else {
await this.initiateThread();
}
}
async save() {
await this.init();
if (this.envelope.inReplyTo) {
this.inReplyTo = this.envelope.inReplyTo;
this.saveReply();
} else {
if (await this.isFromUs()) {
if (this.isDm()) {
// create or add new message to DM
if (!this.envelope.to) throw new Error("Who send a DM and put the recipient in cc ?");
const userTo = await getAddresseId(createAddress(this.envelope.to[0]));
await this.createOrRegisterOnExistence(userTo, roomType.DM);
} else {
// it is not a reply and not a dm
// so it is a channel, which can be possibly a group
this.initiateRoom(this.ownerId, roomType.ROOM);
}
} else {
await this.createOrRegisterOnExistence(this.ownerId, roomType.ROOM);
}
}
}
async saveReply() {
await findRoomsFromMessage(this.inReplyTo).then(async (rooms) => {
if (rooms.length == 0) {
// no rooms, so is a transfer
// todo test if members of transferred message are included
} else if (rooms.length === 1) {
// only one room so message is only in a room and not in a thread
// as a thread is associated to a room
await isRoomGroup(rooms[0].room_id).then(async (isGroup: boolean) => {
if (isGroup) {
this.createOrRegisterOnMembers(rooms[0].room_id);
} else {
// reply from channel
// todo
// if (sender == owner) { // correction from the original sender
// // leave in the same channel
// }
}
});
} else if (rooms.length > 1) {
// get the lowest thread (order by room_id)
const roomId = rooms[rooms.length - 1].room_id;
this.createOrRegisterOnMembers(roomId);
}
});
}
}

128
back/mails/storeMessage.ts Normal file
View File

@ -0,0 +1,128 @@
import { getAddresseId } from "../db/mail";
import {simpleParser} from "mailparser";
import moment from "moment";
import Imap from "imap";
import {
registerMessage,
registerMailbox_message,
saveHeader_fields,
saveAddress_fields,
registerBodypart,
saveBodypart,
saveSource,
} from "../db/saveMessage";
import { getFieldId } from "../db/mail";
import logger from "../system/Logger";
import { AttrsWithEnvelope } from "../interfaces/mail/attrs.interface";
export function saveMessage(attrs: AttrsWithEnvelope, mailboxId: number, imap: Imap): Promise<number> {
const envelope = attrs.envelope;
const ts = moment(new Date(envelope.date).getTime()).format("YYYY-MM-DD HH:mm:ss");
const rfc822size = attrs.size;
const messageID = envelope.messageId;
return new Promise((resolve, reject) => {
registerMessage(ts, rfc822size, messageID)
.then((messageId) => {
const isSeen = attrs.flags.includes("\\Seen") ? 1 : 0; // todo verify
const deleted = attrs.flags.includes("\\Deleted") ? 1 : 0; // todo verify
registerMailbox_message(mailboxId, attrs.uid, messageId, attrs?.modseq, isSeen, deleted);
const f = imap.fetch(attrs.uid, { bodies: "" });
let buffer = "";
f.on("message", function (msg, seqno) {
msg.on("body", function (stream, info) {
stream.on("data", function (chunk) {
buffer += chunk.toString("utf8");
});
stream.once("end", () => {
// save raw data todo
// saveSource(messageId, buffer);
// parse data
simpleParser(buffer, async (err, parsed) => {
saveFromParsedData(parsed, messageId)
.then(() => {
resolve(messageId);
})
.catch((err) => {
reject(err);
});
});
});
});
});
f.once("error", function (err) {
logger.warn("Fetch error: " + err);
});
f.once("end", function () {
// logger.log("Done fetching data of " + messageID); // todo
});
})
.catch((err) => {
logger.warn("Unable to register message: " + err);
reject(err);
});
});
}
async function saveFromParsedData(parsed, messageId) {
const promises: Promise<any>[] = [];
Object.keys(parsed).forEach((key) => {
if (["from", "to", "cc", "bcc", "replyTo"].includes(key)) {
promises.push(
// save address field
getFieldId(key).then((fieldId) => {
parsed[key].value.forEach((addr, nb) => {
getAddresseId(addr.address, addr.name).then(async (addressId) => {
await saveAddress_fields(messageId, fieldId, addressId, nb);
});
});
}),
);
} else if (["subject", "inReplyTo", "references"].includes(key)) {
// todo : "references" (array)
if (key == "references") return;
promises.push(
getFieldId(key).then(async (fieldId) => {
await saveHeader_fields(messageId, fieldId, undefined, undefined, parsed[key]);
}),
);
} else if (["html", "text", "textAsHtml"].includes(key)) {
const hash = "0";
const size = "0";
let partType = "text/plain";
if (key == "html") {
partType = "text/html";
} else if (key == "textAsHtml") {
partType = "text/TexAsHtml";
}
saveBodypart(size, hash, parsed[key], "").then((bodypartId) => {
getFieldId(key).then((fieldId) => {
saveHeader_fields(messageId, fieldId, bodypartId, partType, undefined);
});
});
} else if (key == "attachments") {
// todo
} else if (["date", "messageId", "headers", "headerLines"].includes(key)) {
// messageId and date are already saved
// other field are not important and can be retrieved in source
return;
} else {
logger.warn("doesn't know key: " + key);
return;
}
});
return Promise.all(promises);
// todo when transfered
}
if (process.env["NODE_DEV"] == "TEST") {
module.exports = {
saveFromParsedData,
};
}

View File

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

8848
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

54
back/routes/mail.ts Normal file
View File

@ -0,0 +1,54 @@
import statusCodes from "../utils/statusCodes";
import express from "express";
const router = express.Router();
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
import schema_account from "../schemas/account_schema.json";
import { addAccount } from "../controllers/addAccount";
import { getAccounts } from "../db/api";
import { rooms } from "../controllers/rooms";
import { messages } from "../controllers/messages";
import { members } from "../controllers/members";
const validate_account = ajv.compile(schema_account);
/**
* Return all mailboxes and folders for an user
*/
router.get("/accounts", (req, res) => {
getAccounts().then((data) => {
res.status(statusCodes.OK).json(data);
});
});
router.get("/:mailboxId/rooms", async (req, res) => {
// todo use offset limit
await rooms(req.params, res);
});
router.get("/:roomId/messages", async (req, res) => {
const { roomId } = req.params;
await messages(req.params, res);
});
router.get("/:roomId/members", async (req, res) => {
const { roomId } = req.params;
await members(req.params, res);
});
/**
* Register a new mailbox inside the app
*/
router.post("/account", async (req, res) => {
const valid = validate_account(req.body);
if (!valid) {
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validate_account.errors });
} else {
await addAccount(req.body, res);
}
});
export default router;

42
back/saveLogic.js Normal file
View File

@ -0,0 +1,42 @@
if (message.isReply()) {
// todo transfer
if (message.replyToThread()) {
if (message.hasSameMember(parent)) {
addToThread();
} else {
createThread();
}
} else {
if (message.isInGroup()) {
if (message.hasSameMember(parent)) {
addToGroup();
} else {
createThread();
}
} else {
if (message.isFromChannelOwner()) {
addToChannel(); // todo not sure (with references)
} else {
createThread();
}
}
}
} else {
if (message.isFromUser()) {
if (message.isDm()) {
if (firstMessage()) {
createDM();
} else {
addToDm();
}
} else {
createRoom();
}
} else {
if (firstMessage()) {
createRoom();
} else {
addToRoom();
}
}
}

View File

@ -0,0 +1,14 @@
{
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"pwd": { "type": "string" },
"xoauth": { "type": "string" },
"xoauth2": { "type": "string" },
"host": { "type": "string", "format": "hostname" },
"port": { "type": "number", "maximum": 65535 },
"tls": { "type": "boolean" }
},
"required": ["email", "host", "port", "tls"],
"additionalProperties": false
}

View File

@ -1,20 +0,0 @@
const mails = require("./api/mails.js");
const express = require('express');
const cors = require('cors')
const app = express();
app.use(express.json());
app.use(
express.urlencoded({
extended: true,
})
);
app.use(cors());
app.listen(process.env.PORT || 5500);
// anecdote
app.get("/api/mails/mailboxes", mails.getMailboxes);

19
back/server.ts Normal file
View File

@ -0,0 +1,19 @@
import express from "express";
import cors from "cors";
const app = express();
import ImapSync from "./mails/imap/ImapSync";
app.use(express.json());
app.use(
express.urlencoded({
extended: true,
}),
);
app.use(cors());
app.listen(process.env.PORT || 5500);
import mailRouter from "./routes/mail";
app.use("/api/mail", mailRouter);
const imapSync = new ImapSync();
imapSync.init();

View File

@ -1,23 +0,0 @@
const mysql = require("mysql");
const MYSQL = require("./config.json").mysql;
const DEBUG = require("../utils/debug.js").DEBUG;
const bdd = mysql.createConnection({
host: MYSQL.host,
user: MYSQL.user,
password: MYSQL.pwd,
database: MYSQL.database,
});
bdd.connect(function (err) {
if (err) {
DEBUG.log("Impossible de se connecter", err.code);
} else {
DEBUG.log("Database successfully connected");
}
});
module.exports = {
bdd: bdd,
};

View File

@ -1,48 +0,0 @@
const bdd = require("./bdd.js").bdd;
const DEBUG = require("../utils/debug").DEBUG;
function registerMessage(timestamp, rfc822size) {
return new Promise((resolve, reject) => {
resolve(0);
//todo
const query = `INSERT INTO messages (idate, rfc822size) VALUES (UNIX_TIMESTAMP('${timestamp}'), '${rfc822size}')`;
bdd.query(query, (err, results, fields) => {
if (err) reject(err);
// resolve(results.insertId);
});
});
}
function registerMailbox_message(mailboxId, uid, messageId, modseq, seen, deleted) {
return new Promise((resolve, reject) => {
const query = `INSERT IGNORE INTO mailbox_messages (mailbox, uid, message, modseq, seen, deleted) VALUES ('${mailboxId}', '${uid}', '${messageId}', '${modseq}', '${seen}', '${deleted}')`;
bdd.query(query, (err, results, fields) => {
if (err) reject(err);
resolve();
// resolve(results.insertId);
});
});
}
function saveBodyparts() {}
function saveHeader_fields(message, part, position, field, value) {
const query = `INSERT IGNORE INTO header_fields (message, part, position, field, value) VALUES ('${message}', '${part}', '${position}', '${field}', '${value}')`;
bdd.query(query, (err, results, fields) => {
if (err) throw err;
});
}
function saveAddress_fields(message, part, position, field, number, address) {
const query = `INSERT IGNORE INTO address_fields (message, part, position, field, number, address) VALUES ('${message}', '${part}', '${position}', '${field}', '${number}', '${address}')`;
bdd.query(query, (err, results, fields) => {
if (err) throw err;
});
}
module.exports = {
registerMessage,
registerMailbox_message,
saveHeader_fields,
saveAddress_fields,
}

52
back/system/Logger.ts Normal file
View File

@ -0,0 +1,52 @@
import color from "colors";
const logType = {
LOG: 0,
DEBUG: 1,
WARN: 2,
ERR: 3,
};
class Logger {
constructor() {}
print(header: string, message: string, type: number) {
const content = `[${this._timestamp}] - [${header}] -- ${message}`;
switch (type) {
case logType.LOG:
console.log(content);
break;
case logType.DEBUG:
console.debug(content);
break;
case logType.WARN:
console.warn(content);
break;
case logType.ERR:
console.error(content);
break;
}
}
log = (...message: any[]) => this.print("LOG", `${message}`, logType.LOG);
err = (...message: any[]) => this.print("ERR", `${message}`, logType.ERR);
warn = (...message: any[]) => this.print("WARN", `${message}`, logType.WARN);
debug = (...message: any[]) => this.print("DEBUG", `${message}`, logType.DEBUG);
success = (...message: any[]) => this.print("SUCCESS".green, `${message}`, logType.LOG);
get = (url: string, state: string, ...message: string[]) =>
this.print("GET".green, `[${state} - ${url}]: ${message}`, logType.LOG);
post = (url: string, state: string, ...message: string[]) =>
this.print("POST".blue, `[${state} - ${url}]: ${message}`, logType.LOG);
put = (url: string, state: string, ...message: string[]) =>
this.print("PUT".yellow, `[${state} - ${url}]: ${message}`, logType.LOG);
del = (url: string, state: string, ...message: string[]) =>
this.print("DEL".red, `[${state} - ${url}]: ${message}`, logType.LOG);
get _timestamp() {
return new Date().toLocaleString("en-GB", { hour12: false });
}
}
const logger = new Logger();
export default logger;

View File

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

View File

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

View File

@ -0,0 +1,19 @@
// beforeAll(async () => {
// // const schema = fs.readFileSync('../../sql/structureV2.sql', 'utf8');
// // await setupDB(mysqlConfig, schema);
// });
// afterAll(async () => {
// global.db.query("DROP database mail_test");
// });
// describe('saveMessageApp', async () => {
// beforeEach(() => {
// });
// it("", () => {
// });
// });

View File

@ -0,0 +1,62 @@
const mysql from ";
export class TestDb {
constructor (options) {
this.options = options;
this.db;
}
async init () {
return new Promise((resolve, reject) => {
this.db = mysql.createConnection({
host: this.options.databaseOptions.host,
user: this.options.databaseOptions.user,
password: this.options.databaseOptions.pwd,
database: this.options.databaseOptions.database,
});
this.db.connect(async function (err) {
if (err) {
reject("Impossible de se connecter", err.code)
} else {
if (this.options.dbSchema) {
const schema = fs.readFileSync(this.options.dbSchema, 'utf8');
await this.execQueryAsync(schema, []);
resolve("Database successfully connected and created !")
} else {
resolve("Database successfully connected");
}
}
});
});
}
cleanTables() {
const query = "SELECT table_name FROM INFORMATION_SCHEMA.tables WHERE table_schema = ?";
const values = [this.options.databaseOptions.database];
this.execQueryAsync(query, values).then((results) => {
this.execQuery("SET FOREIGN_KEY_CHECKS=0");
results.map((table) => {
execQuery("DROP TABLE " + table.table_name);
});
});
}
execQuery(query, values) {
db.query(query, values, (err, results, fields) => {
});
}
async execQueryAsync(query, values) {
return new Promise((resolve, reject) => {
db.query(query, values, (err, results, fields) => {
if (err) {
reject(err);
} else {
resolve(results);
}
});
});
}
}

View File

@ -0,0 +1,204 @@
const names = [
"James",
"Mary",
"Robert",
"Patricia",
"John",
"Jennifer",
"Michael",
"Linda",
"David",
"Elizabeth",
"William",
"Barbara",
"Richard",
"Susan",
"Joseph",
"Jessica",
"Thomas",
"Sarah",
"Charles",
"Karen",
"Christopher",
"Lisa",
"Daniel",
"Nancy",
"Matthew",
"Betty",
"Anthony",
"Margaret",
"Mark",
"Sandra",
"Donald",
"Ashley",
"Steven",
"Kimberly",
"Paul",
"Emily",
"Andrew",
"Donna",
"Joshua",
"Michelle",
"Kenneth",
"Carol",
"Kevin",
"Amanda",
"Brian",
"Dorothy",
"George",
"Melissa",
"Timothy",
"Deborah",
"Ronald",
"Stephanie",
"Edward",
"Rebecca",
"Jason",
"Sharon",
"Jeffrey",
"Laura",
"Ryan",
"Cynthia",
"Jacob",
"Kathleen",
"Gary",
"Amy",
"Nicholas",
"Angela",
"Eric",
"Shirley",
"Jonathan",
"Anna",
"Stephen",
"Brenda",
"Larry",
"Pamela",
"Justin",
"Emma",
"Scott",
"Nicole",
"Brandon",
"Helen",
"Benjamin",
"Samantha",
"Samuel",
"Katherine",
"Gregory",
"Christine",
"Alexander",
"Debra",
"Frank",
"Rachel",
"Patrick",
"Carolyn",
"Raymond",
"Janet",
"Jack",
"Catherine",
"Dennis",
"Maria",
"Jerry",
"Heather",
"Tyler",
"Diane",
"Aaron",
"Ruth",
"Jose",
"Julie",
"Adam",
"Olivia",
"Nathan",
"Joyce",
"Henry",
"Virginia",
"Douglas",
"Victoria",
"Zachary",
"Kelly",
"Peter",
"Lauren",
"Kyle",
"Christina",
"Ethan",
"Joan",
"Walter",
"Evelyn",
"Noah",
"Judith",
"Jeremy",
"Megan",
"Christian",
"Andrea",
"Keith",
"Cheryl",
"Roger",
"Hannah",
"Terry",
"Jacqueline",
"Gerald",
"Martha",
"Harold",
"Gloria",
"Sean",
"Teresa",
"Austin",
"Ann",
"Carl",
"Sara",
"Arthur",
"Madison",
"Lawrence",
"Frances",
"Dylan",
"Kathryn",
"Jesse",
"Janice",
"Jordan",
"Jean",
"Bryan",
"Abigail",
"Billy",
"Alice",
"Joe",
"Julia",
"Bruce",
"Judy",
"Gabriel",
"Sophia",
"Logan",
"Grace",
"Albert",
"Denise",
"Willie",
"Amber",
"Alan",
"Doris",
"Juan",
"Marilyn",
"Wayne",
"Danielle",
"Elijah",
"Beverly",
"Randy",
"Isabella",
"Roy",
"Theresa",
"Vincent",
"Diana",
"Ralph",
"Natalie",
"Eugene",
"Brittany",
"Russell",
"Charlotte",
"Bobby",
"Marie",
"Mason",
"Kayla",
"Philip",
"Alexis",
"Louis",
"Lori",
];
export default names;

View File

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

12
back/tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"outDir": "./build",
"target": "es6",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node", "jest"],
}
}

9
back/utils/array.ts Normal file
View File

@ -0,0 +1,9 @@
export function removeDuplicates(array: []) {
const unique = [];
for (let i = 0; i < array.length; i++) {
if (!unique.includes(array[i])) {
unique.push(array[i]);
}
}
return unique;
}

View File

@ -1,9 +0,0 @@
const DEBUG = (function() {
const timestamp = function(){};
timestamp.toString = () => "[DEBUG " + new Date().toLocaleDateString() + "]";
return { log: console.log.bind(console, "%s", timestamp) };
})();
module.exports = {
DEBUG: DEBUG
}

View File

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

19
back/utils/string.ts Normal file
View File

@ -0,0 +1,19 @@
export function transformEmojis(str :string): string {
if (!str) return str;
// Use a regular expression to match emojis in the string
const regex =
/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F900}-\u{1F9FF}\u{1F1E0}-\u{1F1FF}]/gu;
// Replace each matched emoji with its Unicode code point
const transformedStr = str.replace(regex, (match) => {
return "\\u{" + match.codePointAt(0)?.toString(16).toUpperCase() + "}";
});
return transformedStr;
}
export function decodeEmojis(text: string): string {
const regex = /\\u{([^}]+)}/g;
const decodedText = text.replace(regex, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
return decodedText;
}

34
doc.js Normal file
View File

@ -0,0 +1,34 @@
if (isReply) {
if (inThread) {
if (hasSameMembers) {
// register new message in thread
// possibly convert to room only if parent is channel
} else {
// create sub thread
}
} else if (inRoom) {
if (isGroup) {
if (hasSameMembers) {
// register new message in group
} else {
// create thread
}
} else { // reply from channel
if (messageInRoom == 1) { // was new channel transform to group
// register new message in group
} else if (sender == owner) { // correction from the original sender
// leave in the same channel
} else { // user response to announcement
// create new thread
}
}
} else { // transfer
// todo test if members of transferred message are included
}
} else {
if (inRoomByOwner) {
// register message in room
} else {
// create room
}
}

View File

@ -1,3 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.2756 4.69628C21.8922 4.07969 21.8922 3.08 21.2756 2.46342C20.659 1.84683 19.6593 1.84683 19.0427 2.46342L11.8917 9.61447L4.74063 2.46342C4.12404 1.84683 3.12436 1.84683 2.50777 2.46342C1.89118 3.08 1.89118 4.07969 2.50777 4.69628L9.65882 11.8473L2.20145 19.3047C1.58487 19.9213 1.58487 20.921 2.20145 21.5376C2.81804 22.1541 3.81773 22.1541 4.43431 21.5376L11.8917 14.0802L19.349 21.5375C19.9656 22.1541 20.9653 22.1541 21.5819 21.5375C22.1985 20.921 22.1985 19.9213 21.5819 19.3047L14.1245 11.8473L21.2756 4.69628Z" fill="#737D8C"/>
</svg>
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};

Before

Width:  |  Height:  |  Size: 650 B

After

Width:  |  Height:  |  Size: 138 B

View File

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

7639
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,25 +5,45 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@vueuse/components": "^9.13.0",
"@vueuse/core": "^9.13.0",
"axios": "^1.3.4",
"core-js": "^3.8.3",
"vue": "^3.2.13",
"vue-router": "^4.1.6",
"vuex": "^4.0.2"
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@babel/preset-typescript": "^7.21.4",
"@types/jest": "^27.0.1",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-plugin-unit-jest": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-typescript": "^9.1.0",
"@vue/test-utils": "^2.0.0-0",
"@vue/vue3-jest": "^27.0.0-alpha.1",
"@vueuse/components": "^9.13.0",
"@vueuse/core": "^9.13.0",
"axios": "^1.3.4",
"babel-jest": "^27.0.6",
"core-js": "^3.8.3",
"dompurify": "^3.0.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3",
"jest": "^27.0.5",
"prettier": "^2.4.1",
"ts-jest": "^27.0.4",
"typescript": "~4.5.5"
},
"eslintConfig": {
"root": true,
@ -32,14 +52,27 @@
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
"ecmaVersion": 2020
},
"rules": {
"vue/multi-word-component-names": "off"
},
"overrides": [
{
"files": [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)"
],
"env": {
"jest": true
}
}
]
},
"browserslist": [
"> 1%",

View File

@ -4,24 +4,20 @@ import { RouterView } from "vue-router";
<template>
<div id="app">
<!-- <router-link to="/">Home</router-link> -->
<RouterView/>
<Sidebar />
<RoomView />
<RouterView />
</div>
</template>
<script>
import Sidebar from './views/sidebar/Sidebar'
import RoomView from './views/room/RoomView'
import Sidebar from "./views/sidebar/Sidebar";
export default {
name: 'App',
name: "App",
components: {
Sidebar,
RoomView,
},
}
};
</script>
<style>
@ -30,5 +26,4 @@ export default {
height: 100%;
width: 100%;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<span :class="['badge', type]" :style="{ backgroundColor: color }">
{{ value }}
</span>
</template>
<script>
export default {
props: {
value: {
type: [Number, String],
default: 0,
},
color: {
type: String,
default: "#007bff",
},
type: {
type: String,
default: "badge-primary",
},
},
};
</script>
<style scoped>
.badge {
display: inline-block;
padding: 0.25em 0.4em;
font-size: 75%;
font-weight: 700;
/* line-height: 1; */
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
}
.badge-primary {
color: #fff;
background-color: #007bff;
}
/* add more type classes for other types/colors */
</style>

View File

@ -1,11 +0,0 @@
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
import roomsStore from './store/rooms'
const app = createApp(App);
app.use(router);
app.use(roomsStore);
app.mount('#app');

10
front/src/main.ts Normal file
View File

@ -0,0 +1,10 @@
import { createApp } from "vue";
import router from "./router";
import App from "./App.vue";
import store from "./store/store";
const app = createApp(App);
app.use(router);
app.use(store);
app.mount("#app");

View File

@ -1,26 +0,0 @@
import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home";
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"),
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;

30
front/src/router/index.ts Normal file
View File

@ -0,0 +1,30 @@
import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home.vue";
import RoomView from "../views/room/RoomView.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ "../views/About.vue"),
},
{
path: "/:id",
component: RoomView,
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;

View File

@ -1,7 +0,0 @@
import axios from 'axios'
export default(url='localhost') => {
return axios.create({
baseURL: url,
});
}

View File

@ -0,0 +1,7 @@
import axios from "axios";
export default (url = "/api") => {
return axios.create({
baseURL: url,
});
};

View File

@ -1,7 +0,0 @@
import API from './API'
export default {
getQuote() {
return API().get('/');
}
}

View File

@ -0,0 +1,19 @@
import API from "./API";
export default {
registerAccount(data: object) {
return API().post("/mail/account", data);
},
getAccounts() {
return API().get("/mail/accounts");
},
getRooms(mailboxId: number) {
return API().get(`/mail/${mailboxId}/rooms`);
},
getMessages(roomId: number) {
return API().get(`/mail/${roomId}/messages`);
},
getMembers(roomId: number) {
return API().get(`/mail/${roomId}/members`);
},
};

6
front/src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/* eslint-disable */
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@ -0,0 +1,36 @@
export enum RoomType {
ROOM = 0,
CHANNEL = 1,
GROUP = 2,
DM = 3,
THREAD = 4,
}
export interface Message {
todo: true;
}
export interface Room {
id: number;
roomName: string;
roomType: RoomType;
mailboxId: number;
user: string;
userId: number;
unseen: number;
messages: Message[];
messagesFetched: boolean;
threads: object[];
}
export interface Account {
id: number;
email: string;
fetched: boolean;
}
export interface Address {
id: number;
name: string | null;
email: string;
}

View File

@ -1,52 +0,0 @@
import { createStore } from "vuex";
const roomsStore = createStore({
state() {
return {
rooms: [
{
users: "clemnce",
object:
"Lorem magna minim cillum labore ex eiusmod proident excepteur sint irure ipsum.",
mailbox: 1,
},
{
users: "juliette",
object:
"Lorem magna minim cillum labore ex eiusmod proident excepteur sint irure ipsum.",
mailbox: 1,
},
{
users: "jean",
object:
"Lorem magna minim cillum labore ex eiusmod proident excepteur sint irure ipsum.",
mailbox: 2,
},
{
users: "luc",
object:
"Lorem magna minim cillum labore ex eiusmod proident excepteur sint irure ipsum.",
mailbox: 2,
},
],
mailboxes: [
{ mail: "mail@domain.com", id: 1 },
{ mail: "name@example.com", id: 2 },
],
activeMailbox: -1
};
},
mutations: {
setActiveMailbox(state, payload) {
state.activeMailbox = payload;
}
},
getters: {
folderRooms: (state) => () => {
if (state.activeMailbox == -1) return state.rooms;
return state.rooms.filter(room => room.mailbox == state.activeMailbox);
}
}
});
export default roomsStore;

170
front/src/store/store.ts Normal file
View File

@ -0,0 +1,170 @@
import API from "@/services/imapAPI";
import { decodeEmojis } from "@/utils/string";
import { AxiosError, AxiosResponse } from "axios";
import { createStore, Store } from "vuex";
import { Room, Account, Address, RoomType, Message } from "./models/model";
interface RoomFromBack {
id: number;
roomName: string;
roomType: RoomType;
mailboxId: number;
user: string;
userId: number;
// unseen: number;
// todo thread
}
interface AccountFromBack {
id: number;
email: string;
}
function createRoom(options: RoomFromBack): Room {
console.log(options.roomType);
return {
id: options.id,
roomName: decodeEmojis(options.roomName),
roomType: options.roomType,
mailboxId: options.mailboxId,
userId: options.userId,
user: options.user,
unseen: 0,
messages: [],
messagesFetched: false,
threads: [],
};
}
export interface State {
rooms: Room[];
accounts: Account[];
addresses: Address[];
activeAccount: number;
activeRoom: number;
}
// // define injection key todo
// export const key: InjectionKey<Store<State>> = Symbol()
const store = createStore<State>({
state: {
rooms: [createRoom({ id: 12, userId: 1, user: "user", roomName: "room name", mailboxId: 2, roomType: 1 })],
accounts: [{ id: 0, email: "All", fetched: false }],
addresses: [],
activeAccount: 0,
activeRoom: 0,
},
mutations: {
setactiveAccount(state, payload) {
state.activeAccount = payload;
const account = state.accounts.find((account) => account.id == payload);
store.dispatch("fetchRooms", { accountId: payload, account: account });
},
setActiveRoom(state, payload) {
state.activeRoom = payload;
const room = state.rooms.find((room) => room.id == payload);
store.dispatch("fetchMessages", { roomId: payload, room: room });
},
addAccounts(state, payload) {
payload.forEach((account: AccountFromBack) => {
state.accounts.push({ id: account.id, email: account.email, fetched: false });
});
},
addRooms(state, payload) {
// todo add if not exist
payload.rooms.forEach((room: RoomFromBack) => {
state.rooms.push(createRoom(room));
});
},
addMessages(state, payload) {
// todo add if not exist
const room = state.rooms.find((room) => room.id == payload.roomId);
if (!room) return;
payload.messages.forEach((message: Message) => {
room.messages.push(message);
});
room.messagesFetched = true;
},
addAddress(state, payload) {
// todo add if not exist
payload.addresses.forEach((address: Address) => {
state.addresses.push(address);
});
},
},
getters: {
rooms: (state) => (): Room[] => {
if (state.activeAccount === 0) return state.rooms;
return state.rooms.filter((room) => room.mailboxId == state.activeAccount);
},
room:
(state) =>
(roomId: number): Room | undefined => {
const room = state.rooms.find((room) => room.id == roomId);
return room;
},
address:
(state) =>
(addressId: number): Address | undefined => {
const address = state.addresses.find((address) => address.id == addressId);
return address;
},
messages:
(state) =>
(roomId: number): Message[] => {
const room = state.rooms.find((room) => room.id == roomId);
if (!room) return [];
if (!room.messagesFetched) {
store.dispatch("fetchMessages", { roomId: room.id });
}
return room.messages;
},
},
actions: {
fetchAccounts: async (context) => {
API.getAccounts()
.then((res: AxiosResponse) => {
context.commit("addAccounts", res.data);
})
.catch((err: AxiosError) => {
console.log(err);
});
},
fetchRooms: async (context, data) => {
if (data.account?.fetched == false) {
API.getRooms(data.accountId)
.then((res: AxiosResponse) => {
data.account.fetched = true;
context.commit("addRooms", { rooms: res.data });
})
.catch((err: AxiosError) => {
console.log(err);
});
}
},
fetchMessages: async (context, data) => {
if (!data.room || data.room?.fetched == false) {
store.dispatch("fetchRoomMembers", { roomId: data.roomId });
API.getMessages(data.roomId)
.then((res: AxiosResponse) => {
context.commit("addMessages", { messages: res.data, roomId: data.roomId });
})
.catch((err: AxiosError) => {
console.log(err);
});
}
},
fetchRoomMembers: async (context, data) => {
API.getMembers(data.roomId)
.then((res: AxiosResponse) => {
context.commit("addAddress", { addresses: res.data, roomId: data.roomId });
})
.catch((err: AxiosError) => {
console.log(err);
});
},
},
});
export default store;

9
front/src/utils/array.ts Normal file
View File

@ -0,0 +1,9 @@
export function removeDuplicates(array: []) {
const unique: [] = [];
for (let i = 0; i < array.length; i++) {
if (!unique.includes(array[i])) {
unique.push(array[i]);
}
}
return unique;
}

View File

@ -0,0 +1,8 @@
// todo optimize
export function decodeEmojis(text: string): string {
if (!text) return text;
const regex = /\\u{([^}]+)}/g;
const decodedText = text.replace(regex, (_: string, hex: string) => String.fromCodePoint(parseInt(hex, 16)));
return decodedText;
}

View File

@ -1,15 +1,14 @@
<template>
<img :src="require('../../assets/'+ url)" >
<img :src="require('../../assets/' + url)" />
</template>
<script>
export default {
name: 'BaseAvatar',
name: "BaseAvatar",
props: {
url: String
}
}
url: String,
},
};
</script>
<style scoped>

View File

@ -0,0 +1,226 @@
<script setup>
import { ref, computed, watchEffect } from "vue";
import Modal from "./Modal";
import API from "../../services/imapAPI";
const modal = ref(false);
const email = ref("");
const pwd = ref("");
const xoauth = ref("");
const xoauth2 = ref("");
const host = ref("");
const port = ref(993);
const error = ref("");
const knownHosts = {
"outlook.com": {
host: "outlook.office365.com",
},
"hotmail.com": {
host: "outlook.office365.com",
},
"live.com": {
host: "outlook.office365.com",
},
"zoho.com": {
host: "imap.zoho.eu",
},
"yahoo.com": {
host: "imap.mail.yahoo.com",
},
"icloud.com": {
host: "imap.mail.me.com",
},
};
const refHost = computed({
set: (val) => {
host.value = val;
},
});
const err = computed({
set: (val) => {
error.value = val;
},
});
function validateEmail(email) {
const re =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
function checkError() {
if (!validateEmail(email.value)) {
err.value = "The email is not valid.";
} else if (((pwd.value == xoauth.value) == xoauth2.value) == "") {
err.value = "You need at least one authentification method.";
} else if ([pwd.value, xoauth.value, xoauth2.value].filter((val) => val != "").length > 1) {
err.value = "You need only one authentification method.";
} else if (host.value == "" || port.value == "") {
err.value = "You need to complete the port and the host.";
} else {
err.value = "";
}
}
function addAccountRequest() {
checkError();
const data = {
email: email.value,
pwd: pwd.value,
xoauth: xoauth.value,
xoauth2: xoauth2.value,
host: host.value,
port: port.value,
tls: true,
};
API.registerAccount(data)
.then((res) => {
console.log(res.status);
})
.catch((err) => {
console.log(err.request.status);
});
}
watchEffect(() => {
if (error.value != "") {
checkError();
}
});
function mailChange() {
if (email.value.includes("@")) {
const domain = email.value.split("@")[1];
if (!knownHosts[domain]) {
refHost.value = "imap." + domain;
} else {
refHost.value = knownHosts[domain].host;
}
// todo check if manual
}
}
</script>
<template>
<div>
<button @click="modal = true">Open Modal!</button>
<Modal v-if="modal" title="Add new account" @close-modal="modal = false">
<template v-slot:body>
<div class="field">
<label>Email: </label>
<input @change="mailChange" v-model="email" type="email" required />
</div>
<fieldset>
<legend>Authentification method</legend>
<div class="field">
<label>Plain password:</label>
<input v-model="pwd" type="password" />
</div>
<div class="field">
<label>Xoauth:</label>
<input v-model="xoauth" type="text" />
</div>
<div class="field">
<label>Xoauth2:</label>
<input v-model="xoauth2" type="text" />
</div>
</fieldset>
<div class="config">
<input v-model="host" id="host" type="text" placeholder="imap host" />
<input v-model="port" id="port" type="number" placeholder="port" />
</div>
<!-- tls -->
<div>
<button :disabled="error != ''" @click="addAccountRequest">Add</button>
{{ error }}
</div>
</template>
</Modal>
</div>
</template>
<style>
/* 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 {
margin-bottom: 5px;
}
.field > input {
margin-top: 2px;
width: 95%;
}
fieldset {
margin-top: 5px;
border-radius: 5px;
display: grid;
}
.config {
margin: 10px 0;
}
#host {
margin-right: 8px;
width: calc(95% - 100px);
}
#port {
width: 70px;
}
button {
padding: 5px;
padding: 7px 18px;
background-color: #09a35b;
color: white;
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: #303a46;
border: none;
border-radius: 4px;
color: #fff;
-ms-flex: 1;
flex: 1;
font-family: inherit;
font-size: 1.4rem;
font-weight: 400;
min-width: 0;
padding: 8px 9px;
}
input:focus {
outline: none;
}
</style>

View File

@ -1,222 +0,0 @@
<script setup>
import { ref, computed, watchEffect } from 'vue'
import Modal from './Modal'
import API from '../../services/imapAPI';
const modal = ref(true);
const email = ref("");
const pwd = ref("");
const xoauth = ref("");
const xoauth2 = ref("");
const host = ref("");
const port = ref("993");
const error = ref("");
const knownHosts = {
'outlook.com': {
'host': 'outlook.office365.com',
},
'hotmail.com': {
'host': 'outlook.office365.com',
},
'live.com': {
'host': 'outlook.office365.com',
},
'zoho.com': {
'host': 'imap.zoho.eu',
},
'yahoo.com': {
'host': 'imap.mail.yahoo.com',
},
'icloud.com': {
'host': 'imap.mail.me.com',
},
}
const refHost = computed({
set: (val) => {
host.value = val
}
});
const err = computed({
set: (val) => { error.value = val }
});
function validateEmail(email) {
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
function checkError() {
if (!validateEmail(email.value)) {
err.value = "The email is not valid.";
} else if (pwd.value == xoauth.value == xoauth2.value == "") {
err.value = "You need at least one authentification method.";
} else if ([pwd.value, xoauth.value, xoauth2.value].filter((val) => val != "").length > 1) {
err.value = "You need only one authentification method.";
} else if (host.value == "" || port.value == "") {
err.value = "You need to complete the port and the host.";
} else {
err.value = "";
}
}
function addMailboxRequest(event) {
console.log(event.target.disabled = true)
checkError();
API.getQuote().then((res) => {
console.log(res.status);
}).catch((err) => {
console.log(err.request.status)
});
}
watchEffect(() => {
if (error.value != "") {
checkError();
}
});
function mailChange() {
if (email.value.includes('@')) {
const domain = email.value.split('@')[1];
if (!knownHosts[domain]) {
refHost.value = ("imap."+domain);
} else {
refHost.value = (knownHosts[domain].host);
}
// todo check if manual
}
}
</script>
<template>
<div>
<button
@click="modal = true"
>
Open Modal!
</button>
<Modal
v-if="modal"
title="Add mailbox"
@close-modal="modal = false"
>
<template v-slot:body>
<div class="field">
<label>Email: </label>
<input @change="mailChange" v-model="email" type="email" required>
</div>
<fieldset>
<legend>Authentification method</legend>
<div class="field">
<label>Plain password:</label>
<input v-model="pwd" type="password">
</div>
<div class="field">
<label>Xoauth:</label>
<input v-model="xoauth" type="text">
</div>
<div class="field">
<label>Xoauth2:</label>
<input v-model="xoauth2" type="text">
</div>
</fieldset>
<div class="config">
<input v-model="host" id="host" type="text" placeholder="imap host">
<input v-model="port" id="port" type="number" placeholder="port">
</div>
<!-- tls -->
<div>
<button :disabled="error != ''" @click="addMailboxRequest">Add</button>
{{ error }}
</div>
</template>
</Modal>
</div>
</template>
<style>
/* 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 {
margin-bottom: 5px;
}
.field > input {
margin-top: 2px;
width: 95%;
}
fieldset {
margin-top: 5px;
border-radius: 5px;
display: grid;
}
.config {
margin: 10px 0;
}
#host {
margin-right: 8px;
width: calc(95% - 100px);
}
#port {
width: 70px;
}
button {
padding: 5px;
padding: 7px 18px;
background-color: #09a35b;
color: white;
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: #303a46;
border: none;
border-radius: 4px;
color: #fff;
-ms-flex: 1;
flex: 1;
font-family: inherit;
font-size: 1.4rem;
font-weight: 400;
min-width: 0;
padding: 8px 9px;
}
input:focus {
outline: none;
}
</style>

View File

@ -1,12 +1,13 @@
<script setup>
import { vOnClickOutside } from '@vueuse/components'
import { defineEmits, defineProps } from 'vue'
import { vOnClickOutside } from "@vueuse/components";
import { defineEmits, defineProps } from "vue";
const emit = defineEmits(['close-modal']);
const emit = defineEmits(["close-modal"]);
const props = defineProps({ title: String });
// todo close on escape
function close() {
emit('close-modal');
emit("close-modal");
}
</script>
@ -16,22 +17,17 @@ function close() {
<header class="modal-header">
<h2>{{ props.title }}</h2>
<div class="close-button" @click="close"></div>
</header>
<div class="modal-body">
<slot name="body">
This is the default body!
</slot>
</div>
<slot name="body"> This is the default body! </slot>
</div>
</div>
</template>
</div>
</template>
<style scoped>
.modal-wrapper {
.modal-wrapper {
display: flex;
align-items: center;
position: fixed;
@ -42,30 +38,29 @@ function close() {
top: 0;
z-index: 4000;
background-color: #000;
opacity: .7;
}
background-color: rgba(0, 0, 0, 0.8);
}
.modal {
.modal {
display: flex;
flex-direction: column;
border-radius: 5px;
color: white;
background-color: #1D1D23;
padding: 20px
}
background-color: #1d1d23;
padding: 20px;
}
.modal-header {
.modal-header {
margin-bottom: 10px;
}
}
h2 {
h2 {
display: inline-block;
font-size: 2.4rem;
margin: 0;
}
}
.close-button {
.close-button {
background-color: #9fa9ba;
cursor: pointer;
height: 18px;
@ -75,6 +70,5 @@ function close() {
width: 18px;
float: right;
margin-top: 5px;
}
}
</style>

View File

@ -1,26 +1,54 @@
<script setup lang="ts">
import store from "../../store/store";
import { defineProps, ref, watch } from "vue";
import Badge from "@/components/Badge.vue";
import { RoomType } from "@/store/models/model";
const props = defineProps({ id: Number });
const room = ref(store.getters.room(props.id));
watch(
() => props.id,
(newId) => {
room.value = store.getters.room(newId);
},
);
const roomTitle = () => {
const type = room.value?.roomType;
if (type === RoomType.DM || type == RoomType.CHANNEL || type == RoomType.ROOM) {
return room.value?.user;
}
return room.value?.roomName;
};
</script>
<template>
<div id="main">
header -> sender
-> thread
-> single click open side
-> double click open plain
is thread
<div class="main">
<div class="infos">
<Badge :value="RoomType[room?.roomType]" />
{{ roomTitle() }}
</div>
<div class="action">action: threads message important</div>
</div>
</template>
<script>
export default {
name: 'Header',
components: {
}
}
</script>
<style scoped>
#main {
background-color: blue;
.main {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #505050;
width: 100%;
height: 51px;
}
.infos {
margin-left: 15px;
}
.action {
margin-right: 15px;
}
</style>

View File

@ -0,0 +1,108 @@
<script setup>
import { defineProps, onMounted, ref } from "vue";
import { decodeEmojis } from "../../utils/string";
import { removeDuplicates } from "../../utils/array";
import DOMPurify from "dompurify";
import store from "@/store/store";
const props = defineProps({ data: Object });
const date = new Date(props.data.date);
const iframe = ref(null);
onMounted(() => {
const doc = iframe.value.contentDocument || iframe.value.contentWindow.document;
const html = DOMPurify.sanitize(props.data.content);
doc.open();
// todo dompurify
// background vs color
doc.write(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body style="margin: 0;">
${html}
</body>
</html>
`);
doc.close();
});
const displayAddresses = (addressesId) => {
// todo store members in rooms ?
addressesId = removeDuplicates(addressesId);
let res = "";
addressesId.forEach((addressId) => {
const address = store.getters.address(addressId);
if (address) res += address.email;
});
return res;
};
</script>
<!-- to if to is more than me
cc -->
<!-- object (channel only)
content
attachments -->
<template>
<div class="message">
<div id="context">
<div class="left" id="profile">
{{ displayAddresses(props.data.fromA?.split(",")) }} - {{ props.data.fromA }}
</div>
<div class="middle">{{ decodeEmojis(props.data.subject) }}</div>
<div class="right" id="date">
{{
date.toLocaleString("en-GB", {
weekday: "short",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
timezone: "UTC+1",
})
}}
</div>
</div>
<iframe ref="iframe"></iframe>
</div>
</template>
<style scoped>
.message {
width: auto;
border: white 1px solid;
padding: 10px;
margin: 5px;
}
#context {
display: flex;
flex-direction: row;
justify-content: space-between;
}
iframe {
overflow-y: auto;
max-height: 300px;
width: 100%;
max-width: 750px; /* template width being 600px to 640px up to 750px (experiment and test) */
background-color: rgb(234, 234, 234);
}
.left,
.right {
white-space: nowrap;
}
.middle {
margin: 0 10px;
text-align: center;
}
</style>

View File

@ -1,31 +1,72 @@
<script setup>
import { useStore } from "vuex";
import { useRoute, onBeforeRouteUpdate } from "vue-router";
import { onBeforeMount, ref } from "vue";
import Header from "./Header.vue";
import Message from "./Message.vue";
const store = useStore();
const route = useRoute();
const id = ref(parseInt(route.params?.id));
onBeforeMount(async () => {
console.log(id);
// get data
let room = store.getters.room(id);
if (!room || room?.fetched === false) {
// todo
// await store.dispatch("fetchMessages", );
}
store.commit("setActiveRoom", id);
});
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
id.value = parseInt(to.params.id);
console.log(id);
store.commit("setActiveRoom", id);
}
});
</script>
<template>
<div>
<Header></Header>
<div>
Room
is thread
<div id="main">
<Header :id="id"></Header>
<div id="RoomViewBody">
<div class="content">
<Message v-for="(message, index) in store.getters.messages(id)" :key="index" :data="message" />
</div>
<div id="composer">COMPOSER</div>
</div>
</div>
</template>
<script>
import Header from './Header.vue'
export default {
name: 'RoomView',
components: {
Header
}
}
</script>
<style scoped>
div {
background-color: #1D1D23;
#main {
background-color: #1d1d23;
color: white;
width: 100%;
}
</style>
#RoomViewBody {
display: flex;
flex-direction: column;
height: 100%;
}
#composer {
position: absolute;
bottom: 0;
width: 100%;
padding-top: 10px;
height: 35px;
background-color: red;
}
.content {
display: flex;
flex-direction: column-reverse;
overflow: auto;
margin-bottom: 100px;
}
</style>

View File

@ -1,21 +1,21 @@
<template>
<div>
<Mailboxes />
<Users id="users"/>
<Accounts class="accounts" />
<Rooms class="rooms" />
</div>
</template>
<script>
import Mailboxes from './mailboxes/Mailboxes'
import Users from './users/Users.vue'
import Accounts from "./accounts/Accounts";
import Rooms from "./rooms/Rooms.vue";
export default {
name: 'Sidebar',
name: "Sidebar",
components: {
Mailboxes,
Users,
}
}
Accounts,
Rooms,
},
};
</script>
<style scoped>
@ -24,11 +24,12 @@ div {
height: 100%;
}
#users {
max-width: 300px;
min-width: 250px;
.accounts {
width: 82px;
}
.rooms {
width: 300px;
}
/* todo setup max size */
</style>

View File

@ -0,0 +1,46 @@
<template>
<div class="container" @click="setactiveAccount(data.id)" :class="activeAccount == data.id && 'active'">
{{ data.email }}
</div>
</template>
<script>
import { mapMutations, mapState } from "vuex";
export default {
name: "Account",
components: {},
props: {
data: { mail: String, id: Number },
},
computed: {
...mapState(["activeAccount"]),
},
methods: {
...mapMutations(["setactiveAccount"]),
},
};
</script>
<style scoped>
.container {
width: 65px;
text-align: center;
white-space: normal;
word-wrap: break-word;
padding: 5px;
margin: 3px 0;
cursor: pointer;
border-radius: 8px;
border: 2px solid transparent;
}
.container:hover {
background-color: aqua !important;
}
.active {
border: 2px solid white;
opacity: 0.9;
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<div id="main">
<div id="userMenu">
<!-- deconnect -->
</div>
<span class="divider"></span>
<Account v-for="(account, index) in accounts" :key="index" :data="account" />
<span class="divider"></span>
<AddAccountModal />
</div>
</template>
<script>
import { mapState } from "vuex";
import Account from "./Account";
import AddAccountModal from "../../modals/AddAccountModal";
import store from "@/store/store";
export default {
name: "Accounts",
components: {
Account,
AddAccountModal,
},
computed: {
...mapState(["accounts"]),
},
created() {
store.dispatch("fetchAccounts");
},
};
</script>
<style scoped>
#main {
display: flex;
flex-direction: column;
align-items: center;
padding: 5px;
background-color: #2a2a33;
color: white;
}
#userMenu {
width: 32px;
height: 32px;
background-color: yellow !important;
}
.divider {
border-top: 1px solid #bbb;
margin: 5px 0;
width: 90%;
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<div id="main">jij</div>
</template>
<script>
export default {
name: "All",
components: {},
};
</script>
<style scoped></style>

View File

@ -11,13 +11,10 @@
</template>
<script>
export default {
name: 'Account',
components: {
}
}
name: "Account",
components: {},
};
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -1,18 +0,0 @@
<template>
<div id="main">
jij
</div>
</template>
<script>
export default {
name: 'All',
components: {
}
}
</script>
<style scoped>
</style>

View File

@ -1,48 +0,0 @@
<template>
<div class="container" @click="setActiveMailbox(data.id)" :class="activeMailbox == data.id && 'active'">
{{ data.mail }}
</div>
</template>
<script>
import { mapMutations, mapState } from 'vuex'
export default {
name: 'Mailbox',
components: {
},
props: {
data: {mail: String, id: Number}
},
computed: {
...mapState(['activeMailbox'])
},
methods: {
...mapMutations(['setActiveMailbox'])
}
}
</script>
<style scoped>
.container {
max-width: 32px;
white-space: normal;
word-wrap: break-word;
padding: 5px;
margin: 1px 0;
cursor: pointer;
border-radius: 8px;
border: 2px solid transparent;
}
.container:hover {
background-color: aqua !important;
}
.active {
border: 2px solid white;
opacity: 0.9;
}
</style>

View File

@ -1,54 +0,0 @@
<template>
<div id="main">
<div id="userMenu">
<!-- deconnect -->
</div>
<span class="divider"></span>
<Mailbox :data="{'id': -1, 'mail': 'all'}"/>
<Mailbox v-for="(mailbox, index) in mailboxes" :key="index" :data="mailbox"/>
<span class="divider"></span>
<AddMailboxModal />
</div>
</template>
<script>
import { mapState } from 'vuex'
import Mailbox from './Mailbox'
import AddMailboxModal from '../../modals/AddMailboxModal'
export default {
name: 'Mailboxes',
components: {
Mailbox,
AddMailboxModal
},
computed: {
...mapState(['mailboxes'])
},
}
</script>
<style scoped>
#main {
display: flex;
flex-direction: column;
align-items: center;
padding: 5px;
background-color: #2A2A33;
color: white;
}
#userMenu {
width: 32px;
height: 32px;
background-color: yellow !important;
}
.divider {
border-top: 1px solid #bbb;
margin: 5px 0;
width: 90%;
}
</style>

View File

@ -0,0 +1,92 @@
<script setup>
import { useRouter } from "vue-router";
import { defineProps } from "vue";
import BaseAvatar from "../../avatars/BaseAvatar.vue";
import Badge from "../../../components/Badge.vue";
import ThreadList from "./threads/ThreadList.vue";
import store from "@/store/store";
const props = defineProps({
data: {
id: Number,
roomName: String,
user: String,
userId: Number,
notSeen: Number,
mailboxId: Number,
threads: [Object],
},
});
const router = useRouter();
</script>
<template>
<div>
<div
class="room"
@click="router.push(`/${props.data.id}`)"
v-bind:class="store.state.activeRoom == props.data.id ? 'selected' : ''"
>
<BaseAvatar url="vue.png" />
<div id="content">
<div id="sender">{{ props.data.user }}</div>
<div id="object">{{ props.data.roomName }}</div>
</div>
<Badge class="badge" v-if="props.data.unseen > 0"
><template v-slot:body>{{ props.data.unseen }}</template>
</Badge>
</div>
<ThreadList :threads="props.data.threads" />
</div>
</template>
<style scoped>
.badge {
margin: auto 7px auto 1px;
}
.room {
box-sizing: border-box;
contain: content;
display: flex;
margin: 4px;
padding: 4px;
cursor: pointer;
}
.room:hover,
.selected {
background-color: #41474f;
border-radius: 8px;
}
#content {
display: flex;
flex-grow: 1;
flex-direction: column;
height: 32px;
justify-content: center;
margin-left: 8px;
min-width: 0;
}
#sender {
font-size: 1.4rem;
line-height: 1.8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
color: white;
}
#object {
color: #a9b2bc;
line-height: 1.8rem;
font-size: 1.3rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 100%;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="test">
<Room v-for="(room, index) in rooms()" :key="index" :data="room" />
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Room from "./Room";
export default {
name: "Rooms",
props: {},
components: {
Room,
},
computed: {
...mapGetters(["rooms"]),
},
};
</script>
<style scoped>
.test {
display: flex;
flex-direction: column;
background-color: #24242b;
overflow: auto;
}
</style>

View File

@ -0,0 +1,38 @@
<script setup>
import { defineProps } from "vue";
const props = defineProps({
thread: Object,
});
console.log(props.thread);
</script>
<template>
<div class="room">
{{ props.thread.roomName }}
</div>
</template>
<style scoped>
.room {
box-sizing: border-box;
contain: content;
display: flex;
margin: -4px 4px 1px 4px;
padding: 4px;
cursor: pointer;
color: #a9b2bc;
}
.room:hover,
.selected {
background-color: #41474f;
border-radius: 8px;
}
.room::before {
border-right: 1px solid white;
margin: 0 10px;
content: "";
}
</style>

View File

@ -0,0 +1,13 @@
<script setup>
import Thread from "./Thread.vue";
import { defineProps } from "vue";
const props = defineProps({ threads: [Object] });
</script>
<template>
<div>
<Thread v-for="(thread, index) in props.threads" :key="index" :thread="thread" />
</div>
</template>
<style scoped></style>

View File

@ -1,77 +0,0 @@
<template>
<div>
<div id="user">
<BaseAvatar url="vue.png"/>
<div id="content">
<div id="sender">{{ sender }}</div>
<div id="object">{{ object }}</div>
</div>
</div>
<ThreadList />
</div>
</template>
<script>
import BaseAvatar from '../../avatars/BaseAvatar.vue'
import ThreadList from './threads/ThreadList.vue'
export default {
name: 'User',
props: {
sender: String,
object: String
},
components: {
BaseAvatar,
ThreadList
}
}
</script>
<style scoped>
#user {
box-sizing: border-box;
contain: content;
display: flex;
margin-bottom: 4px;
margin: 4px;
padding: 4px;
cursor: pointer;
}
#user:hover {
background-color: #41474f;;
border-radius: 8px;
}
#content {
display: flex;
flex-grow: 1;
flex-direction: column;
height: 32px;
justify-content: center;
margin-left: 8px;
min-width: 0;
}
#sender {
font-size: 1.4rem;
line-height: 1.8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
color: white;
}
#object {
color: #a9b2bc;
line-height: 1.8rem;
font-size: 1.3rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 100%;
}
</style>

View File

@ -1,32 +0,0 @@
<template>
<div>
<User v-for="(room, index) in folderRooms()" :key="index" :sender="room.users" :object="room.object" />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import User from './User'
export default {
name: 'Users',
props: {
},
components: {
User
},
computed: {
...mapGetters(['folderRooms'])
}
}
</script>
<style scoped>
div {
display: flex;
flex-direction: column;
background-color: #24242B;
}
</style>

View File

@ -1,17 +0,0 @@
<template>
<div>
</div>
</template>
<script>
export default {
name: 'ThreadList',
props: {
},
}
</script>
<style scoped>
</style>

25
front/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"allowJs": true,
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": ["webpack-env", "jest"],
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -1,4 +1,11 @@
const { defineConfig } = require('@vue/cli-service')
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true
transpileDependencies: true,
devServer: {
proxy: {
"/api": {
target: "http://localhost:5500",
}
}
}
})

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,43 +0,0 @@
{
"name": "mail",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"vue": "^3.2.13"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

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