Skip to content

feat(skills): move SMTP sends to PostHog - #463

Draft
Silthus wants to merge 52 commits into
feat/migrate-email-loopsfrom
feat/migrate-email-smtp
Draft

Silthus wants to merge 52 commits into
feat/migrate-email-loopsfrom
feat/migrate-email-smtp

Conversation

@Silthus

@Silthus Silthus commented Nov 4, 2026 •

Copy link
Copy Markdown

Problem

A person who sends over SMTP and asks to move those sends to PostHog gets no help: migrate-email covers Resend and, with #459 and the PRs stacked on it, SendGrid, Postmark, Mailgun and Loops. Each of those keeps its own SMTP sends with "PostHog has no SMTP relay".

With this PR, migrate-email-smtp moves sends through an SMTP client, such as Nodemailer, smtplib or PHPMailer, to PostHog's email send in each language where PostHog has an SDK with an email send, and wizard migrate smtp runs it. The other providers' runs now point their SMTP sends here.

Important

Stacked on #462. Merge that first. This PR's own changes: b02e1b0...3e93eac.
It adds one provider folder, one config variant, one AGENTS.md row and the provider in the bundle test, and changes the SMTP row of four other providers' mapping.md to point here. The shared steps it uses, and their review fixes, come from #459.
Like #459, it waits for the PostHog SDK releases with the code-form email send. Nothing merges until code sends are live and usable for customers: Michael's rule for #451 and every PR stacked on it.

Impact

Sends that arrive with migratedFrom: 'smtp' show whether people use this skill to move off SMTP. Read it 14 days after the Wizard runs migrate as a family.

Merge risk

Warning

🟡 Merge risk: medium · two-way door

  • The new files run only when a person picks wizard migrate smtp, so reach is few. Then the skill edits their send code in up to eight languages, and a wrong rewrite harms silently once they deploy: a refused connection handled as a lost one, an error branch widened, or tracking turned on where the host had it off. Each moved site keeps its error behavior and its host's tracking, a site stays whenever a rule can't be checked (the host's tracking, Python's connection errors, a read of Nodemailer's info), and the report's "Before cutover" list names what the person checks first.
  • Outside references/smtp/, only the reason text of one row changes in Resend's, SendGrid's, Postmark's and Mailgun's mapping.md: their SMTP sends still stay, and the report now names wizard migrate smtp. No shared step and no build code changes.
  • Revert: git revert the merge commit. It removes the SMTP variant and the four pointers.

Summary

references/smtp/ holds the three files each variant has:

  • sdk-reference.md: the SMTP clients in Node.js (Nodemailer), Python (smtplib, aiosmtplib), Ruby (mail, net-smtp), PHP (PHPMailer, Symfony Mailer), Go (net/smtp, gomail, go-mail), .NET (MailKit, System.Net.Mail), Elixir (gen_smtp) and Rust (lettre), the framework mailers, and the two Node.js renderers that run inside the send. A host table names the provider behind each host, and the host or a Nodemailer service name sets that provider's region. smtp.loops.so carries a Loops template send as JSON, so its sites stay with the reason Loops SMTP send: run wizard migrate loops. Nodemailer's non-SMTP transports, smtp-server and mailparser are listed as not SMTP.
  • mapping.md:
    • Fields. Each message field maps to PostHog's or names what keeps a site on SMTP: headers (including list), icalEvent, priority, sender, dsn, AMP and other alternatives, a message the code writes by hand (raw, net/smtp, Net::SMTP), several recipients, a message with no To, framework mailers, and a language without a PostHog SDK that sends email. Sites with cc, bcc, envelope-only recipients or attachments stay too, because no PostHog SDK takes them yet. A date of the send time and a messageId nothing reads move without it. A replyTo passed in as a parameter is read at its callers.
    • Tracking. An SMTP server tracks nothing, but the provider behind the host may. The person is asked once per host: both tracked moves as it is, neither moves with captureEngagement: false, and one of each, or <host> tracking not checked, stays.
    • Code around the send. A catch with no branch moves. A branch on an invalid send (EENVELOPE, SMTPRecipientsRefused, Net::SMTPFatalError, MailKit's RecipientNotAccepted, a 5xx reply) reads validation_error. A branch on a failed login, a connection error or a temporary 4xx refusal keeps the site, in the site or in a caller its error reaches. A site that reads Nodemailer's info beyond messageId, such as info.envelope, stays. A check that skips the send when the SMTP settings are missing checks the PostHog secret key instead, with the same log line. A local catcher the code picks by environment, such as Mailpit or a jsonTransport, keeps its branch.
    • Errors. Never widen a catch. Node.js throws PostHog's error with name, code and statusCode, never responseCode, which SMTP code reads as a reply where 4xx is temporary. A Python except smtplib.SMTPException moves only when a refused and a lost connection ran the same code before: a handler that is a bare raise and nothing else, or an except OSError on the same try that runs the same code. Otherwise it stays, with the reason PostHog can't tell a refused connection from a lost one. Ruby's Net::SMTP group rescue and MailKit's SmtpCommandException raise network_error again first, since neither covered a failed connection; System.Net.Mail's SmtpException did, so it needs no check.
    • Attachments. Sites with attachments stay, because no PostHog SDK takes them yet. The rules for attachments by path are gone.
    • Templates. SMTP stores none: rendered HTML moves as it is. nodemailer-express-handlebars renders the view with the plugin's own options first, and sends the message's own html when it has one. email-templates moves only with send: true and preview: false, merges views.locals and the config's message as send does, in one defaultsDeep, since a moved site has no attachments to pick between, and adds lodash when the app lacks it. A header that names a template stored at the provider keeps its site.
    • Webhooks. SMTP has none. Each event of the provider behind the host maps to a $workflows_email_* event. A handler that matches by the Message-ID, the provider's message id or a header keeps its sites.
    • The SMTP client. Which packages serve more than these sends and stay, such as Nodemailer's other transports, the mail gem under Action Mailer, and MailKit's IMAP.
  • account.md: the provider behind the host holds the DNS records and any suppression list, read in the region the host names. Its records sit next to PostHog's, and a mailbox provider's MX, SPF and DKIM records stay. No provider key is created. A sending service's dashboard export maps to bounce, complaint and manual entries. Mailbox providers and a server the person runs keep no list, and soft bounces and unsubscribes are left out, with the reason why.

Outside the folder, Resend's, SendGrid's, Postmark's and Mailgun's SMTP row now reads "Always here", and names wizard migrate smtp, or wizard skill migrate-email-smtp on a Wizard whose migrate takes no provider yet.

The mapping names PostHog's fields, client and errors through the rewrite file for the site's language. Those files come from #472, which the stack now sits on under #459: one references/rewrite-<language>.md per PostHog SDK (Node.js, Python, Go, Ruby, PHP, .NET, Rust and Elixir), and Step 7's single list of what every SDK's send takes. Steps 7 and 8 run each rewrite file's check on the whole project, in place of a typecheck per language. #459 deletes posthog-sdks.md, and the plan's lang column is now language.

Evidence

  • Lane gate on 3e93eac: bunx pnpm@10 test passes 257 tests in 29 files.

  • CI: green, 7 checks.

  • Proof gate: fleet proof gate at 3e93eac: gate: pass (kinds: live-run, prod-shape; review: carried from 0d981d6; CI: green; judge: fail accepted by standing rule). The judge's gap is recorded under the standing rule: the dry run is the proof form the brief names, and an applied rewrite with typecheck and send can't run until the PostHog SDK releases with the code-form email send are out.

  • Final read at 0d981d6: GPT-6.1 Sol read this round's change, a810b96 and the forward merges below it. Two findings, each answered:

  • Test seams (migrate-email-skill.test.js): the SMTP bundle resolves every reference, holds its three files and the eight rewrite files, no posthog-sdks.md, and no other provider's folder; its mapping.md names migratedFrom: 'smtp'; it is a migrate subcommand.

  • Reused at 3e93eac. This head adds only clean merges of #472, at 5398fd9 and 6924d48 (one test assertion), which leave this PR's own diff unchanged. 5398fd9 changes one Step 7 rule: behind a reverse proxy, email gets its own client on PostHog's host. Every fixture's PostHog host is PostHog's own, so the rule can't change a site, and the dry run and final read at 0d981d6 stand. The lane gate above ran at this head.

  • Dry run at 0d981d6 (live-run proof). 0d981d6 adds a810b96, which points the SMTP mapping at the rewrite file for the site's language in place of posthog-sdks.md (7 references). It turns the cc and bcc, envelope-only recipient and attachments rows into stay rows ("Always. PostHog's send doesn't take it yet"); an .ics file sent as an attachment now stays too, and an envelope is compared with the message's from and to only. It drops the "Attachments by path" section, the attachment conversion bullets, and cc, bcc and attachments from the Python EmailMessage, Ruby mail and Go call shapes. The email-templates merge becomes one defaultsDeep of the site's message over the config's, since a moved site has no attachments to pick between. The row for a language without a PostHog SDK that sends email now names Java's Jakarta Mail or Kotlin and points at Step 7's list. The forward merge below it brings feat(migrate-email): move Resend sends in every PostHog SDK's language #472's rewrite files and Step 7's list of what every SDK's send takes. The same headless dry run, on the same fixture, now runs with answers that no longer say an SDK takes cc, bcc or attachments. The expected outcome was written before the run: receipt.ts stays on cc and attachments, and every other site lands as in the last run. Result: 12 found, 5 moved, 7 left, as expected. receipt.ts stays with the reason "cc and a PDF attachment". The plan uses the language column, the run never names posthog-sdks.md, and the fixture was unchanged.

  • Dry run on 1ea906f. Claude Code on Sonnet 5.5 ran the built skill as a dry run, headless, with only read tools, on a fixture app with twelve SMTP sends in TypeScript, Python, Go and PHP through a Mailgun host and a Gmail host, plus a Mailgun webhook route and a Nodemailer jsonTransport preview script that is not a site. The person's answers were in the prompt (Mailgun tracks neither opens nor clicks, and their Mailgun suppression exports); the expected outcomes were written before each run. The fixture was unchanged after the run.

    Site Expected Result
    welcome.ts: { name, address } recipient, catch logs code and responseCode moves moved. Throws an error with name, code and statusCode, and the log reads statusCode. The report notes the To line loses the name
    receipt.ts: cc and a PDF Buffer attachment moves moved with cc and the attachment. Logs data.id where it logged messageId (stays from 0d981d6: cc and attachments)
    invoice.ts: nodemailer-express-handlebars view moves moved. Renders invoice.hbs with the plugin's options, shared with transport.ts, and sends the result as html
    digest.ts: several recipients, a list header stays stays: several recipients, and a List-Unsubscribe header
    demo-booking.ts: icalEvent stays stays: icalEvent
    order-shipped.ts: messageId stored for the webhook stays stays: the Mailgun webhook matches delivered events by the Message-ID
    worker/password_reset.py: except smtplib.SMTPException returns False stays stays: PostHog can't tell a refused connection from a lost one
    worker/export_ready.py: a message written by hand for sendmail stays stays: message written by hand
    worker/contact_notify.py: smtp.gmail.com, from a gmail.com address stays stays: no sender can be verified for a gmail.com address
    notifier/alert.go: gomail, text and html moves moved. Returns the wrapped error as before
    notifier/mailer.go: gomail helper with to ...string, both callers pass one address moves moved. The parameter becomes to string, the helper takes templateKey and distinctID, and both callers in trial.go are updated
    portal/password_reset.php: PHPMailer, skips the send when SMTP_HOST is unset moves moved. Catches PostHog\EmailError, logs its message and returns false. The skip checks POSTHOG_PROJECT_SECRET_API_KEY with the same log line

    Every moved site sends captureEngagement: false, as the person's tracking answer says, and logs recipient_suppressed and goes on. The webhook route is listed: failed and complained by address map to $workflows_email_bounced and $workflows_email_blocked, and the report names the webhook destination. Summary block: from: smtp, sites_found: 12, sites_rewritten: 6, sites_left: 6, as expected.

    One site changed outcome over the review, by rule. Up to b1815fd, worker/password_reset.py moved (12/7/5). The Python rule then changed under the standing rule (see Decisions), its expected outcome was rewritten first, and the run at this head keeps it (12/6/6). The other eleven sites match.

  • Prod-shape. A survey of real codebases' SMTP send shapes (saved at d651c6e, reused: the commits after it change tracking, error, renderer and routing rules, which the dry run at this head covers or Gaps names, the forward merge of feat(skills): move Resend sends to posthog-node in migrate-email #451's Step 6 key check, and feat(migrate-email): move Resend sends in every PostHog SDK's language #472's rewrite files). It added the skip check on missing SMTP settings, a replyTo read at its callers, a helper that takes several recipients while every caller passes one, a PHPMailer site, and a sender on a mailbox provider's domain.

  • Gaps:

    • No run applies the rewrite, typechecks or sends. The SDK capabilities are stated in the person's answers, not read from an installed release. That waits for the SDK releases.
    • The dry run moves sites in Node.js, Go and PHP, and classifies Python sites that stay. No run at this head rewrites a Python site, and none rewrites a Ruby, .NET, Elixir or Rust site.
    • Rule paths without a fixture site: email-templates (its send and preview defaults); a host that tracks both, only one, or is unknown; a Python except SMTPException that moves; a branch on an invalid send, or on a login, connection or temporary error; Ruby's and MailKit's network_error raise; a read of Nodemailer's info beyond messageId, and the callback form; a local catcher picked by environment; a regional host; smtp.loops.so. A fixture for these is a follow-up.
    • No run imports suppressions or has the PostHog MCP connected, and none runs inside the Wizard.

Decisions

Question Answer Who Where
When this merges Not before code sends are live and usable for customers, for #451 and every PR on it 👤 Human this PR
Which providers Mailgun, Loops, SendGrid, Postmark and SMTP, plus the providers the Wizard detects that a real codebase sends transactional email from 👤 Human Silthus/posthog#461
Stack shape One PR per provider in a row, on top of #459 🤖 Agent #459
Where DNS records and suppressions come from The provider that runs the SMTP host, named from the host and read in its region, from its dashboard and exports. No provider key is created 🤖 Agent this PR
A message the code writes by hand Stays on SMTP, since the skill would have to parse the message to move it 🤖 Agent this PR
Packages that render inside the send nodemailer-express-handlebars and email-templates sites move when the moved code renders the same content first, merging locals and message as the package did. email-templates with default send or preview stays, since the moved send would send from every environment 🤖 Agent this PR
A branch on one SMTP error An invalid send becomes a branch on PostHog's validation_error. A failed login, a connection error or a temporary refusal keeps the site, since PostHog has no counterpart 🤖 Agent this PR
Python except SMTPException Moves only when a refused and a lost connection ran the same code before; otherwise stays. Decided under the standing rule after final reads contradicted each other: raising network_error broke a handler that returned on a lost connection, and handing it to that handler swallowed a refused one. Replaced both earlier rules; the fixture's worker/password_reset.py now stays 🤖 Agent this PR
SMTP tracking Ask once per host: both tracked moves, neither moves with captureEngagement: false, mixed or unknown stays. Replaced the first version, which let PostHog start capturing what the host didn't 🤖 Agent this PR
Node.js error fields The thrown error carries statusCode, never responseCode, since SMTP code reads that as an SMTP reply 🤖 Agent this PR
Reads of Nodemailer's info messageId becomes PostHog's data.id; any other field, such as envelope or response, keeps the site, since PostHog answers only the id 🤖 Agent this PR
A skip on missing SMTP settings Checks the PostHog secret key instead, with the same log line or answer, since a check left on the SMTP variables would skip every send after the cutover 🤖 Agent this PR
A local catcher picked by environment The branch stays; the moved code sends through PostHog only where the old code reached the real host 🤖 Agent this PR
Other providers' SMTP sends Stay in their own run, and the row names wizard migrate smtp or wizard skill migrate-email-smtp; smtp.loops.so sends go to wizard migrate loops 🤖 Agent this PR
A fixture for email-templates attachment selection Refused as a unit test: the rule copies the package's own send, context-mill has no harness that runs an agent on a fixture, and the missing fixture site is under Gaps. The judge's repeat is accepted under the standing rule. From 0d981d6 the selection is gone: a moved site has no attachments 🤖 Agent this PR
cc, bcc and attachments Stay on SMTP in every language, since no PostHog SDK takes them yet (#472). Replaced moving them where the installed SDK took them 🤖 Agent this PR
Proof kinds Live run and a survey of real codebases' send shapes. No shadow diff: the other providers' change is the reason text of one row, which keeps the same sites 🤖 Agent this PR
Runtime proof the judge asked for Accepted as a gap under the standing rule: the dry run is the brief's proof form, and an applied rewrite with typecheck and send waits for the SDK releases 🤖 Agent this PR
More final reads after round 2 Continued without escalating, under the standing rule: each read found a new, narrower issue, and the one that came back (Python's connection errors, in opposite directions) is settled by the row above. The last read, at this head, had two findings, on the cc, bcc and attachment stays and on #472's tests, both answered under Evidence 🤖 Agent this PR

Involvement: qa-swarm, 2 panel rounds (round 1: fresh-eyes, correctness, risk, design, anti-overengineering, sweep; round 2: correctness, fresh-eyes, risk, sweep) and 16 final reads, the last at 0d981d6, with 2 findings, one on this round's mapping change and one on #472's tests, both answered. Proof captured: 17 dry runs over the review, the last at 0d981d6, a prod-shape survey, and 4 judge runs.

🤖 Agent context

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5, high), as the migration-skills slice of Silthus/posthog#461. Reviews on GPT-6.1 Sol (high), the fresh-eyes lens on Opus 5.5; fixes on Opus 5.5 (high). Dry runs on Claude Sonnet 5.5 (claude-sonnet-5-5).

🤖 Generated with Claude Code

Silthus and others added 9 commits November 4, 2026 07:26
Adds the smtp variant of migrate-email: Nodemailer and the SMTP clients
of each language with a PostHog SDK, the hosts that name the provider
behind them, and how DNS and suppressions follow that provider.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- headers, list headers and Message-ID headers always stay: no PostHog
  SDK takes headers yet.
- A from name with special characters is quoted.
- Go and Elixir decode a base64 attachment; Node.js and Python keep it.
- A catch of one SMTP error class keeps the site; a broad catch stays
  broad, and except OSError keeps catching OS errors.
- No claims about how PostHog signs or builds the send.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts:
#	context/skills/migrate-email/config.yaml
#	scripts/lib/tests/migrate-email-skill.test.js
- Invalid-send branches (EENVELOPE, EMESSAGE, SMTPRecipientsRefused,
  SMTPFatalError, MailKit *NotAccepted, 5xx replies) map to PostHog's
  validation_error name, never a status; Node's thrown error carries name.
- Name the region a host or Nodemailer SES service sets, and read records
  and suppressions in that region of the provider's dashboard.
- Say which SMTP packages serve more than these sends.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

🧙 Wizard CI

Run the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands:

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci ai-observability
  • /wizard-ci basic-integration
  • /wizard-ci feature-flags
  • /wizard-ci mcp-analytics
  • /wizard-ci replay-vision
  • /wizard-ci revenue
  • /wizard-ci self-driving
  • /wizard-ci warehouse
  • /wizard-ci warehouse-seeded

Test an individual app:

  • /wizard-ci ai-observability/anthropic
  • /wizard-ci ai-observability/google-adk
  • /wizard-ci ai-observability/groq
Show more apps
  • /wizard-ci ai-observability/manual-capture
  • /wizard-ci ai-observability/openai
  • /wizard-ci ai-observability/openai-agents
  • /wizard-ci ai-observability/opentelemetry
  • /wizard-ci ai-observability/vercel-ai
  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/flutter
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci feature-flags/django
  • /wizard-ci feature-flags/next-js
  • /wizard-ci mcp-analytics/custom-dispatcher
  • /wizard-ci mcp-analytics/typescript-sdk
  • /wizard-ci replay-vision/javascript-node
  • /wizard-ci replay-vision/next-js
  • /wizard-ci replay-vision/react-vite
  • /wizard-ci revenue/stripe
  • /wizard-ci self-driving/astro
  • /wizard-ci self-driving/fastapi
  • /wizard-ci self-driving/nuxt
  • /wizard-ci self-driving/react-router
  • /wizard-ci self-driving/sveltekit
  • /wizard-ci warehouse/monorepo-env
  • /wizard-ci warehouse/multi-source-next
  • /wizard-ci warehouse/stripe-node
  • /wizard-ci warehouse/zero-source
  • /wizard-ci warehouse-seeded/next-stripe
  • /wizard-ci warehouse-seeded/next-stripe-declined

Test against a wizard branch:

  • /wizard-ci all wizard:my-branch

Add wizard:<branch> to any command above to pin the wizard branch. It defaults to main.

Results will be posted here when complete.

Silthus and others added 19 commits November 4, 2026 08:49
- Node: never copy PostHog's HTTP status into responseCode; check
  callers the error reaches for branches on SMTP error kinds.
- Python: SMTPException covers a dropped connection, not a refused one;
  note the network_error change. MailKit SmtpCommandException rethrows
  network_error.
- Attachments: decode hex and text strings, strip Content-ID brackets.
- A message with no To stays; a send-time Date and an unread Message-ID
  move without the header.
- Handlebars plugin skips a message with html; email-templates merges
  locals and message defaults as send() does.
- Keep a local-catcher branch picked by environment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ate smtp

Each provider's SMTP row said PostHog has no SMTP relay. With the SMTP
variant in this layer, those sends stay for that provider's run and move
with wizard migrate smtp.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A check that skips the send when the SMTP settings are missing moves,
  and checks the PostHog secret key variable instead.
- A replyTo passed in as a parameter is read at its callers, and moves
  when every caller passes one bare address or none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts:
#	context/skills/migrate-email/references/postmark/mapping.md
# Conflicts:
#	context/skills/migrate-email/references/loops/mapping.md
…me the skill fallback for SMTP

smtp.loops.so carries the JSON of a Loops template send, so the SMTP skill keeps such a site and points to wizard migrate loops. Each provider's SMTP row also names wizard skill migrate-email-smtp for a wizard whose migrate takes no provider yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…dd lodash when the app lacks it

send takes the site's attachments or else the config's, and never deep-merges the two lists. pnpm doesn't resolve lodash from the app when only email-templates depends on it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus and others added 24 commits November 4, 2026 12:38
… than the message id stays

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…t connection as network_error

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…used and a lost connection went to the same place

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…used and a lost connection ran the same code

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…l-smtp

Brings #472's rewrite-<language>.md files through the layer below. posthog-sdks.md is gone; this provider's mapping moves to the rewrite file in the next commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	context/skills/migrate-email/references/resend/mapping.md
Point every posthog-sdks.md reference at the rewrite file for the site's language.
cc, bcc, envelope-only recipients, and attachments now stay on SMTP, because no PostHog SDK takes them.
Drop the attachment conversion bullets, the Attachments by path section, and cc/bcc from the call and field bullets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's Elixir fix (fall back past a nil :posthog host) and each lower provider's move to the rewrite file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's single list of error names in Step 7 and the reply-to rule in each rewrite file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's Step 7 rule: behind a reverse proxy, email gets its own client on PostHog's host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's added assertion in the Step 7 proxy test. No skill text changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant