101 lines
No EOL
3.9 KiB
TypeScript
101 lines
No EOL
3.9 KiB
TypeScript
import {SMTPServer} from "smtp-server";
|
|
import {deriveNsecForEmail, getNDK} from "./utils";
|
|
import {NDKEvent, NDKKind, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk";
|
|
import {PrismaClient} from "@prisma/client";
|
|
import {logger} from "./utils/logs";
|
|
import {encryptEventForRecipient, parseEmail} from "@arx/utils";
|
|
|
|
export class NostrSmtpServer {
|
|
private server: SMTPServer;
|
|
|
|
constructor(db: PrismaClient, port: number) {
|
|
this.server = new SMTPServer({
|
|
authOptional: true,
|
|
logger: false,
|
|
|
|
onData: (stream, session, callback) => {
|
|
let mailData = '';
|
|
|
|
stream.on('data', (chunk: Buffer) => {
|
|
mailData += chunk.toString();
|
|
});
|
|
|
|
stream.on('end', async () => {
|
|
if (!session.envelope.mailFrom) {
|
|
logger.warn('Ignoring email without sender');
|
|
callback();
|
|
return;
|
|
}
|
|
try {
|
|
const parsedEmail = parseEmail(mailData);
|
|
for (let recipientEmail of session.envelope.rcptTo) {
|
|
const address = recipientEmail.address;
|
|
const parts = address.split('@');
|
|
if (parts[1] !== process.env.BASE_DOMAIN) {
|
|
logger.warn('Not sending email to', address, 'because it is not in the allowed domain');
|
|
continue;
|
|
}
|
|
const alias = parts[0];
|
|
const user = await db.alias.findUnique({
|
|
where: {
|
|
alias
|
|
},
|
|
include: {
|
|
user: true
|
|
}
|
|
});
|
|
if (!user) {
|
|
logger.warn('No user found for', alias, 'skipping');
|
|
continue;
|
|
}
|
|
const timeRemainingInSubscription = user.user.subscriptionDuration === null ? Infinity : (user.user.subscriptionDuration * 1000) - Date.now() + user.user.lastPayment.getTime();
|
|
if (timeRemainingInSubscription <= 0) {
|
|
logger.warn(`Subscription has expired for ${alias}`);
|
|
continue;
|
|
}
|
|
const recipient = user.npub;
|
|
const randomKeySinger = new NDKPrivateKeySigner(deriveNsecForEmail(
|
|
process.env.MASTER_NSEC!,
|
|
session.envelope.mailFrom?.address
|
|
));
|
|
const ndk = getNDK();
|
|
ndk.signer = randomKeySinger;
|
|
await ndk.connect();
|
|
const ndkUser = ndk.getUser({
|
|
npub: recipient
|
|
});
|
|
const randomKeyUser = await randomKeySinger.user();
|
|
const event = new NDKEvent();
|
|
event.kind = NDKKind.Article;
|
|
event.content = parsedEmail.body;
|
|
event.created_at = Math.floor(Date.now() / 1000);
|
|
event.pubkey = randomKeyUser.pubkey;
|
|
event.tags.push(['p', ndkUser.pubkey])
|
|
event.tags.push(['subject', parsedEmail.subject]);
|
|
event.tags.push(['email:localIP', session.localAddress]);
|
|
event.tags.push(['email:remoteIP', session.remoteAddress]);
|
|
event.tags.push(['email:isEmail', 'true']);
|
|
for (let to of session.envelope.rcptTo)
|
|
event.tags.push(['email:to', to.address]);
|
|
for (let header of Object.keys(parsedEmail.headers))
|
|
event.tags.push([`email:header:${header}`, parsedEmail.headers[header]]);
|
|
event.tags.push(['email:session', session.id]);
|
|
event.tags.push(['email:from', session.envelope.mailFrom?.address ?? '']);
|
|
|
|
await event.sign(randomKeySinger);
|
|
const encryptedEvent = await encryptEventForRecipient(ndk, event, ndkUser);
|
|
await encryptedEvent.publish();
|
|
}
|
|
} catch (e) {
|
|
logger.error(JSON.stringify(e));
|
|
} finally {
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
this.server.listen(port, '0.0.0.0');
|
|
logger.info(`SMTP Server running on port ${port}`);
|
|
}
|
|
} |