Compare commits

..

3 Commits

Author SHA1 Message Date
grimhilt
cd996d851a sync flags on server start 2023-04-17 13:08:54 +02:00
grimhilt
0f063deff9 minor fixes 2023-04-16 15:22:49 +02:00
grimhilt
614f7d9802 routes of update flags with imap 2023-04-16 12:31:46 +02:00
12 changed files with 190 additions and 38 deletions

View File

@ -1,15 +1,54 @@
import statusCode from "../utils/statusCodes"; import statusCode from "../utils/statusCodes";
import { Response } from "express"; import { Response } from "express";
import { getMessageUid, getUserOfMailbox } from "../db/utils/mail";
import emailManager from "../mails/EmailManager";
export default class Message { export default class Message {
static async addFlag(body, res: Response) { static async addFlag(body, res: Response) {
console.log("hit") const { mailboxId, messageId, flag } = body;
const uid = (await getMessageUid(messageId))[0]?.uid;
if (!uid) {
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
}
const user = (await getUserOfMailbox(mailboxId))[0]?.user;
if (!user) {
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
}
emailManager
.getImap(user)
.getMailbox(mailboxId)
.addFlag(uid.toString(), [flag])
.then(() => {
res.status(statusCode.OK).send(); res.status(statusCode.OK).send();
})
.catch((err) => {
console.log(err);
res.status(statusCode.METHOD_FAILURE).send({ error: err });
});
} }
static async removeFlag(body, res: Response) { static async removeFlag(body, res: Response) {
console.log("hit") const { mailboxId, messageId, flag } = body;
const uid = (await getMessageUid(messageId))[0]?.uid;
if (!uid) {
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
}
const user = (await getUserOfMailbox(mailboxId))[0]?.user;
if (!user) {
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
}
emailManager
.getImap(user)
.getMailbox(mailboxId)
.removeFlag(uid.toString(), [flag])
.then(() => {
res.status(statusCode.OK).send(); res.status(statusCode.OK).send();
})
.catch((err) => {
console.log(err);
res.status(statusCode.METHOD_FAILURE).send({ error: err });
});
} }
} }

View File

@ -122,7 +122,7 @@ export async function getMessages(roomId: number) {
WHERE msg.room_id = ? WHERE msg.room_id = ?
GROUP BY msg.message_id GROUP BY msg.message_id
ORDER BY message.idate DESC; ORDER BY message.idate ASC;
`; `;
const values = [roomId]; const values = [roomId];
return await execQueryAsync(query, values); return await execQueryAsync(query, values);

View File

@ -16,26 +16,38 @@ export async function getAllAccounts() {
return await execQueryAsync(query, values); return await execQueryAsync(query, values);
} }
export async function getAllMailboxes(accountId) { export async function getAllMailboxes(accountId: number) {
const query = 'SELECT * FROM mailbox WHERE mailbox.account_id = ?'; const query = "SELECT * FROM mailbox WHERE mailbox.account_id = ?";
const values = [accountId]; const values = [accountId];
return await execQueryAsync(query, values) return await execQueryAsync(query, values);
} }
export async function registerMailbox(accountId, mailboxName) { export async function registerMailbox(accountId: number, mailboxName: string) {
const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`; const query = `INSERT INTO mailbox (account_id, mailbox_name) VALUES (?, ?)`;
const values = [accountId, mailboxName]; const values = [accountId, mailboxName];
return await execQueryAsyncWithId(query, values); return await execQueryAsyncWithId(query, values);
} }
export async function getMailbox(mailboxId) { export async function getMailbox(mailboxId: number) {
const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`; const query = `SELECT * FROM mailbox WHERE mailbox_id = ?`;
const values = [mailboxId]; const values = [mailboxId];
return await execQueryAsync(query, values); return await execQueryAsync(query, values);
} }
export function updateMailbox(mailboxId, uidnext) { export function updateMailbox(mailboxId: number, uidnext: number) {
const query = `UPDATE mailbox SET uidnext = ? WHERE mailbox_id = ?`; const query = `UPDATE mailbox SET uidnext = ? WHERE mailbox_id = ?`;
const values = [uidnext, mailboxId]; const values = [uidnext, mailboxId];
execQuery(query, values); execQuery(query, values);
} }
export async function updateMailboxModseq(mailboxId: number, modseq: number) {
const query = `UPDATE mailbox SET nextmodseq = ? WHERE mailbox_id = ?`;
const values = [modseq, mailboxId];
return await execQueryAsync(query, values);
}
export async function getMailboxModseq(mailboxId: number): Promise<{ modseq: number }[]> {
const query = `SELECT nextmodseq AS modseq FROM mailbox WHERE mailbox_id = ?`;
const values = [mailboxId];
return await execQueryAsync(query, values);
}

View File

@ -34,6 +34,12 @@ export async function getMessageIdOnUid(uid: number): Promise<{ message_id: numb
return await execQueryAsync(query, values); return await execQueryAsync(query, values);
} }
export async function getMessageUid(messageId: number): Promise<{uid: number}[]> {
const query = `SELECT uid FROM mailbox_message WHERE message_id = ?`;
const values = [messageId];
return await execQueryAsync(query, values);
}
export async function findRoomByOwner(ownerId: number): Promise<{ room_id: number }[]> { export async function findRoomByOwner(ownerId: number): Promise<{ room_id: number }[]> {
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`; const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
const values = [ownerId]; const values = [ownerId];
@ -50,3 +56,15 @@ export async function getUserIdOfMailbox(boxId: number): Promise<{ user_id: numb
const values = [boxId]; const values = [boxId];
return await execQueryAsync(query, values); return await execQueryAsync(query, values);
} }
export async function getUserOfMailbox(mailboxId: number): Promise<{ user: string }[]> {
const query = `
SELECT address.email AS user
FROM mailbox
INNER JOIN app_account ON app_account.account_id = mailbox.account_id
INNER JOIN address on address.address_id = app_account.user_id
WHERE mailbox.mailbox_id = ?
`;
const values = [mailboxId];
return await execQueryAsync(query, values);
}

View File

@ -42,7 +42,11 @@ class EmailManager {
} }
getSmtp(email: string): SmtpInstance | undefined { getSmtp(email: string): SmtpInstance | undefined {
return this.smtpInstances.find((instance) => instance.user == email); return this.smtpInstances.find((instance) => instance.user === email);
}
getImap(email: string): ImapInstance | undefined {
return this.imapInstances.find((instance) => instance.account.user === email);
} }
} }

View File

@ -23,7 +23,7 @@ export class ImapInstance {
this.boxes = []; this.boxes = [];
/** /**
* IMAP * IMAP init
*/ */
this.imap.once("ready", () => { this.imap.once("ready", () => {
logger.log("Imap connected for " + this.account.user); logger.log("Imap connected for " + this.account.user);
@ -59,7 +59,7 @@ export class ImapInstance {
getAllBox(boxes) { getAllBox(boxes) {
// ideally we should get the all box to get all messages // ideally we should get the all box to get all messages
let allBox = ''; let allBox = "";
Object.keys(boxes).forEach((key) => { Object.keys(boxes).forEach((key) => {
if (key === "INBOX") return; if (key === "INBOX") return;
if (allBox.includes("/")) return; // already found if (allBox.includes("/")) return; // already found
@ -74,4 +74,8 @@ export class ImapInstance {
if (!allBox.includes("/")) logger.warn("Did not find 'All' mailbox"); if (!allBox.includes("/")) logger.warn("Did not find 'All' mailbox");
return allBox; return allBox;
} }
getMailbox(mailboxId: number): Mailbox {
return this.boxes.find((box) => box.id === mailboxId);
}
} }

View File

@ -1,5 +1,5 @@
import Imap, { ImapMessageAttributes, Box } from "imap"; import Imap, { ImapMessageAttributes, Box } from "imap";
import { getMailbox, updateMailbox } from "../../db/imap/imap-db"; import { getMailbox, getMailboxModseq, updateMailbox, updateMailboxModseq } from "../../db/imap/imap-db";
import { Attrs, AttrsWithEnvelope } from "../../interfaces/mail/attrs.interface"; import { Attrs, AttrsWithEnvelope } from "../../interfaces/mail/attrs.interface";
import logger from "../../system/Logger"; import logger from "../../system/Logger";
import RegisterMessageInApp from "../message/saveMessage"; import RegisterMessageInApp from "../message/saveMessage";
@ -20,7 +20,7 @@ export default class Mailbox {
msgToSync: number; msgToSync: number;
syncing: boolean; syncing: boolean;
constructor(_imap, _boxId, _boxName) { constructor(_imap: Imap, _boxId: number, _boxName: string) {
this.imap = _imap; this.imap = _imap;
this.boxName = _boxName; this.boxName = _boxName;
this.id = _boxId; this.id = _boxId;
@ -33,37 +33,70 @@ export default class Mailbox {
async init() { async init() {
// get mailbox from the database // get mailbox from the database
this.box = (await getMailbox(this.id))[0]; this.box = (await getMailbox(this.id))[0];
const isReadOnly = false;
const readOnly = true; this.imap.openBox(this.boxName, isReadOnly, (err, box) => {
this.imap.openBox(this.boxName, readOnly, (err, box) => {
if (err) logger.err(err); if (err) logger.err(err);
// sync only if has new messages // sync messages and flags
if (this.box.uidnext < box.uidnext) { this.initSync(box);
this.sync(this.box.uidnext, box.uidnext);
} else {
logger.log("Already up to date")
}
// wait for new mails // wait for new mails
this.imap.on("mail", (numNewMsgs: number) => { this.imap.on("mail", (numNewMsgs: number) => {
if (!this.syncing) { if (!this.syncing) {
// if not syncing restart a sync // if not syncing restart a sync
this.sync(this.box.uidnext, this.box.uidnext + numNewMsgs); this.syncMail(this.box.uidnext, this.box.uidnext + numNewMsgs);
} else { } else {
// else save number of message to sync latter // else save number of message to sync latter
this.msgToSync += numNewMsgs; this.msgToSync += numNewMsgs;
} }
}); });
// wait for flags update
this.imap.on("update", (seqno: number, info: ImapInfo) => { this.imap.on("update", (seqno: number, info: ImapInfo) => {
logger.log(`Update message ${info.uid} with ${info.flags}`);
const updateMsg = new updateMessage(info.uid, info.flags); const updateMsg = new updateMessage(info.uid, info.flags);
updateMsg.updateFlags(); updateMsg.updateFlags();
}); });
}); });
} }
async sync(savedUid: number, currentUid: number) { async updateModseq(newModseq: number) {
updateMailboxModseq(this.id, newModseq).then(() => {
this.box.highestmodseq = newModseq;
});
}
async initSync(box: Box) {
// sync mail only if has new messages
if (this.box.uidnext < box.uidnext) {
this.syncMail(this.box.uidnext, box.uidnext);
} else {
logger.log("Mail already up to date");
}
// sync flags
const lastModseq = (await getMailboxModseq(this.id))[0]?.modseq ?? 0;
if (box.highestmodseq > lastModseq) {
const fetchStream = this.imap.fetch("1:*", { bodies: "", modifiers: { changedsince: lastModseq } });
fetchStream.on("message", (message) => {
message.once("attributes", (attrs) => {
const updateMsg = new updateMessage(attrs.uid, attrs.flags);
updateMsg.updateFlags();
});
});
fetchStream.once("error", function (err) {
logger.err("Fetch error when syncing flags: " + err);
});
fetchStream.once("end", function () {
logger.log("Done fetching new flags");
});
} else {
logger.log("Flags already up to date")
}
this.updateModseq(box.highestmodseq);
}
async syncMail(savedUid: number, currentUid: number) {
this.syncing = true; this.syncing = true;
const promises: Promise<unknown>[] = []; const promises: Promise<unknown>[] = [];
const mails: Attrs[] = []; const mails: Attrs[] = [];
@ -75,7 +108,6 @@ export default class Mailbox {
f.on("message", (msg, seqno) => { f.on("message", (msg, seqno) => {
msg.once("attributes", (attrs: AttrsWithEnvelope) => { msg.once("attributes", (attrs: AttrsWithEnvelope) => {
console.log(attrs.envelope);
mails.push(attrs); mails.push(attrs);
promises.push(saveMessage(attrs, this.id, this.imap)); promises.push(saveMessage(attrs, this.id, this.imap));
}); });
@ -113,8 +145,32 @@ export default class Mailbox {
this.box.uidnext += this.msgToSync; this.box.uidnext += this.msgToSync;
// reset value to allow to detect new incoming message while syncing // reset value to allow to detect new incoming message while syncing
this.msgToSync = 0; this.msgToSync = 0;
this.sync(currentUid, this.box.uidnext); this.syncMail(currentUid, this.box.uidnext);
} }
}); });
} }
addFlag(source: string, flags: string[]): Promise<void> {
return new Promise((resolve, reject) => {
this.imap.addFlags(source, flags, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
removeFlag(source: string, flags: string[]): Promise<void> {
return new Promise((resolve, reject) => {
this.imap.delFlags(source, flags, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
} }

View File

@ -12,7 +12,8 @@ export default class updateMessage {
} }
async updateFlags() { async updateFlags() {
const messageId = (await getMessageIdOnUid(this.uid))[0].message_id; const messageId = (await getMessageIdOnUid(this.uid))[0]?.message_id;
if (!messageId) return;
const currentFlags = await getFlags(this.uid); const currentFlags = await getFlags(this.uid);
const flagsToAdd = this.flags.filter((flag) => !currentFlags.find((f) => flag == f.flag_name)); const flagsToAdd = this.flags.filter((flag) => !currentFlags.find((f) => flag == f.flag_name));

View File

@ -1,8 +1,9 @@
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
const app = express(); const app = express();
import ImapSync from "./mails/EmailManager";
import { execQueryAsync, execQuery } from "./db/db"; import { execQueryAsync, execQuery } from "./db/db";
import mailRouter from "./routes/mail";
import emailManager from "./mails/EmailManager";
app.use(express.json()); app.use(express.json());
app.use( app.use(
@ -13,8 +14,6 @@ app.use(
app.use(cors()); app.use(cors());
app.listen(process.env.PORT || 5500); app.listen(process.env.PORT || 5500);
import mailRouter from "./routes/mail";
import emailManager from "./mails/EmailManager";
app.use("/api/mail", mailRouter); app.use("/api/mail", mailRouter);
emailManager.init(); emailManager.init();

View File

@ -235,6 +235,7 @@ function sendMessage() {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 0 10px 10px 10px; padding: 0 10px 10px 10px;
max-height: 500px;
overflow: auto; overflow: auto;
} }

View File

@ -4,6 +4,7 @@ import { Message } from "@/store/models/model";
import API from "@/services/imapAPI"; import API from "@/services/imapAPI";
import store from "@/store/store"; import store from "@/store/store";
import { isSeenFc } from "@/utils/flagsUtils"; import { isSeenFc } from "@/utils/flagsUtils";
import SvgLoader from "@/components/utils/SvgLoader.vue";
const props = defineProps({ const props = defineProps({
msg: Object as PropType<Message>, msg: Object as PropType<Message>,
@ -33,7 +34,7 @@ const setFlag = (flag: string) => {
}; };
</script> </script>
<template> <template>
<div> <div id="main">
<div class="button" @click="setFlag('\\Seen')"> <div class="button" @click="setFlag('\\Seen')">
{{ isSeenFc(props.msg?.flags) ? "Mark as not read" : "Mark as read" }} {{ isSeenFc(props.msg?.flags) ? "Mark as not read" : "Mark as read" }}
</div> </div>
@ -44,14 +45,30 @@ const setFlag = (flag: string) => {
<div>transfer</div> <div>transfer</div>
<div>see source</div> <div>see source</div>
<div>{{ props.msg?.flags }}</div> <div>{{ props.msg?.flags }}</div>
<div class="icons">
<SvgLoader svg="flag-line" />
<SvgLoader svg="reply-line" />
<SvgLoader svg="delete-bin-4-line" />
<SvgLoader svg="delete-bin-6-line" />
<SvgLoader svg="share-forward-line" />
<SvgLoader svg="reply-all-line" />
<SvgLoader svg="mail-check-line" />
<SvgLoader svg="mail-unread-line" />
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
div { #main {
text-align: center; text-align: center;
} }
.icons {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.button { .button {
border: solid 1px; border: solid 1px;
border-radius: 6px; border-radius: 6px;

View File

@ -77,5 +77,6 @@ provide("room", room);
.messages { .messages {
flex-grow: 1; flex-grow: 1;
overflow: auto;
} }
</style> </style>