mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-10 19:52:53 +00:00
feat: mail
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user