email-best-practices
Attaches itself automatically when a prompt matches.
Guidance for building deliverable, compliant, user-friendly email flows. Use when the user adds email signup, password reset, magic links, transactional sends, newsletters, contact forms, or asks about spam / SPF / DKIM / DMARC / bounces / CAN-SPAM / GDPR. Vendor-agnostic — concept is the same with Resend, SendGrid, Postmark, AWS SES, or fetch+SMTP.
Scaffolds
Required env vars
Source
Adapted from resend/resend-skills (email-best-practices, adapted)
Instructions
The full instruction set the agent receives when this skill is active.
Email Best Practices
Most email problems happen at one of three points: setup (auth records wrong), sending (no retry/idempotency), and consequences (no suppression list, no compliance). Get all three right and the system holds.
The Email Lifecycle
Most "spam folder" problems are actually unauthenticated mail — fix DNS first, not copy.
Deliverability — DNS authentication (do this before sending anything)
Three records, all on the sending domain:
- SPF — TXT record listing the IPs/domains allowed to send. Example:
v=spf1 include:_spf.resend.com ~all - DKIM — public key the receiver uses to verify the signature. Your provider gives you one or more CNAMEs to add.
- DMARC — TXT record at
_dmarc.example.comdeclaring what to do with unauthenticated mail. Start atp=noneto monitor, move top=quarantineorp=rejectonce clean.
Gmail and Yahoo reject unauthenticated bulk mail outright as of 2024. There is no "soft fail" anymore.
Transactional vs Marketing — pick correctly
| Transactional | Marketing | |
|---|---|---|
| Triggered by | A user action | A schedule or campaign |
| Consent | Implicit from the action | Explicit opt-in required |
| Examples | Password reset, OTP, receipt, account confirmation | Newsletter, promo, drip |
| Unsubscribe link | Optional (not legally required) | Required (CAN-SPAM, GDPR, CASL) |
| List membership | Sent regardless of subscription | Sent only to subscribed list |
Don't mix the two. A "thanks for signing up — here are 5 ways to use our product" email is marketing, not transactional.
Transactional emails — common catalog
If the user is building auth/payments/comms, plan for these:
- Email verification (signup)
- Password reset
- Magic link / OTP login
- Order/receipt confirmation
- Shipping/status update
- Security alert (new device, password changed)
- Account deletion confirmation
- Invoice / payment failure / billing reminder
Build templates once. Reuse them. They're text + a CTA, not an art project.
Capturing emails — form basics
- Single field, validated client-side AND server-side
- Honeypot field to defeat dumb bots (
<input name="website" tabindex="-1" autocomplete="off" style="display:none">) - Server-side disposable-domain check (block
mailinator.com, etc.) only if abuse is real - Double opt-in: send a confirmation link, only mark subscribed after click. Reduces bounces and spam complaints, required in EU under GDPR
Sending reliability
- Idempotency key on every send so a retry doesn't double-send (
Idempotency-Key: build_42_user_99) - Exponential backoff on 5xx responses: 1s, 2s, 4s, 8s, give up after 5 tries and queue for manual review
- Async — never block the request that triggered the send waiting for the API. Queue and return immediately. If the queue fails, the user retried login isn't blocked
- Logging — record
{messageId, to, template, status, providerResponse}for every send, redacting PII as needed
Webhooks — what to handle
Subscribe to these events from your provider:
| Event | Action |
|---|---|
delivered | Update lastDeliveredAt |
bounced (hard) | Add to suppression list immediately |
bounced (soft) | Count; suppress after 5 soft bounces in 30 days |
complained | Add to suppression list immediately, unsubscribe from all |
unsubscribed | Add to suppression list for that list type |
Verify webhook signatures. An unsigned POST to /webhooks/email will get spoofed.
List hygiene
- Run weekly: prune addresses with no opens in 90 days from marketing lists
- Run nightly: any new hard-bounce goes into suppression
- Never email an address on the suppression list — even transactional. If a transactional email truly needs to go out (e.g. a final account-deletion confirmation), document the exception in code
Compliance — quick checklist
| Law | Applies if | Key requirements |
|---|---|---|
| CAN-SPAM (US) | Sending commercial mail to US recipients | Working unsubscribe link, physical address in footer, accurate From and subject |
| GDPR (EU) | Any EU recipient on a marketing list | Explicit opt-in consent, right to access/erase, lawful basis recorded |
| CASL (Canada) | Any CA recipient | Express OR implied consent, identification of sender, unsubscribe |
Don't make this lawyer territory unless you have real reach. The 3 essentials: opt-in, unsubscribe link, sender address cover most.
Platform notes (Codebase)
- scaffold-next: send from a Route Handler (
app/api/email/send/route.ts). Use a queue (e.g. Upstash QStash or in-process for low volume) so the request doesn't block. - Use react-email to template (
npm install react-email @react-email/components) — works with Resend, SendGrid, AWS SES, or hand-rolled SMTP. - The user MUST supply their own API key as an env var. Never hard-code a key, never invent one.
What NOT to do
- Send first, ask for consent later — you're already on a deny list by week two
- Use a
noreply@address as the From — modern inboxes flag these. Use a monitored mailbox - Skip the unsubscribe link "because it's transactional" — fine for receipts, but the first time you send marketing, you need one
- Mix templates between transactional and marketing infrastructure — keep the IP/domain reputation separate
See also: [[security-review]] (don't log raw email contents with credentials).