NAME

Mail::DKIM2::MessageInstance - Compute, verify and undo Message-Instance headers

SYNOPSIS

use Mail::DKIM2::MessageInstance;

# First hop: record the message as it is.
my $mi = Mail::DKIM2::MessageInstance->calculate($msg);
print "Message-Instance: " . $mi->as_string . "\n";

# A later hop that changed the message: record the new state and a
# Recipe for getting back to the state it received.
my $mi = Mail::DKIM2::MessageInstance->calculate($modified, $received);

# Does the top instance describe this message?
my $m = Mail::DKIM2::MessageInstance->verify($msg);
my ($m, $why) = Mail::DKIM2::MessageInstance->verify($msg);

# Does the whole chain undo cleanly, each instance matching?
my ($ok, $why) = Mail::DKIM2::MessageInstance->chain_verifies($msg);

# Apply the top Recipe: the message as the previous hop sent it.
my $previous = Mail::DKIM2::MessageInstance->undo($msg);

DESCRIPTION

A Message-Instance header (-07 sections 4 to 9) records the message at one point in its journey: a hash of its header fields and a hash of its body, and, from the second instance on, a Recipe for turning this instance back into the previous one. The wire format is

m=N; h=<alg>:<header-hash>:<body-hash>[,<alg>:...]; r=<base64 JSON>;

where m= numbers the instance from 1, h= carries one hash set per algorithm the signer chose (section 7.3: sha256, sha512, or both; this module emits sha256 unless told otherwise and verifies every set it implements), and r= is the Recipe: "b" for the body and "h" for header fields, each a list of steps (section 5): {"c":[start,end]} copies lines or field instances of this version, {"d":[...]} gives UTF-8 literals (ASCII included) as JSON text, and {"u":[...]} gives literals whose raw octets are not UTF-8 (a Latin-1 or ISO-2022-JP line, say) as canonical base64. Copy ranges must ascend: each starts after the one before it ends. This module emits every JSON key in lower case and reads keys in any case; a Recipe that breaks the section 5 schema -- a step with other than exactly one of c, d and u, a "u" string that is not canonical base64, a literal holding a CR or LF, two keys that differ only in case, an empty "h" -- is "contains invalid JSON".

Messages are accepted as Email::MIME objects or as strings, which are parsed. verify, undo and chain_verifies look at the highest numbered Message-Instance the message carries.

This module implements draft-ietf-dkim-dkim2-spec-07; see "STATUS" in Mail::DKIM2 for what that means for the wire format and the API, and "CONVENTIONS" in Mail::DKIM2 for the option, input and error conventions every module here follows.

OPTIONS

The class methods below take these options after their positional arguments:

IgnorePrefixes

An arrayref of header-field-name prefixes to leave out of the header hash and header Recipes; see "Operator-local header fields" in Mail::DKIM2.

Algs

calculate only: an arrayref of hash algorithm names for h=, from sha256 and sha512. Default ['sha256'].

BodyRecipe

calculate with a previous message only: the caller supplies the body Recipe and no body diff runs (the body of $previous is ignored, and may be empty). One of 'none' (no b key), 'null' ("b": null), or an ARRAY ref in the internal form: [from,to] arrays for copy ranges (1-based body lines of $msg, ascending) and plain strings for literal lines, each one line without its line break. An empty array gives "b": []. Croaks on a malformed value: undef, an undefined step, a bad range, a literal containing CR or LF, or BodyRecipe with no $previous. 'none' declares the body unchanged; with BodyHash as well, croaks unless that hash equals the body hash of the top Message-Instance of $previous (for every algorithm both carry, and at least one).

BodyHash

calculate only, with no $previous or with BodyRecipe: the body hash, already computed with body_digest_raw (below) over the body of $msg. A base64 string is the sha256 hash; a hashref maps each algorithm in Algs to its hash. The body of $msg is then neither hashed nor read, so $msg may be the header block alone (the fields and the blank line after them): a list manager sending many copies of a large body hashes each body once and never has the module parse it. A string $msg that does not end in a line break gets a CRLF appended, so a header block whose last field lacks one is still complete. Croaks if an algorithm is missing or unsupported, if a value is not the base64 of a digest of that algorithm's length (32 octets for sha256, 64 for sha512), or if the body is needed (a previous message without BodyRecipe, which runs the body diff).

UseEpilogue, EpilogueThreshold, MaxRecipeLiterals

calculate with a previous message only; see below. UseEpilogue (true) and EpilogueThreshold are disabled in 0.23: they croak. The instance would be computed over the message rewritten with the epilogue while a caller that holds the message as a string signs the original bytes, and that mail fails verification.

CLASS METHODS

calculate($msg, [$previous], %options)

Returns a new instance describing $msg. With no $previous, $msg must carry no Message-Instance and the result is m=1. With $previous, both messages must carry the same existing instances; the result is the next m= (m=1 when neither carries any: a first instance with Recipes, -07 section 12.1), with hashes of $msg and Recipes that rebuild $previous from it. Dies if the message cannot be processed: it already has 32 instances, its instances do not form a chain, or $previous is not an earlier form of the same message.

The body Recipe is a line diff by default: a Myers diff with the fewest literal lines, bounded by MaxRecipeLiterals => N literal lines (default 1000) and a fixed amount of work. A body it cannot rebuild within those bounds gets the null body Recipe (the previous body is unrecoverable). (Disabled in 0.23, see above:) With UseEpilogue => 1, the previous body is instead appended after the final MIME boundary (the message is wrapped in a multipart/mixed container if it is not already multipart) and the Recipe copies it from there; with EpilogueThreshold => N, that happens only when the diff would need more than N literal lines (or more work than the fixed bound), and an unchanged body gets no Recipe. EpilogueThreshold and MaxRecipeLiterals are mutually exclusive; both must be non-negative integers. Both epilogue forms modify $msg in place, and the hashes cover the modified message.

calculate_first($msg, $previous, %options)

Returns an m=1 instance for a message none of whose instances is signed: the hashes of $msg less its Message-Instance fields, and Recipes that rebuild $previous as it was before its first instance (-07 section 12.1 lets the first instance record the changes made as the message enters DKIM2). $previous may carry instances, a stamped m=1 and local ones over it; their whole chain must verify (chain_verifies), and every one is undone, m=1 included. Past a null body Recipe the earlier body is gone and the result gets the null body Recipe. $msg's Message-Instance fields are removed from it in place (pass a string to leave yours alone); the caller removes them from the message it sends. Takes calculate's options except BodyHash and BodyRecipe. Dies if the chain of $previous does not verify, with the reason chain_verifies gives.

verify($msg, %options)

Checks the top instance against the message. Returns its m= on success. On failure, including an instance that does not parse, returns 0 in scalar context and (0, $reason) in list context, the reason in the -07 words with its state, e.g. "FAIL: Message-Instance m=2 body hash sha256 mismatch" or "PERMERROR: Message-Instance m=2 contains invalid JSON". Never dies.

With HeadersOnly => 1 only the header hashes are checked and the body hash is skipped; this is how instances below a null body Recipe are checked, since the body they hashed is gone.

With BodyHashes => { $alg => $base64, ... } the body hashes are those given, computed elsewhere (Mail::DKIM2::BodyPlan), not those of $msg's body.

undo($msg, %options)

Applies the top instance's Recipes and removes that instance, returning the Email::MIME of the previous form of the message; undef if there is no instance. Dies if the Recipe is malformed (a copy range outside the message, overlapping another or out of order, or anything the section 5 schema forbids) or the instances do not form a chain.

With HeadersOnly => 1 only the header Recipes are applied and the body is left as it is.

chain_verifies($msg, %options)

Runs verify and undo down the whole chain to m=1. Past an instance with a null body Recipe (previous body unrecoverable) it carries on header-only (HeadersOnly), so every lower instance's header hashes are still checked. Returns (1, undef) only when the whole header history checks out, or (0, $reason) at the first instance that does not match or does not undo, the reason as verify gives it. Every Recipe that does not undo is "PERMERROR: Message-Instance m=N contains invalid JSON" (the -07 contract); a third value then says why ("body Recipe copies lines 2-2 twice"). Never dies. A forwarder runs this before signing so it does not put its name to a chain its recipients will reject.

parse($header_value)

Parses a Message-Instance header value into an object. Dies with a PERMERROR: string on a missing m= or h=, a hash set that is not alg:hash:hash, an algorithm named twice, a tag repeated in any case, or a Recipe that is not valid JSON or breaks the section 5 schema ("PERMERROR: Message-Instance m=N contains invalid JSON").

recipe_order_error()

The message-independent part of the Recipe rules for a parsed instance: every copy range well formed, ascending, and not overlapping another. Returns the reason as a string, or undef when the Recipes keep the rules. A verifier reports a failure as the instance's "contains invalid JSON", with the field's other errors; whether a range lies within the message is for applying the Recipe ("undo") to find.

hash_algs()

A hashref of the hash algorithms this module implements, name to function.

parse_hash_sets($h_value)

Splits an h= value into an arrayref of [alg, header_hash, body_hash], lowercasing the names and stripping folding whitespace from around the names and inside the hashes. Returns undef if any hash-set is malformed.

INSTANCE METHODS

as_string()

The header value in wire format, unfolded. Fold it with "fold_header" in Mail::DKIM2::Common before inserting it.

header_hash()

The base64 sha256 header hash, or undef if the instance carries no sha256 set.

body_hash()

The base64 sha256 body hash, or undef if the instance carries no sha256 set.

body_digest_raw($body, [$alg])

Function, not a method. The body hash of a raw body string with LF or CRLF line ends, equal to what the instance records for a message with that body. $alg defaults to sha256.

unrecoverable()

True if the body Recipe is null: the body changed and the previous state cannot be recreated (section 4.2), so the chain cannot be undone past this instance.

set_null_body_recipe()

Marks the body Recipe null.

get_tag($name), set_tag($name, $value)

The parsed fields: m, hashes (a hashref of algorithm to [header_hash, body_hash]), rb and rh (the body and header Recipes in internal form), and h1/b1, the sha256 pair.

FUNCTIONS

h_digest($email_mime, [$alg], [\@prefixes])

The base64 header hash of a message: every field not excluded by "should_skip" in Mail::DKIM2::Common, canonicalized, sorted by name, repeated fields in bottom-up order.

b_digest($email_mime, [$alg])

The base64 body hash, over the body with trailing empty lines removed and one CRLF added.

AUTHOR

Bron Gondwana <brong@fastmailteam.com>

COPYRIGHT AND LICENSE

Copyright (c) 2025-2026 Fastmail Pty Ltd. This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.