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 (spec-06 sections 4 to 7) 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 ASCII literals as JSON text, and {"b":[...]} gives literals whose raw octets are not ASCII (a Latin-1 or ISO-2022-JP line, say) as base64. Copy ranges must ascend: each starts after the one before it ends. The "b" step and the ascending rule for body Recipes are an agreed extension to spec-06 that is being proposed to the working group; this module emits "b" for every non-ASCII literal and rejects a Recipe that breaks either rule.
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-06; 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
-
calculateonly: an arrayref of hash algorithm names forh=, fromsha256andsha512. Default['sha256']. - BodyRecipe
-
calculatewith a previous message only: the caller supplies the body Recipe and no body diff runs (the body of$previousis ignored, and may be empty). One of'none'(nobkey),'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, orBodyRecipewith no$previous.'none'declares the body unchanged; withBodyHashas 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
-
calculateonly, with no$previousor withBodyRecipe: the body hash, already computed withbody_digest_raw(below) over the body of$msg. A base64 string is thesha256hash; a hashref maps each algorithm inAlgsto its hash. The body of$msgis then neither hashed nor read, so$msgmay 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$msgthat 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 forsha256, 64 forsha512), or if the body is needed (a previous message withoutBodyRecipe, which runs the body diff). - UseEpilogue, EpilogueThreshold
-
calculatewith a previous message only; see below.
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=, 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. 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 carry more than N literal lines. Both epilogue forms modify $msg in place, and the hashes cover the modified message. Recipe computation uses Algorithm::Diff, loaded on first use.
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. 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.
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 a "b" literal that is not base64 or decodes to a CR or LF) 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. 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=, a hash set that is not alg:hash:hash, or an algorithm named twice.
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.
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.