UNPKG

@adonisjs/mail

Version:

Mail provider for adonis framework and has support for all common mailing services to send emails

1,602 lines (1,594 loc) 43.9 kB
import { JSONTransport } from "./chunk-F3J2XAU5.js"; import { MemoryQueueMessenger } from "./chunk-TDHID2NL.js"; import { stubsRoot } from "./chunk-ASVKPASJ.js"; import { debug_default } from "./chunk-ZF2M7BIF.js"; import { __name } from "./chunk-XE4OXN2W.js"; // src/message.ts import { basename } from "node:path"; import { fileURLToPath } from "node:url"; import Macroable from "@poppinss/macroable"; import { AssertionError } from "node:assert"; import { cuid } from "@adonisjs/core/helpers"; import { RuntimeException } from "@poppinss/utils"; import ical from "ical-generator"; var Message = class _Message extends Macroable { static { __name(this, "Message"); } static templateEngine; /** * Use the configured template engine to compute * the email contents from templates */ static async computeContentsFor({ message, views }) { const getTemplateEngine = /* @__PURE__ */ __name(() => { if (!this.templateEngine) { throw new RuntimeException("Cannot render email templates without a template engine"); } return this.templateEngine; }, "getTemplateEngine"); const viewHelpers = { embedImage: /* @__PURE__ */ __name((filePath, options) => { const cid = cuid(); message.attachments = message.attachments || []; message.attachments.push({ path: filePath, cid, filename: basename(filePath), ...options }); return `cid:${cid}`; }, "embedImage"), embedImageData: /* @__PURE__ */ __name((data, options) => { const cid = cuid(); message.attachments = message.attachments || []; message.attachments.push({ content: data, cid, ...options }); return `cid:${cid}`; }, "embedImageData") }; if (!message.html && views.html) { debug_default("computing mail html contents %O", views.html); message.html = await getTemplateEngine().render(views.html.template, viewHelpers, views.html.data); } if (!message.text && views.text) { debug_default("computing mail text contents %O", views.text); message.text = await getTemplateEngine().render(views.text.template, {}, views.text.data); } if (!message.watch && views.watch) { debug_default("computing mail watch contents %O", views.watch); message.watch = await getTemplateEngine().render(views.watch.template, viewHelpers, views.watch.data); } } /** * Nodemailer internally mutates the "attachments" object * and removes the path property with the contents of * the file. * * Therefore, we need an additional attachment array we can * use to searching attachments and writing assertions */ #attachmentsForSearch = []; /** * Templates to use for rendering email body for * HTML, plain text and watch */ contentViews = {}; /** * Reference to the underlying node mailer message */ nodeMailerMessage = {}; /** * Converts a recipient email and name to formatted * string */ formatRecipient(recipient) { if (!recipient) { return void 0; } if (typeof recipient === "string") { return recipient; } if (!recipient.name) { return recipient.address; } return `${recipient.name} <${recipient.address}>`; } /** * Check if a given recipient exists for the mentioned * email and name. */ hasRecipient(property, address, name) { const recipients = this.nodeMailerMessage[property]; if (!recipients) { return false; } if (name) { return !!recipients.find((recipient) => { if (typeof recipient === "string") { return false; } return recipient.address === address && recipient.name === name; }); } return !!recipients.find((recipient) => { if (typeof recipient === "string") { return recipient === address; } return recipient.address === address; }); } /** * Assert the message is sent to the mentioned address */ assertRecipient(property, address, name) { if (!this.hasRecipient(property, address, name)) { const expected = this.formatRecipient({ address, name: name || "" }); const actual = this.nodeMailerMessage[property]?.map((recipient) => { return this.formatRecipient(recipient); }) || []; throw new AssertionError({ message: `Expected message to be delivered to "${expected}"`, expected: [ expected ], actual, operator: "includes" }); } } /** * Add recipient as `to` */ to(address, name) { this.nodeMailerMessage.to = this.nodeMailerMessage.to || []; this.nodeMailerMessage.to.push(name ? { address, name } : address); return this; } /** * Check if message is sent to the mentioned address */ hasTo(address, name) { return this.hasRecipient("to", address, name); } /** * Assert the message is sent to the mentioned address */ assertTo(address, name) { return this.assertRecipient("to", address, name); } /** * Add `from` name and email */ from(address, name) { this.nodeMailerMessage.from = name ? { address, name } : address; return this; } /** * Check if message is sent from the mentioned address */ hasFrom(address, name) { const fromAddress = this.nodeMailerMessage.from; if (!fromAddress) { return false; } if (name) { if (typeof fromAddress === "string") { return false; } return fromAddress.address === address && fromAddress.name === name; } if (typeof fromAddress === "string") { return fromAddress === address; } return fromAddress.address === address; } /** * Assert the message is sent from the mentioned address */ assertFrom(address, name) { if (!this.hasFrom(address, name)) { const expected = this.formatRecipient({ address, name: name || "" }); const actual = this.formatRecipient(this.nodeMailerMessage.from); throw new AssertionError({ message: `Expected message to be sent from "${expected}"`, expected, actual }); } } cc(addresses, name) { this.nodeMailerMessage.cc = this.nodeMailerMessage.cc || []; if (typeof addresses === "string") { this.nodeMailerMessage.cc.push(name ? { address: addresses, name } : addresses); } else { addresses.forEach((address) => { this.nodeMailerMessage.cc.push(address); }); } return this; } /** * Check if message is sent to the mentioned address */ hasCc(address, name) { return this.hasRecipient("cc", address, name); } /** * Assert the message is sent to the mentioned address */ assertCc(address, name) { return this.assertRecipient("cc", address, name); } bcc(addresses, name) { this.nodeMailerMessage.bcc = this.nodeMailerMessage.bcc || []; if (typeof addresses === "string") { this.nodeMailerMessage.bcc.push(name ? { address: addresses, name } : addresses); } else { addresses.forEach((address) => { this.nodeMailerMessage.bcc.push(address); }); } return this; } /** * Check if message is sent to the mentioned address */ hasBcc(address, name) { return this.hasRecipient("bcc", address, name); } /** * Assert the message is sent to the mentioned address */ assertBcc(address, name) { return this.assertRecipient("bcc", address, name); } /** * Define custom message id */ messageId(messageId) { this.nodeMailerMessage.messageId = messageId; return this; } /** * Define subject */ subject(message) { this.nodeMailerMessage.subject = message; return this; } /** * Check if the message has the mentioned subject */ hasSubject(message) { return !!this.nodeMailerMessage.subject && this.nodeMailerMessage.subject === message; } /** * Assert the message has the mentioned subject */ assertSubject(message) { if (!this.hasSubject(message)) { throw new AssertionError({ message: `Expected message subject to be "${message}"`, expected: message, actual: this.nodeMailerMessage.subject }); } } /** * Define replyTo email and name */ replyTo(address, name) { this.nodeMailerMessage.replyTo = this.nodeMailerMessage.replyTo || []; this.nodeMailerMessage.replyTo.push(name ? { address, name } : address); return this; } /** * Check if the mail has the mentioned reply to address */ hasReplyTo(address, name) { return this.hasRecipient("replyTo", address, name); } /** * Assert the mail has the mentioned reply to address */ assertReplyTo(address, name) { if (!this.hasRecipient("replyTo", address, name)) { const expected = this.formatRecipient({ address, name: name || "" }); const actual = this.nodeMailerMessage.replyTo?.map((recipient) => { return this.formatRecipient(recipient); }) || []; throw new AssertionError({ message: `Expected reply-to addresses to include "${expected}"`, expected: [ expected ], actual, operator: "includes" }); } } /** * Define inReplyTo message id */ inReplyTo(messageId) { this.nodeMailerMessage.inReplyTo = messageId; return this; } /** * Define multiple message id's as references */ references(messagesIds) { this.nodeMailerMessage.references = messagesIds; return this; } /** * Optionally define email envolpe */ envelope(envelope) { this.nodeMailerMessage.envelope = envelope; return this; } /** * Define contents encoding */ encoding(encoding) { this.nodeMailerMessage.encoding = encoding; return this; } /** * Define email prority */ priority(priority) { this.nodeMailerMessage.priority = priority; return this; } /** * Compute email html from defined view */ htmlView(template, data) { this.contentViews.html = { template, data }; return this; } /** * Compute email text from defined view */ textView(template, data) { this.contentViews.text = { template, data }; return this; } /** * Compute apple watch html from defined view */ watchView(template, data) { this.contentViews.watch = { template, data }; return this; } /** * Compute email html from raw text */ html(content) { this.nodeMailerMessage.html = content; return this; } /** * Compute email text from raw text */ text(content) { this.nodeMailerMessage.text = content; return this; } /** * Compute email watch html from raw text */ watch(content) { this.nodeMailerMessage.watch = content; return this; } /** * Assert content of mail to include substring or match * a given regular expression */ assertContent(property, substring) { const contents = this.nodeMailerMessage[property]; if (!contents) { throw new AssertionError({ message: `Expected message ${property} body to match substring, but it is undefined` }); } if (typeof substring === "string") { if (!String(contents).includes(substring)) { throw new AssertionError({ message: `Expected message ${property} body to include "${substring}"` }); } return; } if (!substring.test(String(contents))) { throw new AssertionError({ message: `Expected message ${property} body to match "${substring}"` }); } } /** * Assert message plain text contents to include * substring or match the given regular expression */ assertTextIncludes(substring) { return this.assertContent("text", substring); } /** * Assert message HTML contents to include substring * or match the given regular expression */ assertHtmlIncludes(substring) { return this.assertContent("html", substring); } /** * Assert message watch contents to include substring * or match the given regular expression */ assertWatchIncludes(substring) { return this.assertContent("watch", substring); } /** * Define one or attachments */ attach(file, options) { const filePath = typeof file === "string" ? file : fileURLToPath(file); this.nodeMailerMessage.attachments = this.nodeMailerMessage.attachments || []; this.nodeMailerMessage.attachments.push({ path: filePath, filename: basename(filePath), ...options }); this.#attachmentsForSearch.push({ path: filePath, filename: basename(filePath), ...options }); return this; } hasAttachment(file, options) { const attachments = this.#attachmentsForSearch; if (typeof file === "function") { return !!attachments.find(file); } const filePath = typeof file === "string" ? file : fileURLToPath(file); return !!attachments.find((attachment) => { const hasMatchingPath = attachment.path ? String(attachment.path).endsWith(filePath) : false; if (!options) { return hasMatchingPath; } if (options.filename && attachment.filename !== options.filename) { return false; } if (options.cid && attachment.cid !== options.cid) { return false; } return true; }); } assertAttachment(file, options) { if (typeof file === "function") { if (!this.hasAttachment(file)) { throw new AssertionError({ message: `Expected assertion callback to find an attachment` }); } return; } if (!this.hasAttachment(file, options)) { throw new AssertionError({ message: `Expected message attachments to include "${file}"`, expected: [ { path: file, ...options } ], actual: this.nodeMailerMessage.attachments, operator: "includes" }); } } /** * Define attachment from raw data */ attachData(content, options) { this.nodeMailerMessage.attachments = this.nodeMailerMessage.attachments || []; this.nodeMailerMessage.attachments.push({ content, ...options }); return this; } /** * Embed attachment inside content using `cid` */ embed(file, cid, options) { const filePath = typeof file === "string" ? file : fileURLToPath(file); this.nodeMailerMessage.attachments = this.nodeMailerMessage.attachments || []; this.nodeMailerMessage.attachments.push({ path: filePath, cid, filename: basename(filePath), ...options }); this.#attachmentsForSearch.push({ path: filePath, cid, filename: basename(filePath), ...options }); return this; } /** * Embed attachment from raw data inside content using `cid` */ embedData(content, cid, options) { this.nodeMailerMessage.attachments = this.nodeMailerMessage.attachments || []; this.nodeMailerMessage.attachments.push({ content, cid, ...options }); return this; } /** * Define custom headers for email */ header(key, value) { if (!this.nodeMailerMessage.headers) { this.nodeMailerMessage.headers = {}; } if (!Array.isArray(this.nodeMailerMessage.headers)) { this.nodeMailerMessage.headers[key] = value; } return this; } /** * Check if a header has been defined and optionally * check for values as well. */ hasHeader(key, value) { const headers = this.nodeMailerMessage.headers; if (!headers || Array.isArray(headers)) { return false; } const headerValue = headers[key]; if (!headerValue) { return false; } if (value) { return !!(Array.isArray(value) ? value : [ value ]).every((one) => { return typeof headerValue === "string" ? headerValue === one : Array.isArray(headerValue) ? headerValue.includes(one) : headerValue.value === one; }); } return true; } /** * Assert a header has been defined and optionally * check for values as well. */ assertHeader(key, value) { if (!this.hasHeader(key, value)) { const headers = this.nodeMailerMessage.headers; const actual = headers && !Array.isArray(headers) ? headers[key] : void 0; if (!value || !actual) { throw new AssertionError({ message: `Expected message headers to include "${key}"` }); } throw new AssertionError({ message: `Expected message headers to include "${key}" with value "${value}"`, actual, expected: value }); } } /** * Define custom prepared headers for email */ preparedHeader(key, value) { if (!this.nodeMailerMessage.headers) { this.nodeMailerMessage.headers = {}; } if (!Array.isArray(this.nodeMailerMessage.headers)) { this.nodeMailerMessage.headers[key] = { prepared: true, value }; } return this; } /** * Defines a `List-` prefix header on the email. Calling * this method multiple times for the same key will * override the old value. */ addListHeader(key, value) { this.nodeMailerMessage.list = this.nodeMailerMessage.list || {}; this.nodeMailerMessage.list[key] = value; return this; } /** * Add `List-Help` header. Calling this method multiple * times will override the existing value */ listHelp(value) { return this.addListHeader("help", value); } /** * Add `List-Unsubscribe` header. Calling this method multiple * times will override the existing value */ listUnsubscribe(value) { return this.addListHeader("unsubscribe", value); } /** * Add `List-Subscribe` header. Calling this method multiple * times will override the existing value */ listSubscribe(value) { return this.addListHeader("subscribe", value); } /** * Attach a calendar event and define contents as string */ icalEvent(contents, options) { if (typeof contents === "function") { const calendar = ical(); contents(calendar); contents = calendar.toString(); } this.nodeMailerMessage.icalEvent = { content: contents, ...options }; return this; } /** * Attach a calendar event and load contents from a file */ icalEventFromFile(file, options) { const filePath = typeof file === "string" ? file : fileURLToPath(file); this.nodeMailerMessage.icalEvent = { path: filePath, ...options }; return this; } /** * Attach a calendar event and load contents from a url */ icalEventFromUrl(url, options) { this.nodeMailerMessage.icalEvent = { href: url, ...options }; return this; } /** * Computes email contents by rendering the configured * templates */ async computeContents() { await _Message.computeContentsFor({ message: this.nodeMailerMessage, views: this.contentViews }); } /** * Object representation of the message */ toObject() { return { message: this.nodeMailerMessage, views: { ...this.contentViews } }; } /** * JSON representation of the message */ toJSON() { return this.toObject(); } }; // src/base_mail.ts var BaseMail = class { static { __name(this, "BaseMail"); } /** * A flag to avoid build email message for * multiple times */ built = false; /** * Reference to the mail message object */ message = new Message(); /** * Define the email subject */ subject; /** * Define the from address for the email */ from; /** * Define the replyTo email address */ replyTo; /** * Defines the subject on the message using the mail * class subject property */ defineSubject() { if (this.subject) { this.message.subject(this.subject); } } /** * Defines the from on the message using the mail * class from property */ defineSender() { if (this.from) { typeof this.from === "string" ? this.message.from(this.from) : this.message.from(this.from.address, this.from.name); } if (this.replyTo) { typeof this.replyTo === "string" ? this.message.replyTo(this.replyTo) : this.message.replyTo(this.replyTo.address, this.replyTo.name); } } /** * Builds the mail message for sending it */ async build() { if (this.built) { return; } this.built = true; this.defineSubject(); this.defineSender(); await this.prepare(); } /** * Builds the mail message with the email contents. * This method will render the templates ahead of * time */ async buildWithContents() { if (this.built) { return; } await this.build(); await this.message.computeContents(); } /** * Sends the mail */ async send(mailer, config) { await this.build(); return mailer.sendCompiled(this.message.toObject(), config); } /** * Sends the mail by using the background * messenger */ async sendLater(mailer, config) { await this.build(); return mailer.sendLaterCompiled(this.message.toObject(), config); } }; // src/mailer.ts var Mailer = class { static { __name(this, "Mailer"); } name; transport; config; /** * Reference to AdonisJS application emitter */ #emitter; /** * Messenger to use for queuing emails */ #messenger; constructor(name, transport, emitter, config = {}) { this.name = name; this.transport = transport; this.config = config; this.#emitter = emitter; this.#messenger = new MemoryQueueMessenger(this, this.#emitter); } /** * Configure the messenger to use for sending email asynchronously */ setMessenger(messenger) { this.#messenger = messenger; return this; } /** * Sends a compiled email using the underlying transport */ async sendCompiled(mail, sendConfig) { if (!mail.message.from && this.config.from) { mail.message.from = this.config.from; } if (!mail.message.replyTo && this.config.replyTo) { mail.message.replyTo = [ this.config.replyTo ]; } this.#emitter.emit("mail:sending", { ...mail, mailerName: this.name }); await Message.computeContentsFor(mail); debug_default('sending email, subject "%s"', mail.message.subject); const response = await this.transport.send(mail.message, sendConfig); debug_default('email sent, message id "%s"', response.messageId); this.#emitter.emit("mail:sent", { ...mail, mailerName: this.name, response }); return response; } /** * Queues a compiled email */ async sendLaterCompiled(compiledMessage, sendConfig) { this.#emitter.emit("mail:queueing", { ...compiledMessage, mailerName: this.name }); debug_default("queueing email"); const metaData = await this.#messenger.queue(compiledMessage, sendConfig); this.#emitter.emit("mail:queued", { ...compiledMessage, metaData, mailerName: this.name }); } /** * Sends email */ async send(callbackOrMail, config) { if (callbackOrMail instanceof BaseMail) { return callbackOrMail.send(this, config); } const message = new Message(); if (this.config.from) { typeof this.config.from === "string" ? message.from(this.config.from) : message.from(this.config.from.address, this.config.from.name); } await callbackOrMail(message); const compiledMessage = message.toObject(); return this.sendCompiled(compiledMessage, config); } /** * Send an email asynchronously using the mail messenger. The * default messenger uses an in-memory queue, unless you have * configured a custom messenger. */ async sendLater(callbackOrMail, config) { if (callbackOrMail instanceof BaseMail) { return callbackOrMail.sendLater(this, config); } const message = new Message(); await callbackOrMail(message); const compiledMessage = message.toObject(); return this.sendLaterCompiled(compiledMessage, config); } /** * Invokes `close` method on the transport */ async close() { await this.transport.close?.(); } }; // configure.ts import string from "@poppinss/utils/string"; var ENV_VARIABLES = { smtp: [ "SMTP_HOST", "SMTP_PORT" ], ses: [ "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION" ], mailgun: [ "MAILGUN_API_KEY", "MAILGUN_DOMAIN" ], sparkpost: [ "SPARKPOST_API_KEY" ], resend: [ "RESEND_API_KEY" ], brevo: [ "BREVO_API_KEY" ] }; var KNOWN_TRANSPORTS = Object.keys(ENV_VARIABLES); async function configure(command) { let selectedTransports = command.parsedFlags.transports; if (!selectedTransports) { selectedTransports = await command.prompt.multiple("Select the mail services you want to use", KNOWN_TRANSPORTS, { validate(values) { return !values || !values.length ? "Please select one or more transports" : true; } }); } const transports2 = typeof selectedTransports === "string" ? [ selectedTransports ] : selectedTransports; const unknownTransport = transports2.find((transport) => !KNOWN_TRANSPORTS.includes(transport)); if (unknownTransport) { command.exitCode = 1; command.logger.logError(`Invalid transport "${unknownTransport}". Supported transports are: ${string.sentence(KNOWN_TRANSPORTS)}`); return; } const codemods = await command.createCodemods(); await codemods.makeUsingStub(stubsRoot, "config/mail.stub", { transports: transports2 }); await codemods.updateRcFile((rcFile) => { rcFile.addProvider("@adonisjs/mail/mail_provider"); rcFile.addCommand("@adonisjs/mail/commands"); }); await codemods.defineEnvVariables(transports2.reduce((result, transport) => { ENV_VARIABLES[transport].forEach((envVariable) => { result[envVariable] = ""; }); return result; }, {})); await codemods.defineEnvValidations({ leadingComment: "Variables for configuring the mail package", variables: transports2.reduce((result, transport) => { ENV_VARIABLES[transport].forEach((envVariable) => { result[envVariable] = "Env.schema.string()"; }); return result; }, {}) }); } __name(configure, "configure"); // src/fake_mailer.ts import string2 from "@poppinss/utils/string"; import { AssertionError as AssertionError2 } from "node:assert"; var MailsCollection = class MailsCollection2 { static { __name(this, "MailsCollection"); } #sent = []; #queued = []; trackSent(mail) { this.#sent.push(mail); } trackQueued(mail) { this.#queued.push(mail); } clear() { this.#sent = []; this.#queued = []; } /** * Returns a list of sent emails captured by the fake mailer */ sent(filterFn) { return filterFn ? this.#sent.filter(filterFn) : this.#sent; } /** * Returns a list of queued emails captured by the fake mailer */ queued(filterFn) { return filterFn ? this.#queued.filter(filterFn) : this.#queued; } /** * Assert the mentioned mail was sent during the fake * mode */ assertSent(mailConstructor, findFn) { const matchingMail = this.#sent.find((mail) => { if (!findFn) { return mail instanceof mailConstructor; } return mail instanceof mailConstructor && findFn(mail); }); if (!matchingMail) { throw new AssertionError2({ message: `Expected mail "${mailConstructor.name}" was not sent` }); } } /** * Assert the mentioned mail was NOT sent during the fake * mode */ assertNotSent(mailConstructor, findFn) { const matchingMail = this.#sent.find((mail) => { if (!findFn) { return mail instanceof mailConstructor; } return mail instanceof mailConstructor && findFn(mail); }); if (matchingMail) { throw new AssertionError2({ message: `Unexpected mail "${mailConstructor.name}" was sent` }); } } assertSentCount(mailConstructor, count) { if (typeof mailConstructor === "number") { const actual2 = this.#sent.length; const expected2 = mailConstructor; if (actual2 !== expected2) { throw new AssertionError2({ message: `Expected to send "${expected2}" ${string2.pluralize("mail", expected2)}, instead received "${actual2}" ${string2.pluralize("mail", actual2)}`, actual: actual2, expected: expected2 }); } return; } const actual = this.sent((mail) => mail instanceof mailConstructor).length; const expected = count; if (actual !== expected) { throw new AssertionError2({ message: `Expected "${mailConstructor.name}" to be sent "${expected}" ${string2.pluralize("time", expected)}, instead it was sent "${actual}" ${string2.pluralize("time", actual)}`, actual, expected }); } } /** * Assert zero emails were sent */ assertNoneSent() { if (this.#sent.length) { throw new AssertionError2({ message: `Expected zero mail to be sent, instead received "${this.#sent.length}" mail`, expected: [], actual: [ this.#sent.map((mail) => mail.constructor.name) ] }); } } /** * Assert the mentioned mail was queued during the fake * mode */ assertQueued(mailConstructor, findFn) { const matchingMail = this.#queued.find((mail) => { if (!findFn) { return mail instanceof mailConstructor; } return mail instanceof mailConstructor && findFn(mail); }); if (!matchingMail) { throw new AssertionError2({ message: `Expected mail "${mailConstructor.name}" was not queued` }); } } /** * Assert the mentioned mail was NOT queued during the fake * mode */ assertNotQueued(mailConstructor, findFn) { const matchingMail = this.#queued.find((mail) => { if (!findFn) { return mail instanceof mailConstructor; } return mail instanceof mailConstructor && findFn(mail); }); if (matchingMail) { throw new AssertionError2({ message: `Unexpected mail "${mailConstructor.name}" was queued` }); } } assertQueuedCount(mailConstructor, count) { if (typeof mailConstructor === "number") { const actual2 = this.#queued.length; const expected2 = mailConstructor; if (actual2 !== expected2) { throw new AssertionError2({ message: `Expected to queue "${expected2}" ${string2.pluralize("mail", expected2)}, instead received "${actual2}" ${string2.pluralize("mail", actual2)}`, actual: actual2, expected: expected2 }); } return; } const actual = this.queued((mail) => mail instanceof mailConstructor).length; const expected = count; if (actual !== expected) { throw new AssertionError2({ message: `Expected "${mailConstructor.name}" to be queued "${expected}" ${string2.pluralize("time", expected)}, instead it was queued "${actual}" ${string2.pluralize("time", actual)}`, actual, expected }); } } /** * Assert zero emails were queued */ assertNoneQueued() { if (this.#queued.length) { throw new AssertionError2({ message: `Expected zero mail to be queued, instead received "${this.#queued.length}" mail`, expected: [], actual: [ this.#queued.map((mail) => mail.constructor.name) ] }); } } }; var MessagesCollection = class MessagesCollection2 { static { __name(this, "MessagesCollection"); } #sent = []; #queued = []; /** * Default finder to find a message using search options */ #messageFinder = /* @__PURE__ */ __name((message, searchOptions) => { if (searchOptions.from && !message.hasFrom(searchOptions.from)) { return false; } if (searchOptions.to && !message.hasTo(searchOptions.to)) { return false; } if (searchOptions.subject && !message.hasSubject(searchOptions.subject)) { return false; } if (searchOptions.attachments && !searchOptions.attachments.every((attachment) => message.hasAttachment(attachment))) { return false; } return true; }, "#messageFinder"); trackSent(message) { this.#sent.push(message); } trackQueued(message) { this.#queued.push(message); } clear() { this.#sent = []; this.#queued = []; } /** * Returns a list of sent messages captured by the fake mailer */ sent(filterFn) { return filterFn ? this.#sent.filter(filterFn) : this.#sent; } /** * Returns a list of queued messages captured by the fake mailer */ queued(filterFn) { return filterFn ? this.#queued.filter(filterFn) : this.#queued; } /** * Assert the mentioned message was sent during the fake * mode */ assertSent(finder) { const matchingMessage = this.#sent.find(typeof finder === "function" ? finder : (message) => this.#messageFinder(message, finder)); if (!matchingMessage) { throw new AssertionError2({ message: `Expected message was not sent` }); } } /** * Assert the mentioned message was NOT sent during the fake * mode */ assertNotSent(finder) { const matchingMessage = this.#sent.find(typeof finder === "function" ? finder : (message) => this.#messageFinder(message, finder)); if (matchingMessage) { throw new AssertionError2({ message: `Unexpected message was sent` }); } } assertSentCount(finder, count) { if (typeof finder === "number") { const actual2 = this.#sent.length; const expected2 = finder; if (actual2 !== expected2) { throw new AssertionError2({ message: `Expected to send "${expected2}" ${string2.pluralize("message", expected2)}, instead received "${actual2}" ${string2.pluralize("message", actual2)}`, actual: actual2, expected: expected2 }); } return; } const actual = this.sent(typeof finder === "function" ? finder : (message) => this.#messageFinder(message, finder)).length; const expected = count; if (actual !== expected) { throw new AssertionError2({ message: `Expected to send "${expected}" ${string2.pluralize("message", expected)}, instead received "${actual}" ${string2.pluralize("message", actual)}`, actual, expected }); } } /** * Assert zero messages were sent */ assertNoneSent() { if (this.#sent.length) { throw new AssertionError2({ message: `Expected zero messages to be sent, instead received "${this.#sent.length}" ${string2.pluralize("message", this.#sent.length)}` }); } } /** * Assert the mentioned message was queued during the fake * mode */ assertQueued(finder) { const matchingMessage = this.#queued.find(typeof finder === "function" ? finder : (message) => this.#messageFinder(message, finder)); if (!matchingMessage) { throw new AssertionError2({ message: `Expected message was not queued` }); } } /** * Assert the mentioned message was NOT queued during the fake * mode */ assertNotQueued(finder) { const matchingMessage = this.#queued.find(typeof finder === "function" ? finder : (message) => this.#messageFinder(message, finder)); if (matchingMessage) { throw new AssertionError2({ message: `Unexpected message was queued` }); } } assertQueuedCount(finder, count) { if (typeof finder === "number") { const actual2 = this.#queued.length; const expected2 = finder; if (actual2 !== expected2) { throw new AssertionError2({ message: `Expected to queue "${expected2}" ${string2.pluralize("message", expected2)}, instead received "${actual2}" ${string2.pluralize("message", actual2)}`, actual: actual2, expected: expected2 }); } return; } const actual = this.queued(typeof finder === "function" ? finder : (message) => this.#messageFinder(message, finder)).length; const expected = count; if (actual !== expected) { throw new AssertionError2({ message: `Expected to queue "${expected}" ${string2.pluralize("message", expected)}, instead received "${actual}" ${string2.pluralize("message", actual)}`, actual, expected }); } } /** * Assert zero messages were queued */ assertNoneQueued() { if (this.#queued.length) { throw new AssertionError2({ message: `Expected zero messages to be queued, instead received "${this.#queued.length}" ${string2.pluralize("message", this.#queued.length)}` }); } } }; var FakeMailer = class extends Mailer { static { __name(this, "FakeMailer"); } mails = new MailsCollection(); messages = new MessagesCollection(); constructor(name, emitter, config) { super(name, new JSONTransport(), emitter, config); super.setMessenger({ queue: /* @__PURE__ */ __name(async (mail, sendConfig) => { return this.sendCompiled(mail, sendConfig); }, "queue") }); } /** * Define the messenger to use for queueing emails. * The fake mailer ignores using a custom messenger */ setMessenger(_) { return this; } /** * @inheritdoc */ async send(callbackOrMail, config) { if (callbackOrMail instanceof BaseMail) { this.mails.trackSent(callbackOrMail); const response2 = await super.send(callbackOrMail, config); return response2; } const response = await super.send((message) => { callbackOrMail(message); this.messages.trackSent(message); }, config); return response; } /** * @inheritdoc */ async sendLater(callbackOrMail, config) { if (callbackOrMail instanceof BaseMail) { this.mails.trackQueued(callbackOrMail); await callbackOrMail.sendLater(this, config); return; } await super.sendLater((message) => { callbackOrMail(message); this.messages.trackQueued(message); }, config); } async close() { this.messages.clear(); this.mails.clear(); super.close(); } }; // src/mail_manager.ts import { RuntimeException as RuntimeException2 } from "@poppinss/utils"; var MailManager = class { static { __name(this, "MailManager"); } config; #emitter; /** * Messenger to use on all mailers created * using the mail manager */ #messenger; /** * Reference to the fake mailer (if any) */ #fakeMailer; /** * Cache of mailers */ #mailersCache; constructor(emitter, config) { this.config = config; this.#mailersCache = {}; debug_default("creating mail manager %O", config); this.#emitter = emitter; } /** * Configure the messenger for all the mailers managed * by the mail manager class. */ setMessenger(messenger) { this.#messenger = messenger; Object.keys(this.#mailersCache).forEach((name) => { const mailer = this.#mailersCache[name]; mailer.setMessenger(messenger(mailer)); }); return this; } /** * Send email using the default mailer */ send(callbackOrMail, config) { return this.use().send(callbackOrMail, config); } /** * Queue email using the default mailer */ async sendLater(callbackOrMail, config) { await this.use().sendLater(callbackOrMail, config); } /** * Create/use an instance of a known mailer. The mailer * instances are cached for the lifecycle of the process */ use(mailerName) { let mailerToUse = mailerName || this.config.default; if (!mailerToUse) { throw new RuntimeException2("Cannot create mailer instance. No default mailer is defined in the config"); } if (!this.config.mailers[mailerToUse]) { throw new RuntimeException2(`Unknow mailer "${String(mailerToUse)}". Make sure it is configured inside the config file`); } if (this.#fakeMailer) { return this.#fakeMailer; } const cachedMailer = this.#mailersCache[mailerToUse]; if (cachedMailer) { debug_default('using mailer from cache. name: "%s"', cachedMailer); return cachedMailer; } const transportFactory = this.config.mailers[mailerToUse]; debug_default('creating mailer transport. name: "%s"', mailerToUse); const mailer = new Mailer(mailerToUse, transportFactory(), this.#emitter, this.config); if (this.#messenger) { mailer.setMessenger(this.#messenger(mailer)); } this.#mailersCache[mailerToUse] = mailer; return mailer; } /** * Turn on fake mode. After this all calls to "mail.use" will * return an instance of the fake mailer */ fake() { this.restore(); debug_default("creating fake mailer"); this.#fakeMailer = new FakeMailer("fake", this.#emitter, this.config); return this.#fakeMailer; } /** * Turn off fake mode and restore normal behavior */ restore() { if (this.#fakeMailer) { this.#fakeMailer.close(); this.#fakeMailer = void 0; debug_default("restoring mailer fake"); } } /** * Clear mailer from cache and close its transport */ async close(mailerName) { const mailer = this.#mailersCache[mailerName]; if (mailer) { debug_default("closing mailer %s", mailerName); await mailer.close(); delete this.#mailersCache[mailerName]; } } /** * Clear all mailers from cache and close their transports */ async closeAll() { await Promise.all(Object.keys(this.#mailersCache).map((mailerName) => this.close(mailerName))); } }; // src/define_config.ts import { configProvider } from "@adonisjs/core"; function defineConfig(config) { return configProvider.create(async (app) => { const { mailers, default: defaultMailer, ...rest } = config; const mailersNames = Object.keys(mailers); const transports2 = {}; for (let mailerName of mailersNames) { const mailerTransport = mailers[mailerName]; if (typeof mailerTransport === "function") { transports2[mailerName] = mailerTransport; } else { transports2[mailerName] = await mailerTransport.resolver(app); } } return { default: defaultMailer, mailers: transports2, ...rest }; }); } __name(defineConfig, "defineConfig"); var transports = { smtp(config) { return configProvider.create(async () => { const { SMTPTransport } = await import("./src/transports/smtp.js"); return () => new SMTPTransport(config); }); }, ses(config) { return configProvider.create(async () => { const { SESTransport } = await import("./src/transports/ses.js"); return () => new SESTransport(config); }); }, mailgun(config) { return configProvider.create(async () => { const { MailgunTransport } = await import("./src/transports/mailgun.js"); return () => new MailgunTransport(config); }); }, sparkpost(config) { return configProvider.create(async () => { const { SparkPostTransport } = await import("./src/transports/sparkpost.js"); return () => new SparkPostTransport(config); }); }, resend(config) { return configProvider.create(async () => { const { ResendTransport } = await import("./src/transports/resend.js"); return () => new ResendTransport(config); }); }, brevo(config) { return configProvider.create(async () => { const { BrevoTransport } = await import("./src/transports/brevo.js"); return () => new BrevoTransport(config); }); } }; export { Message, BaseMail, Mailer, configure, FakeMailer, MailManager, defineConfig, transports }; //# sourceMappingURL=chunk-ZEBABAZZ.js.map