feat: mail

This commit is contained in:
2024-11-06 17:19:53 +01:00
parent 31c54083a9
commit 2d7906f8ee
16 changed files with 458 additions and 158 deletions

View File

@@ -29,6 +29,7 @@
"@openstapps/core-tools": "workspace:*",
"@openstapps/logger": "workspace:*",
"commander": "10.0.0",
"cors": "2.8.5",
"dotenv": "16.4.5",
"express": "4.18.2",
"imapflow": "1.0.162",
@@ -41,6 +42,7 @@
"@openstapps/eslint-config": "workspace:*",
"@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*",
"@types/cors": "2.8.13",
"@types/express": "4.17.17",
"@types/imapflow": "1.0.18",
"@types/mailparser": "3.4.4",

View File

@@ -1,36 +1,74 @@
import {config} from 'dotenv';
import {ImapFlow} from 'imapflow';
import {Logger} from '@openstapps/logger';
import {createHash} from 'node:crypto';
import express from 'express';
import cors from 'cors';
config({path: '.env.local'});
const app = express();
const port = process.env.PORT || 4000;
const maxClientAge = 10_000; // 10 seconds
const clients = new Map<string, {destroyRef: NodeJS.Timeout; client: Promise<ImapFlow>}>();
/**
*
*/
async function destroyClient(clientUid: string) {
const client = clients.get(clientUid);
if (!client) return;
clients.delete(clientUid);
clearTimeout(client.destroyRef);
try {
await client.client.then(it => it.logout());
} catch (error) {
await Logger.error(error);
}
}
app.use(cors());
app.use(async (request, response, next) => {
try {
const [user, pass] = Buffer.from(request.headers['authorization']!.replace(/^Basic /, ''), 'base64')
.toString('utf8')
.split(':');
const authorization = request.headers['authorization'];
if (!authorization) {
response.status(401).send();
return;
}
const client = new ImapFlow({
host: 'imap.server.uni-frankfurt.de',
port: 993,
secure: true,
emitLogs: false,
auth: {user, pass},
});
response.locals.client = client;
const clientUid = createHash('sha256').update(authorization).digest('hex');
await client.connect();
response.on('finish', async () => {
await client.logout();
client.close();
});
let client = clients.get(clientUid);
if (client === undefined) {
const [user, pass] = Buffer.from(authorization.replace(/^Basic /, ''), 'base64')
.toString('utf8')
.split(':');
const imapClient = new ImapFlow({
host: 'imap.server.uni-frankfurt.de',
port: 993,
secure: true,
emitLogs: false,
auth: {user, pass},
});
client = {
destroyRef: undefined as unknown as NodeJS.Timeout,
client: imapClient.connect().then(() => imapClient),
};
clients.set(clientUid, client);
}
clearTimeout(client.destroyRef);
client.destroyRef = setTimeout(() => destroyClient(clientUid), maxClientAge);
response.locals.client = await client.client;
next();
} catch {
response.status(401).send();
} catch (error) {
await Logger.error(error);
response.status(500).send();
}
});
@@ -41,52 +79,67 @@ app.get('/', async (_request, response) => {
app.get('/:mailbox', async (request, response) => {
try {
await response.locals.client.mailboxOpen(request.params.mailbox);
const since = Number(request.query.since) || undefined;
const preData = await response.locals.client.status(request.params.mailbox, {messages: true});
if (preData.messages === 0) {
response.json([]);
return;
}
const data = response.locals.client.fetch(
'1:*',
{},
{
// caution, BigInt can throw
changedSince: typeof since === 'string' ? BigInt(since) : undefined,
},
);
const messages = [];
for await (const message of data) {
messages.push(message.seq.toString());
}
response.json(messages);
response.json({messages: preData.messages});
} catch (error) {
console.error(error);
await Logger.error(error);
response.status(404).send();
}
});
app.get('/:mailbox/:id', async (request, response) => {
try {
await response.locals.client.mailboxOpen(request.params.mailbox);
await response.locals.client.mailboxOpen(request.params.mailbox, {readOnly: true});
const message = await response.locals.client.fetchOne(request.params.id, {
envelope: true,
labels: true,
flags: true,
bodyStructure: true,
bodyStructure: request.query.partial ? false : true,
});
response.json({
bodyStructure: message.bodyStructure,
bodyStructure: request.query.partial ? undefined : message.bodyStructure,
labels: [...(message.labels ?? [])],
flags: [...(message.flags ?? [])],
envelope: message.envelope,
seq: message.seq,
});
} catch (error) {
console.error(error);
await Logger.error(error);
response.status(404).send();
}
});
/**
*
*/
function parseFlags(query: Record<string, unknown>): string[] {
const rawFlags = query['flags'] ?? [];
const flagArray = Array.isArray(rawFlags) ? rawFlags : [rawFlags];
return flagArray.filter(it => typeof it === 'string');
}
app.post('/:mailbox/:id', async (request, response) => {
try {
await response.locals.client.mailboxOpen(request.params.mailbox, {readOnly: false});
response.json(await response.locals.client.messageFlagsAdd(request.params.id, parseFlags(request.query)));
} catch (error) {
await Logger.error(error);
response.status(404).send();
}
});
app.delete('/:mailbox/:id', async (request, response) => {
try {
await response.locals.client.mailboxOpen(request.params.mailbox, {readOnly: false});
if ('flags' in request.query) {
response.json(
await response.locals.client.messageFlagsRemove(request.params.id, parseFlags(request.query)),
);
} else {
response.json(await response.locals.client.messageDelete(request.params.id));
}
} catch (error) {
await Logger.error(error);
response.status(404).send();
}
});
@@ -113,11 +166,11 @@ app.get('/:mailbox/:id/:part', async (request, response) => {
});
}
} catch (error) {
console.error(error);
await Logger.error(error);
response.status(404).send();
}
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
Logger.info(`Server listening on port ${port}`);
});