Skip to content

fix(email): support attachments, and stop emailTestMode='false' swallowing every email - #11

Open
MaxAdams98 wants to merge 1 commit into
boycce:masterfrom
MaxAdams98:fix/email-attachments
Open

MaxAdams98 wants to merge 1 commit into
boycce:masterfrom
MaxAdams98:fix/email-attachments

Conversation

@MaxAdams98

Copy link
Copy Markdown

Two independent problems in sendEmail, both found while building a monthly report that has to reach a government
contract manager with a signed PDF attached.

sendEmail cannot attach a file

There is no way to pass one, so an app that needs an attachment has to bypass sendEmail entirely and build the
Mailgun message itself, losing the template rendering, the recipient variables and the subject extraction.

nodemailer-mailgun-transport already reads mail.attachments and maps it onto Mailgun's attachment / inline
fields, so this is just a matter of passing the option through. The shape is nodemailer's, and an entry carrying a
cid is embedded inline rather than attached.

await sendEmail({
  config, template: 'report-submission', to: 'Thomas<thomas@example.gov>',
  attachments: [{ filename: 'monthly-2026-07.pdf', content: buffer, contentType: 'application/pdf' }],
})

emailTestMode=false reads as true, and the app silently sends nothing

config.emailTestMode is almost always populated from process.env, where every value is a string, and the
example app's server/config.js passes it straight through:

emailTestMode: process.env.emailTestMode,

const isTest = config.emailTestMode || test then treats the string 'false' as true. The app stays in test mode,
every sendEmail call resolves happily with the rendered HTML, and nothing is ever delivered. There is no error to
notice, which is what makes it worth fixing rather than documenting: an app that writes the flag out in full rather
than leaving it unset never sends an email and has no way to tell.

I hit this in production code that had been "sending" for weeks. The flag is now coerced, with the words that mean
off spelled out ('', false, 0, no, off); everything else keeps normal truthiness, so passing a real boolean
behaves exactly as before.

Also

config.emailTestMode was read one line before if (!config) throw, so calling sendEmail without a config threw
a TypeError instead of the intended message. The guard now comes first.

Verified

Patched into a real app's node_modules and exercised against it: the string 'false' now falls through to the
real send path rather than being swallowed, and attachments reach the transport. Sending end to end is blocked only
by that project's Mailgun key, which is read-only.

Independent of #6, which touches different lines of the same file.

sendEmail had no way to attach a file, so an app that needs one has to bypass it and build the Mailgun message
itself. It now takes `attachments` in nodemailer's shape and passes them through; the transport already maps them
to Mailgun's attachment/inline fields, so nothing else changes.

emailTestMode is nearly always populated from process.env, where every value is a string. `config.emailTestMode ||
test` therefore reads `emailTestMode=false` as true, and the app silently sends nothing while every call still
resolves. The example app's config passes the raw env value, so this bites any project that writes the flag out
in full rather than leaving it unset. The flag is now coerced, with the words that mean "off" spelled out.

Also moves the `config` guard above its first use: sendEmail read config.emailTestMode before checking config
existed, so a missing config threw a TypeError rather than the intended message.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant