From 60134c3581386660ae379950868f86e0f9017d28 Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 3 Sep 2024 21:02:42 +0200 Subject: [PATCH] MailBuffer to easily wait for specific mail --- docs/api.md | 3 + src/lib/mailbuffer.ts | 67 ++++++++++++++++++ src/lib/mailserver.ts | 10 +++ test/mailbuffer.test.js | 149 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 src/lib/mailbuffer.ts create mode 100644 test/mailbuffer.test.js diff --git a/docs/api.md b/docs/api.md index cffcd058..dc6683e1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -132,6 +132,9 @@ The `close` and `delete` event subjects are reserved and cannot be used to wait **iterator(subject): AsyncIterator** - Generator to iterate over received email with matching event subject. Use an internal array to store received email even when not consumming. Don't forget to use `.return()` to close it. +**buffer(subject): MailBuffer** - Return a struct which store received emails. +Then **MailBuffer.next( (Mail) => bollean )** allows to wait for a specific `Mail` independant of the order of arrival. + ### Callbacks **on('new', callback)** - Event called when a new email is received. Callback diff --git a/src/lib/mailbuffer.ts b/src/lib/mailbuffer.ts new file mode 100644 index 00000000..19dc56e5 --- /dev/null +++ b/src/lib/mailbuffer.ts @@ -0,0 +1,67 @@ +"use strict"; + +import type { Attachment, Envelope, Mail, ParsedMail } from "./type"; +import type { MailServer } from "./mailserver"; + +interface Next { + filter: (Mail) => boolean; + resolve: (Mail) => any; + reject: (Error) => any; + consume: boolean; +} + +export class MailBuffer { + mails: Mail[] = []; + nexts: Next[] = []; + + close: () => any; + _receive: (Mail) => any; + + constructor(mailServer: MailServer, subject: String) { + this._receive = (mail) => { + this.mails.push(mail); + + for (const { filter, resolve, consume, ..._ } of this.nexts) { + const index = this.mails.findIndex(filter); + if (index > -1) { + resolve(this.mails[index]); + if (consume) { + this.mails.splice(index, 1); + } + } + } + }; + + this.close = () => { + mailServer.removeListener("close", this.close); + mailServer.removeListener(subject, this._receive); + + const error = new Error("Closing buffer"); + for (const { reject, ..._ } of this.nexts) { + reject(error); + } + }; + + mailServer.on(subject, this._receive); + mailServer.once("close", this.close); + } + + next(filter: (Mail) => boolean, consume: boolean = true): Promise { + return new Promise((resolve, reject) => { + const index = this.mails.findIndex(filter); + if (index > -1) { + resolve(this.mails[index]); + if (consume) { + this.mails.splice(index, 1); + } + } else { + this.nexts.push({ + filter, + resolve, + reject, + consume, + }); + } + }); + } +} diff --git a/src/lib/mailserver.ts b/src/lib/mailserver.ts index 903f8d2c..06e3d385 100644 --- a/src/lib/mailserver.ts +++ b/src/lib/mailserver.ts @@ -9,6 +9,7 @@ import type { ReadStream } from "fs"; import { calculateBcc } from "./helpers/bcc"; import { createOnAuthCallback } from "./helpers/smtp"; +import { MailBuffer } from "./mailbuffer"; import { parse as mailParser } from "./mailparser"; import { Outgoing } from "./outgoing"; import { SMTPServer } from "smtp-server"; @@ -131,6 +132,15 @@ export class MailServer { return inner(subject); } + /** + * Return a struct which store received emails. + * Then allow to obtain a `Promise` dependant on a predicate `(Mail) => boolean`. + * Allow to wait for `Mail` independant of their order of arrival. + */ + buffer(subject: string): MailBuffer { + return new MailBuffer(this, subject); + } + constructor( options?: MailServerOptions, mailEventSubjectMapper: (Mail) => string | undefined = (m) => m.to[0]?.address, diff --git a/test/mailbuffer.test.js b/test/mailbuffer.test.js new file mode 100644 index 00000000..f5698239 --- /dev/null +++ b/test/mailbuffer.test.js @@ -0,0 +1,149 @@ +/* global describe, it, before, after */ +"use strict"; + +/** + * MailDev - mailserver.js -- test the mailserver options + */ + +const assert = require("assert"); +const SMTPConnection = require("nodemailer/lib/smtp-connection"); +const MailServer = require("../dist/lib/mailserver").MailServer; +const nodemailer = require("nodemailer"); +const port = 9025; + +async function createTransporter(port, auth) { + return nodemailer.createTransport({ + port: port, + auth, + }); +} + +describe("MailBuffer", () => { + let mailServer; + let transporter; + + before(async () => { + mailServer = new MailServer({ + port: port, + auth: { user: "bodhi", pass: "surfing" }, + }); + await mailServer.listen(); + + transporter = await createTransporter(port, { type: "login", user: "bodhi", pass: "surfing" }); + return transporter.verify(); + }); + + after((done) => { + mailServer.close().finally(() => { + done(); + }); + }); + + const emailOpts = { + from: "johnny.utah@fbi.gov", + to: "bodhi@gmail.com", + subject: "Test", + html: "Test", + }; + + function sendMail(subject = emailOpts.subject) { + const mail = { + ...emailOpts, + subject, + }; + transporter.sendMail(mail); + return subject; + } + + it("should resolve when receiving email", async () => { + const buffer = mailServer.buffer(emailOpts.to); + const p = buffer.next((_) => true); + + sendMail(); + + const received = await p; + + assert.strictEqual(received.from[0]?.address, emailOpts.from); + assert.strictEqual(received.to[0]?.address, emailOpts.to); + assert.strictEqual(received.subject, emailOpts.subject); + + buffer.close(); + }); + + it("should resolve an already received email", async () => { + const buffer = mailServer.buffer(emailOpts.to); + + sendMail(); + await mailServer.next(emailOpts.to); + + const received = await buffer.next((_) => true); + + assert.strictEqual(received.from[0]?.address, emailOpts.from); + assert.strictEqual(received.to[0]?.address, emailOpts.to); + assert.strictEqual(received.subject, emailOpts.subject); + + buffer.close(); + }); + + it("should resolve out of order mails", async () => { + const buffer = mailServer.buffer(emailOpts.to); + + const subject1 = sendMail("Not Dropped1"); + const subject2 = sendMail("Not Dropped2"); + const subject3 = sendMail("Not Dropped3"); + const subject4 = sendMail("Not Dropped4"); + + const mail4 = await buffer.next((m) => m.subject === subject4); + assert.strictEqual(mail4.subject, subject4); + + const mail3 = await buffer.next((m) => m.subject === subject3); + assert.strictEqual(mail3.subject, subject3); + + const mail2 = await buffer.next((m) => m.subject === subject2); + assert.strictEqual(mail2.subject, subject2); + + const mail1 = await buffer.next((m) => m.subject === subject1); + assert.strictEqual(mail1.subject, subject1); + + buffer.close(); + }); + + it("should consume email by default", async () => { + const buffer = mailServer.buffer(emailOpts.to); + + sendMail(); + + const received = await buffer.next((_) => true); + assert.strictEqual(received.subject, emailOpts.subject); + + const rejected = buffer.next((_) => true); + buffer.close(); + await assert.rejects(rejected); + }); + + it("should not consume if specified", async () => { + const buffer = mailServer.buffer(emailOpts.to); + + sendMail(); + + const received1 = await buffer.next((_) => true, false); + assert.strictEqual(received1.subject, emailOpts.subject); + const received2 = await buffer.next((_) => true, true); + assert.strictEqual(received2.subject, emailOpts.subject); + const rejected = buffer.next((_) => true); + + buffer.close(); + await assert.rejects(rejected); + }); + + it("should reject promises when closing", async () => { + const buffer = mailServer.buffer(emailOpts.to); + const p1 = buffer.next((_) => true); + const p2 = buffer.next((_) => true); + + buffer.close(); + + await assert.rejects(p1); + await assert.rejects(p2); + }); +});