diff --git a/profiles/opencode/skill/email-best-practices/SKILL.md b/profiles/opencode/skill/email-best-practices/SKILL.md new file mode 100644 index 0000000..4314e9a --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/SKILL.md @@ -0,0 +1,59 @@ +--- +name: email-best-practices +description: Use when building email features, emails going to spam, high bounce rates, setting up SPF/DKIM/DMARC authentication, implementing email capture, ensuring compliance (CAN-SPAM, GDPR, CASL), handling webhooks, retry logic, or deciding transactional vs marketing. +--- + +# Email Best Practices + +Guidance for building deliverable, compliant, user-friendly emails. + +## Architecture Overview + +``` +[User] → [Email Form] → [Validation] → [Double Opt-In] + ↓ + [Consent Recorded] + ↓ +[Suppression Check] ←──────────────[Ready to Send] + ↓ +[Idempotent Send + Retry] ──────→ [Email API] + ↓ + [Webhook Events] + ↓ + ┌────────┬────────┬─────────────┐ + ↓ ↓ ↓ ↓ + Delivered Bounced Complained Opened/Clicked + ↓ ↓ + [Suppression List Updated] + ↓ + [List Hygiene Jobs] +``` + +## Quick Reference + +| Need to... | See | +|------------|-----| +| Set up SPF/DKIM/DMARC, fix spam issues | [Deliverability](./resources/deliverability.md) | +| Build password reset, OTP, confirmations | [Transactional Emails](./resources/transactional-emails.md) | +| Plan which emails your app needs | [Transactional Email Catalog](./resources/transactional-email-catalog.md) | +| Build newsletter signup, validate emails | [Email Capture](./resources/email-capture.md) | +| Send newsletters, promotions | [Marketing Emails](./resources/marketing-emails.md) | +| Ensure CAN-SPAM/GDPR/CASL compliance | [Compliance](./resources/compliance.md) | +| Decide transactional vs marketing | [Email Types](./resources/email-types.md) | +| Handle retries, idempotency, errors | [Sending Reliability](./resources/sending-reliability.md) | +| Process delivery events, set up webhooks | [Webhooks & Events](./resources/webhooks-events.md) | +| Manage bounces, complaints, suppression | [List Management](./resources/list-management.md) | + +## Start Here + +**New app?** +Start with the [Catalog](./resources/transactional-email-catalog.md) to plan which emails your app needs (password reset, verification, etc.), then set up [Deliverability](./resources/deliverability.md) (DNS authentication) before sending your first email. + +**Spam issues?** +Check [Deliverability](./resources/deliverability.md) first—authentication problems are the most common cause. Gmail/Yahoo reject unauthenticated emails. + +**Marketing emails?** +Follow this path: [Email Capture](./resources/email-capture.md) (collect consent) → [Compliance](./resources/compliance.md) (legal requirements) → [Marketing Emails](./resources/marketing-emails.md) (best practices). + +**Production-ready sending?** +Add reliability: [Sending Reliability](./resources/sending-reliability.md) (retry + idempotency) → [Webhooks & Events](./resources/webhooks-events.md) (track delivery) → [List Management](./resources/list-management.md) (handle bounces). diff --git a/profiles/opencode/skill/email-best-practices/resources/compliance.md b/profiles/opencode/skill/email-best-practices/resources/compliance.md new file mode 100644 index 0000000..01460a0 --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/compliance.md @@ -0,0 +1,103 @@ +# Email Compliance + +Legal requirements for email by jurisdiction. **Not legal advice—consult an attorney for your specific situation.** + +## Quick Reference + +| Law | Region | Key Requirement | Penalty | +|-----|--------|-----------------|---------| +| CAN-SPAM | US | Opt-out mechanism, physical address | $53k/email | +| GDPR | EU | Explicit opt-in consent | €20M or 4% revenue | +| CASL | Canada | Express/implied consent | $10M CAD | + +## CAN-SPAM (United States) + +**Requirements:** +- Accurate header info (From, To, Reply-To) +- Non-deceptive subject lines +- Physical mailing address in every email +- Clear opt-out mechanism +- Honor opt-out within 10 business days + +**Transactional emails:** Can send without opt-in if related to a transaction and not promotional. + +## GDPR (European Union) + +**Requirements:** +- Explicit opt-in consent (not pre-checked boxes) +- Consent must be freely given, specific, informed +- Easy to withdraw consent (as easy as giving it) +- Right to access data and deletion ("right to be forgotten") +- Process unsubscribe immediately + +**Consent records:** Document who, when, how, and what they consented to. + +**Transactional emails:** Can send based on contract fulfillment or legitimate interest. + +## CASL (Canada) + +**Consent types:** +- **Express consent:** Explicit opt-in (preferred) +- **Implied consent:** Existing business relationship (2 years) or inquiry (6 months) + +**Requirements:** +- Clear sender identification +- Unsubscribe functional for 60 days after send +- Process unsubscribe within 10 business days +- Keep consent records 3 years after expiration + +## Other Regions + +| Region | Law | Key Points | +|--------|-----|------------| +| Australia | Spam Act 2003 | Consent required, honor unsubscribe within 5 days | +| UK | PECR + GDPR | Same as GDPR | +| Brazil | LGPD | Similar to GDPR, explicit consent for marketing | + +## Unsubscribe Requirements Summary + +| Law | Timing | Notes | +|-----|--------|-------| +| CAN-SPAM | 10 business days | Must work 30 days after send | +| GDPR | Immediately | Must be as easy as opting in | +| CASL | 10 business days | Must work 60 days after send | + +**Universal best practices:** Prominent link, one-click when possible, no login required, free, confirm action. + +## Consent Management + +**Record:** +- Email address +- Date/time of consent +- Method (form, checkbox) +- What they consented to +- Source (which page/form) + +**Storage:** Database with timestamps, audit trail of changes, link to user account. + +## Data Retention + +| Law | Requirement | +|-----|-------------| +| GDPR | Keep only as long as necessary, delete when no longer needed | +| CASL | Keep consent records 3 years after expiration | + +**Best practice:** Have clear retention policy, honor deletion requests promptly, review and clean regularly. + +## Privacy Policy Must Include + +- What data you collect +- How you use data +- Who you share data with +- User rights (access, deletion) +- How to contact about privacy + +## International Sending + +**Best practice:** Follow the most restrictive requirements (usually GDPR) to ensure compliance across all regions. + +## Related + +- [Email Capture](./email-capture.md) - Implement consent forms and double opt-in +- [Marketing Emails](./marketing-emails.md) - Consent and unsubscribe requirements +- [List Management](./list-management.md) - Handle unsubscribes and deletion requests diff --git a/profiles/opencode/skill/email-best-practices/resources/deliverability.md b/profiles/opencode/skill/email-best-practices/resources/deliverability.md new file mode 100644 index 0000000..dd03157 --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/deliverability.md @@ -0,0 +1,120 @@ +# Email Deliverability + +Ensuring emails reach inboxes through proper authentication and sender reputation. + +## Email Authentication + +**Required by Gmail/Yahoo** - unauthenticated emails will be rejected or spam-filtered. + +### SPF (Sender Policy Framework) + +Specifies which servers can send email for your domain. + +``` +v=spf1 include:_spf.resend.com ~all +``` + +- Add TXT record to DNS +- Use `~all` (soft fail) for testing, `-all` (hard fail) for production +- Keep under 10 DNS lookups + +### DKIM (DomainKeys Identified Mail) + +Cryptographic signature proving email authenticity. + +- Generate keys (provided by email service) +- Add public key as TXT record in DNS +- Use 2048-bit keys, rotate every 6-12 months + +### DMARC + +Policy for handling SPF/DKIM failures + reporting. + +``` +v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com +``` + +**Rollout:** `p=none` (monitor) → `p=quarantine; pct=25` → `p=reject` + +### BIMI (Optional) + +Display brand logo in email clients. Requires DMARC `p=quarantine` or `p=reject`. + +### Verify Your Setup + +Check DNS records directly: + +```bash +# SPF record +dig TXT yourdomain.com +short + +# DKIM record (replace 'resend' with your selector) +dig TXT resend._domainkey.yourdomain.com +short + +# DMARC record +dig TXT _dmarc.yourdomain.com +short +``` + +**Expected output:** Each command should return your configured record. No output = record missing. + +## Sender Reputation + +### IP Warming + +New IP/domain? Gradually increase volume: + +| Week | Daily Volume | +|------|-------------| +| 1 | 50-100 | +| 2 | 200-500 | +| 3 | 1,000-2,000 | +| 4 | 5,000-10,000 | + +Start with engaged users. Send consistently. Don't rush. + +### Maintaining Reputation + +**Do:** Send to engaged users, keep bounce <2%, complaints <0.1%, remove inactive subscribers + +**Don't:** Send to purchased lists, ignore bounces/complaints, send inconsistent volumes + +## Bounce Handling + +| Type | Cause | Action | +|------|-------|--------| +| Hard bounce | Invalid email, domain doesn't exist | Remove immediately | +| Soft bounce | Mailbox full, server down | Retry: 1h → 4h → 24h, remove after 3-5 failures | + +**Targets:** <2% good, 2-5% acceptable, >5% concerning, >10% critical + +## Complaint Handling + +**Targets:** <0.05% excellent, 0.05-0.1% good, >0.2% critical + +**Reduce complaints:** +- Only send to opted-in users +- Make unsubscribe easy and immediate +- Use clear sender names and "From" addresses + +**Feedback loops:** Set up with Gmail (Postmaster Tools), Yahoo, Microsoft, AOL. Remove complainers immediately. + +## Infrastructure + +**Dedicated sending domain:** Use subdomain (e.g., `mail.yourdomain.com`) to protect main domain reputation. + +**DNS TTL:** Low (300s) during setup, high (3600s+) after stable. + +## Troubleshooting + +**Emails going to spam?** Check in order: +1. Authentication (SPF, DKIM, DMARC) +2. Sender reputation (blacklists, complaint rates) +3. Content (spammy words, HTML issues) +4. Sending patterns (sudden volume spikes) + +**Diagnostic tools:** [mail-tester.com](https://mail-tester.com), [mxtoolbox.com](https://mxtoolbox.com), [Google Postmaster Tools](https://postmaster.google.com) + +## Related + +- [List Management](./list-management.md) - Handle bounces and complaints to protect reputation +- [Sending Reliability](./sending-reliability.md) - Retry logic and error handling diff --git a/profiles/opencode/skill/email-best-practices/resources/email-capture.md b/profiles/opencode/skill/email-best-practices/resources/email-capture.md new file mode 100644 index 0000000..311fb2d --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/email-capture.md @@ -0,0 +1,126 @@ +# Email Capture Best Practices + +Collecting email addresses responsibly with validation, verification, and proper consent. + +## Email Validation + +### Client-Side + +**HTML5:** +```html + +``` + +**Best practices:** +- Validate on blur or with short debounce +- Show clear error messages +- Don't be too strict (allow unusual but valid formats) +- Client-side validation ≠ deliverability + +### Server-Side (Required) + +Always validate server-side—client-side can be bypassed. + +**Check:** +- Email format (RFC 5322) +- Domain exists (DNS lookup) +- Domain has MX records +- Optionally: disposable email detection + +## Email Verification + +Confirms address belongs to user and is deliverable. + +### Process + +1. User submits email +2. Send verification email with unique link/token +3. User clicks link +4. Mark as verified +5. Allow access/add to list + +**Timing:** Send immediately, include expiration (24-48 hours), allow resend after 60 seconds, limit resend attempts (3/hour). + +## Single vs Double Opt-In + +| | Single Opt-In | Double Opt-In | +|--|---------------|---------------| +| **Process** | Add to list immediately | Require email confirmation first | +| **Pros** | Lower friction, faster growth | Verified addresses, better engagement, meets GDPR/CASL | +| **Cons** | Higher invalid rate, lower engagement | Some users don't confirm | +| **Use for** | Account creation, transactional | Marketing lists, newsletters | + +**Recommendation:** Double opt-in for all marketing emails. + +## Form Design + +### Email Input + +- Use `type="email"` for mobile keyboard +- Include placeholder ("you@example.com") +- Clear error messages ("Please enter a valid email address" not "Invalid") + +### Consent Checkboxes (Marketing) + +- **Unchecked by default** (required) +- Specific language about what they're signing up for +- Separate checkboxes for different email types +- Link to privacy policy + +``` +☐ Subscribe to our weekly newsletter with product updates +☐ Send me promotional offers and deals +``` + +**Don't:** Pre-check boxes, use vague language, hide in terms. + +### Form Layout + +- Keep simple and focused +- One primary action +- Clear value proposition +- Mobile-friendly +- Accessible (labels, ARIA) + +## Error Handling + +### Invalid Email + +- Show clear error message +- Suggest corrections for common typos (@gmial.com → @gmail.com) +- Allow user to fix and resubmit + +### Already Registered + +- Accounts: "This email is already registered. [Sign in]" +- Marketing: "You're already subscribed! [Manage preferences]" +- Don't reveal if account exists (security) + +### Rate Limiting + +- Limit verification emails (3/hour per email) +- Rate limit form submissions +- Use CAPTCHA sparingly if needed +- Monitor for abuse patterns + +## Verification Emails + +**Content:** +- Clear purpose ("Verify your email address") +- Prominent verification button +- Expiration time +- Resend option +- "I didn't request this" notice + +**Design:** +- Mobile-friendly +- Large, tappable button +- Clear call-to-action + +See [Transactional Emails](./transactional-emails.md) for detailed email design guidance. + +## Related + +- [Compliance](./compliance.md) - Legal requirements for consent (GDPR, CASL) +- [Marketing Emails](./marketing-emails.md) - What happens after capture +- [Deliverability](./deliverability.md) - How validation improves sender reputation diff --git a/profiles/opencode/skill/email-best-practices/resources/email-types.md b/profiles/opencode/skill/email-best-practices/resources/email-types.md new file mode 100644 index 0000000..235c56c --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/email-types.md @@ -0,0 +1,177 @@ +# Email Types: Transactional vs Marketing + +Understanding the difference between transactional and marketing emails is crucial for compliance, deliverability, and user experience. This guide explains the distinctions and provides a catalog of transactional emails your app should include. + +## When to Use This + +- Deciding whether an email should be transactional or marketing +- Understanding legal distinctions between email types +- Planning what transactional emails your app needs +- Ensuring compliance with email regulations +- Setting up separate sending infrastructure + +## Transactional vs Marketing: Key Differences + +### Transactional Emails + +**Definition:** Emails that facilitate or confirm a transaction the user initiated or expects. They're directly related to an action the user took. + +**Characteristics:** +- User-initiated or expected +- Time-sensitive and actionable +- Required for the user to complete an action +- Not promotional in nature +- Can be sent without explicit opt-in (with limitations) + +**Examples:** +- Password reset links +- Order confirmations +- Account verification +- OTP/2FA codes +- Shipping notifications + +### Marketing Emails + +**Definition:** Emails sent for promotional, advertising, or informational purposes that are not directly related to a specific transaction. + +**Characteristics:** +- Promotional or informational content +- Not time-sensitive to complete a transaction +- Require explicit opt-in (consent) +- Must include unsubscribe options +- Subject to stricter compliance requirements + +**Examples:** +- Newsletters +- Product announcements +- Promotional offers +- Company updates +- Educational content + +## Legal Distinctions + +### CAN-SPAM Act (US) + +**Transactional emails:** +- Can be sent without opt-in +- Must be related to a transaction +- Cannot contain promotional content (with exceptions) +- Must identify sender and provide contact information + +**Marketing emails:** +- Require opt-out mechanism (not opt-in in US) +- Must include clear sender identification +- Must include physical mailing address +- Must honor opt-out requests within 10 business days + +### GDPR (EU) + +**Transactional emails:** +- Can be sent based on legitimate interest or contract fulfillment +- Must be necessary for service delivery +- Cannot contain marketing content without consent + +**Marketing emails:** +- Require explicit opt-in consent +- Must clearly state purpose of data collection +- Must provide easy unsubscribe +- Subject to data protection requirements + +### CASL (Canada) + +**Transactional emails:** +- Can be sent without consent if related to ongoing business relationship +- Must be factual and not promotional + +**Marketing emails:** +- Require express or implied consent +- Must include unsubscribe mechanism +- Must identify sender clearly + +## When to Use Each Type + +### Use Transactional When: + +- User needs the email to complete an action +- Email confirms a transaction or account change +- Email provides security-related information +- Email is expected based on user action +- Content is time-sensitive and actionable + +### Use Marketing When: + +- Promoting products or services +- Sending newsletters or updates +- Sharing educational content +- Announcing features or company news +- Content is not required for a transaction + +## Hybrid Emails: The Gray Area + +Some emails mix transactional and marketing content. Be careful: + +**Best practice:** Keep transactional and marketing separate. If you must include marketing in a transactional email: +- Make transactional content primary +- Keep marketing content minimal and clearly separated +- Ensure transactional purpose is clear +- Check local regulations (some regions prohibit this) + +**Example of acceptable hybrid:** +- Order confirmation (transactional) with a small "You might also like" section (marketing) + +**Example of problematic hybrid:** +- Newsletter (marketing) with a small order status update (transactional) + +## Transactional Email Catalog + +For a complete catalog of transactional emails and recommended combinations by app type, see [Transactional Email Catalog](./transactional-email-catalog.md). + +**Quick reference - Essential emails for most apps:** +1. **Email verification** - Required for account creation +2. **Password reset** - Required for account recovery +3. **Welcome email** - Good user experience + +The catalog includes detailed guidance for: +- Authentication-focused apps +- Newsletter / content platforms +- E-commerce / marketplaces +- SaaS / subscription services +- Financial / fintech apps +- Social / community platforms +- Developer tools / API platforms +- Healthcare / HIPAA-compliant apps + +## Sending Infrastructure + +### Separate Infrastructure + +**Best practice:** Use separate sending infrastructure for transactional and marketing emails. + +**Benefits:** +- Protect transactional deliverability +- Different authentication domains +- Independent reputation +- Easier compliance management + +**Implementation:** +- Use different subdomains (e.g., `mail.app.com` for transactional, `news.app.com` for marketing) +- Separate email service accounts or API keys +- Different monitoring and alerting + +### Email Service Considerations + +Choose an email service that: +- Provides reliable delivery for transactional emails +- Offers separate sending domains +- Has good API for programmatic sending +- Provides webhooks for delivery events +- Supports authentication setup (SPF, DKIM, DMARC) + +Services like Resend are designed for transactional emails and provide the infrastructure and tools needed for reliable delivery. + +## Related Topics + +- [Transactional Emails](./transactional-emails.md) - Best practices for sending transactional emails +- [Marketing Emails](./marketing-emails.md) - Best practices for marketing emails +- [Compliance](./compliance.md) - Legal requirements for each email type +- [Deliverability](./deliverability.md) - Ensuring transactional emails are delivered diff --git a/profiles/opencode/skill/email-best-practices/resources/list-management.md b/profiles/opencode/skill/email-best-practices/resources/list-management.md new file mode 100644 index 0000000..f39ee27 --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/list-management.md @@ -0,0 +1,157 @@ +# List Management + +Maintaining clean email lists through suppression, hygiene, and data retention. + +## Suppression Lists + +A suppression list prevents sending to addresses that should never receive email. + +### What to Suppress + +| Reason | Action | Can Unsuppress? | +|--------|--------|-----------------| +| Hard bounce | Add immediately | No (address invalid) | +| Complaint (spam) | Add immediately | No (legal requirement) | +| Unsubscribe | Add immediately | Only if user re-subscribes | +| Soft bounce (3x) | Add after threshold | Yes, after 30-90 days | +| Manual removal | Add on request | Only if user requests | + +### Implementation + +```typescript +// Suppression list schema +interface SuppressionEntry { + email: string; + reason: 'hard_bounce' | 'complaint' | 'unsubscribe' | 'soft_bounce' | 'manual'; + created_at: Date; + source_email_id?: string; // Which email triggered this +} + +// Check before every send +async function canSendTo(email: string): Promise { + const suppressed = await db.suppressions.findOne({ email }); + return !suppressed; +} + +// Add to suppression list +async function suppressEmail(email: string, reason: string, sourceId?: string) { + await db.suppressions.upsert({ + email: email.toLowerCase(), + reason, + created_at: new Date(), + source_email_id: sourceId, + }); +} +``` + +### Pre-Send Check + +**Always check suppression before sending:** + +```typescript +async function sendEmail(to: string, emailData: EmailData) { + if (!await canSendTo(to)) { + console.log(`Skipping suppressed email: ${to}`); + return { skipped: true, reason: 'suppressed' }; + } + + return await resend.emails.send({ to, ...emailData }); +} +``` + +## List Hygiene + +Regular maintenance to keep lists healthy. + +### Automated Cleanup + +| Task | Frequency | Action | +|------|-----------|--------| +| Remove hard bounces | Real-time (via webhook) | Immediate suppression | +| Remove complaints | Real-time (via webhook) | Immediate suppression | +| Process unsubscribes | Real-time | Remove from marketing lists | +| Review soft bounces | Daily | Suppress after 3 failures | +| Remove inactive | Monthly | Re-engagement → remove | + +### Re-engagement Campaigns + +Before removing inactive subscribers: + +1. **Identify inactive:** No opens/clicks in 90-180 days +2. **Send re-engagement:** "We miss you" or "Still interested?" +3. **Wait 14-30 days** for response +4. **Remove non-responders** from active lists + +```typescript +async function runReengagement() { + const inactive = await getInactiveSubscribers(90); // 90 days + + for (const subscriber of inactive) { + if (!subscriber.reengagement_sent) { + await sendReengagementEmail(subscriber); + await markReengagementSent(subscriber.email); + } else if (daysSince(subscriber.reengagement_sent) > 30) { + await removeFromMarketingLists(subscriber.email); + } + } +} +``` + +## Data Retention + +### Email Logs + +| Data Type | Recommended Retention | Notes | +|-----------|----------------------|-------| +| Send attempts | 90 days | Debugging, analytics | +| Delivery status | 90 days | Compliance, reporting | +| Bounce/complaint events | 3 years | Required for CASL | +| Suppression list | Indefinite | Never delete | +| Email content | 30 days | Storage costs | +| Consent records | 3 years after expiry | Legal requirement | + +### Retention Policy Implementation + +```typescript +// Daily cleanup job +async function cleanupOldData() { + const now = new Date(); + + // Delete old email logs (keep 90 days) + await db.emailLogs.deleteMany({ + created_at: { $lt: subDays(now, 90) } + }); + + // Delete old email content (keep 30 days) + await db.emailContent.deleteMany({ + created_at: { $lt: subDays(now, 30) } + }); + + // Never delete: suppressions, consent records +} +``` + +## Metrics to Monitor + +| Metric | Target | Alert Threshold | +|--------|--------|-----------------| +| Bounce rate | <2% | >5% | +| Complaint rate | <0.1% | >0.2% | +| Suppression list growth | Stable | Sudden spike | +| List churn | <2%/month | >5%/month | + +## Transactional vs Marketing Lists + +**Keep separate:** +- Transactional: Can send to anyone with account relationship +- Marketing: Only opted-in subscribers + +**Suppression applies to both:** Hard bounces and complaints suppress across all email types. + +**Unsubscribe is marketing-only:** User unsubscribing from marketing can still receive transactional emails (password resets, order confirmations). + +## Related + +- [Webhooks & Events](./webhooks-events.md) - Receive bounce/complaint notifications +- [Deliverability](./deliverability.md) - How list hygiene affects sender reputation +- [Compliance](./compliance.md) - Legal requirements for data retention diff --git a/profiles/opencode/skill/email-best-practices/resources/marketing-emails.md b/profiles/opencode/skill/email-best-practices/resources/marketing-emails.md new file mode 100644 index 0000000..b8c3748 --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/marketing-emails.md @@ -0,0 +1,115 @@ +# Marketing Email Best Practices + +Promotional emails that require explicit consent and provide value to recipients. + +## Core Principles + +1. **Consent first** - Explicit opt-in required (especially GDPR/CASL) +2. **Value-driven** - Provide useful content, not just promotions +3. **Respect preferences** - Let users control frequency and content types + +## Opt-In Requirements + +### Explicit Opt-In + +**What counts:** +- User checks unchecked box +- User clicks "Subscribe" button +- User completes form with clear subscription intent + +**What doesn't count:** +- Pre-checked boxes +- Opt-out model +- Assumed consent from purchase +- Purchased/rented lists + +### Informed Consent + +Disclose: email types, frequency, sender identity, how to unsubscribe. + +✅ "Subscribe to our weekly newsletter with product updates and tips" +❌ "Sign up for emails" + +### Double Opt-In (Recommended) + +1. User submits email +2. Send confirmation email with verification link +3. User clicks to confirm +4. Add to list only after confirmation + +Benefits: Verifies deliverability, confirms intent, reduces complaints, required in some regions (Germany). + +## Unsubscribe Requirements + +**Must be:** +- Prominent in every email +- One-click (preferred) or simple process +- Immediate (GDPR) or within 10 days (CAN-SPAM) +- Free, no login required + +**Preference center options:** Frequency (daily/weekly/monthly), content types, complete unsubscribe. + +## Content and Design + +### Subject Lines + +- Clear and specific (50 chars or less for mobile) +- Create curiosity without misleading +- A/B test regularly + +✅ "Your weekly digest: 5 productivity tips" +❌ "You won't believe what happened!" + +### Structure + +**Above fold:** Value proposition, primary CTA, engaging visual + +**Body:** Scannable (short paragraphs, bullets), clear hierarchy, multiple CTAs + +**Footer:** Unsubscribe link, company info, physical address (CAN-SPAM), social links + +### Mobile-First + +- Single column layout +- 44x44px minimum buttons +- 16px minimum text +- Test on iOS, Android, dark mode + +## Segmentation + +**Segment by:** Behavior (purchases, activity), demographics, preferences, engagement level, signup source. + +Benefits: Higher open/click rates, lower unsubscribes, better experience. + +## Personalization + +**Options:** Name in subject/greeting, location-specific content, behavior-based recommendations, purchase history. + +**Don't over-personalize** - can feel intrusive. Use data you have permission to use. + +## Frequency and Timing + +**Frequency:** Start conservative, increase based on engagement, let users set preferences, monitor unsubscribe rates. + +**Timing:** Weekday mornings (9-11 AM local), Tuesday-Thursday often best. Test your specific audience. + +## List Hygiene + +**Remove immediately:** Hard bounces, unsubscribes, complaints + +**Remove after inactivity:** Send re-engagement campaign first, then remove non-responders + +**Monitor:** Bounce rate <2%, complaint rate <0.1% + +## Required Elements (All Marketing Emails) + +- Clear sender identification +- Physical mailing address (CAN-SPAM) +- Unsubscribe mechanism +- Indication it's marketing (GDPR) + +## Related + +- [Compliance](./compliance.md) - Detailed legal requirements by region +- [Email Capture](./email-capture.md) - Collecting consent properly +- [List Management](./list-management.md) - Maintaining list hygiene diff --git a/profiles/opencode/skill/email-best-practices/resources/sending-reliability.md b/profiles/opencode/skill/email-best-practices/resources/sending-reliability.md new file mode 100644 index 0000000..2143b36 --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/sending-reliability.md @@ -0,0 +1,155 @@ +# Sending Reliability + +Ensuring emails are sent exactly once and handling failures gracefully. + +## Idempotency + +Prevent duplicate emails when retrying failed requests. + +### The Problem + +Network issues, timeouts, or server errors can leave you uncertain if an email was sent. Retrying without idempotency risks sending duplicates. + +### Solution: Idempotency Keys + +Send a unique key with each request. If the same key is sent again, the server returns the original response instead of sending another email. + +```typescript +// Generate deterministic key based on the business event +const idempotencyKey = `password-reset-${userId}-${resetRequestId}`; + +await resend.emails.send({ + from: 'noreply@example.com', + to: user.email, + subject: 'Reset your password', + html: emailHtml, +}, { + headers: { + 'Idempotency-Key': idempotencyKey + } +}); +``` + +### Key Generation Strategies + +| Strategy | Example | Use When | +|----------|---------|----------| +| Event-based | `order-confirm-${orderId}` | One email per event (recommended) | +| Request-scoped | `reset-${userId}-${resetRequestId}` | Retries within same request | +| UUID | `crypto.randomUUID()` | No natural key (generate once, reuse on retry) | + +**Best practice:** Use deterministic keys based on the business event. If you retry the same logical send, the same key must be generated. Avoid `Date.now()` or random values generated fresh on each attempt. + +**Key expiration:** Idempotency keys are typically cached for 24 hours. Retries within this window return the original response. After expiration, the same key triggers a new send—so complete your retry logic well within 24 hours. + +## Retry Logic + +Handle transient failures with exponential backoff. + +### When to Retry + +| Error Type | Retry? | Notes | +|------------|--------|-------| +| 5xx (server error) | ✅ Yes | Transient, likely to resolve | +| 429 (rate limit) | ✅ Yes | Wait for rate limit window | +| 4xx (client error) | ❌ No | Fix the request first | +| Network timeout | ✅ Yes | Transient | +| DNS failure | ✅ Yes | May be transient | + +### Exponential Backoff + +```typescript +async function sendWithRetry(emailData, maxRetries = 3) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await resend.emails.send(emailData); + } catch (error) { + if (!isRetryable(error) || attempt === maxRetries - 1) { + throw error; + } + const delay = Math.min(1000 * Math.pow(2, attempt), 30000); + await sleep(delay + Math.random() * 1000); // Add jitter + } + } +} + +function isRetryable(error) { + return error.statusCode >= 500 || + error.statusCode === 429 || + error.code === 'ETIMEDOUT'; +} +``` + +**Backoff schedule:** 1s → 2s → 4s → 8s (with jitter to prevent thundering herd) + +## Error Handling + +### Common Error Codes + +| Code | Meaning | Action | +|------|---------|--------| +| 400 | Bad request | Fix payload (invalid email, missing field) | +| 401 | Unauthorized | Check API key | +| 403 | Forbidden | Check permissions, domain verification | +| 404 | Not found | Check endpoint URL | +| 422 | Validation error | Fix request data | +| 429 | Rate limited | Back off, retry after delay | +| 500 | Server error | Retry with backoff | +| 503 | Service unavailable | Retry with backoff | + +### Error Handling Pattern + +```typescript +try { + const result = await resend.emails.send(emailData); + await logSuccess(result.id, emailData); +} catch (error) { + if (error.statusCode === 429) { + await queueForRetry(emailData, error.retryAfter); + } else if (error.statusCode >= 500) { + await queueForRetry(emailData); + } else { + await logFailure(error, emailData); + await alertOnCriticalEmail(emailData); // For password resets, etc. + } +} +``` + +## Queuing for Reliability + +For critical emails, use a queue to ensure delivery even if the initial send fails. + +**Benefits:** +- Survives application restarts +- Automatic retry handling +- Rate limit management +- Audit trail + +**Simple pattern:** +1. Write email to queue/database with "pending" status +2. Process queue, attempt send +3. On success: mark "sent", store message ID +4. On retryable failure: increment retry count, schedule retry +5. On permanent failure: mark "failed", alert + +## Timeouts + +Set appropriate timeouts to avoid hanging requests. + +```typescript +const controller = new AbortController(); +const timeout = setTimeout(() => controller.abort(), 10000); + +try { + await resend.emails.send(emailData, { signal: controller.signal }); +} finally { + clearTimeout(timeout); +} +``` + +**Recommended:** 10-30 seconds for email API calls. + +## Related + +- [Webhooks & Events](./webhooks-events.md) - Process delivery confirmations and failures +- [List Management](./list-management.md) - Handle bounces and suppress invalid addresses diff --git a/profiles/opencode/skill/email-best-practices/resources/transactional-email-catalog.md b/profiles/opencode/skill/email-best-practices/resources/transactional-email-catalog.md new file mode 100644 index 0000000..9990ac7 --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/transactional-email-catalog.md @@ -0,0 +1,418 @@ +# Transactional Email Catalog + +A comprehensive catalog of transactional emails organized by category, plus recommended email combinations for different app types. + +## When to Use This + +- Planning what transactional emails your app needs +- Choosing the right emails for your app type +- Understanding what content each email type should include +- Implementing transactional email features + +## Email Combinations by App Type + +Use these combinations as a starting point based on what you're building. + +### Authentication-Focused App + +Apps where user accounts and security are core (login systems, identity providers, account management). + +**Essential:** +- Email verification +- Password reset +- OTP / 2FA codes +- Security alerts (new device, password change) +- Account update notifications + +**Optional:** +- Welcome email +- Account deletion confirmation + +### Newsletter / Content Platform + +Apps focused on content delivery and subscriptions. + +**Essential:** +- Email verification +- Password reset +- Welcome email +- Subscription confirmation + +**Optional:** +- OTP / 2FA codes +- Account update notifications + +### E-commerce / Marketplace + +Apps where users buy products or services. + +**Essential:** +- Email verification +- Password reset +- Welcome email +- Order confirmation +- Shipping notifications +- Invoice / receipt +- Payment failed notices + +**Optional:** +- OTP / 2FA codes +- Security alerts +- Subscription confirmations (for recurring orders) + +### SaaS / Subscription Service + +Apps with paid subscription tiers and ongoing billing. + +**Essential:** +- Email verification +- Password reset +- Welcome email +- OTP / 2FA codes +- Security alerts +- Subscription confirmation +- Subscription renewal notice +- Payment failed notices +- Invoice / receipt + +**Optional:** +- Account update notifications +- Feature change notifications (for breaking changes) + +### Financial / Fintech App + +Apps handling money, payments, or sensitive financial data. + +**Essential:** +- Email verification +- Password reset +- OTP / 2FA codes (required for sensitive actions) +- Security alerts (all types) +- Account update notifications +- Transaction confirmations +- Invoice / receipt +- Payment failed notices + +**Optional:** +- Welcome email +- Compliance notices + +### Social / Community Platform + +Apps focused on user interaction and community features. + +**Essential:** +- Email verification +- Password reset +- Welcome email +- Security alerts + +**Optional:** +- OTP / 2FA codes +- Account update notifications +- Activity notifications (mentions, replies) + +### Developer Tools / API Platform + +Apps targeting developers with API access and integrations. + +**Essential:** +- Email verification +- Password reset +- OTP / 2FA codes +- Security alerts +- API key notifications (creation, expiration) +- Subscription confirmation +- Payment failed notices + +**Optional:** +- Welcome email +- Usage alerts (approaching limits) +- Feature change notifications + +### Healthcare / HIPAA-Compliant App + +Apps handling protected health information. + +**Essential:** +- Email verification +- Password reset +- OTP / 2FA codes (required) +- Security alerts (all types, detailed) +- Account update notifications +- Appointment confirmations + +**Optional:** +- Welcome email +- Compliance notices + +**Note:** Healthcare apps have strict requirements. Emails should contain minimal PHI and link to secure portals for sensitive information. + +--- + +## Full Email Catalog + +### Authentication & Security + +#### Email Verification / Account Verification + +**When to send:** Immediately after user signs up or changes email address. + +**Purpose:** Verify the email address belongs to the user. + +**Content should include:** +- Clear verification link or code +- Expiration time (typically 24-48 hours) +- Instructions on what to do +- Security notice if link is clicked by mistake + +**Best practices:** +- Send immediately (within seconds) +- Include expiration notice +- Provide resend option +- Link to support if issues + +#### OTP / 2FA Codes + +**When to send:** When user requests two-factor authentication code. + +**Purpose:** Provide time-sensitive authentication code. + +**Content should include:** +- The OTP code (clearly displayed) +- Expiration time (typically 5-10 minutes) +- Security warnings +- Instructions on what to do if not requested + +**Best practices:** +- Send immediately +- Code should be large and easy to read +- Include expiration prominently +- Warn about sharing codes +- Provide "I didn't request this" link + +#### Password Reset + +**When to send:** When user requests password reset. + +**Purpose:** Allow user to securely reset forgotten password. + +**Content should include:** +- Reset link (with token) +- Expiration time (typically 1 hour) +- Security warnings +- Instructions if not requested + +**Best practices:** +- Send immediately +- Link expires quickly (1 hour) +- Include IP address and location if available +- Provide "I didn't request this" link +- Don't include the old password + +#### Security Alerts + +**When to send:** When security-relevant events occur (login from new device, password change, etc.). + +**Purpose:** Notify user of account security events. + +**Content should include:** +- What happened (clear description) +- When it happened +- Location/IP if available +- Action to take if suspicious +- Link to security settings + +**Best practices:** +- Send immediately +- Be clear and specific +- Include actionable steps +- Provide way to report suspicious activity + +### Account Management + +#### Welcome Email + +**When to send:** Immediately after successful account creation and verification. + +**Purpose:** Welcome new users and guide them to next steps. + +**Content should include:** +- Welcome message +- Key features or next steps +- Links to important resources +- Support contact information + +**Best practices:** +- Send after email verification +- Keep it focused and actionable +- Don't overwhelm with information +- Set expectations about future emails + +#### Account Update Notifications + +**When to send:** When user changes account settings (email, password, profile, etc.). + +**Purpose:** Confirm account changes and provide security notice. + +**Content should include:** +- What changed +- When it changed +- Action to take if unauthorized +- Link to account settings + +**Best practices:** +- Send immediately after change +- Be specific about what changed +- Include security notice +- Provide easy way to revert if needed + +### E-commerce & Transactions + +#### Order Confirmations + +**When to send:** Immediately after order is placed. + +**Purpose:** Confirm order details and provide receipt. + +**Content should include:** +- Order number +- Items ordered with quantities +- Pricing breakdown +- Shipping address +- Estimated delivery date +- Order tracking link (if available) + +**Best practices:** +- Send within minutes of order +- Include all order details +- Make it easy to print or save +- Provide customer service contact + +#### Shipping Notifications + +**When to send:** When order ships, with tracking updates. + +**Purpose:** Notify user that order has shipped and provide tracking. + +**Content should include:** +- Order number +- Tracking number +- Carrier information +- Expected delivery date +- Tracking link +- Shipping address confirmation + +**Best practices:** +- Send when order ships +- Include tracking number prominently +- Provide carrier tracking link +- Update on major tracking milestones + +#### Invoices and Receipts + +**When to send:** After payment is processed. + +**Purpose:** Provide payment confirmation and receipt. + +**Content should include:** +- Invoice/receipt number +- Payment amount +- Payment method +- Items/services purchased +- Payment date +- Downloadable PDF (if applicable) + +**Best practices:** +- Send immediately after payment +- Include all payment details +- Make it easy to download/save +- Include tax information if applicable + +### Subscriptions & Billing + +#### Subscription Confirmations + +**When to send:** When user subscribes or changes subscription. + +**Purpose:** Confirm subscription details and billing information. + +**Content should include:** +- Subscription plan details +- Billing amount and frequency +- Next billing date +- Payment method +- Link to manage subscription + +**Best practices:** +- Send immediately after subscription +- Clearly state billing terms +- Provide easy cancellation option +- Include support contact + +#### Subscription Renewal Notices + +**When to send:** Before subscription renews (typically 3-7 days before). + +**Purpose:** Notify user of upcoming renewal and charge. + +**Content should include:** +- Renewal date +- Amount to be charged +- Payment method on file +- Link to update payment method +- Link to cancel if desired + +**Best practices:** +- Send with enough notice (3-7 days) +- Be clear about amount and date +- Make it easy to update payment method +- Provide cancellation option + +#### Payment Failed Notices + +**When to send:** When subscription payment fails. + +**Purpose:** Notify user of payment failure and provide resolution steps. + +**Content should include:** +- What happened +- Amount that failed +- Reason for failure (if available) +- Steps to resolve +- Link to update payment method +- Consequences if not resolved + +**Best practices:** +- Send immediately after failure +- Be clear about consequences +- Provide easy resolution path +- Include support contact + +### Notifications & Updates + +#### Feature Announcements (Transactional) + +**When to send:** When a feature the user is using changes significantly. + +**Purpose:** Notify users of changes that affect their use of the service. + +**Content should include:** +- What changed +- How it affects the user +- What action (if any) is needed +- Link to more information + +**Best practices:** +- Only for significant changes +- Focus on user impact +- Provide clear next steps +- Link to documentation + +**Note:** General feature announcements are marketing emails. Only send as transactional if the change directly affects an active feature the user is using. + +## Related Topics + +- [Email Types](./email-types.md) - Understanding transactional vs marketing +- [Transactional Emails](./transactional-emails.md) - Best practices for sending transactional emails +- [Compliance](./compliance.md) - Legal requirements for each email type diff --git a/profiles/opencode/skill/email-best-practices/resources/transactional-emails.md b/profiles/opencode/skill/email-best-practices/resources/transactional-emails.md new file mode 100644 index 0000000..57a2eeb --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/transactional-emails.md @@ -0,0 +1,92 @@ +# Transactional Email Best Practices + +Clear, actionable emails that users expect and need—password resets, confirmations, OTPs. + +## Core Principles + +1. **Clarity over creativity** - Users need to understand and act quickly +2. **Action-oriented** - Clear purpose, obvious primary action +3. **Time-sensitive** - Send immediately (within seconds) + +## Subject Lines + +**Be specific and include context:** + +| ✅ Good | ❌ Bad | +|---------|--------| +| Reset your password for [App] | Action required | +| Your order #12345 has shipped | Update on your order | +| Your 2FA code: 123456 | Security code | +| Verify your email for [App] | Verify your email | + +Include identifiers when helpful: order numbers, account names, expiration times. + +## Pre-Header + +The text snippet after subject line. Use it to: +- Reinforce subject ("This link expires in 1 hour") +- Add urgency or context +- Call-to-action preview + +Keep under 90-100 characters. + +## Content Structure + +**Above the fold (first screen):** +- Clear purpose +- Primary action button +- Time-sensitive details (expiration) + +**Hierarchy:** Header → Primary message → Details → Action button → Secondary info + +**Format:** Short paragraphs (2-3 sentences), bullet points, bold for emphasis, white space. + +## Mobile-First Design + +60%+ emails opened on mobile. + +- **Layout:** Single column, stack vertically +- **Buttons:** 44x44px minimum, full-width on mobile +- **Text:** 16px minimum body, 20-24px headings +- **OTP codes:** 24-32px, monospace font + +## Sender Configuration + +| Field | Best Practice | Example | +|-------|--------------|---------| +| From Name | App/company name, consistent | [App Name] | +| From Email | Subdomain, real address | hello@mail.yourdomain.com | +| Reply-To | Monitored inbox | support@yourdomain.com | + +Avoid `noreply@` - users reply to transactional emails. + +## Code and Link Display + +**OTP/Verification codes:** +- Large (24-32px), monospace font +- Centered, clear label +- Include expiration nearby +- Make copyable + +**Buttons:** +- Large, tappable (44x44px+) +- Contrasting colors +- Clear action text ("Reset Password", "Verify Email") +- HTTPS links only + +## Error Handling + +**Resend functionality:** +- Allow after 60 seconds +- Limit attempts (3 per hour) +- Show countdown timer + +**Expired links:** +- Clear "expired" message +- Offer to send new link +- Provide support contact + +**"I didn't request this":** +- Include in password resets, OTPs, security alerts +- Link to security contact +- Log clicks for monitoring diff --git a/profiles/opencode/skill/email-best-practices/resources/webhooks-events.md b/profiles/opencode/skill/email-best-practices/resources/webhooks-events.md new file mode 100644 index 0000000..75454e8 --- /dev/null +++ b/profiles/opencode/skill/email-best-practices/resources/webhooks-events.md @@ -0,0 +1,163 @@ +# Webhooks and Events + +Receiving and processing email delivery events in real-time. + +## Event Types + +| Event | When Fired | Use For | +|-------|------------|---------| +| `email.sent` | Email accepted by Resend | Confirming send initiated | +| `email.delivered` | Email delivered to recipient server | Confirming delivery | +| `email.bounced` | Email bounced (hard or soft) | List hygiene, alerting | +| `email.complained` | Recipient marked as spam | Immediate unsubscribe | +| `email.opened` | Recipient opened email | Engagement tracking | +| `email.clicked` | Recipient clicked link | Engagement tracking | + +## Webhook Setup + +### 1. Create Endpoint + +Your endpoint must: +- Accept POST requests +- Return 2xx status quickly (within 5 seconds) +- Handle duplicate events (idempotent processing) + +```typescript +app.post('/webhooks/resend', async (req, res) => { + // Return 200 immediately to acknowledge receipt + res.status(200).send('OK'); + + // Process asynchronously + processWebhookAsync(req.body).catch(console.error); +}); +``` + +### 2. Verify Signatures + +Always verify webhook signatures to prevent spoofing. + +```typescript +import { Webhook } from 'svix'; + +const webhook = new Webhook(process.env.RESEND_WEBHOOK_SECRET); + +app.post('/webhooks/resend', (req, res) => { + try { + const payload = webhook.verify( + JSON.stringify(req.body), + { + 'svix-id': req.headers['svix-id'], + 'svix-timestamp': req.headers['svix-timestamp'], + 'svix-signature': req.headers['svix-signature'], + } + ); + // Process verified payload + } catch (err) { + return res.status(400).send('Invalid signature'); + } +}); +``` + +### 3. Register Webhook URL + +Configure your webhook endpoint in the Resend dashboard or via API. + +## Processing Events + +### Bounce Handling + +```typescript +async function handleBounce(event) { + const { email_id, email, bounce_type } = event.data; + + if (bounce_type === 'hard') { + // Permanent failure - remove from all lists + await suppressEmail(email, 'hard_bounce'); + await removeFromAllLists(email); + } else { + // Soft bounce - track and remove after threshold + await incrementSoftBounce(email); + const count = await getSoftBounceCount(email); + if (count >= 3) { + await suppressEmail(email, 'soft_bounce_limit'); + } + } +} +``` + +### Complaint Handling + +```typescript +async function handleComplaint(event) { + const { email } = event.data; + + // Immediate suppression - no exceptions + await suppressEmail(email, 'complaint'); + await removeFromAllLists(email); + await logComplaint(event); // For analysis +} +``` + +### Delivery Confirmation + +```typescript +async function handleDelivered(event) { + const { email_id } = event.data; + await updateEmailStatus(email_id, 'delivered'); +} +``` + +## Idempotent Processing + +Webhooks may be sent multiple times. Use event IDs to prevent duplicate processing. + +```typescript +async function processWebhook(event) { + const eventId = event.id; + + // Check if already processed + if (await isEventProcessed(eventId)) { + return; // Skip duplicate + } + + // Process event + await handleEvent(event); + + // Mark as processed + await markEventProcessed(eventId); +} +``` + +## Error Handling + +### Retry Behavior + +If your endpoint returns non-2xx, webhooks will retry with exponential backoff: +- Retry 1: ~30 seconds +- Retry 2: ~1 minute +- Retry 3: ~5 minutes +- (continues for ~24 hours) + +### Best Practices + +- **Return 200 quickly** - Process asynchronously to avoid timeouts +- **Be idempotent** - Handle duplicate deliveries gracefully +- **Log everything** - Store raw events for debugging +- **Alert on failures** - Monitor webhook processing errors +- **Queue for processing** - Use a job queue for complex handling + +## Testing Webhooks + +**Local development:** Use ngrok or similar to expose localhost. + +```bash +ngrok http 3000 +# Use the ngrok URL as your webhook endpoint +``` + +**Verify handling:** Send test events through Resend dashboard or manually trigger each event type. + +## Related + +- [List Management](./list-management.md) - What to do with bounce/complaint data +- [Sending Reliability](./sending-reliability.md) - Retry logic when sends fail