Compare commits
27 Commits
master
...
e2bd0bafea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2bd0bafea | ||
|
|
1a63b3154a | ||
|
|
053213eecb | ||
|
|
34c6a43fdc | ||
|
|
d39d66195a | ||
|
|
e0482eb511 | ||
|
|
94c7a7176b | ||
|
|
bffcdafe7a | ||
|
|
a9d15027aa | ||
|
|
d0d666f4cb | ||
|
|
b156c5954d | ||
|
|
097dd8bf21 | ||
|
|
cb5021750a | ||
|
|
7008e24941 | ||
|
|
0f87bdc715 | ||
|
|
47b8c54122 | ||
|
|
ace2063309 | ||
|
|
6b96815b93 | ||
|
|
520eb95d37 | ||
|
|
3e029a26d4 | ||
|
|
28b2b69dc8 | ||
|
|
c81042a223 | ||
|
|
749127ac19 | ||
|
|
61d7eb386c | ||
|
|
02e3af693a | ||
|
|
ac8211defd | ||
|
|
5f2cbd82b6 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -21,3 +21,17 @@ pnpm-debug.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.tmp
|
||||
|
||||
.s.*
|
||||
log*
|
||||
config.json
|
||||
.direnv
|
||||
.envrc
|
||||
*.txt
|
||||
*.json
|
||||
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,
|
||||
}
|
||||
22
back/controllers/addAccount.js
Normal file
22
back/controllers/addAccount.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const statusCode = require("../utils/statusCodes").statusCodes;
|
||||
const { registerAccount } = require("../db/api");
|
||||
const { getAddresseId } = require("../db/mail");
|
||||
|
||||
async function addAccount(body, res) {
|
||||
const { email, pwd, xoauth, xoauth2, host, port, tls } = body;
|
||||
getAddresseId(email).then((addressId) => {
|
||||
registerAccount(addressId, pwd, xoauth, xoauth2, host, port, tls)
|
||||
.then((mailboxId) => {
|
||||
res.status(statusCode.OK).json({ id: mailboxId });
|
||||
})
|
||||
.catch(() => {
|
||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addAccount,
|
||||
};
|
||||
|
||||
// todo change mailbox to account
|
||||
17
back/controllers/members.js
Normal file
17
back/controllers/members.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const statusCode = require("../utils/statusCodes").statusCodes;
|
||||
const { getMembers } = require("../db/api.js");
|
||||
const { logger } = require("../system/Logger");
|
||||
|
||||
async function members(body, res) {
|
||||
const { roomId } = body;
|
||||
getMembers(roomId).then((addresses) => {
|
||||
res.status(statusCode.OK).json(addresses);
|
||||
}).catch((err) => {
|
||||
logger.error(err)
|
||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
members,
|
||||
};
|
||||
17
back/controllers/messages.js
Normal file
17
back/controllers/messages.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const statusCode = require("../utils/statusCodes").statusCodes;
|
||||
const { getMessages } = require("../db/api.js");
|
||||
const { logger } = require("../system/Logger");
|
||||
|
||||
async function messages(body, res) {
|
||||
const { roomId } = body;
|
||||
getMessages(roomId).then((messages) => {
|
||||
res.status(statusCode.OK).json(messages);
|
||||
}).catch((err) => {
|
||||
logger.err(err)
|
||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
messages,
|
||||
};
|
||||
17
back/controllers/rooms.js
Normal file
17
back/controllers/rooms.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const statusCode = require("../utils/statusCodes").statusCodes;
|
||||
const { getRooms } = require("../db/api.js");
|
||||
const { logger } = require("../system/Logger");
|
||||
|
||||
async function rooms(body, res) {
|
||||
const { mailboxId, offset, limit } = body;
|
||||
getRooms(mailboxId).then((rooms) => {
|
||||
res.status(statusCode.OK).json(rooms);
|
||||
}).catch((err) => {
|
||||
logger.error(err)
|
||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
rooms,
|
||||
};
|
||||
120
back/db/api.js
Normal file
120
back/db/api.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const { execQueryAsync, execQueryAsyncWithId } = require("./db.js");
|
||||
const { queryCcId, queryToId, queryFromId } = require("./utils/addressQueries.js");
|
||||
|
||||
async function registerAccount(userId, pwd, xoauth, xoauth2, host, port, tls) {
|
||||
const query = `
|
||||
INSERT INTO app_account
|
||||
(user_id, account_pwd, xoauth, xoauth2, host, port, tls) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const values = [userId, pwd, xoauth, xoauth2, host, port, tls];
|
||||
return await execQueryAsyncWithId(query, values);
|
||||
}
|
||||
|
||||
async function getAccounts() {
|
||||
// todo mailbox or account id ?
|
||||
const query = `
|
||||
SELECT
|
||||
mailbox.mailbox_id AS id,
|
||||
address.email
|
||||
FROM app_account
|
||||
INNER JOIN address
|
||||
INNER JOIN mailbox
|
||||
WHERE
|
||||
address.address_id = app_account.user_id AND
|
||||
mailbox.account_id = app_account.account_id
|
||||
`;
|
||||
const values = [];
|
||||
return await execQueryAsync(query, values);
|
||||
}
|
||||
|
||||
async function getRooms(mailboxId) {
|
||||
const query = `
|
||||
SELECT
|
||||
app_room.room_id AS id,
|
||||
app_room.room_name AS roomName,
|
||||
address.email AS user,
|
||||
app_room.owner_id AS userId,
|
||||
app_room.notSeen,
|
||||
mailbox_message.mailbox_id AS mailboxId
|
||||
FROM app_room
|
||||
INNER JOIN message
|
||||
INNER JOIN mailbox_message
|
||||
INNER JOIN address
|
||||
WHERE
|
||||
message.message_id = app_room.message_id AND
|
||||
mailbox_message.mailbox_id = ? AND
|
||||
mailbox_message.message_id = message.message_id AND
|
||||
address.address_id = app_room.owner_id
|
||||
ORDER BY app_room.lastUpdate DESC
|
||||
`;
|
||||
const values = [mailboxId];
|
||||
return await execQueryAsync(query, values);
|
||||
}
|
||||
|
||||
async function getMessages(roomId) {
|
||||
// todo attachements name
|
||||
const query = `
|
||||
SELECT
|
||||
msg.message_id AS id,
|
||||
GROUP_CONCAT(fromT.address_id) AS fromA,
|
||||
GROUP_CONCAT(toT.address_id) AS toA,
|
||||
GROUP_CONCAT(ccT.address_id) AS ccA,
|
||||
subjectT.value AS subject,
|
||||
content.text AS content,
|
||||
message.idate AS date
|
||||
FROM app_room_message msg
|
||||
|
||||
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||
${queryToId} toT ON msg.message_id = toT.message_id
|
||||
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
||||
|
||||
LEFT JOIN (
|
||||
SELECT header_field.message_id, header_field.value
|
||||
FROM header_field
|
||||
INNER JOIN field_name
|
||||
WHERE
|
||||
field_name.field_id = header_field.field_id AND
|
||||
field_name.field_name = 'subject'
|
||||
) subjectT ON msg.message_id = subjectT.message_id
|
||||
|
||||
LEFT JOIN (
|
||||
SELECT bodypart.text, header_field.message_id FROM bodypart
|
||||
INNER JOIN header_field
|
||||
INNER JOIN field_name
|
||||
WHERE
|
||||
field_name.field_id = header_field.field_id AND
|
||||
field_name.field_name = 'html' AND
|
||||
bodypart.bodypart_id = header_field.bodypart_id
|
||||
) content ON msg.message_id = content.message_id
|
||||
|
||||
INNER JOIN message ON message.message_id = msg.message_id
|
||||
|
||||
WHERE msg.room_id = ?
|
||||
GROUP BY msg.message_id
|
||||
ORDER BY message.idate;
|
||||
`;
|
||||
const values = [roomId];
|
||||
return await execQueryAsync(query, values);
|
||||
}
|
||||
|
||||
async function getMembers(roomId) {
|
||||
const query = `
|
||||
SELECT
|
||||
address.address_id,
|
||||
address.address_name,
|
||||
address.email
|
||||
FROM app_room_member
|
||||
INNER JOIN address ON address.address_id = app_room_member.member_id
|
||||
WHERE app_room_member.room_id = ?
|
||||
`;
|
||||
const values = [roomId];
|
||||
return await execQueryAsync(query, values);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
registerAccount,
|
||||
getAccounts,
|
||||
getRooms,
|
||||
getMessages,
|
||||
getMembers
|
||||
};
|
||||
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`);
|
||||
59
back/db/db.js
Normal file
59
back/db/db.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const mysql = require("mysql");
|
||||
const { logger } = require("../system/Logger");
|
||||
const MYSQL = require("./config.json").mysql;
|
||||
|
||||
const db = mysql.createConnection({
|
||||
host: MYSQL.host,
|
||||
user: MYSQL.user,
|
||||
password: MYSQL.pwd,
|
||||
database: MYSQL.database,
|
||||
});
|
||||
|
||||
db.connect(function (err) {
|
||||
if (err) {
|
||||
logger.error(`Unable to connect database ${err.code}`);
|
||||
} else {
|
||||
logger.log("Database successfully connected");
|
||||
}
|
||||
});
|
||||
|
||||
function execQueryAsync(query, values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(query, values, (err, results, fields) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function execQueryAsyncWithId(query, values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.query(query, values, (err, results, fields) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(results.insertId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function execQuery(query, values) {
|
||||
db.query(query, values, (err, results, fields) => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
throw (err);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
db, // todo remove this
|
||||
execQuery,
|
||||
execQueryAsync,
|
||||
execQueryAsyncWithId
|
||||
};
|
||||
49
back/db/imap/imap.js
Normal file
49
back/db/imap/imap.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const { execQueryAsyncWithId, execQueryAsync, execQuery } = require("../db");
|
||||
|
||||
async function getAllAccounts() {
|
||||
const query = `
|
||||
SELECT
|
||||
app_account.account_id AS id,
|
||||
address.email AS user,
|
||||
app_account.account_pwd AS password,
|
||||
app_account.host AS host,
|
||||
app_account.port AS port,
|
||||
app_account.tls AS tls
|
||||
FROM app_account INNER JOIN address
|
||||
WHERE address.address_id = app_account.user_id
|
||||
`;
|
||||
const values = [];
|
||||
return await execQueryAsync(query, values);
|
||||
}
|
||||
|
||||
async function getAllMailboxes(accountId) {
|
||||
const query = 'SELECT * FROM mailbox WHERE mailbox.account_id = ?';
|
||||
const values = [accountId];
|
||||
return await execQueryAsync(query, values)
|
||||
}
|
||||
|
||||
async function registerMailbox(accountId, mailboxName) {
|
||||
const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`;
|
||||
const values = [accountId, mailboxName];
|
||||
return await execQueryAsyncWithId(query, values);
|
||||
}
|
||||
|
||||
async function getMailbox(mailboxId) {
|
||||
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
|
||||
const values = [mailboxId];
|
||||
return await execQueryAsync(query, values);
|
||||
}
|
||||
|
||||
function updateMailbox(mailboxId, uidnext) {
|
||||
const query = `UPDATE mailbox SET uidnext = ? WHERE mailbox_id = ?`;
|
||||
const values = [uidnext, mailboxId];
|
||||
execQuery(query, values);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAllAccounts,
|
||||
getAllMailboxes,
|
||||
registerMailbox,
|
||||
getMailbox,
|
||||
updateMailbox
|
||||
}
|
||||
41
back/db/mail.js
Normal file
41
back/db/mail.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { execQueryAsync, execQueryAsyncWithId } = require("./db.js");
|
||||
|
||||
async function getAddresseId(email, name) {
|
||||
const localpart = email.split("@")[0];
|
||||
const domain = email.split("@")[1];
|
||||
const query = `INSERT INTO address
|
||||
(address_name, localpart, domain, email) VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE address_name = ?, address_id = LAST_INSERT_ID(address_id)`;
|
||||
const values = [name, localpart, domain, email, name];
|
||||
return await execQueryAsyncWithId(query, values);
|
||||
}
|
||||
|
||||
async function getFieldId(field) {
|
||||
const query = `INSERT INTO field_name (field_name) VALUES (?) ON DUPLICATE KEY UPDATE field_id=LAST_INSERT_ID(field_id)`;
|
||||
const values = [field]
|
||||
return await execQueryAsyncWithId(query, values);
|
||||
}
|
||||
|
||||
async function findRoomByOwner(ownerId) {
|
||||
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
|
||||
const values = [ownerId];
|
||||
return await execQueryAsync(query, values);
|
||||
}
|
||||
|
||||
async function getUserIdOfMailbox(boxId) {
|
||||
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);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAddresseId,
|
||||
getFieldId,
|
||||
findRoomByOwner,
|
||||
getUserIdOfMailbox
|
||||
};
|
||||
76
back/db/saveMessage.js
Normal file
76
back/db/saveMessage.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const { transformEmojis } = require("../utils/string.js");
|
||||
const { db, execQuery, execQueryAsync, execQueryAsyncWithId } = require("./db.js");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
registerMessage,
|
||||
registerMailbox_message,
|
||||
saveHeader_fields,
|
||||
saveAddress_fields,
|
||||
registerBodypart,
|
||||
saveBodypart,
|
||||
saveSource
|
||||
}
|
||||
129
back/db/saveMessageApp.js
Normal file
129
back/db/saveMessageApp.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const { transformEmojis } = require("../utils/string.js");
|
||||
const { db, execQueryAsync, execQueryAsyncWithId, execQuery } = require("./db.js");
|
||||
const { queryFromId, queryToId, queryCcId } = require("./utils/addressQueries.js");
|
||||
|
||||
async function createRoom(roomName, ownerId, messageId) {
|
||||
if (!roomName) roomName = "No room name";
|
||||
roomName = transformEmojis(roomName);
|
||||
const query = `INSERT IGNORE INTO app_room (room_name, owner_id, message_id) VALUES (?, ?, ?)`;
|
||||
const values = [roomName.substring(0, 255), ownerId, messageId];
|
||||
return await execQueryAsyncWithId(query, values);
|
||||
// todo add members
|
||||
}
|
||||
|
||||
async function registerMessageInRoom(messageId, roomId, isSeen, idate) {
|
||||
const query = `INSERT IGNORE INTO app_room_message (message_id, room_id) VALUES (?, ?)`;
|
||||
const values = [messageId, roomId];
|
||||
await execQueryAsync(query, values);
|
||||
|
||||
updateLastUpdateRoom(roomId, idate);
|
||||
|
||||
if (!isSeen) {
|
||||
incrementNotSeenRoom(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLastUpdateRoom(roomId, idate) {
|
||||
const query = `UPDATE app_room SET lastUpdate = ? WHERE room_id = ?`;
|
||||
const values = [idate, roomId];
|
||||
execQuery(query, values);
|
||||
}
|
||||
|
||||
function incrementNotSeenRoom(roomId) {
|
||||
// todo
|
||||
}
|
||||
|
||||
async function createThread(threadName, ownerId, messageId, parentRoomId, isDm) {
|
||||
const rootRoomId = -1; // todo
|
||||
const threadId = await createRoom(threadName, ownerId, messageId);
|
||||
const query = `INSERT INTO app_thread (room_id, parent_room_id, root_room_id, isDm) VALUES (?, ?, ?, ?)`;
|
||||
const values = [threadId, parentRoomId, rootRoomId, isDm];
|
||||
return await execQueryAsync(query, values);
|
||||
// todo add members
|
||||
}
|
||||
|
||||
async function registerMessageInThread(messageId, threadId, isSeen) {
|
||||
// todo check if it is still a thread or should be a room
|
||||
// todo isdm
|
||||
console.log("register message in thread")
|
||||
}
|
||||
|
||||
function updateLastUpdateThread(threadId) {
|
||||
// todo
|
||||
// check for parent
|
||||
}
|
||||
|
||||
function incrementNotSeenThread(threadId) {
|
||||
// todo
|
||||
// also increment root room
|
||||
}
|
||||
|
||||
async function isRoomGroup(roomId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = `SELECT isGroup FROM app_room WHERE room_id = '${roomId}'`;
|
||||
db.query(query, (err, results, fields) => {
|
||||
if (err) reject(err);
|
||||
resolve(results[0].isGroup);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function findRoomsFromMessage(messageId) {
|
||||
const query = `SELECT room_id FROM app_room_message WHERE message_id = ? ORDER BY room_id`;
|
||||
const values = [messageId];
|
||||
return await execQueryAsync(query, values);
|
||||
}
|
||||
|
||||
async function hasSameMembersAsParent(messageId, messageID) {
|
||||
const query1 = `
|
||||
SELECT
|
||||
GROUP_CONCAT(fromT.address_id) AS fromA,
|
||||
GROUP_CONCAT(toT.address_id) AS toA,
|
||||
GROUP_CONCAT(ccT.address_id) AS ccA
|
||||
FROM message msg
|
||||
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||
${queryToId} toT ON msg.message_id = toT.message_id
|
||||
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
||||
WHERE msg.message_id = ?
|
||||
`;
|
||||
const values1 = [messageId];
|
||||
let addressesMsg1 = await execQueryAsync(query1, values1);
|
||||
|
||||
const query2 = `
|
||||
SELECT
|
||||
GROUP_CONCAT(fromT.address_id) AS fromA,
|
||||
GROUP_CONCAT(toT.address_id) AS toA,
|
||||
GROUP_CONCAT(ccT.address_id) AS ccA
|
||||
FROM message msg
|
||||
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||
${queryToId} toT ON msg.message_id = toT.message_id
|
||||
${queryCcId} ccT ON msg.message_id = ccT.message_id
|
||||
WHERE msg.messageID = ?
|
||||
`;
|
||||
const values2 = [messageID];
|
||||
let addressesMsg2 = await execQueryAsync(query2, values2);
|
||||
|
||||
addressesMsg1 = addressesMsg1[0]?.fromA
|
||||
?.split(",")
|
||||
.concat(addressesMsg1[0]?.toA?.split(","))
|
||||
.concat(addressesMsg1[0]?.ccA?.split(","));
|
||||
addressesMsg2 = addressesMsg2[0]?.fromA
|
||||
?.split(",")
|
||||
.concat(addressesMsg2[0]?.toA?.split(","))
|
||||
.concat(addressesMsg2[0]?.ccA?.split(","));
|
||||
|
||||
return (
|
||||
addressesMsg1.length == addressesMsg2.length &&
|
||||
addressesMsg1.reduce((a, b) => a && addressesMsg2.includes(b), true)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRoom,
|
||||
registerMessageInRoom,
|
||||
createThread,
|
||||
registerMessageInThread,
|
||||
isRoomGroup,
|
||||
findRoomsFromMessage,
|
||||
hasSameMembersAsParent,
|
||||
};
|
||||
@@ -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,
|
||||
isGroup BOOLEAN NOT NULL DEFAULT false,
|
||||
notSeen INT NOT NULL DEFAULT 0,
|
||||
lastUpdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
|
||||
PRIMARY KEY (room_id),
|
||||
UNIQUE KEY (owner_id, message_id, isGroup),
|
||||
FOREIGN KEY (owner_id) REFERENCES address(address_id),
|
||||
FOREIGN KEY (message_id) REFERENCES message(message_id)
|
||||
);
|
||||
|
||||
-- 12
|
||||
CREATE TABLE app_thread (
|
||||
room_id INT NOT NULL,
|
||||
parent_room_id INT,
|
||||
root_room_id INT,
|
||||
isDm BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (room_id),
|
||||
UNIQUE KEY (room_id, parent_room_id, root_room_id),
|
||||
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (parent_room_id) REFERENCES app_room(room_id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (root_room_id) REFERENCES app_room(room_id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 13
|
||||
CREATE TABLE app_room_message (
|
||||
message_id INT NOT NULL,
|
||||
room_id INT,
|
||||
UNIQUE KEY (message_id, room_id),
|
||||
FOREIGN KEY (message_id) REFERENCES message(message_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 14
|
||||
CREATE TABLE app_room_member (
|
||||
room_id INT NOT NULL,
|
||||
member_id INT NOT NULL,
|
||||
FOREIGN KEY (room_id) REFERENCES app_room(room_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (member_id) REFERENCES address(address_id)
|
||||
);
|
||||
20
back/db/utils/addressQueries.js
Normal file
20
back/db/utils/addressQueries.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const queryAddress = (type) => `
|
||||
LEFT JOIN (
|
||||
SELECT address_field.address_id, address_field.message_id
|
||||
FROM address_field
|
||||
INNER JOIN field_name
|
||||
WHERE
|
||||
field_name.field_id = address_field.field_id AND
|
||||
field_name.field_name = '${type}'
|
||||
)
|
||||
`;
|
||||
|
||||
const queryFromId = queryAddress("from");
|
||||
const queryToId = queryAddress("to");
|
||||
const queryCcId = queryAddress("cc");
|
||||
|
||||
module.exports = {
|
||||
queryFromId,
|
||||
queryToId,
|
||||
queryCcId
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
14
back/jest-mysql-config.js
Normal file
14
back/jest-mysql-config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const MYSQL = require("./sql/config.json").mysql;
|
||||
|
||||
module.exports = {
|
||||
databaseOptions: {
|
||||
host: "localhost",
|
||||
port: 3306,
|
||||
user: MYSQL.user,
|
||||
password: MYSQL.pwd,
|
||||
database: "mail_test",
|
||||
},
|
||||
createDatabase: true,
|
||||
dbSchema: "./sql/structureV2.sql",
|
||||
truncateDatabase: true,
|
||||
};
|
||||
76
back/mails/imap/Box.js
Normal file
76
back/mails/imap/Box.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const { getMailbox, updateMailbox } = require("../../db/imap/imap");
|
||||
const { logger } = require("../../system/Logger");
|
||||
const { registerMessageInApp } = require("../saveMessage");
|
||||
const { saveMessage } = require("../storeMessage");
|
||||
|
||||
class Box {
|
||||
constructor(_imap, _boxId, _boxName) {
|
||||
this.imap = _imap;
|
||||
this.boxName = _boxName;
|
||||
this.id = _boxId;
|
||||
this.box;
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.box = (await getMailbox(this.id))[0];
|
||||
|
||||
const readOnly = true;
|
||||
this.imap.openBox(this.boxName, readOnly, (err, box) => {
|
||||
if (err) logger.error(err);
|
||||
this.sync(this.box.uidnext, box.uidnext);
|
||||
});
|
||||
}
|
||||
|
||||
sync(savedUid, currentUid) {
|
||||
const promises = [];
|
||||
const mails = [];
|
||||
logger.log(`Syncing from ${savedUid} to ${currentUid} uid`);
|
||||
const f = this.imap.seq.fetch(`${savedUid}:${currentUid}`, {
|
||||
size: true,
|
||||
envelope: true,
|
||||
});
|
||||
|
||||
f.on("message", (msg, seqno) => {
|
||||
msg.once("attributes", (attrs) => {
|
||||
mails.push(attrs);
|
||||
promises.push(saveMessage(attrs, this.id, this.imap));
|
||||
});
|
||||
});
|
||||
|
||||
f.once("error", (err) => {
|
||||
logger.error("Fetch error: " + err);
|
||||
});
|
||||
|
||||
f.once("end", async () => {
|
||||
let step = 20;
|
||||
for (let i = 0; i < promises.length; i += step) {
|
||||
for (let j = i; j < (i + step && promises.length); j++) {
|
||||
console.log(j, promises.length, promises[j])
|
||||
await new Promise((resolve, reject) => {
|
||||
promises[j]
|
||||
.then(async (res) => {
|
||||
await registerMessageInApp(res, mails[j], this.id);
|
||||
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);
|
||||
}
|
||||
// await Promise.all(promises).then(async (res) => {
|
||||
// for (let i = 0; i < mails.length; i++) {
|
||||
// logger.log(`Saved message ${i}/${mails.length}`);
|
||||
// }
|
||||
// });
|
||||
updateMailbox(this.id, currentUid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Box,
|
||||
};
|
||||
75
back/mails/imap/ImapInstance.js
Normal file
75
back/mails/imap/ImapInstance.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const Imap = require("imap");
|
||||
const { getAllMailboxes, registerMailbox } = require("../../db/imap/imap");
|
||||
const { logger } = require("../../system/Logger");
|
||||
const { Box } = require("./Box");
|
||||
|
||||
class ImapInstance {
|
||||
constructor(account) {
|
||||
this.imap = new Imap({
|
||||
user: account.user,
|
||||
password: account.password,
|
||||
tlsOptions: { servername: account.host },
|
||||
host: account.host,
|
||||
port: account.port,
|
||||
tls: account.tls,
|
||||
});
|
||||
this.account = account;
|
||||
this.boxes = [];
|
||||
|
||||
/**
|
||||
* IMAP
|
||||
*/
|
||||
this.imap.once("ready", () => {
|
||||
logger.log("Imap connected for " + this.account.user);
|
||||
this.imapReady();
|
||||
});
|
||||
|
||||
this.imap.once("error", (err) => {
|
||||
logger.error("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.error(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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ImapInstance,
|
||||
};
|
||||
28
back/mails/imap/ImapSync.js
Normal file
28
back/mails/imap/ImapSync.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const { getAllAccounts } = require("../../db/imap/imap");
|
||||
const { logger } = require("../../system/Logger");
|
||||
const { ImapInstance } = require("./ImapInstance");
|
||||
|
||||
class ImapSync {
|
||||
constructor() {
|
||||
this.instances = [];
|
||||
}
|
||||
|
||||
init() {
|
||||
getAllAccounts().then((accounts) => {
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
accounts[i].password = accounts[i]?.password.toString().replace(/[\u{0080}-\u{FFFF}]/gu,"");
|
||||
this.addInstance(accounts[i]);
|
||||
}
|
||||
}).catch((err) => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
addInstance(config) {
|
||||
this.instances.push(new ImapInstance(config));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ImapSync
|
||||
}
|
||||
145
back/mails/saveMessage.js
Normal file
145
back/mails/saveMessage.js
Normal file
@@ -0,0 +1,145 @@
|
||||
const {
|
||||
createRoom,
|
||||
registerMessageInRoom,
|
||||
createThread,
|
||||
registerMessageInThread,
|
||||
isRoomGroup,
|
||||
findRoomsFromMessage,
|
||||
hasSameMembersAsParent,
|
||||
} = require("../db/saveMessageApp");
|
||||
|
||||
const { findRoomByOwner, getAddresseId, getUserIdOfMailbox } = require("../db/mail");
|
||||
const { nbMembers } = require("./utils/statusUtils");
|
||||
|
||||
/**
|
||||
* take object address and join mailbox and host to return mailbox@host
|
||||
*/
|
||||
function createAddress(elt) {
|
||||
return `${elt.mailbox}@${elt.host}`;
|
||||
}
|
||||
|
||||
async function initiateRoom(envelope, ownerId, messageId, isSeen) {
|
||||
await createRoom(envelope.subject, ownerId, messageId).then(async (roomId) => {
|
||||
// todo register members
|
||||
await registerMessageInRoom(messageId, roomId, isSeen, envelope.date);
|
||||
});
|
||||
}
|
||||
|
||||
const roomType = {
|
||||
ROOM: 'room',
|
||||
CHANNEL: 'channel',
|
||||
GROUP: 'group',
|
||||
DM: 'dm',
|
||||
THREAD: 'thread'
|
||||
}
|
||||
|
||||
class registerMessageInApp {
|
||||
constructor(_messageId, _attrs, _boxId) {
|
||||
this.messageId = _messageId;
|
||||
this.attrs = _attrs;
|
||||
this.envelope = this.attrs.envelope;
|
||||
this.messageID = this.envelope.messageId;
|
||||
this.boxId = _boxId;
|
||||
this.isSeen = this.attrs.flags.includes("\\Seen") ? 1 : 0;
|
||||
this.ownerId;
|
||||
this.userId;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.ownerId = await getAddresseId(createAddress(this.envelope.from[0])); // todo use sender or from ?
|
||||
}
|
||||
|
||||
isDm = () => nbMembers(this.envelope) == 2;
|
||||
|
||||
async isFromUs() {
|
||||
if (!this.userId) this.userId = (await getUserIdOfMailbox(this.boxId))[0]?.user_id;
|
||||
return this.ownerId == this.userId;
|
||||
}
|
||||
|
||||
async initiateRoom(owner, roomType) {
|
||||
// todo roomType
|
||||
await createRoom(this.envelope.subject, owner, this.messageId).then(async (roomId) => {
|
||||
// todo register members
|
||||
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
|
||||
});
|
||||
}
|
||||
|
||||
async createOrRegisterOnExistence(owner, roomType) {
|
||||
await findRoomByOwner(owner).then(async (res) => {
|
||||
if (res.length == 0) {
|
||||
// first message with this sender
|
||||
await 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 createOrRegisterOnMembers(roomId) {
|
||||
const hasSameMembers = await hasSameMembersAsParent(this.messageID, this.envelope.inReplyTo);
|
||||
if (hasSameMembers) {
|
||||
await registerMessageInRoom(this.messageId, roomId, this.isSeen, this.envelope.date);
|
||||
} else {
|
||||
await createThread(this.envelope.subject, this.ownerId, this.messageId, roomId, this.isDm()).then(
|
||||
async (threadId) => {
|
||||
await registerMessageInThread(this.messageId, threadId, this.isSeen);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.init();
|
||||
if (this.envelope.inReplyTo) {
|
||||
this.saveReply();
|
||||
} else {
|
||||
if (await this.isFromUs()) {
|
||||
if (this.isDm()) {
|
||||
// create or add new message to DM
|
||||
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
|
||||
initiateRoom(this.envelope, this.ownerId, this.messageId, this.isSeen);
|
||||
}
|
||||
} else {
|
||||
await this.createOrRegisterOnExistence(this.ownerId, roomType.ROOM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async saveReply() {
|
||||
const messageID = envelope.messageId;
|
||||
await findRoomsFromMessage(messageId).then(async (rooms) => {
|
||||
if (rooms.length == 0) {
|
||||
// no rooms, so is a transfer
|
||||
// todo test if members of transferred message are included
|
||||
} else if (rooms.length === 1) {
|
||||
// only one room so message is only in a room and not in a thread
|
||||
// as a thread is associated to a room
|
||||
await isRoomGroup(rooms[0].room_id).then(async (isGroup) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
registerMessageInApp,
|
||||
roomType
|
||||
};
|
||||
129
back/mails/storeMessage.js
Normal file
129
back/mails/storeMessage.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const { getAddresseId } = require("../db/mail");
|
||||
const { simpleParser } = require("mailparser");
|
||||
const moment = require("moment");
|
||||
const {
|
||||
registerMessage,
|
||||
registerMailbox_message,
|
||||
saveHeader_fields,
|
||||
saveAddress_fields,
|
||||
registerBodypart,
|
||||
saveBodypart,
|
||||
saveSource,
|
||||
} = require("../db/saveMessage");
|
||||
|
||||
const { getFieldId } = require("../db/mail");
|
||||
const { logger } = require("../system/Logger");
|
||||
|
||||
function saveMessage(attrs, mailboxId, imap) {
|
||||
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 = [];
|
||||
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
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveMessage,
|
||||
};
|
||||
|
||||
if (process.env["NODE_DEV"] == "TEST") {
|
||||
module.exports = {
|
||||
saveFromParsedData,
|
||||
};
|
||||
}
|
||||
18
back/mails/utils/statusUtils.js
Normal file
18
back/mails/utils/statusUtils.js
Normal file
@@ -0,0 +1,18 @@
|
||||
function nbMembers(envelope) {
|
||||
let nbMembers =
|
||||
(envelope.bcc?.length ?? 0) +
|
||||
(envelope.cc?.length ?? 0) +
|
||||
(envelope.to?.length ?? 0) +
|
||||
(envelope.from?.length ?? 0);
|
||||
if (
|
||||
envelope.sender?.length > 0 &&
|
||||
!(envelope.sender[0].mailbox == envelope.from[0].mailbox && envelope.sender[0].host == envelope.from[0].host)
|
||||
) {
|
||||
nbMembers += envelope.sender?.length ?? 0;
|
||||
}
|
||||
return nbMembers;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nbMembers,
|
||||
};
|
||||
7904
back/package-lock.json
generated
7904
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,22 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"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": {
|
||||
"jest": "^29.5.0"
|
||||
},
|
||||
"jest": {
|
||||
"testMatch": [
|
||||
"<rootDir>/test//**/*-test.[jt]s?(x)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
53
back/routes/mail.js
Normal file
53
back/routes/mail.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const statusCodes = require("../utils/statusCodes.js").statusCodes;
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const Ajv = require("ajv");
|
||||
const addFormats = require("ajv-formats");
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
addFormats(ajv);
|
||||
const schema_account = require("../schemas/account_schema.json");
|
||||
const { addAccount } = require("../controllers/addAccount.js");
|
||||
const { getAccounts } = require("../db/api.js");
|
||||
const { rooms } = require("../controllers/rooms.js");
|
||||
const { messages } = require("../controllers/messages.js");
|
||||
|
||||
const validate_account = ajv.compile(schema_account);
|
||||
|
||||
/**
|
||||
* Return all mailboxes and folders for an user
|
||||
*/
|
||||
router.get("/accounts", (req, res) => {
|
||||
getAccounts().then((data) => {
|
||||
res.status(statusCodes.OK).json(data);
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/:mailboxId/rooms", async (req, res) => {
|
||||
// todo use offset limit
|
||||
await rooms(req.params, res);
|
||||
});
|
||||
|
||||
router.get("/:roomId/messages", async (req, res) => {
|
||||
const { roomId } = req.params;
|
||||
await messages(req.params, res);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = 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 +1,19 @@
|
||||
const mails = require("./api/mails.js");
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors')
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const app = express();
|
||||
const { ImapSync } = require("./mails/imap/ImapSync");
|
||||
|
||||
app.use(express.json());
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
})
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
}),
|
||||
);
|
||||
app.use(cors());
|
||||
app.listen(process.env.PORT || 5500);
|
||||
|
||||
const mailRouter = require("./routes/mail");
|
||||
app.use("/api/mail", mailRouter);
|
||||
|
||||
// anecdote
|
||||
app.get("/api/mails/mailboxes", mails.getMailboxes);
|
||||
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
45
back/system/Logger.js
Normal file
45
back/system/Logger.js
Normal file
@@ -0,0 +1,45 @@
|
||||
class Logger {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
log(content) {
|
||||
console.log(this._prefix("log"), content);
|
||||
}
|
||||
|
||||
warn(content) {
|
||||
console.warn(this._prefix("warn"), content);
|
||||
}
|
||||
|
||||
error(content) {
|
||||
console.error(this._prefix("err"), content);
|
||||
}
|
||||
|
||||
_prefix(type) {
|
||||
let typeStr = "";
|
||||
switch (type) {
|
||||
case "log":
|
||||
typeStr = "LOG"
|
||||
break;
|
||||
case "warn":
|
||||
typeStr = "WARN"
|
||||
break;
|
||||
case "err":
|
||||
typeStr = "ERR"
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return `[${typeStr}: ${this._timestamp()}]`;
|
||||
}
|
||||
|
||||
_timestamp() {
|
||||
return new Date().toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
module.exports = {
|
||||
logger,
|
||||
}
|
||||
41
back/test/mail/saveMessage-test.js
Normal file
41
back/test/mail/saveMessage-test.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { getAddresseId, getUserIdOfMailbox } = require("../../db/mail");
|
||||
const { generateAttrs, generateUsers } = require("../test-utils/test-attrsUtils");
|
||||
const { registerMessageInApp, roomType } = require("../../mails/saveMessage");
|
||||
|
||||
jest.mock("../../db/mail");
|
||||
|
||||
// todo mock db
|
||||
|
||||
describe("saveMessage", () => {
|
||||
describe("is not a reply", () => {
|
||||
it("DM from user should be assigned to other user", async () => {
|
||||
const users = generateUsers(2);
|
||||
const attrs = generateAttrs({ from: [users[0].user], to: [users[1].user] });
|
||||
|
||||
getUserIdOfMailbox.mockReturnValue([{ user_id: users[0].id }]);
|
||||
getAddresseId.mockImplementation((email) => {
|
||||
const match = users.find((user) => user.user.mailbox + "@" + user.user.host == email);
|
||||
return match.id;
|
||||
});
|
||||
|
||||
const register = new registerMessageInApp(1, attrs, 1);
|
||||
|
||||
const createOrRegisterOnExistence = jest
|
||||
.spyOn(register, "createOrRegisterOnExistence")
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
await register.save();
|
||||
|
||||
expect(createOrRegisterOnExistence).toHaveBeenCalledWith(users[1].id, roomType.DM);
|
||||
});
|
||||
|
||||
it("DM message from user should be added to DM room", async () => {
|
||||
// todo multiple messages
|
||||
});
|
||||
// it("first GROUP message should create a group", () => {});
|
||||
});
|
||||
describe("replies", () => {
|
||||
it("", () => {});
|
||||
});
|
||||
describe("", () => {});
|
||||
});
|
||||
40
back/test/mail/utils/statusUtils-test.js
Normal file
40
back/test/mail/utils/statusUtils-test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { nbMembers } = require("../../../mails/utils/statusUtils");
|
||||
|
||||
describe("statusUtils", () => {
|
||||
it("sender and from shouldn't be counted twice if there are the same", () => {
|
||||
const envelope = {
|
||||
from: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
sender: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
replyTo: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
to: null,
|
||||
cc: null,
|
||||
bcc: null,
|
||||
inReplyTo: null,
|
||||
};
|
||||
expect(nbMembers(envelope)).toBe(1);
|
||||
});
|
||||
it("sender and from shoud be counted twice if there are the same", () => {
|
||||
const envelope = {
|
||||
from: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
sender: [{ name: "", mailbox: "user_2", host: "provider.com" }],
|
||||
replyTo: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
to: null,
|
||||
cc: null,
|
||||
bcc: null,
|
||||
inReplyTo: null,
|
||||
};
|
||||
expect(nbMembers(envelope)).toBe(2);
|
||||
});
|
||||
it("should count every members", () => {
|
||||
const envelope = {
|
||||
from: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
sender: [{ name: "", mailbox: "user_2", host: "provider.com" }],
|
||||
replyTo: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
to: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
cc: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
bcc: [{ name: "", mailbox: "user_1", host: "provider.com" }],
|
||||
inReplyTo: null,
|
||||
};
|
||||
expect(nbMembers(envelope)).toBe(5);
|
||||
});
|
||||
});
|
||||
19
back/test/sql/saveMessageApp-test.js
Normal file
19
back/test/sql/saveMessageApp-test.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 = require("mysql");
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
206
back/test/test-utils/names.js
Normal file
206
back/test/test-utils/names.js
Normal file
@@ -0,0 +1,206 @@
|
||||
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",
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
names,
|
||||
};
|
||||
13
back/utils/array.js
Normal file
13
back/utils/array.js
Normal file
@@ -0,0 +1,13 @@
|
||||
function removeDuplicates(array) {
|
||||
let unique = [];
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (!unique.includes(array[i])) {
|
||||
unique.push(array[i]);
|
||||
}
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
removeDuplicates,
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
25
back/utils/string.js
Normal file
25
back/utils/string.js
Normal file
@@ -0,0 +1,25 @@
|
||||
function transformEmojis(str) {
|
||||
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;
|
||||
}
|
||||
|
||||
function decodeEmojis(text) {
|
||||
const regex = /\\u{([^}]+)}/g;
|
||||
const decodedText = text.replace(regex, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
|
||||
return decodedText;
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
transformEmojis,
|
||||
decodeEmojis
|
||||
}
|
||||
2923
back/yarn.lock
Normal file
2923
back/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
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 +0,0 @@
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 650 B After Width: | Height: | Size: 0 B |
@@ -12,6 +12,7 @@
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"axios": "^1.3.4",
|
||||
"core-js": "^3.8.3",
|
||||
"dompurify": "^3.0.1",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.1.6",
|
||||
"vuex": "^4.0.2"
|
||||
|
||||
@@ -4,22 +4,18 @@ import { RouterView } from "vue-router";
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- <router-link to="/">Home</router-link> -->
|
||||
<RouterView/>
|
||||
<Sidebar />
|
||||
<RoomView />
|
||||
<RouterView/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sidebar from './views/sidebar/Sidebar'
|
||||
import RoomView from './views/room/RoomView'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Sidebar,
|
||||
RoomView,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
28
front/src/components/Badge.vue
Normal file
28
front/src/components/Badge.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<slot class="badge" name="body">
|
||||
0
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #4d5970;
|
||||
height: 1.6rem;
|
||||
width: 1.6rem;
|
||||
min-width: 1.6rem;
|
||||
min-height: 1.6rem;
|
||||
border-radius: 1.6rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.4rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createApp } from 'vue'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import roomsStore from './store/rooms'
|
||||
import store from './store/store'
|
||||
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(roomsStore);
|
||||
app.use(store);
|
||||
|
||||
app.mount('#app');
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import Home from "../views/Home";
|
||||
import RoomView from "../views/room/RoomView";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -16,6 +17,10 @@ const routes = [
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "about" */ "../views/About.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:id",
|
||||
component: RoomView
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export default(url='localhost') => {
|
||||
export default(url='/api') => {
|
||||
return axios.create({
|
||||
baseURL: url,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import API from './API'
|
||||
|
||||
export default {
|
||||
getQuote() {
|
||||
return API().get('/');
|
||||
}
|
||||
registerAccount(data) {
|
||||
return API().post('/mail/account', data);
|
||||
},
|
||||
getAccounts() {
|
||||
return API().get('/mail/accounts');
|
||||
},
|
||||
getRooms(mailboxId) {
|
||||
return API().get(`/mail/${mailboxId}/rooms`);
|
||||
},
|
||||
getMessages(roomId) {
|
||||
return API().get(`/mail/${roomId}/messages`);
|
||||
},
|
||||
getMembers(roomId) {
|
||||
return API().get(`/mail/${roomId}/members`);
|
||||
},
|
||||
}
|
||||
7
front/src/store/models/Account.js
Normal file
7
front/src/store/models/Account.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default class Account {
|
||||
constructor(_id, _mail) {
|
||||
this.id = _id;
|
||||
this.email = _mail;
|
||||
this.fetched = false;
|
||||
}
|
||||
}
|
||||
23
front/src/store/models/Room.js
Normal file
23
front/src/store/models/Room.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default class Room {
|
||||
constructor(room, thread) {
|
||||
this.id = room.id;
|
||||
this.user = room.user;
|
||||
this.userId = room.userId;
|
||||
this.roomName = room.roomName;
|
||||
this.mailboxId = room.mailboxId;
|
||||
this.unseen = room.unseen;
|
||||
|
||||
this.messages = [];
|
||||
this.messagesFetched = false;
|
||||
|
||||
if (!thread) {
|
||||
this.threads = [
|
||||
// new Room({id:12, user: this.user, roomName: "thread 1", mailbboxId:this.mailboxId}, true),
|
||||
// new Room({id:12, user: this.user, roomName: "thread 1", mailbboxId:this.mailboxId}, true),
|
||||
// new Room({id:12, user: this.user, roomName: "thread 1", mailbboxId:this.mailboxId}, true),
|
||||
];
|
||||
} else {
|
||||
this.threads = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
5
front/src/store/models/Thread.js
Normal file
5
front/src/store/models/Thread.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default class Thread {
|
||||
constructor (roomId, name) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
114
front/src/store/store.js
Normal file
114
front/src/store/store.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import API from "@/services/imapAPI";
|
||||
import { createStore } from "vuex";
|
||||
import Room from "./models/Room";
|
||||
import Account from "./models/Account";
|
||||
|
||||
const store = createStore({
|
||||
state() {
|
||||
return {
|
||||
rooms: [new Room({ id: 12, user: "user", roomName: "room name", mailbboxId: 2 })],
|
||||
messages: [],
|
||||
accounts: [new Account(0, "ALL")],
|
||||
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) => {
|
||||
state.accounts.push(new Account(account.id, account.email));
|
||||
});
|
||||
},
|
||||
addRooms(state, payload) {
|
||||
// todo add if not exist
|
||||
payload.rooms.forEach((room) => {
|
||||
state.rooms.push(new Room(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) => {
|
||||
room.messages.push(message);
|
||||
});
|
||||
room.messagesFetched = true;
|
||||
},
|
||||
addAddress(state, payload) {
|
||||
// todo add if not exist
|
||||
payload.addresses.forEach((address) => {
|
||||
state.addresses.push(address);
|
||||
});
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
rooms: (state) => () => {
|
||||
if (state.activeAccount === 0) return state.rooms;
|
||||
return state.rooms.filter((room) => room.mailboxId == state.activeAccount);
|
||||
},
|
||||
messages: (state) => (roomId) => {
|
||||
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) => {
|
||||
context.commit("addAccounts", res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
},
|
||||
fetchRooms: async (context, data) => {
|
||||
if (data.account?.fetched == false) {
|
||||
API.getRooms(data.accountId)
|
||||
.then((res) => {
|
||||
data.account.fetched = true;
|
||||
context.commit("addRooms", { rooms: res.data });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchMessages: async (context, data) => {
|
||||
if (!data.room || data.room?.fetched == false) {
|
||||
store.dispatch("fetchAddress", { roomId: data.roomId });
|
||||
API.getMessages(data.roomId)
|
||||
.then((res) => {
|
||||
context.commit("addMessages", { messages: res.data, roomId: data.roomId });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchRoomMembers: async (context, data) => {
|
||||
API.getMembers(data.roomId)
|
||||
.then((res) => {
|
||||
context.commit("addAddress", { addresses: res.data, roomId: data.roomId });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default store;
|
||||
5
front/src/utils/string.js
Normal file
5
front/src/utils/string.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export function decodeEmojis(text) {
|
||||
const regex = /\\u{([^}]+)}/g;
|
||||
const decodedText = text.replace(regex, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
|
||||
return decodedText;
|
||||
}
|
||||
@@ -3,14 +3,14 @@ import { ref, computed, watchEffect } from 'vue'
|
||||
import Modal from './Modal'
|
||||
import API from '../../services/imapAPI';
|
||||
|
||||
const modal = ref(true);
|
||||
const modal = ref(false);
|
||||
|
||||
const email = ref("");
|
||||
const pwd = ref("");
|
||||
const xoauth = ref("");
|
||||
const xoauth2 = ref("");
|
||||
const host = ref("");
|
||||
const port = ref("993");
|
||||
const port = ref(993);
|
||||
|
||||
const error = ref("");
|
||||
|
||||
@@ -64,10 +64,20 @@ function checkError() {
|
||||
}
|
||||
}
|
||||
|
||||
function addMailboxRequest(event) {
|
||||
console.log(event.target.disabled = true)
|
||||
function addAccountRequest() {
|
||||
checkError();
|
||||
API.getQuote().then((res) => {
|
||||
|
||||
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)
|
||||
@@ -103,7 +113,7 @@ function mailChange() {
|
||||
</button>
|
||||
<Modal
|
||||
v-if="modal"
|
||||
title="Add mailbox"
|
||||
title="Add new account"
|
||||
@close-modal="modal = false"
|
||||
>
|
||||
<template v-slot:body>
|
||||
@@ -135,7 +145,7 @@ function mailChange() {
|
||||
</div>
|
||||
<!-- tls -->
|
||||
<div>
|
||||
<button :disabled="error != ''" @click="addMailboxRequest">Add</button>
|
||||
<button :disabled="error != ''" @click="addAccountRequest">Add</button>
|
||||
{{ error }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,6 +5,7 @@ import { defineEmits, defineProps } from 'vue'
|
||||
const emit = defineEmits(['close-modal']);
|
||||
const props = defineProps({ title: String });
|
||||
|
||||
// todo close on escape
|
||||
function close() {
|
||||
emit('close-modal');
|
||||
}
|
||||
@@ -42,8 +43,7 @@ function close() {
|
||||
top: 0;
|
||||
z-index: 4000;
|
||||
|
||||
background-color: #000;
|
||||
opacity: .7;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.modal {
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
#main {
|
||||
background-color: blue;
|
||||
border-bottom: 1px solid #505050;
|
||||
width: 100%;
|
||||
height: 51px;
|
||||
}
|
||||
|
||||
93
front/src/views/room/Message.vue
Normal file
93
front/src/views/room/Message.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import { defineProps, onMounted, ref } from "vue";
|
||||
import { decodeEmojis } from "../../utils/string";
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
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();
|
||||
});
|
||||
</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">{{ 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,73 @@
|
||||
<script setup>
|
||||
import { useStore } from "vuex";
|
||||
import { useRoute, onBeforeRouteUpdate } from "vue-router";
|
||||
import { onBeforeMount } from "vue";
|
||||
import Header from "./Header.vue";
|
||||
import Message from "./Message.vue";
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
let { id } = route.params;
|
||||
onBeforeMount(async () => {
|
||||
// get data
|
||||
console.log(store.state.rooms.find((room) => room.id === id)?.fetched);
|
||||
let room = store.state.rooms.find((room) => room.id === 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 = to.params.id;
|
||||
store.commit("setActiveRoom", id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div id="main">
|
||||
<Header></Header>
|
||||
<div>
|
||||
|
||||
Room
|
||||
is thread
|
||||
<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;
|
||||
overflow: auto;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<Mailboxes />
|
||||
<Users id="users"/>
|
||||
<Accounts />
|
||||
<Rooms id="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,
|
||||
Accounts,
|
||||
Rooms,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -24,7 +24,7 @@ div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#users {
|
||||
#rooms {
|
||||
max-width: 300px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="container" @click="setActiveMailbox(data.id)" :class="activeMailbox == data.id && 'active'">
|
||||
{{ data.mail }}
|
||||
<div class="container" @click="setactiveAccount(data.id)" :class="activeAccount == data.id && 'active'">
|
||||
{{ data.email }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,17 +8,17 @@
|
||||
import { mapMutations, mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Mailbox',
|
||||
name: 'Account',
|
||||
components: {
|
||||
},
|
||||
props: {
|
||||
data: {mail: String, id: Number}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['activeMailbox'])
|
||||
...mapState(['activeAccount'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['setActiveMailbox'])
|
||||
...mapMutations(['setactiveAccount'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -4,30 +4,34 @@
|
||||
<!-- deconnect -->
|
||||
</div>
|
||||
<span class="divider"></span>
|
||||
<Mailbox :data="{'id': -1, 'mail': 'all'}"/>
|
||||
<Mailbox v-for="(mailbox, index) in mailboxes" :key="index" :data="mailbox"/>
|
||||
<Account v-for="(account, index) in accounts" :key="index" :data="account"/>
|
||||
<span class="divider"></span>
|
||||
|
||||
<AddMailboxModal />
|
||||
<AddAccountModal />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Mailbox from './Mailbox'
|
||||
import AddMailboxModal from '../../modals/AddMailboxModal'
|
||||
import Account from './Account'
|
||||
import AddAccountModal from '../../modals/AddAccountModal'
|
||||
import store from '@/store/store'
|
||||
|
||||
export default {
|
||||
name: 'Mailboxes',
|
||||
name: 'Accounts',
|
||||
components: {
|
||||
Mailbox,
|
||||
AddMailboxModal
|
||||
Account,
|
||||
AddAccountModal
|
||||
},
|
||||
computed: {
|
||||
...mapState(['mailboxes'])
|
||||
...mapState(['accounts'])
|
||||
},
|
||||
created() {
|
||||
store.dispatch('fetchAccounts');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
93
front/src/views/sidebar/rooms/Room.vue
Normal file
93
front/src/views/sidebar/rooms/Room.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<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";
|
||||
import { decodeEmojis } from "@/utils/string";
|
||||
|
||||
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">{{ decodeEmojis(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>
|
||||
@@ -1,32 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<User v-for="(room, index) in folderRooms()" :key="index" :sender="room.users" :object="room.object" />
|
||||
<div class="test">
|
||||
<Room v-for="(room, index) in rooms()" :key="index" :data="room" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import User from './User'
|
||||
import Room from './Room'
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
name: 'Rooms',
|
||||
props: {
|
||||
|
||||
},
|
||||
components: {
|
||||
User
|
||||
Room
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['folderRooms'])
|
||||
...mapGetters(['rooms'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
.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>
|
||||
16
front/src/views/sidebar/rooms/threads/ThreadList.vue
Normal file
16
front/src/views/sidebar/rooms/threads/ThreadList.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import Thread from './Thread.vue';
|
||||
import { defineProps } from 'vue'
|
||||
const props = defineProps({ threads: [Object] });
|
||||
console.log(props.threads)
|
||||
</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,17 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'ThreadList',
|
||||
props: {
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
8568
front/yarn.lock
8568
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"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
26
src/App.vue
26
src/App.vue
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<img alt="Vue logo" src="./assets/logo.png">
|
||||
<HelloWorld msg="Welcome to Your Vue.js App"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
HelloWorld
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -1,4 +0,0 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true
|
||||
})
|
||||
Reference in New Issue
Block a user