Send an email
How to send a transactional email from backend code, for example a "your export is ready" message. Email explains the two mail systems, the queue and SMTP setup; this page is the checklist.
Steps
Import
EmailModuleinto your feature module and injectEmailServiceinto your service. The snippet is in Email, "Sending an email from a service".Call
queue(), notsend():async queue(options: QueueEmailOptions): Promise<EmailJobRow> {queue()inserts anemail_jobsrow and returns at once. TheEmailProcessorcron sends pending jobs in batches and retries a failure with backoff.send()waits on SMTP inside the request and never retries. UsequeueBulk()for many recipients; passcampaignId/campaignNameto group a send in the backoffice email stats.queue()writes throughthis.db, not your transaction'strx. If the email belongs to a transaction, callqueue()after it commits, or a rolled-back change still sends its email.Build the HTML yourself. The backend has no template system:
htmlis a string you pass in, its length capped byQueueEmailOptionsSchemainemail.schemas.ts. Two examples to copy from:frontend/lib/magic-link-email.ts: the sign-in email, a full table-based HTML layout with inline styles. It belongs to NextAuth on the frontend and does not go throughEmailService, but its markup is a working starting point.- The seed's "Welcome Series" campaign in
backend/src/scripts/seed.ts: a minimal body,'<h1>Welcome!</h1><p>Thanks for joining CrabStack. ...</p>'.
Escape anything a user typed before you put it in the HTML, with
escapeHtmlfrombackend/src/common/escape-html.ts.Write a test. In a unit spec, mock
EmailServiceand assert your service calledqueuewith the rightto,subjectandhtml. To prove the whole path against a real database, followbackend/src/email/email.service.integration.spec.ts, which replaces the SMTP transporter with a recorder:(service as unknown as { transporter: unknown }).transporter = { sendMail: async (m: { to: string }) => { sent.push(m); return {}; }, };Look at it locally. Run
npm run dev:emailinbackend/(MailDev: SMTP on port 1025, inbox at http://localhost:1080). The backend's SMTP defaults already point there. Trigger your code, wait for the next batch, and the message shows up in the inbox.Run
./verify.sh.
What the test run found
- The template's own specs covered
processBatchandqueueBulk(through the MCP tool spec) but no spec calledqueue()itself. A test added toemail.service.integration.spec.tsin the test clone calledqueue(), ranprocessBatch(), and checked the recorder received the message and the job was markedsent: verify all green.