mirror of
https://github.com/apache/struts.git
synced 2026-09-12 09:05:04 +00:00
docs(skills): require plain-text list mail in the two mail-producing skills (#1843)
ASF lists reject a text/html part outright. Observed on the S2-070 announcement, 2026-08-14: <announce@apache.org>: ezmlm-reject: fatal: Sorry, a message part has an unacceptable MIME Content-Type: 'text/html' (#5.2.3) <announcements@struts.apache.org>: Must be sent from an @apache.org address. user@struts.apache.org 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 — `announce@apache.org` 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 |
|
||||
|
||||
`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.
|
||||
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 `announce@apache.org` and accepted by `user@struts.apache.org` — 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.
|
||||
|
||||
**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 — `announcements@struts.apache.org`
|
||||
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
|
||||
- `user@struts.apache.org` 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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 — `announce@apache.org`
|
||||
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 `announce@apache.org` and *accepted* by `user@struts.apache.org`. 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 `announce@apache.org` and
|
||||
`dev@`; Struts practice adds `user@struts.apache.org`, which is the list operators actually
|
||||
read. `announcements@struts.apache.org` 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. |
|
||||
|
||||
Reference in New Issue
Block a user