Repository navigation
Conversation
…opens_and_clicks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_and_clicks PostHog never tracks clicks on a code send, so click tracking that is off needs nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace track_opens_and_clicks with tracking in the rewrite step and every SDK's rewrite file. A provider's open tracking that is off turns off the opens switch only, so engagement events stay on. The skill never turns on click tracking, because a link can carry a token, and it notes sites whose provider tracked clicks for the report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ates Use the Go, Rust and .NET names for tracking. A site that moves to a template made in PostHog sends no tracking, because the template uses its own setting, so the report asks the person to check it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o sensitive A code send takes an optional sensitive flag, and its clicks follow it unless tracking sets them. The skill marks a send sensitive when the old call turned off click tracking for that email or marked it sensitive. It never marks a send as not sensitive, and drops the claim that clicks are always off by default for code sends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A code send that leaves sensitive out takes the setting of a PostHog template with the same key. The skill can't see the project's templates or tell whether a link carries a token, so a key that matched a template without protection would start rewriting a reset link. Pin the flag on every code send, and say so in the report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ack-opens-and-clicks Keeps the base's error-handling rules, setup and write-down additions, and Elixir's named-instance send, with this branch's tracking and sensitive rules on top. Step 7 now turns opens off from the plan note instead of waiting for the person's yes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A site whose emails had no open pixel, or whose provider's open tracking nobody can confirm, now moves with the opens switch off instead of staying or waiting for the person's yes. The plan notes it as opens off with its reason, and the report lists those sites, the clicks PostHog no longer counts, and that every code send is marked sensitive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resend's attachments map to the send's attachments in posthog-node: filename, content, content type and inline content ID. A file with a path, no filename, a refused extension, or more than 10 files keeps the site on Resend, as do sites outside JavaScript and TypeScript. Step 8 sends an email with files when a moved site has them, and puts those sites back when PostHog doesn't take files from the project yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 3 decides when a site with files stays, whatever the provider, and lists every extension PostHog refuses. Resend's mapping keeps only its own file fields. The report says what would let a staying site move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o feat/migrate-email-resend-attachments
…nd-clicks' into feat/migrate-email-resend-attachments # Conflicts: # context/skills/migrate-email/references/9-report.md # context/skills/migrate-email/references/resend/mapping.md
Step 7 says what every SDK's code send does with files: the 10-file and 40 MB limits, the upload before the send, and its errors. Each rewrite file names its SDK's field and the form its content takes, since some take only bytes and some read any text string as Base64. The bundled api.md may still call attachments unavailable, so the steps win for files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o feat/migrate-email-resend-attachments
A run of migrate-email-resend on an app with a calendar invite showed where the steps fell short. They now cover: - preflight when the MCP sees another project, or the app already sends code emails - a PostHog key under the app's own name, or in the hosting platform's environment - code and docs around the sends: provider-key switches, counts and limits, promises in legal docs and specs, and the app's own PostHog sends, whose engagement setting moved sends follow - a send moved into a runtime the SDK runs in, such as a Convex "use node" action - a deadline that now limits each request of a send with files - an idempotency key built from a hash of the request body - tests of sends with files: the upload requests, and hashing under a faked clock Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- preflight: an app that already sends code emails wins over an MCP that can't see the project; suppressions never import into another project - Step 3 reads the rewrite file's send for the files check, and counts the release Step 7 would install - the email client splits a shared config function, and goes in the file of the runtime the send runs in - one template key for a site that sends several kinds of email - a used-up quota that the old code retried as a 429 is noted - tests: answer at once under a faked clock, answer the app's own PostHog capture, assert on the added fields, and keep the Node digest stub in a setup file outside Convex's folder - the report's notes placeholder closes where it should Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pp's own distinct ID helper Also reads Step 5's MCP check from what Step 2 found, not what it recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a legal text Run 4 worked around both by hand within Step 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce PostHog takes them The real send on prod-eu refused `text/calendar; charset=utf-8; method=REQUEST` with `not_mime_type`: PostHog takes a bare type only. Taking `method` out makes a mail app show a calendar invite as a file. Step 3 marks such a site, Step 8's check email decides, and a refusal puts the site back on the provider rather than stripping the parameters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt ID in Step 3 A filename takes at most 255 characters and no path separator or control character (unsafe_filename), a content type at most 78 characters, and a content ID at most 78 characters and no angle bracket or control character (unsafe_content_id), as code_send_fields.py on PostHog's master refuses them. A site whose value breaks one stays on its provider with a reason, so each provider's attachments follow-up points here instead of restating them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… content cell Run 5 (Steps 1 to 3 on a fixture with three Resend sends with files) held a user-built filename and passed a fixed one, as the header rules mean, but also held `contentId: "<logo>"`, which Resend's mapping already turns into the bare `logo` the HTML shows. The rules now hold for the value the send passes after the mapping. The re-run moved that site and still held the user-built filename. Each file of a moving site now goes in the plan row's content cell, where both runs had to guess. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-email-smtp-attachments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Map each SMTP client's file options (Nodemailer, Python email, Ruby mail, PHPMailer, Symfony Mime, gomail, go-mail, MailKit, System.Net.Mail, lettre, gen_smtp's mimemail parts) onto PostHog's filename, content, content type and content ID, from the clients' own docs. Files read from a local path move as their bytes; URLs, raw parts, per-file headers and dispositions PostHog doesn't set keep the site. Parts the docs don't describe are read from the installed client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… source Each claim now cites the client's docs or tells the agent to read the installed client: - go-mail: DialAndSendWithContext's context controls the connection, as its docs say; WithTimeout's per-message detail is dropped (the site stays either way). - aiosmtplib: the timeout's reply-versus-read detail is dropped (the site stays either way). - System.Net.Mail: Timeout applies to synchronous Send, as its docs say. - email-templates: how send combines the config's attachments with the site's is read from the installed package. - Go clients: answer an error on a failure, as their docs show; the "never panics" claim is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🧙 Wizard CIRun 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:
Test all apps in a directory:
Test an individual app:
Show more apps
Test against a wizard branch:
Add Results will be posted here when complete. |
…ed types Review round 1 on the SMTP attachments mapping: - Read a file from a path where the client read or checked it (at the add for MailKit, PHPMailer and Ruby add_file; at the send for Nodemailer), and keep the bytes to the send. - Turn string chunks of a stream into bytes before concatenating. - Build Python text parts the same way and pass their decoded payload and charset parameter. - PHPMailer takes the type from the path, not the display name; MIMEImage's guessed type keeps the site. - System.Net.Mail's ContentId counts only on an inline Attachment; MIMEBase headers that map are named; email-templates keeps attachments out of defaultsDeep. - Drop the claim that Ruby mail sets its own inline Content-ID, which no docs back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2 on the SMTP attachments mapping: - Python add_attachment with a str keeps the site: Python rewrites the text, and the bytes depend on the policy and transfer encoding. - PHPMailer is checked at the add and read where the installed client reads it; System.Net.Mail's Attachment(path) keeps the site, since its docs give no read point or name; MailKit's LinkedResources.Add is named beside Attachments.Add. - Nodemailer stream text chunks become UTF-8 bytes, as Node's write takes them. - The MIME-Version header Python's MIME classes add is allowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons on With new PHPMailer(true), addAttachment and addEmbeddedImage throw at the add for a file they can't read. The moved check now throws the same class there, the catch keeps it beside PostHog\EmailError, and the check copies the installed PHPMailer's own test rather than is_readable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At the add, the moved check now does what the installed PHPMailer's method does: answer false, or throw its own class with its message and code. The site keeps its old catch of that class for the throw and adds a PostHog\EmailError catch for the send, so no PostHog-only accessor runs on PHPMailer's exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PHPMailer put a refused file's message in $mail->ErrorInfo, which the moved check doesn't set. Code that reads it after the add, or in either catch, now reads $e->getMessage() or the message the check kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…send's error reaches With exceptions on, $mail->ErrorInfo and $e->errorMessage() in a PostHog\EmailError catch, a broader catch or a caller now become $e->getMessage(), since the moved send sets neither. The kept catch for a refused file keeps errorMessage(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With exceptions on or off, every read of the send's error that the failure reaches, callers included, takes $e->getMessage(): ErrorInfo keeps the code's own escaping, errorMessage() gets htmlspecialchars, base \Exception methods carry over, and a branch on getCode() or a read the moved code can't reach keeps the site as `reads PHPMailer's error`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or its error A broader catch also takes other errors, so turning its ErrorInfo read into getMessage() could show a database or path message. It now takes the message only for a PostHog\EmailError. errorMessage() becomes the HTML the installed PHPMailer builds around the message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The class checks and per-catch rules lost the message when a failure was rethrown, reached a broader catch, or came from the file check. A variable now stands in for $mail->ErrorInfo: empty before the try, set where the send fails and where a file check refuses, and read wherever ErrorInfo was read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The variable is reset where the site starts building the message, set by a PostHog\EmailError catch right around the send that rethrows when the site had none, and never in a catch that also takes other errors. errorMessage() converts wherever the PostHog error reaches it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The variable is set in the site's own catch of the send's error. Where there is none, a new catch right around the send sets it and then does what the old send did: rethrow, or answer false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stand-in variable is now always set in a catch right around the send, before any finally or outer catch runs, which then rethrows, answers false, or goes on as the old send did. isError() reads the variable too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns setting The catch around the send rethrows with exceptions on, and answers false or goes on with exceptions off; the older false-branch sentence is gone. isError() keeps the site, since PHPMailer keeps its count its own way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…run time A site that builds PHPMailer with a run-time exceptions value now gets a catch that branches on the same value, and stays when the agent can't read where that value comes from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eptions value too The run-time exceptions value is kept in one variable where PHPMailer is built, and both the add-time file check and the catch around the send branch on it. A site whose value can't be read stays with its own reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re it is built A site that builds its PHPMailer keeps the run-time exceptions value in a variable there, whatever its source. One built elsewhere, such as in a shared helper, leaves the value out of the site's reach, so the site stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PHPMailer from a DI container or a vendor factory has an exceptions setting the agent can't see, so such a site stays as `PHPMailer's exceptions setting is unknown` rather than assuming the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…own stay A PHPMailer from outside the site moves only when that code builds it with a constant the agent can read, so a container or factory whose setting can't be confirmed keeps the site whatever kind of value it uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…exceptions A shared send function whose callers build the PHPMailer with different settings can't branch on the protected setting, so it stays. A constructor call with no argument counts as exceptions off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… with a literal Each round found another way a PHPMailer reaches a site from elsewhere. One rule ends that: the site moves only when it builds the PHPMailer it sends with as `new PHPMailer()`, `(true)` or `(false)`. A run-time value, a parameter, a helper, a container or a subclass keeps it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s the site's own A wrapper class that builds the PHPMailer in its constructor, or an app helper that returns one, sets a literal the agent can read, so the site moves as long as no other PHPMailer can reach the send. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PHPMailer kept on a property carries what the constructor, other methods and earlier sends set on it, so the message is every call that can reach the send. When those calls can't be told, the site stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A site that stays may send through the same PHPMailer, so the calls outside the moved send stay as they are. A call the moved site made that can reach a staying site keeps it, and the rule covers a helper the PHPMailer is passed to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sites that share one PHPMailer share its calls, its files and its ErrorInfo, so moving one of them can't keep the other's message and errors whole. The moving site stays too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 8 puts sites back one at a time, which could split two sites that share a PHPMailer. They now go back together, and the plan row names the site that keeps another. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 7 can undo a site too, so the rule covers both steps, and the partner's row names the site it shares with and that site's reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A partner of a staying site got a different reason depending on when the stay was found. It now always reads `shares its PHPMailer with <site>: <that site's reason>`, and `a PHPMailer built across calls` is kept for calls that can't be traced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When two sites on one PHPMailer each stay for their own reason, each keeps it. Only a site that would otherwise move names its staying partners and their reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The SMTP variant of
migrate-email(#463) keeps every send that carries a file on SMTP, because PostHog's send didn't take files. The PostHog SDKs now send files from code, and #510 teaches the shared steps how. This PR maps the SMTP clients' file options onto them, so that a send with an invoice, a calendar file or an inline logo can move too. The clients are Nodemailer, Python'semail, Rubymail, PHPMailer, Symfony Mime, gomail, go-mail, MailKit, System.Net.Mail, lettre, and gen_smtp'smimemail. In a separate commit, it also rechecks the SMTP claims that rested on package source instead of the clients' docs.Important
Stacked on #510 (Resend attachments), which is stacked on #486. The base is
feat/migrate-email-smtp(#463), so the diff includes #486 and #510 until its ancestors land and it is retargeted tomain. Onlyreferences/smtp/is this PR's change. It also merges #510's head41e37f1. Merge order: #472, the providers, #486, #510, then this one.Impact
Moved sends that carry files and arrive with
migratedFrom: 'smtp'show whether sites with files now move off SMTP. Read it 14 days after the Wizard runsmigrateas a family with this variant.Warning
🟡 Merge risk: medium · two-way door until the next context-mill release ships the skill; one-way for each app once an agent runs it, because a migration's changes persist in that app's code
references/smtp/mapping.mdandsdk-reference.md. Until the skill ships, nobody runs it, so no running code changes.mail's inline files or for a generated ID. Step 8's check email sends one email with files before cutover, and afield_not_acceptedputs those sites back.tryas that call. A client whose read point its docs don't give is read from the installed client, or keeps the site. A missing file or an oversized stream would fail there, not in the SMTP client. Step 8's check email doesn't exercise a missing file.PHPMaileris built with a literal setting in the send's function, its class's constructor, or an app helper, and is the only one that can reach the send. A site whose setting is a run-time value, or whosePHPMailercomes from a parameter, a container or a subclass, stays asPHPMailer's exceptions setting is unknown, where feat(skills): move SMTP sends to PostHog #463 moved it. APHPMailerwhose calls across the class can't be told keeps the site asa PHPMailer built across calls, and sites that share onePHPMailermove or stay together (shares its PHPMailer with <site>: <its reason>).Rollout
main; ships to agents with the next context-mill release, with no flag of its own.Changes
smtp/mapping.md, "Fields": theattachmentsrow now maps to PostHog'sattachmentsand points to Step 3 "Files" and the new section. TheicalEventrow says an.icsfile inattachmentsis a file like any other, with its content type's parameters checked as Step 3 says.disableFileAccesskeeps a site whose file comes from apath, as it already did fortextandhtml.smtp/mapping.md, new "Attachments" section. Each part of a file, and how it maps:Filename. A name the code sets moves as it is, in each client's spelling. A file read from a path with no name gets the client's own name: gomail's base name, as its
Renameoption says, and for the other clients whatever the installed client gives. A file with neither a name nor a path keeps the site, as Step 3 says. This covers Nodemailer'sfilename: false, Nodemailer content with nofilename, and lettre'snew_inline.Content. Each byte source maps as follows:
encodingor Rubymail'sencoding:, are decoded the same way.add_attachmentwith astrkeeps the site (a text file Python's email package rewrites), since Python changes its line endings and the bytes depend on the policy.add_file, at the send for Nodemailer, at the add for PHPMailer, which copies what the installed PHPMailer does there (answerfalse, or throw its own class with its message and code), and elsewhere where the installed client reads it. A PHPMailer site that throws keeps its old catch for that throw beside a newPostHog\EmailErrorcatch. Since the moved send sets noErrorInfo, a variable stands in for it: empty where the site starts building the message, set where a file check refuses and in aPostHog\EmailErrorcatch right around the send, before anyfinallyor outer catch, which then rethrows with exceptions on, or answersfalseor goes on with exceptions off, as the old send did, with the site moved only when thePHPMailerit sends with is built asnew PHPMailer(),(true)or(false)in the send's function, its class's constructor or an app helper, and is the only one that can reach the send, and kept for a run-time value, a parameter, a container or a subclass (PHPMailer's exceptions setting is unknown). Calls across the class that can't be told keep it too (a PHPMailer built across calls), and sites that share aPHPMailermove or stay together, a site with no reason of its own naming its partner (shares its PHPMailer with <site>: <that site's own reason>). The variable is never set in a catch that also takes other errors, and read wherever$mail->ErrorInfowas read, callers included. A branch ongetCode(), a call ofisError(), or a read the variable can't reach, keeps the site (reads PHPMailer's error). This tightens feat(skills): move SMTP sends to PostHog #463's PHP Rewrite bullet, which this PR already rewrites. System.Net.Mail'sAttachment(path)keeps the site, since its docs give no read point or name.hrefkeeps the site (the provider fetches <file>).data:URI moves as its decoded bytes.files can pass 40 MB).SetCopyFuncis called with an in-memory buffer.Content type. A type the code sets moves as it is, parameters included. A type the client picks from an extension is set the same way and noted in the plan row: the filename's, or for PHPMailer the path's, not the display name's.
MIMEImage's guess from the image's bytes keeps the site (a content type the client guesses).Content ID and disposition. PostHog shows a file with a content ID inline, as the SDK's own docstring says. The content ID moves as each client gives it, in these forms:
<>;cid:<name>, which keeps the name;Embed, which gives the base name;make_msgid()andGenerateMessageId(), so the ID keeps the 78-character limit;mail'sattachments.inline, where the code readurl.A disposition PostHog doesn't give the file keeps the site (
a file's disposition PostHog doesn't set). That covers an inline file with no content ID, an attachment with a content ID, Nodemailer'scidon a non-image, and Nodemailer'smessage/*.What keeps a site. These keep the site, each with its reason:
raw, and parts written by hand.headersrow).httpHeaders.a file this mapping doesn't name).A transfer encoding moves without it.
Limits. The SMTP server's own size limit, and Nodemailer's 50 MB
data:URI limit, appear next to PostHog's 10 files and 40 MB.Templates. SMTP stores no templates.
email-templates' config attachments map like the site's.Where the docs say nothing. The agent reads the installed client, names the file in the plan row, and the case joins the live-run fixtures below. The .NET clients ship no source, so a file whose name or type they would fill in keeps the site (
a file part the client fills in).smtp/mapping.md, "Rewrite": each file becomes oneattachmentsentry.email-templates' merge of the config's attachments is read from the installed package.82cdc20, which rechecks claims that rested on package source against the clients' docs:DialAndSendWithContext's context now cites the docs ("controls the connection's timeout and cancellation";Sendtakes no context). The already-done context rule follows from that.WithTimeout's per-message detail is dropped, and the site stays either way.timeoutlimits a whole reply is dropped, and the site stays either way.Timeoutcounts on a synchronousSendonly, now cited to its docs.sendcombines config and site attachments is read from the installed package. The earlier claims ("never merges the two lists", and later "no attachments to merge") are gone.error, as their docs show.How did you test this code?
2497e64:vitest run scriptspasses 344 tests in 30 files,node scripts/build.jsexits 0, and the tree is clean.email.message,contentmanagerand examples.mailMail::Message2.9.1.AttachmentCollection.Add.Attachmentconstructors andSmtpClient.Timeout.Attachment.mimemail.emails.py, for "content_idshows the file inline".nodemailer,python,PHPMailer,symfony,go-gomail,wneessen,jstedfast,dotnet,lettre,gen-smtp,Vagabond,mikel,ruby,cole,swoosh, andforwardemailfor email-templates. No package from a registry is cited. Claims only source would back were dropped, or became a rule to read the installed client.path, Rubyadd_file, PHPMaileraddAttachment, Symfonynew File($path), and go-mailAttachFile.EmbedFileandEmbedReader.mail'sattachments.inline, with a fixed content ID in place ofurl.email-templates' merge of config and site attachments.7-rewrite.mdand8-verify.mdput a site back without pointing to the provider's rule for sites that share one client. This lane doesn't edit the shared steps, somapping.mdstates the rule for PHPMailer.Decisions
try: the client read it inside the app, so no provider fetched it under Step 3references/smtp/Involvement: one agent wrote the mapping from the clients' docs. Cross-family review (GPT-6.1 Sol on correctness, Opus 5.5 on fresh eyes) with
fleet proof review --scope fixesfrom the merge of #510,3c2558c: round 1 on82cdc20raised nine findings, all fixed in7dabc42(where each client reads a file, stream chunks, Python text, PHPMailer's type source,MIMEImage, inline .NET content IDs,MIMEBaseheaders, email-templates' list merge). Round 2 on7dabc42raised six, fixed in06d6139and this description (Python text keeps the site, PHPMailer's read point, System.Net.Mail'sAttachment(path), Nodemailer stream chunks,MIME-Version). Round 3 on06d6139found that PHPMailer with exceptions on throws at the add, fixed in1f48221. Round 4 on1f48221found that a combined catch ran PostHog-only methods on PHPMailer's exception and that older PHPMailer versions answer or throw differently, fixed in8a9663f. Round 5 on8a9663ffound that code reading$mail->ErrorInfolost the refusal's message, fixed in6a2566a. Round 6 on6a2566afounderrorMessage()and broader catches that readErrorInfo, fixed in8eb51e2. Round 7 on8eb51e2found callers with exceptions off, double escaping, andgetCode()branches, fixed inf47a17bby one rule for every read of the send's error. Round 8 onf47a17bfound that a broader catch would show other errors' messages, fixed in4c09a4b. Round 9 on4c09a4bfound that a rethrown failure or a file refusal in a broader catch lost its message, fixed in9c976c9by one variable that stands in forErrorInfo. Round 10 on9c976c9found where that variable is set and reset, anderrorMessage()in a broader catch, fixed inc98b7ac. Round 11 onc98b7acfound that the new catch's rethrow broke a site whose send answeredfalse, fixed in2887c03. Round 12 on2887c03found afinallythat readsErrorInfobefore the outer catch, andisError(), fixed in4139695: the message is always captured right around the send. Round 13 on4139695found that rethrow andfalsecould both apply with exceptions on, and thatisError()resets per send, fixed in7d9ea49: the answer follows the exceptions setting, andisError()keeps the site. Round 14 on7d9ea49found an exceptions setting read at run time, fixed in5534cd3: the catch branches on the same value, and the site stays when its source can't be read. Round 15 on5534cd3found that the add-time file check needed the same branch, fixed ind025ca6, with the value kept in one variable and the stay given its own reason,PHPMailer's exceptions setting is unknown. Round 16 ond025ca6found that the stay should follow where thePHPMaileris built, not whether the value's source is readable, fixed in4a29771. Round 17 on4a29771found aPHPMailerfrom a DI container or vendor factory, fixed ina1aa719: that site stays too. Round 18 ona1aa719found that stay hung on a run-time value, fixed inca9eb65: aPHPMailerfrom elsewhere moves only when its builder uses a constant setting the agent can read. Round 19 onca9eb65found callers that build it with different settings, fixed in723857f: every builder must use the same one, andnew PHPMailer()counts as off. Round 20 on723857ffound a site whose ownPHPMaileris only a fallback for one a caller passes, and a subclass built with no argument. Fixed in11645c9with one simpler rule, as the lens put forward: the site moves only when it builds its ownPHPMailerwith a literal setting, and every other source keeps it. Round 21 on11645c9found that a wrapper class's constructor or a helper that builds it with a literal counted as elsewhere, fixed inaa81bba: either counts as the site's own when it is the onlyPHPMailerthat can reach the send, and the merge-risk callout names the PHPMailer sites that now stay. Round 22 onaa81bbafound that a property-heldPHPMailercarries what the constructor and setters set, fixed inc18a389: the message is every call on it that can reach the send, and the site stays when those can't be told. Round 23 onc18a389found that a site that stays could lose a shared constructor's calls, fixed in993fc72: they stay as they are while a site that stays shares thePHPMailer. Round 24 on993fc72found a sharedErrorInforead and a doubled failure branch, fixed inb3ba006with one rule: aPHPMailerthat a site that stays also sends through keeps the moving site too. Round 25 onb3ba006found that Step 8 could put back one of two sites sharing aPHPMailer, fixed ina21bc38: they go back together, and the plan row names the shared site. Round 26 ona21bc38found that Step 7 can undo a site too, fixed in94a123f: the rule covers both steps, and the partner's row names the shared site and its reason. Round 27 on94a123ffound that the partner's reason depended on when the stay was found, fixed ine01b296: it always readsshares its PHPMailer with <site>: <that site's reason>. Round 28 one01b296found two sharing sites that each stay for their own reason, fixed in2497e64: each keeps its own reason, and only a site that would otherwise move names its partners.🤖 Generated with Claude Code