Compare commits
39 Commits
569c271c6c
...
fd253197cc
Author | SHA1 | Date | |
---|---|---|---|
|
fd253197cc | ||
|
6b4264fccc | ||
|
4e48c5d813 | ||
|
4bff5be6c1 | ||
|
e3d8d3cf9b | ||
|
7f535f2e95 | ||
|
948ec3c7b4 | ||
|
3042ed972b | ||
|
11ab6a6a21 | ||
|
90dd16ee0d | ||
|
aced3b8914 | ||
|
68e1dfe7d8 | ||
|
a82ff9b85b | ||
|
8306543ddd | ||
|
6507d466ad | ||
|
44125fc55d | ||
|
14dd6b36f8 | ||
|
91898e25a5 | ||
|
185f051a63 | ||
|
838550b6cc | ||
|
5447557f91 | ||
|
62dd43c3d5 | ||
|
0ea7f5865b | ||
|
4d4ef54bcb | ||
|
926dc60920 | ||
|
d6f06f3ca6 | ||
|
9b3ddd291e | ||
|
d7029854b4 | ||
|
095efb5440 | ||
|
14e64c1fc3 | ||
|
95f39cf53a | ||
|
f9fbab3a21 | ||
|
aa9a69e17f | ||
|
3286a2e52b | ||
|
9046ccf137 | ||
|
29bf4bbdbd | ||
|
df69a7dbd9 | ||
|
427ffba725 | ||
|
d3893c682e |
15
.gitignore
vendored
15
.gitignore
vendored
@ -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
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"quoteProps": "consistent",
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
24
README.md
24
README.md
@ -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/).
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
@ -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,
|
||||
}
|
18
back/controllers/addAccount.ts
Normal file
18
back/controllers/addAccount.ts
Normal 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
|
13
back/controllers/members.ts
Normal file
13
back/controllers/members.ts
Normal 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);
|
||||
});
|
||||
}
|
14
back/controllers/messages.ts
Normal file
14
back/controllers/messages.ts
Normal 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
13
back/controllers/rooms.ts
Normal 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
113
back/db/api.ts
Normal 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
140
back/db/database.dart
Normal 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
108
back/db/database.sql
Normal 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
53
back/db/db.ts
Normal 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
41
back/db/imap/imap.ts
Normal 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
34
back/db/mail.ts
Normal 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
66
back/db/saveMessage.ts
Normal 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
145
back/db/saveMessageApp.ts
Normal 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)
|
||||
);
|
||||
}
|
@ -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
162
back/db/structureV2.sql
Normal 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)
|
||||
);
|
14
back/db/utils/addressQueries.ts
Normal file
14
back/db/utils/addressQueries.ts
Normal 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");
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
47
back/interfaces/mail/attrs.interface.ts
Normal file
47
back/interfaces/mail/attrs.interface.ts
Normal file
@ -0,0 +1,47 @@
|
||||
export interface User {
|
||||
name: string;
|
||||
mailbox: string;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export interface Envelope {
|
||||
date?: string | null;
|
||||
subject?: string | null;
|
||||
from?: User[] | null;
|
||||
sender?: User[] | null;
|
||||
replyTo?: User[] | null;
|
||||
to?: User[] | null;
|
||||
cc?: User[] | null;
|
||||
bcc?: User[] | null;
|
||||
inReplyTo?: string | null;
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
export interface Attrs {
|
||||
/** A 32-bit ID that uniquely identifies this message within its mailbox. */
|
||||
uid: number;
|
||||
/** A list of flags currently set on this message. */
|
||||
flags: string[];
|
||||
/** The internal server date for the message. */
|
||||
date: string;
|
||||
/** The message's body structure (only set if requested with fetch()). */
|
||||
struct?: any[] | undefined;
|
||||
envelope?: Envelope;
|
||||
/** The RFC822 message size (only set if requested with fetch()). */
|
||||
size?: number | undefined;
|
||||
}
|
||||
|
||||
export interface AttrsWithEnvelope {
|
||||
/** A 32-bit ID that uniquely identifies this message within its mailbox. */
|
||||
uid: number;
|
||||
/** A list of flags currently set on this message. */
|
||||
flags: string[];
|
||||
/** The internal server date for the message. */
|
||||
date: string;
|
||||
/** The message's body structure (only set if requested with fetch()). */
|
||||
struct?: any[] | undefined;
|
||||
envelope: Envelope;
|
||||
/** The RFC822 message size (only set if requested with fetch()). */
|
||||
size?: number | undefined;
|
||||
modseq?: number;
|
||||
}
|
77
back/mails/imap/Box.ts
Normal file
77
back/mails/imap/Box.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
77
back/mails/imap/ImapInstance.ts
Normal file
77
back/mails/imap/ImapInstance.ts
Normal 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;
|
||||
}
|
||||
}
|
33
back/mails/imap/ImapSync.ts
Normal file
33
back/mails/imap/ImapSync.ts
Normal 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
184
back/mails/saveMessage.ts
Normal 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
128
back/mails/storeMessage.ts
Normal 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,
|
||||
};
|
||||
}
|
17
back/mails/utils/envelopeUtils.ts
Normal file
17
back/mails/utils/envelopeUtils.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Envelope, User } from "../../interfaces/mail/attrs.interface";
|
||||
|
||||
export function nbMembers(envelope: Envelope) {
|
||||
return getMembers(envelope).length;
|
||||
}
|
||||
|
||||
export function getMembers(envelope: Envelope) {
|
||||
const members: User[] = [];
|
||||
const fields = ["from", "to", "sender", "replyTo", "cc", "bcc"] as const;
|
||||
fields.forEach((field) => {
|
||||
envelope[field]?.forEach((member: User) => {
|
||||
if (members.find((m) => m.mailbox === member.mailbox && m.host === member.host)) return;
|
||||
members.push(member);
|
||||
});
|
||||
});
|
||||
return members;
|
||||
}
|
8848
back/package-lock.json
generated
8848
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
54
back/routes/mail.ts
Normal 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
42
back/saveLogic.js
Normal file
@ -0,0 +1,42 @@
|
||||
if (message.isReply()) {
|
||||
// todo transfer
|
||||
if (message.replyToThread()) {
|
||||
if (message.hasSameMember(parent)) {
|
||||
addToThread();
|
||||
} else {
|
||||
createThread();
|
||||
}
|
||||
} else {
|
||||
if (message.isInGroup()) {
|
||||
if (message.hasSameMember(parent)) {
|
||||
addToGroup();
|
||||
} else {
|
||||
createThread();
|
||||
}
|
||||
} else {
|
||||
if (message.isFromChannelOwner()) {
|
||||
addToChannel(); // todo not sure (with references)
|
||||
} else {
|
||||
createThread();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (message.isFromUser()) {
|
||||
if (message.isDm()) {
|
||||
if (firstMessage()) {
|
||||
createDM();
|
||||
} else {
|
||||
addToDm();
|
||||
}
|
||||
} else {
|
||||
createRoom();
|
||||
}
|
||||
} else {
|
||||
if (firstMessage()) {
|
||||
createRoom();
|
||||
} else {
|
||||
addToRoom();
|
||||
}
|
||||
}
|
||||
}
|
14
back/schemas/account_schema.json
Normal file
14
back/schemas/account_schema.json
Normal 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
|
||||
}
|
@ -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
19
back/server.ts
Normal 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();
|
@ -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,
|
||||
};
|
@ -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
52
back/system/Logger.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import color from "colors";
|
||||
|
||||
const logType = {
|
||||
LOG: 0,
|
||||
DEBUG: 1,
|
||||
WARN: 2,
|
||||
ERR: 3,
|
||||
};
|
||||
|
||||
class Logger {
|
||||
constructor() {}
|
||||
|
||||
print(header: string, message: string, type: number) {
|
||||
const content = `[${this._timestamp}] - [${header}] -- ${message}`;
|
||||
switch (type) {
|
||||
case logType.LOG:
|
||||
console.log(content);
|
||||
break;
|
||||
case logType.DEBUG:
|
||||
console.debug(content);
|
||||
break;
|
||||
case logType.WARN:
|
||||
console.warn(content);
|
||||
break;
|
||||
case logType.ERR:
|
||||
console.error(content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log = (...message: any[]) => this.print("LOG", `${message}`, logType.LOG);
|
||||
err = (...message: any[]) => this.print("ERR", `${message}`, logType.ERR);
|
||||
warn = (...message: any[]) => this.print("WARN", `${message}`, logType.WARN);
|
||||
debug = (...message: any[]) => this.print("DEBUG", `${message}`, logType.DEBUG);
|
||||
success = (...message: any[]) => this.print("SUCCESS".green, `${message}`, logType.LOG);
|
||||
|
||||
get = (url: string, state: string, ...message: string[]) =>
|
||||
this.print("GET".green, `[${state} - ${url}]: ${message}`, logType.LOG);
|
||||
post = (url: string, state: string, ...message: string[]) =>
|
||||
this.print("POST".blue, `[${state} - ${url}]: ${message}`, logType.LOG);
|
||||
put = (url: string, state: string, ...message: string[]) =>
|
||||
this.print("PUT".yellow, `[${state} - ${url}]: ${message}`, logType.LOG);
|
||||
del = (url: string, state: string, ...message: string[]) =>
|
||||
this.print("DEL".red, `[${state} - ${url}]: ${message}`, logType.LOG);
|
||||
|
||||
get _timestamp() {
|
||||
return new Date().toLocaleString("en-GB", { hour12: false });
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger();
|
||||
export default logger;
|
112
back/test/mail/saveMessage-test.ts
Normal file
112
back/test/mail/saveMessage-test.ts
Normal 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("", () => {});
|
||||
});
|
||||
});
|
35
back/test/mail/utils/envelopeUtils-test.ts
Normal file
35
back/test/mail/utils/envelopeUtils-test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { nbMembers } from "../../../mails/utils/envelopeUtils";
|
||||
import { generateAttrs, generateUsers } from "../../test-utils/test-attrsUtils";
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
describe("envelopeUtils", () => {
|
||||
const names = generateUsers(6);
|
||||
describe("nbMembers", () => {
|
||||
it("sender and from shouldn't be counted twice if there are the same", () => {
|
||||
const envelope = generateAttrs({
|
||||
from: [names[0].user],
|
||||
sender: [names[0].user],
|
||||
}).envelope;
|
||||
expect(nbMembers(envelope)).toBe(1);
|
||||
});
|
||||
it("sender and from shoud be counted twice if there are the same", () => {
|
||||
const envelope = generateAttrs({
|
||||
from: [names[0].user],
|
||||
sender: [names[1].user],
|
||||
}).envelope;
|
||||
expect(nbMembers(envelope)).toBe(2);
|
||||
});
|
||||
it("should count every members", () => {
|
||||
// todo should merge identic members
|
||||
const envelope = generateAttrs({
|
||||
from: [names[0].user],
|
||||
sender: [names[1].user],
|
||||
replyTo: [names[2].user],
|
||||
to: [names[3].user],
|
||||
cc: [names[4].user],
|
||||
bcc: [names[5].user],
|
||||
}).envelope;
|
||||
expect(nbMembers(envelope)).toBe(6);
|
||||
});
|
||||
});
|
||||
});
|
19
back/test/sql/saveMessageApp-tes.js
Normal file
19
back/test/sql/saveMessageApp-tes.js
Normal file
@ -0,0 +1,19 @@
|
||||
// beforeAll(async () => {
|
||||
// // const schema = fs.readFileSync('../../sql/structureV2.sql', 'utf8');
|
||||
// // await setupDB(mysqlConfig, schema);
|
||||
// });
|
||||
|
||||
// afterAll(async () => {
|
||||
// global.db.query("DROP database mail_test");
|
||||
// });
|
||||
|
||||
// describe('saveMessageApp', async () => {
|
||||
|
||||
// beforeEach(() => {
|
||||
|
||||
// });
|
||||
|
||||
// it("", () => {
|
||||
|
||||
// });
|
||||
// });
|
62
back/test/sql/test-utilsDb.js
Normal file
62
back/test/sql/test-utilsDb.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
204
back/test/test-utils/names.ts
Normal file
204
back/test/test-utils/names.ts
Normal file
@ -0,0 +1,204 @@
|
||||
const names = [
|
||||
"James",
|
||||
"Mary",
|
||||
"Robert",
|
||||
"Patricia",
|
||||
"John",
|
||||
"Jennifer",
|
||||
"Michael",
|
||||
"Linda",
|
||||
"David",
|
||||
"Elizabeth",
|
||||
"William",
|
||||
"Barbara",
|
||||
"Richard",
|
||||
"Susan",
|
||||
"Joseph",
|
||||
"Jessica",
|
||||
"Thomas",
|
||||
"Sarah",
|
||||
"Charles",
|
||||
"Karen",
|
||||
"Christopher",
|
||||
"Lisa",
|
||||
"Daniel",
|
||||
"Nancy",
|
||||
"Matthew",
|
||||
"Betty",
|
||||
"Anthony",
|
||||
"Margaret",
|
||||
"Mark",
|
||||
"Sandra",
|
||||
"Donald",
|
||||
"Ashley",
|
||||
"Steven",
|
||||
"Kimberly",
|
||||
"Paul",
|
||||
"Emily",
|
||||
"Andrew",
|
||||
"Donna",
|
||||
"Joshua",
|
||||
"Michelle",
|
||||
"Kenneth",
|
||||
"Carol",
|
||||
"Kevin",
|
||||
"Amanda",
|
||||
"Brian",
|
||||
"Dorothy",
|
||||
"George",
|
||||
"Melissa",
|
||||
"Timothy",
|
||||
"Deborah",
|
||||
"Ronald",
|
||||
"Stephanie",
|
||||
"Edward",
|
||||
"Rebecca",
|
||||
"Jason",
|
||||
"Sharon",
|
||||
"Jeffrey",
|
||||
"Laura",
|
||||
"Ryan",
|
||||
"Cynthia",
|
||||
"Jacob",
|
||||
"Kathleen",
|
||||
"Gary",
|
||||
"Amy",
|
||||
"Nicholas",
|
||||
"Angela",
|
||||
"Eric",
|
||||
"Shirley",
|
||||
"Jonathan",
|
||||
"Anna",
|
||||
"Stephen",
|
||||
"Brenda",
|
||||
"Larry",
|
||||
"Pamela",
|
||||
"Justin",
|
||||
"Emma",
|
||||
"Scott",
|
||||
"Nicole",
|
||||
"Brandon",
|
||||
"Helen",
|
||||
"Benjamin",
|
||||
"Samantha",
|
||||
"Samuel",
|
||||
"Katherine",
|
||||
"Gregory",
|
||||
"Christine",
|
||||
"Alexander",
|
||||
"Debra",
|
||||
"Frank",
|
||||
"Rachel",
|
||||
"Patrick",
|
||||
"Carolyn",
|
||||
"Raymond",
|
||||
"Janet",
|
||||
"Jack",
|
||||
"Catherine",
|
||||
"Dennis",
|
||||
"Maria",
|
||||
"Jerry",
|
||||
"Heather",
|
||||
"Tyler",
|
||||
"Diane",
|
||||
"Aaron",
|
||||
"Ruth",
|
||||
"Jose",
|
||||
"Julie",
|
||||
"Adam",
|
||||
"Olivia",
|
||||
"Nathan",
|
||||
"Joyce",
|
||||
"Henry",
|
||||
"Virginia",
|
||||
"Douglas",
|
||||
"Victoria",
|
||||
"Zachary",
|
||||
"Kelly",
|
||||
"Peter",
|
||||
"Lauren",
|
||||
"Kyle",
|
||||
"Christina",
|
||||
"Ethan",
|
||||
"Joan",
|
||||
"Walter",
|
||||
"Evelyn",
|
||||
"Noah",
|
||||
"Judith",
|
||||
"Jeremy",
|
||||
"Megan",
|
||||
"Christian",
|
||||
"Andrea",
|
||||
"Keith",
|
||||
"Cheryl",
|
||||
"Roger",
|
||||
"Hannah",
|
||||
"Terry",
|
||||
"Jacqueline",
|
||||
"Gerald",
|
||||
"Martha",
|
||||
"Harold",
|
||||
"Gloria",
|
||||
"Sean",
|
||||
"Teresa",
|
||||
"Austin",
|
||||
"Ann",
|
||||
"Carl",
|
||||
"Sara",
|
||||
"Arthur",
|
||||
"Madison",
|
||||
"Lawrence",
|
||||
"Frances",
|
||||
"Dylan",
|
||||
"Kathryn",
|
||||
"Jesse",
|
||||
"Janice",
|
||||
"Jordan",
|
||||
"Jean",
|
||||
"Bryan",
|
||||
"Abigail",
|
||||
"Billy",
|
||||
"Alice",
|
||||
"Joe",
|
||||
"Julia",
|
||||
"Bruce",
|
||||
"Judy",
|
||||
"Gabriel",
|
||||
"Sophia",
|
||||
"Logan",
|
||||
"Grace",
|
||||
"Albert",
|
||||
"Denise",
|
||||
"Willie",
|
||||
"Amber",
|
||||
"Alan",
|
||||
"Doris",
|
||||
"Juan",
|
||||
"Marilyn",
|
||||
"Wayne",
|
||||
"Danielle",
|
||||
"Elijah",
|
||||
"Beverly",
|
||||
"Randy",
|
||||
"Isabella",
|
||||
"Roy",
|
||||
"Theresa",
|
||||
"Vincent",
|
||||
"Diana",
|
||||
"Ralph",
|
||||
"Natalie",
|
||||
"Eugene",
|
||||
"Brittany",
|
||||
"Russell",
|
||||
"Charlotte",
|
||||
"Bobby",
|
||||
"Marie",
|
||||
"Mason",
|
||||
"Kayla",
|
||||
"Philip",
|
||||
"Alexis",
|
||||
"Louis",
|
||||
"Lori",
|
||||
];
|
||||
|
||||
export default names;
|
70
back/test/test-utils/test-attrsUtils.ts
Normal file
70
back/test/test-utils/test-attrsUtils.ts
Normal 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
12
back/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./build",
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "jest"],
|
||||
}
|
||||
}
|
9
back/utils/array.ts
Normal file
9
back/utils/array.ts
Normal 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;
|
||||
}
|
@ -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
|
||||
}
|
@ -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
19
back/utils/string.ts
Normal 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
34
doc.js
Normal 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
|
||||
}
|
||||
}
|
@ -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 |
@ -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
7639
front/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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%",
|
||||
|
@ -3,32 +3,27 @@ import { RouterView } from "vue-router";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- <router-link to="/">Home</router-link> -->
|
||||
<RouterView/>
|
||||
<Sidebar />
|
||||
<RoomView />
|
||||
</div>
|
||||
<div id="app">
|
||||
<Sidebar />
|
||||
<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',
|
||||
components: {
|
||||
Sidebar,
|
||||
RoomView,
|
||||
},
|
||||
}
|
||||
name: "App",
|
||||
components: {
|
||||
Sidebar,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
45
front/src/components/Badge.vue
Normal file
45
front/src/components/Badge.vue
Normal 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>
|
@ -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
10
front/src/main.ts
Normal 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");
|
@ -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
30
front/src/router/index.ts
Normal 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;
|
@ -1,7 +0,0 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export default(url='localhost') => {
|
||||
return axios.create({
|
||||
baseURL: url,
|
||||
});
|
||||
}
|
7
front/src/services/API.ts
Normal file
7
front/src/services/API.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import axios from "axios";
|
||||
|
||||
export default (url = "/api") => {
|
||||
return axios.create({
|
||||
baseURL: url,
|
||||
});
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
import API from './API'
|
||||
|
||||
export default {
|
||||
getQuote() {
|
||||
return API().get('/');
|
||||
}
|
||||
}
|
19
front/src/services/imapAPI.ts
Normal file
19
front/src/services/imapAPI.ts
Normal 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
6
front/src/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/* eslint-disable */
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
36
front/src/store/models/model.ts
Normal file
36
front/src/store/models/model.ts
Normal 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;
|
||||
}
|
@ -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
170
front/src/store/store.ts
Normal 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
9
front/src/utils/array.ts
Normal 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;
|
||||
}
|
8
front/src/utils/string.ts
Normal file
8
front/src/utils/string.ts
Normal 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;
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
<template>
|
||||
<h1>About</h1>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -1,3 +1,3 @@
|
||||
<template>
|
||||
<h1>home</h1>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -1,15 +1,14 @@
|
||||
<template>
|
||||
<img :src="require('../../assets/'+ url)" >
|
||||
<img :src="require('../../assets/' + url)" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'BaseAvatar',
|
||||
props: {
|
||||
url: String
|
||||
}
|
||||
}
|
||||
name: "BaseAvatar",
|
||||
props: {
|
||||
url: String,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -17,4 +16,4 @@ img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
226
front/src/views/modals/AddAccountModal.vue
Normal file
226
front/src/views/modals/AddAccountModal.vue
Normal 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>
|
@ -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>
|
@ -1,37 +1,33 @@
|
||||
<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>
|
||||
|
||||
<template>
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal" v-on-click-outside="close">
|
||||
<header class="modal-header">
|
||||
<h2>{{ props.title }}</h2>
|
||||
<div class="close-button" @click="close"></div>
|
||||
<div class="modal" v-on-click-outside="close">
|
||||
<header class="modal-header">
|
||||
<h2>{{ props.title }}</h2>
|
||||
<div class="close-button" @click="close"></div>
|
||||
</header>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="modal-body">
|
||||
<slot name="body">
|
||||
This is the default body!
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot name="body"> This is the default body! </slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</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>
|
||||
}
|
||||
</style>
|
||||
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
.infos {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
|
108
front/src/views/room/Message.vue
Normal file
108
front/src/views/room/Message.vue
Normal 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>
|
@ -1,31 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<Header></Header>
|
||||
<div>
|
||||
|
||||
Room
|
||||
is thread
|
||||
</div>
|
||||
<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 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>
|
||||
|
@ -1,34 +1,35 @@
|
||||
<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',
|
||||
components: {
|
||||
Mailboxes,
|
||||
Users,
|
||||
}
|
||||
}
|
||||
name: "Sidebar",
|
||||
components: {
|
||||
Accounts,
|
||||
Rooms,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#users {
|
||||
max-width: 300px;
|
||||
min-width: 250px;
|
||||
.accounts {
|
||||
width: 82px;
|
||||
}
|
||||
.rooms {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
/* todo setup max size */
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
46
front/src/views/sidebar/accounts/Account.vue
Normal file
46
front/src/views/sidebar/accounts/Account.vue
Normal 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>
|
55
front/src/views/sidebar/accounts/Accounts.vue
Normal file
55
front/src/views/sidebar/accounts/Accounts.vue
Normal 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>
|
12
front/src/views/sidebar/accounts/All.vue
Normal file
12
front/src/views/sidebar/accounts/All.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div id="main">jij</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "All",
|
||||
components: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="main">
|
||||
<div id="userMenu">
|
||||
<!-- deconnect -->
|
||||
<!-- deconnect -->
|
||||
</div>
|
||||
<span class="divider"></span>
|
||||
<!-- <h5>Accounts: </h5> -->
|
||||
@ -11,13 +11,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Account',
|
||||
components: {
|
||||
}
|
||||
}
|
||||
name: "Account",
|
||||
components: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<div id="main">
|
||||
jij
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'All',
|
||||
components: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -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>
|
@ -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>
|
92
front/src/views/sidebar/rooms/Room.vue
Normal file
92
front/src/views/sidebar/rooms/Room.vue
Normal 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>
|
30
front/src/views/sidebar/rooms/Rooms.vue
Normal file
30
front/src/views/sidebar/rooms/Rooms.vue
Normal 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>
|
38
front/src/views/sidebar/rooms/threads/Thread.vue
Normal file
38
front/src/views/sidebar/rooms/threads/Thread.vue
Normal 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>
|
13
front/src/views/sidebar/rooms/threads/ThreadList.vue
Normal file
13
front/src/views/sidebar/rooms/threads/ThreadList.vue
Normal 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>
|
@ -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>
|
@ -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>
|
@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'ThreadList',
|
||||
props: {
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
25
front/tsconfig.json
Normal file
25
front/tsconfig.json
Normal 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"]
|
||||
}
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
11066
front/yarn.lock
11066
front/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
43
package.json
43
package.json
@ -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"
|
||||
]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user