diff --git a/.gitignore b/.gitignore index c5afdb2..10b2933 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ reports .env *.eml test.log +data-headers.txt.raw diff --git a/configs/fake-smtp-log.jsonc b/configs/fake-smtp-log.jsonc new file mode 100644 index 0000000..ef51b1d --- /dev/null +++ b/configs/fake-smtp-log.jsonc @@ -0,0 +1,27 @@ +// Can replace fakeSMTP.jar +{ + "$schema": "https://raw.githubusercontent.com/loopingz/smtp-relay/main/config.schema.json", + "flows": { + "localhost": { + "filters": [ + // Allow only localhost + { + "type": "whitelist", + "ips": ["127.0.0.1"] + } + ], + "outputs": [ + { + // We just log to the console + "type": "log" + } + ] + } + }, + "port": 10026, + "options": { + "disableReverseLookup": false, + // Do not require auth + "authOptional": true + } +} diff --git a/configs/nodemailer.jsonc b/configs/nodemailer.jsonc new file mode 100644 index 0000000..ae0e268 --- /dev/null +++ b/configs/nodemailer.jsonc @@ -0,0 +1,38 @@ +// Can replace fakeSMTP.jar +{ + "$schema": "https://raw.githubusercontent.com/loopingz/smtp-relay/main/config.schema.json", + "flows": { + "localhost": { + "filters": [ + // Allow only localhost + { + "type": "whitelist", + "ips": ["127.0.0.1"] + } + ], + "outputs": [ + { + // We just log to the console + "type": "nodemailer", + "nodemailer": { + "host": "localhost", + "port": 10026, + "secure": false, + "auth": { + "user": "user", + "pass": "pass" + }, + "tls": { + "rejectUnauthorized": false + } + } + } + ] + } + }, + "options": { + "disableReverseLookup": false, + // Do not require auth + "authOptional": true + } +} diff --git a/src/index.ts b/src/index.ts index dba6d41..f9e10fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { NodeMailerProcessor } from "./processors/nodemailer"; import { SmtpServer } from "./server"; import { HttpAuthFilter } from "./filters/http-auth"; import { HttpFilter } from "./filters/http-filter"; +import { LogProcessor } from "./processors/log"; export * from "./cloudevent"; /** @@ -48,6 +49,10 @@ export function defaultModules() { * Send the email using PubSub or store it in a Bucket */ SmtpProcessor.register("gcp", GCPProcessor); + /** + * Log the email + */ + SmtpProcessor.register("log", LogProcessor); } // Cannot really test main module diff --git a/src/processors/gcp.spec.ts b/src/processors/gcp.spec.ts index 5a8d00b..c4d1ec7 100644 --- a/src/processors/gcp.spec.ts +++ b/src/processors/gcp.spec.ts @@ -6,6 +6,7 @@ import * as sinon from "sinon"; import { SmtpSession } from "../server"; import { getFakeSession } from "../server.spec"; import { GCPProcessor } from "./gcp"; +import { readFileSync } from "node:fs"; @suite class GCPProcessorTest { @@ -71,9 +72,16 @@ class GCPProcessorTest { } }; }); + session.user = "LZ"; await gcp.onMail(session); + delete session.user; assert.strictEqual(bucket, "test"); - assert.strictEqual(filename, "unit-test-fake-path"); + assert.ok(filename.endsWith("data-headers.txt.raw")); + const updatedData = readFileSync(filename).toString(); + assert.ok(updatedData.includes("X-smtp-relay-MAIL_FROM:test@test.com\n")); + assert.ok(updatedData.includes("X-smtp-relay-CLIENT_HOSTNAME:localhost\n")); + assert.ok(updatedData.includes("X-smtp-relay-HELO:localhost\n")); + assert.ok(updatedData.includes("X-smtp-relay-USER:LZ\n")); assert.strictEqual(destination, "1234.eml"); bucket = filename = destination = undefined; diff --git a/src/processors/log.ts b/src/processors/log.ts index 2e11026..2635042 100644 --- a/src/processors/log.ts +++ b/src/processors/log.ts @@ -8,7 +8,7 @@ export interface LogProcessorConfig extends SmtpComponentConfig { /** * Fields to log * - * @default ["from", "to", "cc", "subject", "text"] + * @default ["from", "to", "cc", "bcc", "subject", "text"] */ fields?: string[]; } @@ -17,7 +17,7 @@ export class LogProcessor ext type: string = "log"; init(): void { - this.config.fields ??= ["from", "to", "cc", "subject", "text"]; + this.config.fields ??= ["from", "to", "cc", "bcc", "subject", "text"]; } /** @@ -30,6 +30,7 @@ export class LogProcessor ext let content = `Email received ${new Date().toISOString()} from ${session.remoteAddress} ${"-".repeat(80)} `; + this.config.fields .filter(f => email[f] !== undefined) .forEach(f => { @@ -40,6 +41,6 @@ ${"-".repeat(80)} content += `${f}: ${value}\n`; }); content += `${"-".repeat(80)}\n`; - this.logger.log("INFO", content); + (this.logger ?? console).log("INFO", content); } } diff --git a/src/processors/nodemailer.spec.ts b/src/processors/nodemailer.spec.ts index 6c460fa..525ec7a 100644 --- a/src/processors/nodemailer.spec.ts +++ b/src/processors/nodemailer.spec.ts @@ -51,4 +51,56 @@ class NodeMailerProcessorTest { await nodemailer.onMail(session); assert.notStrictEqual(msg, undefined); } + + @test + async bccResolution() { + const session = getFakeSession(); + let nodemailer = new NodeMailerProcessor( + undefined, + { + type: "nodemailer", + nodemailer: { + host: "smtp.example.com", + port: 587, + secure: false, // upgrade later with STARTTLS + auth: { + user: "username", + pass: "password" + } + } + }, + new WorkerOutput() + ); + session.envelope.rcptTo = [ + { address: "bcc@test.com", args: [] }, + { address: "to@test.com", args: [] }, + { address: "cc@test.com", args: [] } + ]; + session.email.cc = [ + { + html: "", + text: "", + value: [{ name: "", address: "cc@test.com" }] + } + ]; + session.email.to = [ + { + html: "", + text: "", + value: [{ name: "", address: "to@test.com" }] + } + ]; + session.email.headerLines = [{ key: "plop", line: "test" }]; + NodeMailerProcessor.transformEmail(session); + assert.deepStrictEqual(session.email.bcc, { + html: "bcc@test.com", + text: "bcc@test.com", + value: [ + { + address: "bcc@test.com", + name: "" + } + ] + }); + } } diff --git a/src/processors/nodemailer.ts b/src/processors/nodemailer.ts index 5e2bb95..b46c8ff 100644 --- a/src/processors/nodemailer.ts +++ b/src/processors/nodemailer.ts @@ -1,7 +1,7 @@ import { AddressObject, ParsedMail } from "mailparser"; import * as nodemailer from "nodemailer"; import Mail from "nodemailer/lib/mailer"; -import AddressParser from "nodemailer/lib/addressparser"; +import AddressParser from "nodemailer/lib/addressparser/index"; import { SmtpComponentConfig } from "../component"; import { SmtpProcessor } from "../processor"; import { mapAddressObjects, SmtpSession } from "../server"; @@ -36,7 +36,7 @@ export class NodeMailerProcessor< return a.value.map(ad => ad.address); }; // If no headers we do not fill the bcc as we have no to,cc headers - if (session.email.headerLines.length && !session.email.bcc) { + if (session.email.headerLines?.length && !session.email.bcc) { const noBcc = [session.email.cc, session.email.to].flat().filter(a => a); const bcc: AddressObject[] = session.envelope.rcptTo .filter(a => { diff --git a/src/server.spec.ts b/src/server.spec.ts index 6d8bddd..a440a23 100644 --- a/src/server.spec.ts +++ b/src/server.spec.ts @@ -8,6 +8,7 @@ import { SmtpFilter } from "./filter"; import { SmtpFlow } from "./flow"; import { SmtpProcessor } from "./processor"; import { SmtpServer, SmtpSession, mapAddressObjects } from "./server"; +import { readdirSync, unlinkSync } from "node:fs"; export class SmtpTest { sock: Socket; @@ -168,6 +169,12 @@ class OpenFilter extends SmtpFilter { @suite class SmtpServerTest { + static after() { + readdirSync(".") + .filter(f => f.startsWith(".email_") && f.endsWith(".eml")) + .forEach(f => unlinkSync(f)); + } + @test async middlewareChaining() { defaultModules(); @@ -185,7 +192,10 @@ class SmtpServerTest { ); server.close(); // cov - assert.ok(Array.isArray(mapAddressObjects({value: [], text: "", html: ""}, () => {})), "Should always return an array"); + assert.ok( + Array.isArray(mapAddressObjects({ value: [], text: "", html: "" }, () => {})), + "Should always return an array" + ); // @ts-ignore server.addFlow({ name: "Test" }); // @ts-ignore @@ -252,7 +262,10 @@ class SmtpServerTest { ] }; // @ts-ignore - await server.onDataRead({ flows: { fake: "PENDING" }, envelope: { mailFrom: {address: "test@test.com", args: {}}, rcptTo: [{address: "ok.com", args: {}}] } }); + await server.onDataRead({ + flows: { fake: "PENDING" }, + envelope: { mailFrom: { address: "test@test.com", args: {} }, rcptTo: [{ address: "ok.com", args: {} }] } + }); } @test @@ -318,7 +331,7 @@ export function getFakeSession(): SmtpSession { secure: false, transmissionType: "TEST", time: new Date(), - emailPath: "unit-test-fake-path", + emailPath: import.meta.dirname + "/../tests/data-headers.txt", envelope: { mailFrom: { address: "test@test.com",