Skip to content
Payment Emulator LabDocumentationOpen console

Archive and maintenance

Event emails

Each application has Emails → History, Templates, Rules and Settings.The channel starts disabled. SMTP credentials remain on the server.

6 min read

Each application has Emails → History, Templates, Rules and Settings. The channel starts disabled. SMTP credentials remain on the server.

#First local delivery

  1. Start PostgreSQL, API, worker, console, checkout and Mailpit with Docker Compose.
  2. Open the application's templates and create the starter templates: nine events in Spanish and English, 18 published templates. Repeating this preserves edits.
  3. Edit a template's subject, preheader, color and blocks. Preview HTML and plain text at desktop and mobile widths, then save and publish.
  4. Select local capture and an allowed sender in Settings; enable the channel. The connection check asks the worker to verify SMTP without sending a message.
  5. Add an event rule, audience, published template and recipient source. Evaluate against a payment without creating messages.
  6. Complete a payment or send a template test. Inspect history and local Mailpit.

Mailpit captures SMTP on port 1025 without forwarding to the Internet. Compose uses the hostname mailpit; local processes use localhost. Use a Compose override with free ports when another local installation occupies these ports.

#Events and recipients

Supported events are payment.requires_action, payment.approved, payment.authorized, payment.captured, payment.failed, payment.canceled, payment.refund_created, payment.disputed and payment.settled. Every partial refund produces its own notification with its exact amount. Gateway operations, checkout, timers, chargebacks and settlement use the same scheduling service. Reading a payment, updating metadata and replaying idempotent requests do not generate another notification. Email rules operate independently of webhooks.

Buyer recipients come from the payment's private contact, then from the active account identified by walletKey. Checkout accepts an optional email and uses the buyer session for card payments as well. Operators can save a contact before the payment finishes. Merchant rules use the owner or up to ten explicit addresses. Provider wire contracts do not acquire emulator contact fields.

The selected template determines the language explicitly. Custom templates are not translated or replaced automatically. The contact's language preference is stored for future translation selection; it does not override the selected template.

#Delivery guarantees

Payment and email obligation commit in one PostgreSQL transaction. The obligation freezes operation, recipient, template version, document, context, sender and transport profile. The worker renders HTML/text with the same MJML renderer as preview and performs SMTP after commit. Amounts use bigint minor units.

Status Meaning
pending Waiting; may be canceled
processing Claimed by a worker
retry_scheduled Known temporary failure; another attempt is due
captured Accepted by local capture SMTP
accepted Accepted by external SMTP; inbox delivery is not confirmed
failed Permanent failure or exhausted attempts; manual retry available
unknown Transmission may have completed; requires operator review
skipped Missing recipient/template or unavailable transport; reason recorded
canceled Canceled manually or because the application became inactive

Three automatic retries use 30 seconds, 2 minutes and 10 minutes. Workers claim with SKIP LOCKED, a 60-second lease and a fencing token. SMTP has a 20-second deadline. An expired claim without transmission becomes pending; a claim that started transmission becomes unknown. SMTP cannot guarantee exactly one delivery; unknown results are never repeated automatically.

Retry preserves the original message, payment operation and attempt history. Resend creates a linked message. Actions record actor, time and reason and use idempotent request identifiers. Manual sends are limited to 20 per hour per application and actor; connection checks to three per minute.

Disabling prevents new obligations. Pausing holds existing ones. Changing settings does not redirect queued messages. A changed host, port or TLS fingerprint skips the old message with a visible reason. Terminal content is retained for 30 days, then recipient, subject, HTML, text, document and context are redacted.

#Server configuration

Supply the same environment to API and worker. External SMTP requires explicit enablement and TLS with verified certificates.

Variable Default / purpose
EMAIL_CAPTURE_HOST, EMAIL_CAPTURE_PORT localhost, 1025; Compose uses mailpit
EMAIL_TRANSPORT disabled blocks every transport
EMAIL_EXTERNAL_ENABLED Only true enables external SMTP
EMAIL_SMTP_HOST, EMAIL_SMTP_PORT Authorized server, port 587
EMAIL_SMTP_TLS_MODE starttls or tls (usually port 465)
EMAIL_SMTP_USER, EMAIL_SMTP_PASSWORD Server credentials when required
EMAIL_ALLOWED_FROM Comma-separated senders; sandbox@laboratorio.test
EMAIL_LINK_ORIGINS Allowed button URL origins; defaults to public checkout URL
EMAIL_MANUAL_HOURLY_LIMIT 20
EMAIL_RETENTION_DAYS 30

Templates contain validated blocks, not arbitrary HTML. Only catalog variables and allowed HTTP(S) button origins are accepted. Preview uses a sandboxed iframe without scripts, forms or remote resources. Attachments, remote images and MJML includes are not supported.

The API root is /applications/:applicationId/emails, with settings, catalog, templates and versions, rules/evaluation, tests, messages/attempts/actions and connection checks. Lists use {data, meta}. Configuration mutations use revision checks and return 409 for stale edits. Payment contact uses PATCH /applications/:applicationId/payments/:intentId/contact. Administrators and the owning merchant may access resources; cross-application IDs return 404.

#Local processes and deployment

On Windows, run-local.bat loads email settings from .env and .env.local for API and worker. Start Mailpit separately and set EMAIL_CAPTURE_PORT if it does not use 1025. Payments still work without SMTP; history records transport failures.

Native deployment initializes email variables in /etc/<project>/api.env, also read by the worker, and preserves administrator edits during updates. Production Compose shares email configuration between both processes. Both deployments start with EMAIL_TRANSPORT=disabled. Set it to capture or smtp and configure the server to enable delivery; external SMTP also requires EMAIL_EXTERNAL_ENABLED=true. Restart API and worker after environment changes. Production does not install Mailpit.

npm run smoke:emails checks payment, worker, Mailpit and the captured multipart MIME. npm run smoke:emails:browser checks ES/EN, light/dark themes and 1440/390 px viewports. Both use the test account from smoke:stack and accept API_URL, CONSOLE_URL, MAILPIT_URL, SMOKE_EMAIL and SMOKE_PASSWORD. Setting MAILPIT_URL also includes email checks in npm run smoke:stack. Screenshots, accessibility results and captured.eml are saved in .tmp/shots/emails/. The automated audit covers console accessibility and dialog focus, excluding the isolated email iframe. External email client compatibility needs separate testing; browser screenshots do not establish it.

Implementation lives in apps/api/src/modules/emails and apps/console/app/modules/integrations/emails. Tests are in apps/api/test/email-renderer.spec.ts, apps/api/test/integration/emails.spec.ts, apps/api/test/integration/emails-lifecycle.spec.ts and scripts/emails-browser.mjs.

Engine references: Nodemailer SMTP, MJML and Mailpit.

#Imported HTML templates and sanitization

Templates with type: "html" are treated as untrusted content. The API normalizes and sanitizes HTML on the server before storing it as a draft, reapplies the same boundary before publishing a version, and sanitizes again after variable interpolation before producing the final message. Unsafe source HTML never becomes the authorized canonical document.

The H1 policy allows common email structure, tables, text, links, and presentation-oriented inline CSS through explicit allowlists. It rejects active content (script, iframe, forms, SVG/MathML and equivalents), on* attributes, src/srcset/poster/background resources, CSS capable of loading or executing content (url(), @import, expression(), javascript:, data:, file: and equivalent schemes), and documents larger than 200 KB. Images and other remote resources are outside H1.

Links may use HTTP/HTTPS only and must belong to an origin authorized by EMAIL_LINK_ORIGINS; when it is unset, CHECKOUT_PUBLIC_URL is used, with http://localhost:3001 as the local-development fallback. Embedded credentials and protocol-relative URLs are rejected. Accepted links are normalized with rel="noopener noreferrer".

Variables remain limited to the official Email catalogue. Their values are escaped before HTML insertion and the interpolated output is sanitized again, so data such as <script>...</script> is displayed as text rather than executed. If an HTML template does not define text, the backend derives a plain-text alternative from the already sanitized HTML.

Certification covers both the sanitizer unit and the HTTP/database contract: creating, previewing, and publishing a safe HTML template persists only the sanitized representation, while scripts, remote tracking, handlers, unsafe CSS, and external links receive 400 before they can become an authorized draft.

Payment Emulator Lab · RonuSoftwareMIT