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

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

calculate with 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.