Revision history for Mail::DKIM2

0.14    2026-10-07
        - The Verifier and MessageInstance->chain_verifies walk the header
          history past an instance whose body Recipe is null: the body is
          unrecoverable there, but every lower instance's header hashes
          are still checked down to m=1. (t/mi-null-header-history.t)
        - Validate (and bin/validate.pl): a null body Recipe no longer fails
          the chain; the header history below it is checked, and the lower
          levels report body_hash 'not-checked'. (t/validate-null-body.t)
        - dkim2-milter --allow-null-body-recipe (default off): a message
          whose top Message-Instance has a null body Recipe is no longer
          signed silently; it is refused (X-DKIM2-Info
          not-signed=null-body-recipe) unless the option is set, and signed
          with X-DKIM2-Info null-body-recipe when it is.
        - bin/dkim2sign no longer signs blindly over an existing chain: like
          dkim2-milter it verifies the upstream DKIM2-Signatures (an unsigned
          top Message-Instance is allowed) and that the Message-Instance chain
          undoes cleanly, and refuses (exit 1, nothing on stdout) otherwise.
          A top null body Recipe is refused unless --allow-null-body-recipe.
          New --dns-json (default $DKIM2_DNS_JSON) and --ignore-timestamps.
          The decision is the new Mail::DKIM2::Gate, shared with the milter;
          the Signer library itself still signs ungated. (t/sign-cli.t)
        - nd= bridge: when the top DKIM2-Signature carries nd=, the Gate
          (and so dkim2sign and dkim2-milter) signs only if nd= equals the
          signing d= (case-insensitive) and otherwise refuses with "top
          signature nd=X names another domain". Verifier gains
          next_domain_ok($d) / NextDomainOK; Gate->check takes
          SigningDomain. Every non-nd case behaves as before.
          (t/sign-cli.t, t/milter-script.t)
        - A Message-Instance whose r= has a "b" that is neither null nor an
          array (e.g. 5, "x", {}) is now a PERMERROR instead of being read
          as a null body Recipe; likewise an "h" that is not an object.
          (t/mi-null-recipe.t)
        - A body Recipe's structure (integer bounds, ascending, no overlap)
          is validated even when a null body Recipe above it means it is
          never applied; a malformed one below a null now fails the chain.
          (t/mi-null-header-history.t)
        - DKIM2_DATE is 2026-10-07: what the milter signs and how the
          Verifier judges a null body Recipe changed.

0.13    2026-10-04
        - The header Recipe builder treated "no instances" and "one empty
          instance" of a field as the same, so removing an empty header
          (a bare "Bcc:", which Sympa's egress now removes) went unrecorded
          and the Message-Instance did not verify. (t/recipe-empty-header.t)

0.12    2026-10-04
        - undo() rebuilt a base64 or quoted-printable body encoded twice:
          Recipes work on wire lines, so the rebuilt body is already in its
          transfer encoding, and Email::MIME->body_set encoded it again.
          Every such message a list re-encoded (footer appended, body
          re-wrapped) failed "m=1 does not match content" and the milter
          refused to sign it -- 17 of 88 charset-corpus samples through
          Mailman, while the Python undo rebuilt them byte for byte. The
          body is now set as raw octets. (t/undo-encoded-body.t)
        - DKIM2_DATE is 2026-10-04: the Message-Instance headers this
          library emits changed shape in 0.11 ("b" literals, integer copy
          ranges), and the X-DKIM2-Info date stamp follows emitted-header
          changes.

0.11    2026-10-04
        Fixes found by replaying public-archive mail in assorted charsets
        (ISO-2022-JP, GB2312/GB18030, Big5, EUC-KR, Latin-1, raw 8-bit
        headers) through the signers, verifiers and list managers
        (interop util/charset-corpus.sh).
        - Recipe literals carrying any octet >= 0x80 are emitted as a new
          {"b": [base64, ...]} step instead of {"d": [...]}. A literal is
          the raw octets of a header value or body line; JSON text is
          UTF-8, so the old encoder wrote ISO-2022-JP, GB18030, Big5 and
          Latin-1 octets into the JSON as they were, which no strict JSON
          parser reads back. The decoder rejects a "b" item that is not
          RFC 4648 base64 or decodes to something containing CR or LF.
          Agreed extension to spec-06 §5, proposed to the WG.
          (t/recipe-base64.t)
        - Recipe copy ranges must ascend (spec-06 §5.1): each "c" step
          starts after the one before it ends. undo() used to sort the
          ranges and reject only overlap; it now rejects an out-of-order
          range too, for body and header Recipes alike. The header Recipe
          builder in calculate() no longer emits one: a header instance a
          hop moved above one it left alone is recorded literally.
          (t/recipe-order.t, t/undo-bounds.t)
        - Two more Recipe schema rules the other verifiers already hold, so
          every implementation gives the same verdict: a "c" bound must be
          a JSON integer (a string such as "2" is malformed; told apart by
          the scalar's flags, not its text), an empty "d" or "b" array is
          malformed (minItems 1), and so is a "d" string containing CR or
          LF (§5.1/§5.2 MUST NOT). (t/recipe-order.t, t/recipe-base64.t)
        - Recipe copy ranges are emitted as JSON integers. An index used as
          a hash key while de-duplicating header copies was stringified in
          place, so every Sympa Message-Instance carried {"c":["2","2"]},
          which the spec-06 schema forbids and strict verifiers reject.
          (t/recipe-integers.t)
        - A broken Content-Type (`text/plain; Windows-1252`) no longer makes
          every verification print Email::MIME's "Illegal parameter" warning:
          the library parses with parameter checking relaxed, for the parse
          only, since DKIM2 never reads a MIME parameter.
          (t/malformed-content-type.t)
        - The test suite is self-contained: the test keys and dns.json ship
          under t/data/ (t/data-in-sync.t keeps them equal to the interop
          repository's shared copies), bin/validate.pl takes --dns-json and
          defaults to that copy, and the tests that cross-check the other
          implementations or the deployment templates skip outside the
          repository. 0.10's tests could not run from the tarball.

0.10    2026-10-02
        API cleanup ahead of a CPAN release. Incompatible changes are marked *.
        - New top-level Mail::DKIM2 module documenting the conventions every
          module follows; every module now carries the distribution $VERSION.
        - Constructor options are CamelCase and validated: an unknown option
          croaks. Verifier options (SkipTimestampCheck, AllowUnsignedMI,
          MidProcess, HeadersOnly, PubkeyCallback, Resolver, IgnorePrefixes)
          can be given to new() and are no longer silently discarded.
        - load($input): one-shot PRINT+CLOSE taking a string, scalar ref,
          filehandle or Email::MIME, normalising LF to CRLF. TIEHANDLE lets a
          Signer or Verifier be tied to a filehandle.
        - Verifier->signatures and ->top_signature for Authentication-Results
          writers.
        * Ignore prefixes are per instance: Common::ignore_header_prefixes is
          gone; pass IgnorePrefixes to Verifier->new and to
          MessageInstance->calculate/verify/chain_verifies instead.
          should_skip takes the prefixes as a second argument.
        * Key fetching moves to the Verifier: Signature->fetch_public_key is
          replaced by Verifier->fetch_public_key($signature, $idx), driven by
          the Resolver option. Only NXDOMAIN/NOERROR/NODATA are permanent; any
          other resolver error is temperror. The pubkey callback receives the
          verifier as a third argument.
        * Signer no longer dies from inside PRINT on a chain it cannot
          extend: result() is 'fail' and details() says why. result() is
          undef (not '?') before CLOSE. details() and result_detail() added.
        * Signature: mail_from(), rcpt_to() and flags() are get/set like the
          other tag accessors; set_rcpt_to is removed.
        * Mail::DKIM2::DSN methods take CamelCase named arguments (Message,
          Signer, To, ReportingMTA, Status, Reason, PubkeyCallback,
          ForwarderDomain, SkipAuthentication, SkipTimestampCheck) instead of
          a hashref. Validate::report takes PubkeyCallback, DnsPath,
          SkipTimestampCheck.
        * Command-line tools: dkim2sign (was dkim2sign.pl) and the new
          dkim2verify are installed; verify-sig.pl, calculate-dkim2.pl and
          the *-mailversion tools are removed.
        - POD rewritten for spec-06 (the previous text described
          draft-clayton-08 tags); Net::DNS declared as a prerequisite.
        - dkim2-milter and dkim2-split-lmtp are installed programs (were
          bin/*.pl, run from the checkout). X-DKIM2-Info sw= says
          dkim2-milter.

0.01    2026-03-08
        - Initial release
        - Implements draft-clayton-dkim2-spec-08
        - Signer: streaming DKIM2-Signature generation with SMTP param recording
        - Verifier: full chain verification (all signatures, not just outermost)
        - MessageInstance: calculate, verify, and undo with header/body diff recipes
        - Signature: tag-value parser with base64-encoded JSON tags
        - HeaderParser: thin streaming base class replacing Mail::DKIM::Common
        - Common: shared canonicalization, hashing, domain matching utilities
