Skip to content

fix(inquiry): swap SendGrid for nodemailer + Gmail SMTP (closes #752) - #753

Merged
JoshuaVSherman merged 2 commits into
devfrom
fix-issue-752-sendgrid-to-nodemailer
May 18, 2026
Merged

JoshuaVSherman merged 2 commits into
devfrom
fix-issue-752-sendgrid-to-nodemailer

Conversation

@JoshuaVSherman

Copy link
Copy Markdown
Contributor

Closes #752. Satisfies Phase 3 of #749 (provider swap).

Summary

  • Replaces @sendgrid/mail with nodemailer + Gmail SMTP for the /inquiry route.
  • New env vars: GMAIL_USER, GMAIL_APP_PASSWORD. Removed: SENDGRID_API_KEY, SENDGRID_USERNAME, SENDGRID_PASSWORD.
  • New destination address: joshua.v.sherman@gmail.com (per Josh's decision 2026-05-17, supersedes web.jam.adm@gmail.com).
  • Preserves the 502-on-error pattern from PR fix: don't crash on SendGrid failures from /inquiry (#747) #748 so this route still can't crash the process.

Background

Booking inquiries from joshandmariamusic.com / web-jam.com were silently failing in production. Verified 2026-05-17 that no recent inquiries had arrived at the destination inbox. Combined with the post-mortem in docs/inquiry-sendgrid-crash.md, the most likely cause is the SendGrid free tier hitting its daily 100-email cap (other backend traffic exhausting the quota before form submissions get processed).

Rather than topping up SendGrid or migrating to a different paid provider, this PR drops the SendGrid dependency entirely. Volume is well under Gmail SMTP's 500/day cap, and removing one third-party SaaS billing relationship is a net simplification.

Test plan

  • All 70 existing tests pass unchanged (npm test)
  • Lint + typecheck clean
  • Manual smoke test after deploy: submit an inquiry from joshandmariamusic.com home page; confirm email arrives at joshua.v.sherman@gmail.com
  • Manual smoke test after deploy: submit an inquiry from joshandmariamusic.com Book Us page (reuses the same Inquiry component, so should pass automatically — verifying anyway per Task 17 clarification)
  • Manual smoke test after deploy: kill GMAIL_APP_PASSWORD temporarily and submit; verify the route returns 502 cleanly without crashing

Required follow-up (Josh, after merge)

  • Generate Gmail App Password for joshua.v.sherman@gmail.com (Google Account → Security → 2-Step Verification → App Passwords)
  • Set GMAIL_USER=joshua.v.sherman@gmail.com and GMAIL_APP_PASSWORD=<value> in Heroku config vars
  • Remove SENDGRID_API_KEY from Heroku config vars (do this AFTER verifying delivery, so we can roll back if needed)
  • Close Add SendGrid local-test coverage for /inquiry; consider provider swap if flaky #749 (this PR satisfies its Phase 3 "swap provider" goal; Phase 1/2 SendGrid-local-test work is no longer needed)

Related

🤖 Generated with Claude Code

JoshuaVSherman and others added 2 commits May 17, 2026 19:08
The /inquiry route was silently failing in production — verified
2026-05-17 that no recent inquiries were arriving in the destination
inbox. Combined with the historical "Maximum credits exceeded" incident
captured in docs/inquiry-sendgrid-crash.md, this is the SendGrid free
tier hitting its daily cap.

Replaces @sendgrid/mail with nodemailer using Gmail SMTP. Rationale:
- Volume is well under Gmail SMTP's 500/day cap.
- Removes a third-party SaaS billing relationship.
- Receiver is already a Gmail account; same-provider auth eliminates
  the sender-verification dance.

Changes:
- package.json: -@sendgrid/mail, +nodemailer, +@types/nodemailer.
- src/model/inquiry/InquiryController.ts: rewrite using nodemailer
  with Gmail SMTP. From = process.env.GMAIL_USER. To hardcoded to
  joshua.v.sherman@gmail.com (decided by Josh 2026-05-17, supersedes
  the previous web.jam.adm@gmail.com destination from when SendGrid
  domain-verification permitted any From). Preserves the 502-on-error
  pattern from PR #748 so unhandled rejections still can't crash the
  process.
- .env.example: -SENDGRID_API_KEY/USERNAME/PASSWORD, +GMAIL_USER,
  +GMAIL_APP_PASSWORD.
- README.md: new "Email (`/inquiry` route)" section documenting the
  Gmail App Password generation procedure and required env vars.
- docs/inquiry-sendgrid-crash.md: update note pointing at this PR as
  the resolution of the SendGrid-era failure mode.

Tests: all 70 existing pass unchanged. The route returns 200 in
NODE_ENV=test mode without actually sending, same pattern as before.

Resolves #752. Satisfies Phase 3 of #749 (provider swap).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two fixes triggered by CircleCI smoketest failure on PR #753:

1. CI smoketest runs `npm install --omit=dev` then triggers the
   postinstall TypeScript compile via tsc -p tsconfig.prod.json. With
   @types/nodemailer in devDependencies, tsc fails with TS7016 because
   the type declarations aren't installed in --omit=dev mode. Matches
   the existing convention in this repo: every other @types/* package
   lives in `dependencies`, not `devDependencies`, for exactly this
   reason.

2. README "Email" section: add Heroku CLI command examples (heroku
   config:set / get / unset / logs) AND Heroku dashboard click-by-click
   so Josh has step-by-step instructions for setting GMAIL_USER and
   GMAIL_APP_PASSWORD and unsetting the obsolete SENDGRID_* vars in
   production. Per Josh's 2026-05-17 request.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@JoshuaVSherman
JoshuaVSherman merged commit 2cfa5fe into dev May 18, 2026
2 checks passed
@JoshuaVSherman
JoshuaVSherman deleted the fix-issue-752-sendgrid-to-nodemailer branch May 18, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant