This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch docs/plain-text-list-mail-in-skills in repository https://gitbox.apache.org/repos/asf/struts.git
commit b1aec6932f8e7bdfd0047c7df9bbff1523339b52 Author: Lukasz Lenart <[email protected]> AuthorDate: Fri Aug 14 09:57:45 2026 +0200 docs(skills): require plain-text list mail in the two mail-producing skills ASF lists reject a text/html part outright. Observed on the S2-070 announcement, 2026-08-14: <[email protected]>: ezmlm-reject: fatal: Sorry, a message part has an unacceptable MIME Content-Type: 'text/html' (#5.2.3) <[email protected]>: Must be sent from an @apache.org address. [email protected] accepted the same message, so the HTML advisory reached one list while another rejected it -- a missing bounce is not evidence the format was right. A draft created through the Gmail tool is an HTML draft whatever is passed to it: with body alone, Gmail still synthesises a text/html alternative on send, linkifies the URLs and reflows the plain part. The vote-mail skill's table said body-only was "the least-bad, and is what to use", which is wrong -- no tool argument produces a plain-text mail. The format is decided in the compose window. Both skills now state the deliverable as a contract: draft with body and never htmlBody, the identical body in a file wrapped at 72 columns, and a handover that says to switch on plain-text mode, paste the file, and send from the @apache.org identity. creating-security-bulletins gains the announcement step it never had, pointing at the CVE tool's OSS/ASF Emails tab -- both mails are generated from the record, so copying them keeps the advisory and the CVE from drifting. Publication is also spelled out: clear read and update restrictions completely, and verify with an unauthenticated fetch rather than trusting the API's success response. Co-authored-by: Claude Opus 5 <[email protected]> --- .claude/skills/creating-release-vote-mail/SKILL.md | 53 ++++++++++++++++----- .../vote-mail-template.md | 9 ++-- .../skills/creating-security-bulletins/SKILL.md | 55 +++++++++++++++++++++- 3 files changed, 101 insertions(+), 16 deletions(-) diff --git a/.claude/skills/creating-release-vote-mail/SKILL.md b/.claude/skills/creating-release-vote-mail/SKILL.md index 4604faaf9..e34815b88 100644 --- a/.claude/skills/creating-release-vote-mail/SKILL.md +++ b/.claude/skills/creating-release-vote-mail/SKILL.md @@ -92,22 +92,47 @@ path you hand over. **Never send.** Sending opens a binding vote on a permanently archived public list and commits the PMC to the artifacts as staged. -### Gmail mangles the links — hand over a paste-ready body +### The mail must be text/plain, and no tool argument achieves that -Gmail's linkifier rewrites URLs server-side as the draft is stored, so the four link lines -arrive as `https://www.google.com/url?q=...&source=gmail&ust=...` and the 72-column wrap is -reflowed. There is no way to pass the body through the Gmail tool that avoids it: +An ASF list mail carries **one `text/plain` part and nothing else**. A `text/html` part is a +delivery failure, not a cosmetic one — `[email protected]` rejects it permanently: -| Body passed as | Result | +``` +ezmlm-reject: fatal: Sorry, a message part has an unacceptable MIME Content-Type: 'text/html' (#5.2.3) +``` + +**A draft created through the Gmail tool is an HTML draft, whatever you pass it.** Gmail +synthesises a `text/html` alternative when the draft is sent, linkifies every URL into +`<a href>`, and reflows the plain part: + +| Body passed as | What is actually sent | |---|---| -| `body` only | Wrapped hrefs; plain rendering shows `bare-url <google.com/url?q=…>` | -| `htmlBody` only | No plain-text part at all — HTML-only mail, wrong for an ASF list | -| both | Worst: the plain part's *visible* text becomes the wrapped URL | +| `body` only | `multipart/alternative` — the HTML part is generated for you | +| `htmlBody` only | HTML-only, no plain part at all | +| both | Same, plus the plain part's visible text becomes the wrapped URL | + +Gmail's linkifier also rewrites URLs server-side, so link lines can arrive as +`https://www.google.com/url?q=...&source=gmail&ust=...`. + +**Do not rely on a bounce to catch this.** On the 2026-08-14 advisory run the same message was +rejected by `[email protected]` and accepted by `[email protected]` — the HTML mail +reached one list and not the other, from a single send. + +So the deliverable is three things, and it is incomplete without any of them: + +1. A Gmail draft with To, Bcc, Subject and `body`. **Never `htmlBody`.** +2. The identical body written to a file, whose path you hand over. +3. In your handover, the sending instruction: **switch the compose window to plain-text mode** + (⋮ → *Plain text mode*), then select-all and paste the file over the body. + +Step 3 is what actually produces the plain-text mail; steps 1 and 2 only make it one paste +instead of four hand-edited URLs. Hard-wrap the file at 72 columns — a paste into plain-text +mode keeps the wrapping the file has, and Gmail reflows anything longer. -`body` only is the least-bad, and is what to use. **Also write the exact body to a file and -give the release manager its path.** Pasting plain text over the compose window restores both -the bare URLs and the wrap, making the fix one select-all-paste instead of four hand-edited -URLs. +**Say which identity to send from: `@apache.org`.** The Gmail account's default sender is a +personal address, and a vote arriving in the `dev@` archive from one reads as an outsider +calling a PMC vote. Some ASF lists refuse it outright — `[email protected]` +answers *"Must be sent from an @apache.org address."* **Never re-run the draft-update tool on a draft whose links have already been fixed by hand** — it re-mangles them. A draft the release manager has corrected is finished; leave it alone. @@ -141,6 +166,8 @@ A vote opened on a 404 burns the window before anyone can test. - Severity, CVE, S2-XXX, bulletin link or reporter detail anywhere, on any channel - `[email protected]` in any header, including Cc - Sending rather than drafting +- `htmlBody` passed to the draft tool, for any reason +- A draft handed over without the plain-text-mode instruction and the body file - A new paragraph inserted into the vote boilerplate - A quality checkbox arriving pre-ticked - An opening sentence carried over from the previous release @@ -158,3 +185,5 @@ A vote opened on a 404 burns the window before anyone can test. | "I'm adding to the boilerplate, not changing it" | Insertion is editing. The vote call is byte-frozen. | | "The release notes leave out what integrators need" | Then the page needs fixing. The mail renders the page. | | "Last release's opening sentence fits" | It described last release. Write the one this list supports. | +| "I passed `body`, not `htmlBody`, so it's plain text" | Gmail generates the HTML part itself. The format is set in the compose window. | +| "It went through last time, so HTML is tolerated" | Lists differ. One accepted the same message the other rejected. | diff --git a/.claude/skills/creating-release-vote-mail/vote-mail-template.md b/.claude/skills/creating-release-vote-mail/vote-mail-template.md index 339e72649..8db52adb8 100644 --- a/.claude/skills/creating-release-vote-mail/vote-mail-template.md +++ b/.claude/skills/creating-release-vote-mail/vote-mail-template.md @@ -114,9 +114,10 @@ On behalf of the Apache Struts project ``` Hard-wrap the body at 72 columns, continuation lines unindented, so the list stays legible in -the ASF archives and in quoted replies. Gmail reflows the wrap and rewrites the link lines when -it stores the draft — see *Gmail mangles the links* in [`SKILL.md`](SKILL.md) for why the body -also has to be handed over as a file. +the ASF archives and in quoted replies. Gmail reflows the wrap, rewrites the link lines, and +adds a `text/html` part that ASF lists reject — see *The mail must be text/plain* in +[`SKILL.md`](SKILL.md) for why the body also has to be handed over as a file, and what the +release manager has to do in the compose window before sending. ## Frozen text @@ -147,3 +148,5 @@ repository does not. - [ ] Exactly one mail - [ ] Body also saved to a file and its path handed over, so the mangled links and wrap can be fixed with one paste +- [ ] `htmlBody` not passed to the draft tool +- [ ] Handover states: plain-text mode on, paste the file, send from the `@apache.org` identity diff --git a/.claude/skills/creating-security-bulletins/SKILL.md b/.claude/skills/creating-security-bulletins/SKILL.md index 3af4e66a7..864908c1c 100644 --- a/.claude/skills/creating-security-bulletins/SKILL.md +++ b/.claude/skills/creating-security-bulletins/SKILL.md @@ -1,6 +1,6 @@ --- name: creating-security-bulletins -description: Use when drafting, updating, or reviewing an S2-XXX security bulletin on the Struts cwiki, when preparing bulletin text ahead of a CVE request, or when deciding how much detail about a fixed vulnerability is safe to publish. +description: Use when drafting, updating, or reviewing an S2-XXX security bulletin on the Struts cwiki, when preparing bulletin text ahead of a CVE request, when publishing a bulletin and announcing it to the ASF lists, or when deciding how much detail about a fixed vulnerability is safe to publish. --- # Creating Security Bulletins @@ -159,6 +159,52 @@ Bulletins stay restricted until the coordinated publication date. Expected on the Struts wiki: read and update limited to the author plus `struts-committers`. +Publication is clearing them **completely** — read *and* update, both empty, matching every +already-published bulletin. Verify with an unauthenticated fetch of the public URL, not with the +API's response: the tool reporting success is not the page being readable. + +## Announcing it: the mail is text/plain, or it does not arrive + +Once the page is public the advisory goes to the lists. **The mail carries one `text/plain` +part and nothing else.** A `text/html` part is a delivery failure — `[email protected]` +rejects it permanently: + +``` +ezmlm-reject: fatal: Sorry, a message part has an unacceptable MIME Content-Type: 'text/html' (#5.2.3) +``` + +**Do not rely on a bounce to catch it.** On the S2-070 run, 2026-08-14, one send was rejected +by `[email protected]` and *accepted* by `[email protected]`. The HTML advisory reached +the user list. A partial failure looks like success in the Sent folder. + +Two unrelated defects bounced that morning, each from a different list: + +| Defect | What the list says | +|---|---| +| A `text/html` part | `unacceptable MIME Content-Type: 'text/html' (#5.2.3)` | +| Wrong sender identity | `Must be sent from an @apache.org address.` | + +**The CVE tool generates both mails — use them.** Each record on `cveprocess.apache.org` has an +*OSS/ASF Emails* tab holding a finished `oss-security` mail and a finished ASF-lists mail, built +from the record's own affected ranges, description, credit and references, with send buttons +that go through ASF infrastructure rather than a personal mailbox. Copying that text is how the +mail stays consistent with the CVE record; composing a fresh one is how the two drift. + +If you draft in Gmail instead, the deliverable is three things and is incomplete without any: + +1. A draft with To, Bcc, Subject and `body`. **Never `htmlBody`** — and passing `body` alone + does not make the mail plain text; Gmail generates the HTML part itself on send. +2. The identical body in a file, whose path you hand over, hard-wrapped at 72 columns. +3. The sending instruction in your handover: **plain-text mode on** (⋮ → *Plain text mode*), + paste the file over the body, send from the `@apache.org` identity. + +The `oss-security` copy is a separate mail with no Cc and no Bcc — not the ASF mail with an +extra recipient. + +**Recipients are not interchangeable.** The tool's ASF mail addresses `[email protected]` and +`dev@`; Struts practice adds `[email protected]`, which is the list operators actually +read. `[email protected]` takes only `@apache.org` senders. + ## Start from the template, never from a previous bulletin **[`bulletin-template.md`](bulletin-template.md)** — the field reference, per-section guidance, pre-publication checklist, and a storage-format skeleton ready to POST to the Confluence API. **It is the source of truth.** @@ -189,6 +235,10 @@ Read the whole page and rewrite it; do not patch the fields you happen to notice - No statement of who is *not* affected, when exposure depends on a plugin or an opt-in setting - Writing a page from content you read earlier in the session without re-fetching it first - Publishing without re-checking restrictions +- Treating an API success as proof the page is publicly readable +- `htmlBody` passed to the draft tool, for any reason +- An announcement composed from scratch when the CVE record's *OSS/ASF Emails* tab holds one +- A draft handed over without the plain-text-mode instruction and the body file - A severity rating chosen by feel, or by reachability alone, without checking it against the published scale - Rating something Low because the feature is opt-in — opt-in is the definition of Moderate @@ -211,3 +261,6 @@ Read the whole page and rewrite it; do not patch the fields you happen to notice | "The patch is reviewed, so the release will contain it" | Reviewed is not merged. Re-check at publication, not at drafting. | | "Copying the last bulletin is quicker than the template" | It is how another advisory's CVE ships on your page. Copy the template. | | "I read the page a few minutes ago" | Someone else may have written to it since. Re-fetch, then write. There is no conflict warning. | +| "I passed `body`, not `htmlBody`, so it's plain text" | Gmail generates the HTML part itself on send. The format is decided in the compose window. | +| "It reached the lists, so the format was fine" | One list accepted the same message another rejected. Check every recipient, not the Sent folder. | +| "Writing the mail myself is quicker than opening the CVE tool" | The tool's text is generated from the record. Hand-written text is how the mail and the CVE drift apart. |
