Loading the parity record
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Parity / Nodemailer
Every number on this page was produced by running both implementations over the same cases: the real nodemailer package pinned at 6.9.16 answers first, and its answer is the expectation the Go port is held to. Nothing is a hand-written expectation, so a new upstream release re-scores the port on its own. See Nodemailer for the port's own documentation. Source: github.com/malcolmston/nodemailer.
Not a generic diagram: every node below names an artefact of this harness — the pinned package it installed, the runner files it started, the case files it streamed, and the counts it wrote out.
| Case group | Cases | Match | Mismatch | Group parity |
|---|---|---|---|---|
| attachments | 17 | 17 | 0 | 100.0% |
| addresses | 15 | 15 | 0 | 100.0% |
| headers | 14 | 13 | 0 | 92.9% |
| encoding | 13 | 13 | 0 | 100.0% |
| bodies | 12 | 12 | 0 | 100.0% |
| subject | 8 | 0 | 8 | 0.0% |
| inline | 7 | 7 | 0 | 100.0% |
Every exported symbol of the upstream package, and what the port offers for it. The upstream list is derived mechanically, never from a README: cd parity/nodemailer/node
exported module surface and the transporter instance surface
node -e "const nm=require('nodemailer');
console.log(Object.keys(nm).sort());
const t=nm.createTransport({streamTransport:true,buffer:true});
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(t)).sort());
console.log(Object.getOwnPropertyNames(require('nodemailer/lib/mail-composer').prototype).sort())"
every mail-options key the composer and the mail-message reader actually read
grep -ohE "this\.mail\.[A-Za-z_]+|this\.data\.[A-Za-z_]+|mail\.data\.[A-Za-z_]+" \
…. A symbol with no case is untested, never a match.
| Upstream symbol | Go symbol | Status | Cases | Note |
|---|---|---|---|---|
| nodemailer.createTransport | nodemailer.NewTransporter | untested | — | the harness composes, it does not send |
| nodemailer.createTestAccount | — | missing | — | provisions an Ethereal account over the network |
| nodemailer.getTestMessageUrl | — | missing | — | Ethereal-specific |
| Mail#sendMail | Transporter.SendMail | untested | — | the node runner calls it, but only its composed message is compared; the Go side is compared through Message.Build |
| Mail#verify | SMTPTransport.Verify / Pool.Verify | untested | — | needs a server |
| Mail#close | Pool.Close | untested | — | |
| Mail#use (plugins) | — | missing | — | no plugin pipeline in the port |
| Mail#get / Mail#set | — | missing | — | transport option accessors |
| Mail#getVersionString | — | missing | — | feeds the X-Mailer banner |
| Mail#isIdle | — | missing | — | pool introspection (Pool has no equivalent) |
| Mail#dkim | Message.WithDKIM, DKIM.Sign | untested | — | DKIM signing is not exercised by this harness |
| Mail#_convertDataImages (attachDataUrls) | — | missing | — | |
| Mail#setupProxy | — | missing | — | |
| MailComposer#compile | Message.Build | differs | all 86 | the compared symbol pair; see the divergence list |
| MailComposer#getAlternatives | Message.Alternatives handling | differs | body-watch-html, body-amp, body-watch-and-amp, body-custom-alternative, body-two-alternatives, body-ical-event | alternative ordering and charset differ |
| MailComposer#getAttachments | Message.Attachments handling | differs | att-*, cid-* | related/attached split differs |
| MailComposer#_createMixed | Message.Build (mixed branch) | differs | att-only-no-body, att-explicit-disposition-inline, cid-no-html | the port wraps where upstream collapses |
| MailComposer#_createAlternative | Message.Build (alternative branch) | differs | body-text-and-html, body-watch-html, body-amp | ordering |
| MailComposer#_createRelated | Message.Build (related branch) | differs | cid-single-image, cid-image-with-text-alternative, cid-plus-regular-attachment, cid-two-images, cid-from-path, cid-non-image | nesting and type= parameter |
| MailComposer#_createContentNode | Message.Build (single-part branch) | match | body-text-only, body-html-only | graded on tree shape |
| MailComposer#_processDataUrl | — | missing | — | data: URI attachments |
| from | Message.SetFrom | match | addr-from-bare, addr-from-display-name, addr-non-ascii-display-name, addr-quoted-display-name | port quotes display names, upstream does not; equivalent after unquoting |
| sender | — | missing | addr-sender | no Sender field; the header is simply absent |
| to | Message.AddTo, Message.AddToGroup | differs | addr-to-multiple, addr-group-to, addr-group-empty, addr-idn-domain, addr-invalid-no-at, addr-invalid-empty-domain | IDN domains not punycoded; invalid addresses rejected where upstream accepts |
| cc | Message.AddCc, Message.AddCcGroup | match | addr-to-cc-bcc, addr-group-cc | |
| bcc | Message.AddBcc | differs | addr-to-cc-bcc | the port omits the Bcc header from the message entirely |
| replyTo | Message.AddReplyTo | match | addr-reply-to, addr-reply-to-multiple | |
| subject | Message.SetSubject | differs | subject-ascii, subject-empty, subject-non-ascii, subject-cjk, subject-emoji, subject-long-ascii, subject-long-non-ascii, subject-with-newline | RFC 2047 encoding and folding agree; but the port always emits Subject: (empty when unset) and does **not** strip CRLF, so a subject can inject headers |
| messageId | Message.SetMessageID | match | all | |
| date | Message.SetDate | match | all | identical RFC 5322 rendering |
| inReplyTo | Message.SetInReplyTo | match | hdr-in-reply-to, hdr-references-multiple | |
| references | Message.AddReferences | match | hdr-references-single, hdr-references-multiple | space-separated on both sides |
| text | Message.SetText | match | body-text-only, body-text-multiline, body-text-and-html | decoded payload identical |
| html | Message.SetHTML | match | body-html-only, body-html-non-ascii, body-text-and-html | |
| watchHtml | Message.SetWatchHTML | differs | body-watch-html, body-watch-and-amp | correct text/watch-html part, wrong position in multipart/alternative |
| amp | Message.SetAMP | differs | body-amp, body-watch-and-amp | correct text/x-amp-html part, wrong position |
| icalEvent | Message.ICalEvent | differs | body-ical-event | entirely different tree (see divergence 9) |
| alternatives | Message.AddAlternative | differs | body-custom-alternative, body-two-alternatives | port appends charset=utf-8 to every text/* alternative; non-text alternatives handled differently |
| attachments | Message.Attach* / Embed* | differs | all att-*, cid-*, enc-*attachment* | see Table C |
| headers | Message.AddHeader | match | hdr-custom-single, hdr-custom-multiple, hdr-custom-non-ascii, hdr-custom-long-value | duplicates, folding and RFC 2047 all agree |
| list | Message.SetListUnsubscribe, AddListHeader | differs | hdr-list-unsubscribe, hdr-list-id-and-help | one comma-joined header vs one header per URI; upstream also coerces a bare List-ID to http://… |
| priority | Message.SetPriority | match | hdr-priority-high, hdr-priority-low, hdr-priority-normal | X-Priority, X-MSMail-Priority, Importance all agree |
| textEncoding | — | missing | enc-forced-quoted-printable, enc-forced-base64, enc-7bit-short-ascii, enc-8bit-latin1, enc-mostly-non-ascii, enc-long-lines, enc-trailing-space, enc-equals-sign | no option, and no per-body heuristic: text is always quoted-printable |
| encoding | — | missing | — | message-wide default transfer encoding |
| envelope | Info.Envelope (derived only) | missing | — | no per-message envelope override |
| raw | — | missing | — | pass a pre-built message through |
| attachDataUrls | — | missing | — | |
| disableFileAccess | — | missing | — | the port has no file/URL access switch (and AttachURL fetches unconditionally) |
| disableUrlAccess | — | missing | — | |
| dkim | Message.WithDKIM | untested | — | present in the port, not exercised here |
| newline | — | missing | — | the port always emits CRLF |
| normalizeHeaderKey | — | missing | — | |
| xMailer | — | missing | — | the port never emits X-Mailer; suppressed upstream for comparability |
| baseBoundary | Message.SetBoundary | match | all multipart cases | both accept a pinned base boundary |
| boundaryPrefix | — | missing | — | |
| content (string / Buffer / Stream) | Attachment.Content, AttachBytes, AttachReader | match | att-string-explicit-type, att-buffer, att-from-stream | payloads and base64 wrapping identical |
| path | AttachFile, EmbedFile | match | att-from-path, att-from-path-binary, cid-from-path, att-missing-path | both fail on a missing file |
| filename | Attachment.Filename | differs | att-non-ascii-filename, att-non-ascii-filename-cjk, att-no-filename, att-long-ascii-filename, att-filename-with-quote | malformed MIME for non-ASCII names** (divergence 1); no synthesised filename when absent |
| contentType | Attachment.ContentType | differs | att-string-inferred-type, att-inferred-unknown-extension, att-two-attachments, cid-plus-regular-attachment | inference agrees, but the port adds charset=utf-8 to inferred text/* attachment parts |
| contentDisposition | Attachment.Inline | differs | att-explicit-disposition-inline, cid-non-image | inline also flips the container to multipart/related; non-image cid parts are marked inline where upstream keeps attachment |
| cid | Attachment.ContentID, Embed* | differs | cid-single-image, cid-image-with-text-alternative, cid-two-images, cid-plus-regular-attachment, cid-from-path, cid-no-html, cid-non-image | Content-ID itself matches; container type/nesting differs |
| encoding | (decoded by the runner) | match | att-base64-encoding-option | |
| contentTransferEncoding | — | missing | enc-attachment-explicit-cte-binary, enc-attachment-explicit-cte-7bit | the port always base64s attachments |
| headers | — | missing | — | per-attachment extra headers |
| href | AttachURL (unconditional fetch) | untested | — | deliberately not exercised: no network access in this harness |
| httpHeaders | — | missing | — | |
| content | Alternative.Content | match | body-custom-alternative, body-two-alternatives | |
| contentType | Alternative.ContentType | differs | body-custom-alternative, body-two-alternatives | port appends charset=utf-8 to text/* |
| contentTransferEncoding | — | missing | body-two-alternatives | |
| filename | — | missing | — | |
| path | — | missing | — | |
| href | — | missing | — | |
| — | Message.SetListUnsubscribePost | extra | hdr-list-unsubscribe-post | declared deviation: emits RFC 8058 List-Unsubscribe-Post, which 6.9.16 cannot. The library has no API-DEVIATIONS.md yet, so per HARNESS.md this deviation still needs to be recorded there. |
| — | Message.GenerateTextFromHTML, HTMLToText | untested | — | no upstream equivalent (html-to-text is a separate package upstream) |
| — | Message.AttachURL | untested | — | would require network access |
| — | Message.EmbedReader, Message.AttachReader | extra | att-from-stream | Go-shaped stream helpers |
| — | ParseMIME, ParsedMessage, ParsedAddr, ParsedAttachment, ParseAddresses, ParseAddressesFlatten, ParsedMessage.Get | untested | — | inbound parsing; upstream ships no parser (mailparser is separate) |
| — | ParseAddress, ParseAddressList, ParseAddressGroup, Address.*, AddressGroup.String, FormatAddressList, NormalizeAddress | untested | — | exercised indirectly through the address cases, not as standalone symbols |
| — | EncodeWord, EncodeMimeWord, DecodeHeaderWord, QPEncode, QPWrap, Base64Encode, Base64Wrap, IsPlainText, HasLongerLines, GenerateMessageID | untested | — | encoder internals exposed as public helpers; upstream keeps mime-funcs private |
| — | SMTPTransport, Pool, SendmailTransport, MemoryTransport, StreamTransport, JSONTransport, Transport, Transporter, Info, Envelope, CapturedMessage | untested | — | delivery layer; out of scope (no SMTP in this harness) |
| — | DKIM, Canonicalization, DefaultDKIMHeaders, DKIM.Sign, DKIM.DNSRecord, DKIM.DNSRecordName, ParseRSAPrivateKey | untested | — | |
| — | DSNOptions, DSNNotify, DSNReturn | untested | — | upstream exposes DSN through SMTP transport options |
| — | Service, WellKnownService, WellKnownServiceNames, NewServiceSMTP, XOAuth2Auth, XOAuth2Token | untested | — | |
| — | ErrInvalidAddress, ErrDKIMConfig, ErrPoolClosed | untested | — |
Every case the harness streamed to both runners, with the exact upstream symbol and Go symbol it exercised. A deliberate, documented difference is a deviation and is counted apart from a mismatch.
| Case | Group | Upstream symbol | Go symbol | Status | Note |
|---|---|---|---|---|---|
| addr-from-bare | addresses | mail.from | nodemailer.Message.SetFrom | match | |
| addr-from-display-name | addresses | mail.from | nodemailer.Message.SetFrom | match | |
| addr-to-multiple | addresses | mail.to | nodemailer.Message.AddTo | match | |
| addr-to-cc-bcc | addresses | mail.to / mail.cc / mail.bcc | nodemailer.Message.AddTo / AddCc / AddBcc | match | Bcc is emitted into the message by both implementations |
| addr-reply-to | addresses | mail.replyTo | nodemailer.Message.AddReplyTo | match | |
| addr-reply-to-multiple | addresses | mail.replyTo | nodemailer.Message.AddReplyTo | match | |
| addr-non-ascii-display-name | addresses | mail.to (RFC 2047 display name) | nodemailer.Address.String | match | |
| addr-quoted-display-name | addresses | mail.to (quoted display name) | nodemailer.Address.String | match | |
| addr-group-to | addresses | mail.to (RFC 5322 group) | nodemailer.Message.AddToGroup | match | |
| addr-group-cc | addresses | mail.cc (RFC 5322 group) | nodemailer.Message.AddCcGroup | match | |
| addr-group-empty | addresses | mail.to (empty group) | nodemailer.Message.AddToGroup | match | |
| addr-sender | addresses | mail.sender | — | match | the Go port has no Sender field |
| addr-invalid-no-at | addresses | mail.to | nodemailer.Message.AddTo | match | error case: does either side reject a bare token? |
| addr-invalid-empty-domain | addresses | mail.to | nodemailer.Message.AddTo | match | error case |
| addr-idn-domain | addresses | mail.to (internationalised domain) | nodemailer.Message.AddTo | match | upstream punycodes the domain |
| att-string-explicit-type | attachments | mail.attachments[].content (string) | nodemailer.Message.AttachBytes | match | |
| att-string-inferred-type | attachments | mail.attachments[] (contentType inferred from filename) | nodemailer.Message.AttachBytes | match | |
| att-inferred-unknown-extension | attachments | mail.attachments[] (unknown extension) | nodemailer.Message.AttachBytes | match | expects application/octet-stream on both sides |
| att-no-filename | attachments | mail.attachments[] (no filename) | nodemailer.Message.AttachBytes | match | |
| att-buffer | attachments | mail.attachments[].content (Buffer) | nodemailer.Message.AttachBytes | match | content is base64 in the case file and fed as raw bytes to both runners |
| att-base64-encoding-option | attachments | mail.attachments[].encoding = base64 | nodemailer.Message.AttachBytes | match | |
| att-from-path | attachments | mail.attachments[].path | nodemailer.Message.AttachFile | match | filename and content type both derived from the path |
| att-from-path-binary | attachments | mail.attachments[].path | nodemailer.Message.AttachFile | match | |
| att-from-stream | attachments | mail.attachments[].content (Readable stream) | nodemailer.Message.AttachReader | match | |
| att-two-attachments | attachments | mail.attachments[] | nodemailer.Message.Attach | match | |
| att-non-ascii-filename | attachments | mail.attachments[].filename (RFC 2231) | nodemailer.Message.AttachBytes | match | KNOWN DEFECT in the port: it emits a bare RFC 2047 encoded word as an unquoted Content-Type/Content-Disposition parameter value. '?' and '=' are tspecials, so the parameter is not a valid MIME token and the part is malformed. Upstream uses RFC 2231 filename*0*= continuations. |
| att-non-ascii-filename-cjk | attachments | mail.attachments[].filename (RFC 2231) | nodemailer.Message.AttachBytes | match | same defect as att-non-ascii-filename, with a filename that is entirely non-ASCII |
| att-long-ascii-filename | attachments | mail.attachments[].filename (long, folded) | nodemailer.Message.AttachBytes | match | |
| att-filename-with-quote | attachments | mail.attachments[].filename (quoting) | nodemailer.Message.AttachBytes | match | |
| att-explicit-disposition-inline | attachments | mail.attachments[].contentDisposition | nodemailer.Attachment.Inline | match | |
| att-only-no-body | attachments | mail.attachments[] with no body | nodemailer.Message.AttachBytes | match | upstream collapses a single attachment with no alternatives into a top-level part |
| att-missing-path | attachments | mail.attachments[].path (missing file) | nodemailer.Message.AttachFile | match | error case: both sides should fail |
| body-text-only | bodies | mail.text | nodemailer.Message.SetText | match | |
| body-html-only | bodies | mail.html | nodemailer.Message.SetHTML | match | |
| body-text-and-html | bodies | mail.text + mail.html | nodemailer.Message.SetText + SetHTML | match | expects multipart/alternative |
| body-empty | bodies | (no body) | nodemailer.Message.Build | match | |
| body-watch-html | bodies | mail.watchHtml | nodemailer.Message.SetWatchHTML | match | text/watch-html alternative; upstream orders text, watch-html, amp, html |
| body-amp | bodies | mail.amp | nodemailer.Message.SetAMP | match | text/x-amp-html alternative |
| body-watch-and-amp | bodies | mail.watchHtml + mail.amp | nodemailer.Message.SetWatchHTML + SetAMP | match | |
| body-custom-alternative | bodies | mail.alternatives | nodemailer.Message.AddAlternative | match | |
| body-two-alternatives | bodies | mail.alternatives | nodemailer.Message.AddAlternative | match | |
| body-ical-event | bodies | mail.icalEvent | nodemailer.Message.ICalEvent | match | |
| body-text-multiline | bodies | mail.text | nodemailer.Message.SetText | match | |
| body-html-non-ascii | bodies | mail.html | nodemailer.Message.SetHTML | match | |
| enc-7bit-short-ascii | encoding | MimeNode._getContentTransferEncoding (7bit) | nodemailer mime encoder | match | upstream picks 7bit for clean short ASCII |
| enc-8bit-latin1 | encoding | MimeNode._getContentTransferEncoding (quoted-printable) | nodemailer.QPEncode | match | a handful of 8-bit bytes: upstream picks quoted-printable |
| enc-mostly-non-ascii | encoding | MimeNode._getContentTransferEncoding (base64) | nodemailer mime encoder | match | when non-ASCII dominates, upstream switches to base64 |
| enc-forced-quoted-printable | encoding | mail.textEncoding = quoted-printable | nodemailer mime encoder (always quoted-printable) | match | with textEncoding pinned, both sides should agree on quoted-printable |
| enc-forced-base64 | encoding | mail.textEncoding = base64 | — | match | the Go port has no textEncoding option |
| enc-long-lines | encoding | MimeNode soft line breaks | nodemailer.QPWrap | match | a single line longer than 76 characters must be soft-wrapped |
| enc-trailing-space | encoding | MimeNode quoted-printable escaping | nodemailer.QPEncode | match | trailing whitespace before a line break must be escaped as =20 / =09 |
| enc-equals-sign | encoding | MimeNode quoted-printable escaping | nodemailer.QPEncode | match | |
| enc-boundary-lookalike-body | encoding | MimeNode body escaping | nodemailer mime encoder | match | body text that looks like the multipart boundary must not break the tree |
| enc-binary-attachment | encoding | mail.attachments[] base64 transfer encoding | nodemailer.Base64Wrap | match | attachments are always base64 on both sides |
| enc-attachment-explicit-cte-binary | encoding | mail.attachments[].contentTransferEncoding | — | match | the Go port has no per-attachment contentTransferEncoding override |
| enc-attachment-explicit-cte-7bit | encoding | mail.attachments[].contentTransferEncoding | — | match | |
| enc-long-base64-wrap | encoding | mail.attachments[] base64 line wrapping | nodemailer.Base64Wrap | match | 256 bytes: base64 must wrap at 76 columns on both sides |
| hdr-custom-single | headers | mail.headers | nodemailer.Message.AddHeader | match | |
| hdr-custom-multiple | headers | mail.headers | nodemailer.Message.AddHeader | match | duplicate header field names must survive |
| hdr-custom-non-ascii | headers | mail.headers (RFC 2047 value) | nodemailer.Message.AddHeader | match | |
| hdr-custom-long-value | headers | mail.headers (folding) | nodemailer.Message.AddHeader | match | |
| hdr-priority-high | headers | mail.priority | nodemailer.Message.SetPriority | match | |
| hdr-priority-low | headers | mail.priority | nodemailer.Message.SetPriority | match | |
| hdr-priority-normal | headers | mail.priority | nodemailer.Message.SetPriority | match | normal priority must emit no priority headers |
| hdr-in-reply-to | headers | mail.inReplyTo | nodemailer.Message.SetInReplyTo | match | |
| hdr-references-single | headers | mail.references | nodemailer.Message.AddReferences | match | |
| hdr-references-multiple | headers | mail.references | nodemailer.Message.AddReferences | match | References is a space-separated msg-id list, not comma separated |
| hdr-list-unsubscribe | headers | mail.list.unsubscribe | nodemailer.Message.SetListUnsubscribe | match | |
| hdr-list-unsubscribe-post | headers | mail.headers List-Unsubscribe-Post (RFC 8058) | nodemailer.Message.SetListUnsubscribePost | deviation | the Go port adds a SetListUnsubscribePost helper that emits List-Unsubscribe-Post: List-Unsubscribe=One-Click; upstream 6.9.16 has no such option, so the header is absent there |
| hdr-list-id-and-help | headers | mail.list.id / mail.list.help | nodemailer.Message.AddListHeader | match | list.headers[] drives the Go side; list.id/list.help drive upstream |
| hdr-mixed-everything | headers | mail.headers + priority + references | nodemailer.Message.AddHeader + SetPriority + AddReferences | match | |
| cid-single-image | inline | mail.attachments[].cid | nodemailer.Message.Embed | match | expects multipart/related with type="text/html" |
| cid-image-with-text-alternative | inline | mail.attachments[].cid + mail.text | nodemailer.Message.Embed | match | expects multipart/alternative nested inside multipart/related |
| cid-two-images | inline | mail.attachments[].cid | nodemailer.Message.Embed | match | |
| cid-plus-regular-attachment | inline | mail.attachments[] (mixed related + attached) | nodemailer.Message.Embed + Attach | match | expects mixed > (related > (alternative)) nesting |
| cid-from-path | inline | mail.attachments[].cid + .path | nodemailer.Message.EmbedFile | match | |
| cid-no-html | inline | mail.attachments[].cid without html | nodemailer.Message.Embed | match | upstream demotes a cid attachment to a regular attachment when there is no html node |
| cid-non-image | inline | mail.attachments[].cid on a non-image | nodemailer.Message.Embed | match | upstream only defaults to Content-Disposition: inline for image/* cid parts |
| subject-ascii | subject | mail.subject | nodemailer.Message.SetSubject | mismatch | |
| subject-empty | subject | mail.subject | nodemailer.Message.SetSubject | mismatch | |
| subject-non-ascii | subject | mail.subject (RFC 2047) | nodemailer.EncodeWord | mismatch | |
| subject-cjk | subject | mail.subject (RFC 2047) | nodemailer.EncodeWord | mismatch | |
| subject-emoji | subject | mail.subject (RFC 2047, astral plane) | nodemailer.EncodeWord | mismatch | encoded words must not split a surrogate pair / multi-byte rune |
| subject-long-ascii | subject | mail.subject (header folding) | nodemailer mime header folding | mismatch | |
| subject-long-non-ascii | subject | mail.subject (folded encoded words) | nodemailer mime header folding | mismatch | |
| subject-with-newline | subject | mail.subject (header injection) | nodemailer.Message.SetSubject | mismatch | header-injection guard: neither side may emit an X-Injected header |
A case can show the port is less safe than the library it ports. Those findings are written into security.json and filed as draft advisories, never as public issues with a working repro.
| Finding | Severity | Affected | Cases | Summary |
|---|---|---|---|---|
| GHSA-pgqx-3gm6-xx62 | medium | <= 0.3.0 | — | CRLF in a message subject injects arbitrary headers (email header injection, CWE-93) |