Documentation
¶
Overview ¶
Package email provides MIME email parsing and encoding, preserving original formatting when no modifications are made and reconstructing valid RFC 5322 output when transforms modify content.
When parsing fails or enmime reports severe errors (e.g. malformed MIME parts), the original bytes are passed through unchanged and one or more Sanimail-Error headers are prepended to the output. This ensures delivery is never blocked by a parse failure. Downstream mail filters should match on the Sanimail-Error header to route flagged messages into a Malformed or Untrusted folder for manual review.
A Sanimail-Error means sanimail could not sanitize the message (some or all of it is delivered unsanitized), so its presence is the quarantine signal. Everything else is recorded under non-quarantine headers: Sanimail-Info carries forensic notes about a fully sanitized message (a part stripped on request, a minifier failure, content sanitized to empty), and a remote-inline fetch failure — which does not make the message unsafe, the HTML is sanitized regardless — goes under Sanimail-Inlined-Failed (see AddInlineNote).
Index ¶
- Constants
- func ValidateHeaderPattern(pattern string) error
- type Decrypter
- type Encryptor
- type HCPolicy
- type HeaderField
- type HeaderProtection
- type Message
- func (m *Message) AddHeaders(fields []HeaderField)
- func (m *Message) AddInlineNote(detail string)
- func (m *Message) AddMessageID()
- func (m *Message) ApplyPolicy(p *htmlpolicy.Policy, inliner Remote, detracker URLDetracker, ...)
- func (m *Message) DecryptPGP(d Decrypter) error
- func (m *Message) DecryptSMIME(d SMIMEDecrypter) error
- func (m *Message) DefangFields(calendar, contacts, headers bool)
- func (m *Message) DefangFilenames()
- func (m *Message) DeleteHeadersMatching(patterns []string)
- func (m *Message) DisarmHeadersMatching(patterns []string)
- func (m *Message) EncryptPGP(enc Encryptor) error
- func (m *Message) EncryptSMIME(enc SMIMEEncryptor) error
- func (m *Message) Errors() []string
- func (m *Message) ExistingContentIDs() []string
- func (m *Message) GeneratePlain(policy ReplacePolicy)
- func (m *Message) Infos() []string
- func (m *Message) InputSize() int
- func (m *Message) IsEncrypted() bool
- func (m *Message) MessageID() string
- func (m *Message) Minify()
- func (m *Message) ModificationSummary() string
- func (m *Message) Modified() bool
- func (m *Message) NeutralizeUndecodableParts()
- func (m *Message) NoteError(detail string)
- func (m *Message) PrependPolicyAuditComment()
- func (m *Message) ProtectHeaders(hp HeaderProtection)
- func (m *Message) RepairExchangePGP()
- func (m *Message) Root() *enmime.Part
- func (m *Message) SanitizeNestedMessages(sanitize NestedSanitizer)
- func (m *Message) SanitizeSignedData(sanitize NestedSanitizer)
- func (m *Message) SetDeterministic(on bool)
- func (m *Message) SetHeaderRules(strip, disarm []string)
- func (m *Message) SetHeaders(fields []HeaderField)
- func (m *Message) SetKeepSanimailHeaders(keep bool)
- func (m *Message) SetKeepSignatures(keep bool)
- func (m *Message) SetPolicyAudit(a *PolicyAudit)
- func (m *Message) SignPGP(s Signer) error
- func (m *Message) SignSMIME(s SMIMESigner) error
- func (m *Message) StripSanimailHeaders()
- func (m *Message) StripTypes(types []string)
- func (m *Message) WriteTo(w io.Writer) (int64, error)
- type NestedSanitizer
- type PolicyAudit
- type Remote
- type ReplacePolicy
- type SMIMEDecrypter
- type SMIMEEncryptor
- type SMIMESigner
- type Signer
- type TrackerBlocker
- type URLDetracker
Constants ¶
const ( HPModeClear = "clear" // signed-only: every header integrity-protected, none obscured HPModeCipher = "cipher" // encrypted: headers inside the ciphertext, outer ones obscured per the HCP )
RFC 9788 header-protection modes (the value of the hp= Content-Type parameter on the cryptographic payload's root part).
Variables ¶
This section is empty.
Functions ¶
func ValidateHeaderPattern ¶
ValidateHeaderPattern checks that a glob pattern is valid for use with DeleteHeadersMatching / DisarmHeadersMatching.
Types ¶
type Decrypter ¶
type Decrypter interface {
// Decrypt decrypts an armored/binary OpenPGP message, returning the
// plaintext.
Decrypt(ciphertext []byte) (plaintext []byte, err error)
}
Decrypter decrypts an OpenPGP message. Implemented by *gnupg.Client.
type Encryptor ¶
Encryptor encrypts a plaintext MIME entity to one or more recipients and returns an ASCII-armored OpenPGP message. The email/gnupg package's *gnupg.Client satisfies it; keeping the interface here lets the email package own the RFC 3156 MIME assembly without depending on the OpenPGP implementation (mirroring the Remote interface in policy_apply.go).
type HCPolicy ¶
type HCPolicy int
HCPolicy is an RFC 9788 Header Confidentiality Policy: which outer (cleartext) headers are obscured or removed when encrypting. It has no effect in clear (signed-only) mode, where every header stays visible.
const ( // HCPNone keeps every outer header as-is (integrity + HP-Outer only). HCPNone HCPolicy = iota // HCPBaseline obscures Subject to "[...]" and removes Comments/Keywords. HCPBaseline // HCPShy is HCPBaseline plus From/To/Cc reduced to addr-spec (display names // dropped) and Date converted to UTC. HCPShy )
func ParseHCPolicy ¶
ParseHCPolicy maps a --hp policy name to an HCPolicy. The "off" case is handled by the caller (it disables header protection entirely) and is not a valid value here.
type HeaderField ¶
type HeaderField struct{ Name, Value string }
HeaderField is a name/value pair for AddHeaders and SetHeaders.
type HeaderProtection ¶
type HeaderProtection struct {
// Mode is HPModeClear (signed-only) or HPModeCipher (encrypted); it becomes
// the hp= parameter on the cryptographic payload's root Content-Type.
Mode string
// Policy is the Header Confidentiality Policy applied to the outer headers in
// cipher mode. Ignored in clear mode.
Policy HCPolicy
// Confidential lists extra header names (case-insensitive) to obscure beyond
// the policy: any that would otherwise stay visible is replaced with "[...]".
// Like Policy, it applies in cipher mode only — with signing alone there is
// no ciphertext to hide a value in, so nothing is obscured.
Confidential []string
// Expose lists header names to force-keep visible on the outer envelope,
// overriding both the policy and Confidential. Cipher mode only, for the
// same reason: in clear mode every header is visible already.
Expose []string
}
HeaderProtection configures RFC 9788 header protection for ProtectHeaders.
type Message ¶
type Message struct {
// contains filtered or unexported fields
}
Message wraps a parsed email, holding both the raw original bytes and the parsed enmime Envelope. It tracks whether any transform has modified the message and provides helpers for accessing MIME parts.
func Parse ¶
Parse reads a MIME email from r and returns a Message. Parse never returns an error for malformed email content — instead it records Sanimail-Error details and passes through the raw bytes. The only error Parse returns is a genuine I/O failure from reading r. If logger is non-nil, it is enriched with the message's Message-Id and stored on the Message for per-operation logging.
func (*Message) AddHeaders ¶
func (m *Message) AddHeaders(fields []HeaderField)
AddHeaders appends each field to the root part, leaving any existing header of the same name in place. No-op on a nil envelope (raw passthrough).
func (*Message) AddInlineNote ¶ added in v0.3.0
AddInlineNote records a remote-inline fetch failure as a Sanimail-Inlined-Failed header (NOT Sanimail-Error), so operators routing on Sanimail-Error presence do not quarantine a message merely because a remote image fetch failed — the HTML is sanitized regardless of fetch outcome. Member of the Sanimail-Inlined-* forensic family, so the anti-spoofing strip removes it on reprocessing. Provided as a func(string) callback for the email/remote inliner.
func (*Message) AddMessageID ¶
func (m *Message) AddMessageID()
AddMessageID ensures the message carries a Message-Id header, adding a generated one when it has none. This enables correlation between messages in a mailbox and sanimail's processing logs. The header is absent in two cases: the message arrived without one (m.messageID already holds the value Parse generated), or --headers-strip removed it — in which case a fresh random id is minted rather than reusing the stripped value, so every delivered copy stays globally unique. That uniqueness matters because clients that key their cache or duplicate-suppression on Message-Id (notably Apple Mail) will otherwise collapse two copies of the same original into one. No-op when a Message-Id is already present, when parsing failed (nil envelope), or when the input was empty.
func (*Message) ApplyPolicy ¶
func (m *Message) ApplyPolicy(p *htmlpolicy.Policy, inliner Remote, detracker URLDetracker, blocker TrackerBlocker, extraOpts ...htmlpolicy.ApplyOption)
ApplyPolicy sanitizes text/html parts with htmlpolicy.Policy.ApplyDocument, image/svg+xml parts with htmlpolicy.Policy.ApplyHTML (fragment mode, which preserves the existing <svg> root), and standalone text/css parts with htmlpolicy.Policy.ApplyCSS (CSS inside <style> blocks and style="" attributes is already covered by the text/html pass). On per-part error the original content is kept — delivered unsanitized — and a Sanimail-Error header is added so a downstream filter can quarantine; delivery is never blocked. A nil policy is a no-op.
text/calendar parts (and the application/ics alias some senders attach the same invite under) are sanitized too (see package email/ical): the embedded HTML descriptions Outlook renders inline (X-ALT-DESC and RFC 9073's STYLED-DESCRIPTION) are run through the same HTML pipeline, and the calendar's URLs are de-tracked and scheme-checked against the policy's scheme allowlist (via htmlpolicy.Policy.URLSchemeAction) so a javascript:/data: link in a URL property is neutralized. Inline binary attachments are not decoded.
When inliner is non-nil, htmlpolicy's URL rewriter is wired up so that image URLs discovered during sanitization are fetched and attached as cid: siblings of the source part inside a multipart/related wrapper. See package inline.
When blocker is non-nil, its rewriter is chained ahead of both the detracker's and the inliner's, so a subresource URL a filter list identifies as a tracker is replaced by an inert fragment before anything else looks at it — and, via remote.Config.BlockedSubresource, before it is fetched at all. It acts only on URLs a client fetches on view, never on one the recipient navigates to. See package trackerblock.
When detracker is non-nil, its rewriter is chained ahead of the inliner's so the navigational URLs urldetrack.Navigable names (<a>/<area> href, action, formaction) are de-tracked during the HTML walk, and text/plain parts are scanned for bare URLs and cleaned in a final pass. The detracker and inliner act on disjoint URL contexts (navigation vs subresources), so chaining order only fixes a consistent invariant. See package urldetrack.
Extra htmlpolicy ApplyOptions (e.g. WithVerboseLog) are passed through to every Apply call this transform makes, alongside the rewriter/prefetcher options it wires up itself.
func (*Message) DecryptPGP ¶
DecryptPGP decrypts encrypted content in place: a PGP/MIME multipart/encrypted message becomes its decrypted inner entity, and inline PGP message blocks embedded in text/plain parts (whether the whole body or surrounded by cleartext) are decrypted and spliced back in place. A decrypted inner multipart/signed is left intact — the pipeline sanitizes its content like any other part. No-op when nothing is encrypted. A PGP/MIME decrypt failure is returned (the caller is fail-closed); an inline-block failure is non-fatal (warn and continue — see decryptInline).
Only one layer is decrypted: a payload that is itself PGP/MIME-encrypted is left for a subsequent pass.
func (*Message) DecryptSMIME ¶
func (m *Message) DecryptSMIME(d SMIMEDecrypter) error
DecryptSMIME decrypts S/MIME enveloped-data in place: the opaque application/pkcs7-mime part becomes its decrypted inner entity. A decrypted inner multipart/signed is left intact (the pipeline sanitizes its content like any other part). No-op when the message is not S/MIME enveloped-data. A decrypt failure is returned (the caller is fail-closed).
Only enveloped-data (encryption) is handled here, mirroring DecryptPGP, which decrypts encryption layers but does not unwrap signatures. Opaque signed-data is not encrypted — its content is merely wrapped — so it is unwrapped and sanitized separately by Message.SanitizeSignedData, not decrypted.
func (*Message) DefangFields ¶ added in v0.13.0
DefangFields breaks dangerous HTML tags in calendar and/or contact text fields, and (with headers) in rendered header values — for a client that renders a calendar DESCRIPTION, a vCard NOTE, an ATTENDEE CN parameter, or a Subject / sender display name as HTML without escaping. It backs the --defang-fields feature and is independent of --policy: the action only neutralizes markup, it does not apply the HTML policy. Each target is left byte-identical when it carries no dangerous tag. Called on one message it defangs the matching parts and header values of that tree; the shared sanitizeContent pass invokes it on the outer message and on every re-parsed nested message/rfc822 tree and decrypted inner tree, so all are covered (as for ApplyPolicy).
func (*Message) DefangFilenames ¶ added in v0.12.0
func (m *Message) DefangFilenames()
DefangFilenames neutralizes unsafe characters in every part's attachment filename (Content-Disposition filename= and Content-Type name=, both driven by enmime's Part.FileName). It defends against a mail client that renders an attacker-chosen filename into its HTML UI without escaping — the stored-XSS class Zimbra fixed via malicious attachment filenames. A filename with no unsafe characters is left untouched, so an unmodified message still passes through byte-identically.
func (*Message) DeleteHeadersMatching ¶
DeleteHeadersMatching removes headers whose names match any of the given glob patterns from the root part. Patterns are matched case-insensitively using fnmatch-style wildcards (where * matches any sequence of characters).
func (*Message) DisarmHeadersMatching ¶
DisarmHeadersMatching renames headers matching glob patterns by prepending "Sanimail-Disarmed-" to the original name, on the root part only. Patterns are matched case-insensitively using fnmatch-style wildcards.
func (*Message) EncryptPGP ¶
EncryptPGP rewrites the message as an RFC 3156 PGP/MIME multipart/encrypted structure, encrypting the existing MIME entity to the recipients. Message- level headers (From/To/Subject/Date/...) stay on the outer envelope; the inner MIME entity (Content-* headers + body) is what gets encrypted.
It is fail-closed: when the message failed to parse, the entire raw input is encrypted as the inner payload rather than passing plaintext through, so a requested encryption never leaks the original content. The only error returned is from the encryptor itself; on error the caller must not deliver the message in the clear.
func (*Message) EncryptSMIME ¶
func (m *Message) EncryptSMIME(enc SMIMEEncryptor) error
EncryptSMIME rewrites the message as S/MIME enveloped-data: a single opaque application/pkcs7-mime part holding the CMS encryption of the existing MIME entity. Message-level headers (From/To/Subject/Date/...) stay on the outer part; the inner entity (Content-* headers + body) is what gets encrypted.
Like EncryptPGP it is fail-closed: when the message failed to parse the entire raw input is encrypted, so a requested encryption never leaks plaintext. The only error returned is from the encryptor; on error the caller must not deliver the message in the clear.
func (*Message) Errors ¶ added in v0.10.0
Errors returns the recorded sanitization failures (see addError).
func (*Message) ExistingContentIDs ¶
ExistingContentIDs returns the bracket-stripped Content-ID values already present in the message tree, sorted for stable output. The image inliner uses these to avoid minting a colliding inline-NNN@sanimail cid.
func (*Message) GeneratePlain ¶
func (m *Message) GeneratePlain(policy ReplacePolicy)
GeneratePlain converts HTML to text/plain using html2text and adds or replaces the text/plain sibling in a multipart/alternative. It handles:
- singlepart text/html root
- multipart/alternative anywhere in the MIME tree (Gmail, Outlook, Thunderbird structures including nested mixed/related containers)
- text/html inside multipart/related children (Apple Mail inline images)
- text/html bare inside a multipart/related with no enclosing multipart/alternative (an HTML-only message carrying inline images, including what --remote-inline produces for singlepart HTML) — a new multipart/alternative is inserted around the html inside the related
When several text/html alternatives are present it converts the last one: RFC 2046 §5.1.4 orders alternatives least- to most-preferred, so the last is the richest representation (see findLastHTMLChild).
The policy function decides whether an existing text/plain part should be replaced, given original and generated word counts (see RatioPolicy, MaxWordsPolicy). For a singlepart text/html root (no existing text/plain) a text/plain alternative is always added and the policy is not consulted. Generated text that is empty or whitespace-only is skipped, so no empty text/plain part is ever created.
func (*Message) IsEncrypted ¶
IsEncrypted reports whether the message is already fully encrypted, so that --pgp-skip-encrypted can pass it through untouched. It is deliberately conservative: it returns true only when confident the entire message is encrypted (PGP/MIME, explicit S/MIME enveloped-data, or a message that is solely an inline PGP block). Anything partial — an inline block with extra text, an encrypted part alongside unencrypted attachments, or an opaque signature — returns false and gets encrypted.
func (*Message) MessageID ¶
MessageID returns the message's Message-Id (from header or generated). It is only set when a logger was passed to Parse.
func (*Message) Minify ¶
func (m *Message) Minify()
Minify compresses text/html, text/watch-html, text/css, and image/svg+xml parts. CSS inside <style> blocks and style="" attributes is covered by the HTML pass; standalone text/css parts are covered by the text/css pass. text/watch-html (Apple Watch's HTML subset) is minified with the HTML minifier, exactly like a text/html body. image/svg+xml parts (native or inlined via --remote-inline, which land in the tree as image/svg+xml) are compressed with the SVG minifier.
It runs after Message.ApplyPolicy in the pipeline (so it minifies the sanitized output) but is independent of it — minification happens whether or not a policy is applied. Fail-safe: on a per-part minify error the original content is kept and a Sanimail-Info header is added so delivery is never blocked — the part is unminified, not unsanitized, so this is a forensic note rather than a quarantine signal. A message that failed to parse (nil envelope) is a no-op.
func (*Message) ModificationSummary ¶
ModificationSummary returns a comma-separated list of modifications applied.
func (*Message) NeutralizeUndecodableParts ¶ added in v0.17.0
func (m *Message) NeutralizeUndecodableParts()
NeutralizeUndecodableParts retypes to text/plain every part sanimail would otherwise sanitize but whose characters it never actually saw, so no client renders such a part in a format the sanitizer could not read.
The case is narrow and specific. enmime transcodes a text part's body to UTF-8 on parse, and normalizeCharsets relabels a part whose charset could otherwise reinterpret the bytes — the defense that stops a UTF-7 "+ADw-script+AD4-" from reviving in the client. Both rely on the body having been decoded. When enmime cannot resolve the charset the body arrives raw instead, and reconcileCharset deliberately leaves it alone so it still round-trips. That is right for a label which cannot reinterpret ASCII (iso-8859-*, windows-125*: the '<' we scanned is the '<' the client sees), and wrong for one which can:
Content-Type: text/html; charset=ibm037
carrying EBCDIC bytes reaches htmlpolicy as meaningless Latin text with no '<' anywhere, passes untouched, and ships with the sender's label still on it — so a client honouring ibm037 decodes a live <script>. The sanitizer read one document and the recipient renders another. Measured against enmime v2.4.1 the undecodable-and-not-ASCII-transparent set is the EBCDIC code pages (ibm037, cp037, cp500, cp273, cp1140, ibm424, ibm875, ibm1047) and utf-32; every mainstream charset, including utf-16, the ISO-2022 family and HZ, is transcoded normally and unaffected.
An unrecognized label (a typo, a private x-* name) lands in the same undecodable state but is not a bypass: a client that cannot resolve it falls back to an ASCII-transparent default, so it sees the same bytes the scan did. asciiTransparentCharset does not know such a label either, so the check below would retype it — pointless churn on ordinary broken mail. Hence the guard is on the label being one we can name as dangerous, not on it being unknown; see undecodableCharset.
Retyping (rather than relabelling to utf-8, which would render the body as replacement characters) keeps the most fidelity available: the recipient still reads the sender's real text under the sender's own charset, just as literal text rather than as markup. It mirrors StripTypes' neutralize-by-retype, down to the Sanimail-Info: after the retype nothing unsanitized renders, so this is a forensic note and not the Sanimail-Error quarantine signal.
Scoped to text/* because that is the whole exposure. The other types sanimail sanitizes — image/svg+xml and application/ics, the ones enmime does not decode at all — are already handled a step earlier: normalizeCharsets runs htmlpolicy.ConvertToUTF8 over them and relabels the result utf-8, so their bodies are valid UTF-8 by the time anything here could look. Verified for the EBCDIC pages and utf-32; a check for them would be a branch that never fires.
Called from the shared sanitizeContent pass, so it covers the outer message, every nested message/rfc822 tree and any decrypted inner tree — the same reach as ApplyPolicy.
func (*Message) NoteError ¶ added in v0.15.0
NoteError records a processing failure as a Sanimail-Error — the operator's quarantine signal — for the cmd pipeline to flag a message it is delivering unsanitized, e.g. a permanently undecryptable ciphertext passed through still encrypted rather than requeued forever. The exported analogue of addError; like any error it drives WriteTo's [Sanimail-Malformed-Mime] Subject tag.
func (*Message) PrependPolicyAuditComment ¶
func (m *Message) PrependPolicyAuditComment()
PrependPolicyAuditComment inserts the captured policy-audit comment at the start of every text/html part. It is a no-op when no audit is attached or nothing was stripped, so an otherwise-unmodified message stays byte-identical.
It runs after Minify so the minifier (which strips comments) cannot remove or re-wrap it. The comment is prepended to text/html and text/watch-html bodies alike; a message with neither has nowhere to carry the comment, so the actions go unreported — acceptable, since the comment is an HTML-source audit aid.
func (*Message) ProtectHeaders ¶
func (m *Message) ProtectHeaders(hp HeaderProtection)
ProtectHeaders applies RFC 9788 header protection in preparation for SignPGP/ EncryptPGP. It stamps the hp= parameter on the content-entity root, rewrites that root's headers to the obscured "outer view" (cipher mode only) so moveMessageHeaders carries the obscured values out to the envelope, and stashes the real header values for injectProtectedHeaders to copy — together with HP-Outer records — back into the cryptographic payload.
No-op on a parse failure (nil envelope): header protection only applies to a parsed MIME tree, and unparseable mail falls back to raw passthrough.
func (*Message) RepairExchangePGP ¶
func (m *Message) RepairExchangePGP()
RepairExchangePGP undoes Microsoft Exchange's mangling of incoming PGP/MIME: Exchange rewrites the outer multipart/encrypted to multipart/mixed, drops the protocol parameter, and inserts a spurious (usually empty) text/plain part, so clients no longer recognise the message as encrypted. When the tell-tale shape is present — a multipart/mixed carrying both the application/pgp- encrypted marker and the application/octet-stream ciphertext, and nothing of substance besides — this restores a clean RFC 3156 multipart/encrypted from those two parts. It runs unconditionally (before decrypt and the skip-encrypted check); a no-op otherwise.
"Nothing of substance besides" is what keeps the rebuild from losing mail. A genuine PGP/MIME message has nothing outside the ciphertext — the entire message is sealed inside it — so the only other part a mangled one can carry is Exchange's own empty filler. Any child with actual content is therefore evidence that this multipart/mixed is not a mangled PGP/MIME message at all, just an ordinary one that happens to carry those two media types, and rebuilding the root from two of its parts would silently discard the rest: the body text, the attachments. Such a message is left exactly as it arrived.
func (*Message) SanitizeNestedMessages ¶
func (m *Message) SanitizeNestedMessages(sanitize NestedSanitizer)
SanitizeNestedMessages re-parses every nested-message part (see nestedMessageTypes), runs sanitize over the nested tree, recurses into deeper nested messages, and splices the re-serialized bytes back — but only when the nested tree actually changed, so an unmodified forwarded message stays byte-identical. This closes a --policy bypass: enmime does not descend into a message/rfc822 or message/global part (its body is an opaque leaf in Part.Content), yet clients like Thunderbird (by default) and macOS Mail render that nested message inline with the same HTML engine as the top-level body, so its hostile HTML would otherwise reach the recipient unsanitized.
Recursion is bounded by maxNestedMessageDepth: every level is a full re-parse + re-serialize, so an unbounded nested-message chain would be a CPU/memory DoS (enmime's own per-Parse depth probe resets at each level and does not bound this). A part deeper than the limit — or one that is malformed / over-deep internally (which Parse surfaces as a nil envelope) — is left untouched and a Sanimail-Error is added, so we never lose mail and a downstream filter can quarantine it. forEachPart supplies the walk, the byte-identity check, and the modified/mods bookkeeping.
func (*Message) SanitizeSignedData ¶ added in v0.15.0
func (m *Message) SanitizeSignedData(sanitize NestedSanitizer)
SanitizeSignedData unwraps an opaque S/MIME signed-data message at the root and sanitizes the MIME entity it encapsulates. A signed-data p7m is NOT encrypted — the inner MIME is merely wrapped in a PKCS#7 SignedData, in the clear — so a client unwraps and renders it, but the sanitization pipeline, seeing an opaque application/pkcs7-mime leaf, never reaches the inner HTML: a silent --policy bypass. Extracting the content (no key, no verification: signing is public) lets the pipeline sanitize it.
The inner entity is parsed and handed to sanitize (the same per-tree treatment the outer message gets). Then, mirroring how a clean multipart/signed keeps its still-valid signature:
- inner unchanged → the opaque part is left byte-identical, signature intact.
- inner changed → the opaque part is replaced by the sanitized cleartext entity; the signature is dropped (a content change voids it anyway, exactly as any body edit voids DKIM), envelope headers move down onto it, and a Sanimail-Info records the unwrap.
When the content cannot be safely extracted — signed-data whose CMS is malformed, or whose encapsulated MIME does not parse — it fails closed: the part is delivered intact but flagged with a Sanimail-Error and the quarantine Subject tag, so nothing hostile ships silently. A part that is not signed-data (enveloped-data left encrypted, a detached signature, certs-only, or not CMS) is left untouched.
Scope is the message root, matching DecryptSMIME: it covers a message that is itself opaque-signed and a decrypted enveloped-data revealing an opaque-signed inner (sign-inside-encrypt) — the shapes a client renders as the message body. The encapsulated entity's own nested message/rfc822 trees are sanitized (the caller's sanitize callback runs SanitizeNestedMessages on it).
Out of scope, as rare multi-layer shapes that also needed no new bypass to exist (they were opaque before this and remain so): a signed-data part nested as a non-root attachment (clients show it as an attachment, not inline); an opaque-signed message nested inside a forwarded message/rfc822; and stacked (double) opaque signing. Closing those needs a depth budget shared across the nested-message and signed-data recursions.
func (*Message) SetDeterministic ¶
SetDeterministic enables deterministic mode, which uses predictable boundaries (sanimail-boundary-1, sanimail-boundary-2, ...) and a fixed generated Message-Id. This is for testing only — using it in production will produce duplicate boundaries and Message-Ids across messages.
func (*Message) SetHeaderRules ¶ added in v0.17.0
SetHeaderRules records the operator's --headers-strip and --headers-disarm glob patterns so the decrypt path can apply them a second time, to the headers an RFC 9788 payload supplies.
The pipeline runs those rules against the envelope before decrypting, which is where they belong: they act on the message as it arrived. But a rule that removes a header (or renames it, as disarm does) leaves the envelope without that name, and the sealed payload is then allowed to fill the gap — so an attacker who sets hp=cipher on a message encrypted to the recipient can reinstate exactly what the operator asked to be removed. Applying the rules again, to the promoted headers alone, closes that.
Deliberately scoped to the payload's headers rather than re-run over the whole message: the pipeline's own later steps (--headers-set, --headers-add, and the minted Message-Id) intentionally reintroduce names an earlier strip removed, and a blanket second pass would undo them.
func (*Message) SetHeaders ¶
func (m *Message) SetHeaders(fields []HeaderField)
SetHeaders sets each field on the root part, replacing any existing header(s) of the same name. No-op on a nil envelope (raw passthrough).
func (*Message) SetKeepSanimailHeaders ¶
SetKeepSanimailHeaders controls whether inbound Sanimail-* headers are kept instead of stripped (the anti-spoofing default), both on the parsed message and on a decrypted inner tree. Intended for re-processing sanimail's own output — e.g. decrypting a message sanimail encrypted to inspect what it did, where the strip would discard sanimail's earlier forensic headers (Sanimail-Inlined-From, Sanimail-Error, ...). sanimail cannot tell those apart from sender-forged ones, so this is only safe on trusted input: with the strip disabled a forged Sanimail-* header survives into the output.
func (*Message) SetKeepSignatures ¶
SetKeepSignatures controls whether DKIM-Signature and Authentication-Results headers are preserved on modified messages instead of being disarmed.
func (*Message) SetPolicyAudit ¶
func (m *Message) SetPolicyAudit(a *PolicyAudit)
SetPolicyAudit attaches a PolicyAudit so Message.PrependPolicyAuditComment can render the recorded actions. Nil disables it (the default).
func (*Message) SignPGP ¶
SignPGP rewrites the message as an RFC 3156 multipart/signed structure: the existing MIME entity becomes the first part, with a detached OpenPGP signature over it as the second. Message-level headers move to the outer multipart/signed entity. Composes with EncryptPGP for sign-then-encrypt (sign sets the root, encrypt then wraps it).
On a parse failure (nil envelope) signing is skipped with a warning — there is no MIME entity to sign, and an unsigned message is not a confidentiality leak. A signer error is returned (the caller is fail-closed).
func (*Message) SignSMIME ¶
func (m *Message) SignSMIME(s SMIMESigner) error
SignSMIME rewrites the message as a multipart/signed structure: the existing MIME entity becomes the first part, with a detached CMS signature over it (application/pkcs7-signature) as the second. Message-level headers move to the outer multipart/signed entity. Composes with EncryptSMIME for sign-then-encrypt (sign sets the root, encrypt then wraps it).
On a parse failure (nil envelope) signing is skipped with a warning — there is no MIME entity to sign, and an unsigned message is not a confidentiality leak. A signer error is returned (the caller is fail-closed).
func (*Message) StripSanimailHeaders ¶
func (m *Message) StripSanimailHeaders()
StripSanimailHeaders removes any incoming Sanimail-* headers from every MIME part (anti-spoofing). sanimail emits trust-bearing Sanimail-* headers itself (e.g. Sanimail-Original-Content-Type, Sanimail-Inlined-From, Sanimail-Error, Sanimail-Inlined-Failed, Sanimail-Disarmed-*); stripping inbound ones from all parts — not just the root — prevents a sender from forging one on a nested part to fool a downstream filter that matches anywhere in the message. Run early, before sanimail writes its own. No-op under SetKeepSanimailHeaders.
func (*Message) StripTypes ¶
StripTypes removes every MIME part whose Content-Type matches (case- insensitively) one of the given content types — e.g. text/x-amp-html for AMP, which a strict policy can't sanitize and which Gmail/Yahoo render in preference to the text/html alternative. Removing it lets the client fall back to the sanitized HTML.
To honor "never lose mail" it refuses to remove a part that is the message root or its parent's only child (removal would leave an empty container); such a leaf is instead retyped to text/plain — content delivered verbatim as inert literal text — with its own Sanimail-Info. A matched multipart container in that position can't be retyped (its body is its children), so it is left in place with only a debug log — no header, since a message we deliberately left intact carries nothing to report. Each *removed* part is recorded as a Sanimail-Info (the strip is a successful, operator-requested action, not a sanitization failure) and in the modification summary.
Matching parts are collected first, then removed, so the walk never mutates the list it is iterating; the only-child guard is re-checked at removal time so stripping the first of two matching siblings can still refuse the second once it has become an only child.
func (*Message) WriteTo ¶
WriteTo writes the message to w. When no modifications have been made, the output is byte-identical to the original input (with Sanimail-Error/-Info headers prepended if any). When modified, the message is reconstructed from the parsed envelope with RFC 5322 CRLF line endings.
type NestedSanitizer ¶
type NestedSanitizer func(nested *Message)
NestedSanitizer sanitizes ONE re-parsed nested message tree. The cmd pipeline supplies it so a forwarded message gets the same content treatment — strip, policy, minify, generate-plain — and the SAME inliner (shared fetch budget and cache) as the outer message, without the email package needing to import the inliner or policy configuration.
It must NOT recurse into deeper nested-message parts itself: SanitizeNestedMessages owns the recursion (and its depth bound) and invokes the sanitizer once per level.
type PolicyAudit ¶
type PolicyAudit struct {
// contains filtered or unexported fields
}
PolicyAudit captures htmlpolicy's per-action verbose stream (wired via htmlpolicy.WithVerboseLog) so it can be rendered into the HTML comment that --policy-audit-comment prepends to each text/html body. Each emitted line is one policy action — a strip, defang, scheme rejection, CSS-property removal, etc. — in htmlpolicy's human-readable form.
A *PolicyAudit is an io.Writer; pass it to htmlpolicy.WithVerboseLog. Because sanimail processes one message per process, a single PolicyAudit per run holds exactly that message's actions.
type Remote ¶
type Remote interface {
// Rewriter returns the URL rewriter to pass through
// htmlpolicy.WithApplyURLRewriter. It runs after Prefetch and only reads
// the warmed cache; it performs no network I/O.
Rewriter() htmlpolicy.URLRewriter
// Prefetch is the URL prefetcher to pass through
// htmlpolicy.WithApplyURLPrefetcher. htmlpolicy invokes it once per Apply
// call, before the rewrite walk, with every URL the Rewriter would see, so
// the inliner can fetch them in parallel and warm its cache.
Prefetch(refs []htmlpolicy.URLRef)
// NotifyBeforePart tells the inliner which part is about to be
// sanitized so URL discoveries during the walk are attributed
// correctly.
NotifyBeforePart(p *enmime.Part)
// PendingFor returns the MIME parts that should be attached as
// multipart/related siblings of p after the walk completes, and
// clears the entry.
PendingFor(p *enmime.Part) []*enmime.Part
// FetchImage fetches an image URL through the inliner's guarded path and
// returns processed bytes + content-type for out-of-band embedding (a vCard
// PHOTO), sharing the per-message budget and cache. err is non-nil on any
// rejection so the caller can fall back to de-tracking the URL.
FetchImage(rawURL string) (body []byte, contentType string, err error)
}
Remote is the minimal surface Message.ApplyPolicy uses from an inliner implementation. The production type is *remote.InlineSink; tests pass a mock. Defined as an interface here so the email package doesn't need to import email/remote (no cycle even though there wouldn't be one anyway, this keeps the boundary clean).
type ReplacePolicy ¶
ReplacePolicy decides whether an existing text/plain part should be replaced by generated text, given the original and generated word counts.
func MaxWordsPolicy ¶
func MaxWordsPolicy(n int) ReplacePolicy
MaxWordsPolicy replaces when the original has n or fewer words. MaxWordsPolicy(0) replaces only when the original is empty or absent.
func RatioPolicy ¶
func RatioPolicy(ratio float64) ReplacePolicy
RatioPolicy replaces when generated words >= ratio * original words. A ratio of 0 always replaces; empty originals are always replaced.
type SMIMEDecrypter ¶
SMIMEDecrypter decrypts CMS enveloped-data. Implemented by *gnupg.Client.
type SMIMEEncryptor ¶
SMIMEEncryptor encrypts a plaintext MIME entity to one or more recipients and returns binary CMS (DER) enveloped-data. The email/gnupg package's *gnupg.Client satisfies it. Unlike the PGP Encryptor it returns binary DER, not ASCII armor — the MIME layer base64-encodes it (the S/MIME wire form).
type SMIMESigner ¶
type SMIMESigner interface {
SignDetachedCMS(content []byte) (der []byte, micalg string, err error)
}
SMIMESigner produces a detached CMS (DER) signature over content, plus the micalg parameter (e.g. "sha-256") for the multipart/signed Content-Type.
type Signer ¶
Signer produces a detached, ASCII-armored OpenPGP signature over content, plus the micalg parameter (e.g. "pgp-sha256") naming the digest the signature used, for the multipart/signed Content-Type.
type TrackerBlocker ¶ added in v0.19.0
type TrackerBlocker interface {
// Rewriter returns the URL rewriter (replaces a blocked subresource URL
// with an inert marker) to chain through htmlpolicy.WithApplyURLRewriter.
Rewriter() htmlpolicy.URLRewriter
// Blocked reports whether a URL held out of band -- a vCard PHOTO, an
// iCalendar IMAGE -- is a known tracker.
Blocked(rawURL string, t urlctx.Type) bool
// Marker returns the inert reference that replaces a blocked URL.
Marker(rawURL string) string
}
TrackerBlocker is the minimal surface Message.ApplyPolicy uses from a tracker-blocklist implementation. The production type is *trackerblock.Blocker. Like Remote and URLDetracker it is an interface here so the email package stays free of the implementation import.
type URLDetracker ¶
type URLDetracker interface {
// Rewriter returns the URL rewriter (cleans the navigational URLs
// urldetrack.Navigable names) to chain through
// htmlpolicy.WithApplyURLRewriter.
Rewriter() htmlpolicy.URLRewriter
// DetrackText rewrites bare URLs in a text/plain body, returning the
// result and whether anything changed. delSp is true when the part is
// RFC 3676 format=flowed with DelSp=yes, where a URL may be split across a
// soft line break and so soft-wrapped URLs are left intact.
DetrackText(s string, delSp bool) (string, bool)
// CleanURL cleans a single bare URL held out of band (e.g. an iCalendar
// property value), returning the de-tracked result.
CleanURL(raw string) string
}
URLDetracker is the minimal surface Message.ApplyPolicy uses from a URL-cleaning implementation (tracking-parameter stripping and redirector unwrapping). The production type is *urldetrack.Detracker. Like Remote it's an interface here to keep the email package free of the implementation import.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package adblock implements the Adblock Plus filter-list grammar: splitting a line into its exception marker, URL pattern and option tokens, and compiling a pattern into a matcher.
|
Package adblock implements the Adblock Plus filter-list grammar: splitting a line into its exception marker, URL pattern and option tokens, and compiling a pattern into a matcher. |
|
Package cms handles the CMS (RFC 5652) / PKCS#7 structures used by S/MIME: transcoding gpgsm's streamed BER output to canonical DER, and extracting the signed content from an opaque signed-data message so it can be sanitized.
|
Package cms handles the CMS (RFC 5652) / PKCS#7 structures used by S/MIME: transcoding gpgsm's streamed BER output to canonical DER, and extracting the signed content from an opaque signed-data message so it can be sanitized. |
|
Package fielddefang neutralizes HTML markup that a mail client might mis-render out of a calendar/contact field or a header value.
|
Package fielddefang neutralizes HTML markup that a mail client might mis-render out of a calendar/contact field or a header value. |
|
Package gnupg implements sanimail's GnuPG bridge.
|
Package gnupg implements sanimail's GnuPG bridge. |
|
Package ical surgically rewrites selected property values inside an iCalendar (RFC 5545) body, preserving every other byte — folding, ordering, blank lines, and unknown properties — exactly as received.
|
Package ical surgically rewrites selected property values inside an iCalendar (RFC 5545) body, preserving every other byte — folding, ordering, blank lines, and unknown properties — exactly as received. |
|
Package policies exposes htmlpolicy presets shipped with sanimail and resolves --policy flag values to compiled *htmlpolicy.Policy instances.
|
Package policies exposes htmlpolicy presets shipped with sanimail and resolves --policy flag values to compiled *htmlpolicy.Policy instances. |
|
Package remote implements remote-resource inlining for the --remote-inline flag.
|
Package remote implements remote-resource inlining for the --remote-inline flag. |
|
Package trackerblock neutralises subresource URLs that a filter list identifies as email trackers, driven by one or more operator-supplied Adblock filter lists.
|
Package trackerblock neutralises subresource URLs that a filter list identifies as email trackers, driven by one or more operator-supplied Adblock filter lists. |
|
Package urlctx classifies an htmlpolicy.URLContext into the kind of request a client would make for it — the Adblock Plus "type" taxonomy ($image, $script, $stylesheet, …) that filter rules are written against.
|
Package urlctx classifies an htmlpolicy.URLContext into the kind of request a client would make for it — the Adblock Plus "type" taxonomy ($image, $script, $stylesheet, …) that filter rules are written against. |
|
Package urldetrack strips tracking query parameters from links and unwraps redirector links to their embedded target URL, driven by one or more operator-supplied ClearURLs rules catalogs, a small supplement of redirector rules for corporate mail gateways upstream doesn't cover (data/mailgateways.json: Microsoft Defender Safe Links, Barracuda Link Protect), and code decoders for the two Proofpoint URL Defense encodings no declarative ruleset can express (see proofpoint.go).
|
Package urldetrack strips tracking query parameters from links and unwraps redirector links to their embedded target URL, driven by one or more operator-supplied ClearURLs rules catalogs, a small supplement of redirector rules for corporate mail gateways upstream doesn't cover (data/mailgateways.json: Microsoft Defender Safe Links, Barracuda Link Protect), and code decoders for the two Proofpoint URL Defense encodings no declarative ruleset can express (see proofpoint.go). |
|
Package vcard surgically rewrites selected property values inside a vCard (RFC 6350) body, preserving every other byte — folding, ordering, grouping, blank lines, and unknown properties — exactly as received.
|
Package vcard surgically rewrites selected property values inside a vCard (RFC 6350) body, preserving every other byte — folding, ordering, grouping, blank lines, and unknown properties — exactly as received. |