From 8d8c23c39bba1aa6398039f4cbefb4aa148f049d Mon Sep 17 00:00:00 2001 From: Dan Mahoney Date: Sat, 22 Aug 2026 03:58:17 -0700 Subject: [PATCH 1/2] feat: encode reported-on domain in aggregate report VERP senders For aggregate reports, verp_sender() now embeds the domain the report concerns ahead of the recipient tag, e.g.: postmaster+example.com+dmarc-rua=aggregator.com@reporter.example.com This lets a bounce-processing script recover the domain directly from a returned report's envelope sender, without needing to look up which domain(s) route through a given RUA address -- useful for feeding domain-specific signals (e.g. a StaleMARC checkDomain recheck) off of delivery failures. Forensic report VERP addresses are left unchanged, since --forensic relays a pre-formed AFRF message with no readily-available "domain being reported on" of its own. --- db/README.schema | 6 ++++++ reports/opendmarc-reports.8.in | 32 +++++++++++++++++++++++++++++--- reports/opendmarc-reports.in | 22 ++++++++++++++++------ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/db/README.schema b/db/README.schema index 81656872..55467587 100644 --- a/db/README.schema +++ b/db/README.schema @@ -54,6 +54,12 @@ suppressions A table of RUA/RUF addresses and domains that should be opendmarc-reports sends reports with --verp, bounces return to an encoded envelope sender that identifies the failing address, allowing a bounce handler to INSERT the address here. + For aggregate reports, the envelope sender also encodes the + domain the report concerned, so a bounce handler can act on + that domain too (e.g. prompting a third-party service like + StaleMARC to test its DMARC reporting deliverability) + without needing to look up which domain(s) route through the + failing address. UPGRADING AN EXISTING SCHEMA diff --git a/reports/opendmarc-reports.8.in b/reports/opendmarc-reports.8.in index 3252f911..6e1df9f3 100644 --- a/reports/opendmarc-reports.8.in +++ b/reports/opendmarc-reports.8.in @@ -322,6 +322,24 @@ identify the specific destination that failed, e.g.: postmaster+dmarc-rua=example.com@reporter.example.com .in .sp +For aggregate reports, the domain the report is +.I about +is also encoded ahead of the recipient, so a bounce-processing script can +recover it without having to look up which domain(s) use a given RUA +address, e.g.: +.sp +.in +4n +postmaster+example.com+dmarc-rua=aggregator.com@reporter.example.com +.in +.sp +(here the report concerns +.I example.com +but is delivered to a third-party aggregator's RUA address). Forensic +report VERP addresses are not currently annotated with a domain, since +.I --forensic +mode relays a pre-formed AFRF message without a readily-available +"domain being reported on" of its own. +.sp In forensic mode, enabling VERP causes one SMTP transaction to be issued per recipient rather than grouping all recipients into a single transaction. .TP @@ -371,9 +389,17 @@ INSERT INTO suppressions (address, reason) When .I --verp is enabled, DMARC report bounces return to an encoded envelope sender -that identifies the failing address. A bounce-processing script can -decode that address and insert it into this table automatically, building -a locally-maintained suppression list informed by real delivery failures. +that identifies the failing address (and, for aggregate reports, the +domain the report concerned). A bounce-processing script can decode +that address and insert the failing address into this table +automatically, building a locally-maintained suppression list informed +by real delivery failures. The recovered domain is also useful input +to third-party services such as StaleMARC (see +.IR --stale-check ), +which can be prompted to (re-)test a domain's DMARC reporting +deliverability by requesting its +.I checkDomain +endpoint for that domain. .SH EXTERNAL DESTINATION VERIFICATION Before sending to a cross-domain .RI ( mailto: ) diff --git a/reports/opendmarc-reports.in b/reports/opendmarc-reports.in index df1a722f..270df734 100755 --- a/reports/opendmarc-reports.in +++ b/reports/opendmarc-reports.in @@ -575,14 +575,22 @@ sub archive_report } } -# Build a VERP envelope sender encoding $rcpt into $sender. -# e.g. postmaster@r.example.com + dmarc@target.com -> postmaster+dmarc=target.com@r.example.com +# Build a VERP envelope sender encoding $rcpt (and, optionally, the domain +# being reported on) into $sender. +# e.g. postmaster@r.example.com + dmarc@target.com +# -> postmaster+dmarc=target.com@r.example.com +# e.g. postmaster@r.example.com + dmarc@aggregator.com + example.com +# -> postmaster+example.com+dmarc=aggregator.com@r.example.com +# Including the reported-on domain lets a bounce handler recover it directly +# from the envelope sender of a returned report, without having to look up +# which domain(s) use a given RUA/RUF address. sub verp_sender { - my ($sender, $rcpt) = @_; + my ($sender, $rcpt, $domain) = @_; my ($sloc, $sdom) = split(/@/, $sender, 2); (my $enc = $rcpt) =~ s/@/=/; - return "$sloc+$enc\@$sdom"; + my $tag = (defined($domain) && $domain ne "") ? "$domain+$enc" : $enc; + return "$sloc+$tag\@$sdom"; } sub usage @@ -629,7 +637,9 @@ sub usage print STDERR "\t--stale-server=url StaleMARC API base URL\n"; print STDERR "\t--test don't send reports\n"; print STDERR "\t--verp use VERP envelope senders so bounces\n"; - print STDERR "\t identify the failing recipient\n"; + print STDERR "\t identify the failing recipient (and,\n"; + print STDERR "\t for aggregate reports, the reported-on\n"; + print STDERR "\t domain)\n"; print STDERR "\t (implies --keepfiles --noupdate)\n"; print STDERR "\t--utc operate in UTC\n"; print STDERR "\t--verbose verbose output\n"; @@ -1996,7 +2006,7 @@ foreach (@$domainset) exit(1) unless defined($smtp); } - my $mailfrom = $verp ? verp_sender($repemail, $repdest) : $repemail; + my $mailfrom = $verp ? verp_sender($repemail, $repdest, $domain) : $repemail; if (!$smtp->mail($mailfrom) || !$smtp->to($repdest) || (defined($repbcc) && !$smtp->to($repbcc)) || From 9972122d939dfc144494ca50c3ed531799fbdb78 Mon Sep 17 00:00:00 2001 From: Dan Mahoney Date: Sat, 22 Aug 2026 04:04:08 -0700 Subject: [PATCH 2/2] feat: add opendmarc-bounce-handler to process VERP'd report bounces Adds a mail-alias/pipe-target script that decodes the VERP-encoded recipient of a bounced DMARC report -- the failing RUA/RUF address, and, for aggregate reports, the domain being reported on -- and: - INSERTs the failing address into the suppressions table, so later opendmarc-reports runs skip it automatically; and - optionally prompts StaleMARC's checkDomain endpoint to retest the recovered domain's DMARC reporting deliverability (--stale-notify, on by default). The recipient is read from stdin (Original-Recipient/Final-Recipient/ X-Original-To/Delivered-To/To, in that order) for standard MTA pipe delivery, or supplied directly via --address for MTAs that can pass the original recipient as an argument. Decoding failures are logged and ignored (exit 0) rather than treated as errors, so unrelated mail delivered to the same alias can't cause further bounces. Cross-referenced from opendmarc-reports(8) and db/README.schema, which previously only described this as something "a bounce-processing script" would need to do. --- configure.ac | 2 + db/README.schema | 15 +- reports/.gitignore | 2 + reports/Makefile.am | 8 +- reports/opendmarc-bounce-handler.8.in | 138 +++++++++++ reports/opendmarc-bounce-handler.in | 316 ++++++++++++++++++++++++++ reports/opendmarc-reports.8.in | 21 +- 7 files changed, 481 insertions(+), 21 deletions(-) create mode 100644 reports/opendmarc-bounce-handler.8.in create mode 100644 reports/opendmarc-bounce-handler.in diff --git a/configure.ac b/configure.ac index e12de56e..bac6f660 100644 --- a/configure.ac +++ b/configure.ac @@ -593,6 +593,8 @@ AC_CONFIG_FILES([ Makefile opendmarc/opendmarc-check.8 opendmarc/tests/Makefile reports/Makefile + reports/opendmarc-bounce-handler + reports/opendmarc-bounce-handler.8 reports/opendmarc-expire reports/opendmarc-expire.8 reports/opendmarc-import diff --git a/db/README.schema b/db/README.schema index 55467587..5fb24fb5 100644 --- a/db/README.schema +++ b/db/README.schema @@ -53,13 +53,14 @@ suppressions A table of RUA/RUF addresses and domains that should be manually or automatically from VERP bounce processing: when opendmarc-reports sends reports with --verp, bounces return to an encoded envelope sender that identifies the failing - address, allowing a bounce handler to INSERT the address here. - For aggregate reports, the envelope sender also encodes the - domain the report concerned, so a bounce handler can act on - that domain too (e.g. prompting a third-party service like - StaleMARC to test its DMARC reporting deliverability) - without needing to look up which domain(s) route through the - failing address. + address, and opendmarc-bounce-handler (run as the mail + alias/pipe target for that address) decodes it and INSERTs it + here. For aggregate reports, the envelope sender also encodes + the domain the report concerned, which opendmarc-bounce-handler + also recovers and can use to prompt a third-party service like + StaleMARC to retest that domain's DMARC reporting + deliverability, without needing to look up which domain(s) + route through the failing address. UPGRADING AN EXISTING SCHEMA diff --git a/reports/.gitignore b/reports/.gitignore index e109bb19..a3bf5255 100644 --- a/reports/.gitignore +++ b/reports/.gitignore @@ -1,6 +1,8 @@ *.orig *.rej Makefile.in +opendmarc-bounce-handler +opendmarc-bounce-handler.8 opendmarc-expire opendmarc-expire.8 opendmarc-import diff --git a/reports/Makefile.am b/reports/Makefile.am index b3391404..c452b650 100644 --- a/reports/Makefile.am +++ b/reports/Makefile.am @@ -5,12 +5,12 @@ AUTOMAKE_OPTIONS = foreign dist_doc_DATA = README opendmarc-run.conf.sample -dist_sbin_SCRIPTS = opendmarc-expire opendmarc-import opendmarc-importstats \ - opendmarc-params opendmarc-reports +dist_sbin_SCRIPTS = opendmarc-bounce-handler opendmarc-expire opendmarc-import \ + opendmarc-importstats opendmarc-params opendmarc-reports libexec_SCRIPTS = opendmarc-run CLEANFILES = opendmarc-importstats -dist_man_MANS = opendmarc-expire.8 opendmarc-import.8 opendmarc-params.8 \ - opendmarc-reports.8 opendmarc-importstats.8 +dist_man_MANS = opendmarc-bounce-handler.8 opendmarc-expire.8 opendmarc-import.8 \ + opendmarc-params.8 opendmarc-reports.8 opendmarc-importstats.8 diff --git a/reports/opendmarc-bounce-handler.8.in b/reports/opendmarc-bounce-handler.8.in new file mode 100644 index 00000000..5393b682 --- /dev/null +++ b/reports/opendmarc-bounce-handler.8.in @@ -0,0 +1,138 @@ +.TH opendmarc-bounce-handler 8 "The Trusted Domain Project" +.SH NAME +.B opendmarc-bounce-handler +\- process bounces of VERP'd DMARC reports +.SH SYNOPSIS +.B opendmarc-bounce-handler +[options] +.SH DESCRIPTION +.B opendmarc-reports +.RI ( 8 ), +when run with +.IR --verp , +encodes the failing recipient address -- and, for aggregate reports, the +domain the report concerned -- into the envelope sender of each report it +sends, so that a bounce returns to an address identifying what failed. +See +.B opendmarc-reports (8) +under +.I --verp +for the exact encoding. +.PP +.B opendmarc-bounce-handler +decodes that information back out of a bounce. It is meant to be run as +the pipe target of a mail alias for the mailbox +.I --report-email +uses, e.g. in +.IR /etc/aliases : +.sp +.in +4n +postmaster: "|@sbindir@/opendmarc-bounce-handler" +.in +.sp +When invoked this way, the bounce (or DSN) is read from standard input and +the VERP-encoded recipient is recovered from its +.IR Original-Recipient , +.IR Final-Recipient , +.IR X-Original-To , +.IR Delivered-To ", or" +.I To +fields, in that order of preference. If the MTA can instead pass the +original recipient directly (for example, a Postfix +.I pipe +transport configured with +.IR ${original_recipient} ), +supply it with +.I --address +and standard input is ignored. +.PP +Once decoded, +.B opendmarc-bounce-handler +inserts the failing address into the +.I suppressions +table so subsequent +.B opendmarc-reports +runs skip it, and, if a domain was also recovered, optionally requests +StaleMARC's +.I checkDomain +endpoint for that domain to prompt a recheck of its DMARC reporting +deliverability. +.SH OPTIONS +.TP +.I --address=tag +Decode this VERP tag (the local-part extension after the first +.RI ' + ' +in the envelope recipient the bounce concerns) instead of scanning +standard input for it. +.TP +.I --dbscheme=scheme +Specify "mysql" (the default) or "MariaDB". +.TP +.I --dbhost=host +Attempts to connect to the database server on the named +.I host. +The default is "localhost". +.TP +.I --dbname=name +Requests a connection to the database called +.I name. +The default is "opendmarc". +.TP +.I --dbpasswd=password +Attempts to authenticate to the database server using the specified +.I password. +The default is "opendmarc". +.TP +.I --dbport=port +Tries to connect to the database at the specified TCP +.I port. +The default is 3306. +.TP +.I --dbuser=user +Attempts to authenticate to the database server as the specified +.I user. +The default is "opendmarc". +.TP +.I --help +Prints a usage message and exits. +.TP +.I --reason=text +The value to record in the +.I suppressions +table's +.I reason +column. The default is "bounce". +.TP +.I --stale-notify +Prompts StaleMARC to retest the recovered domain's DMARC reporting +deliverability. Enabled by default; disable with +.IR --no-stale-notify . +Has no effect when no domain was recovered (e.g. a forensic report bounce, +or an older aggregate report sent before domain encoding was added). +.TP +.I --stale-server=url +StaleMARC API base URL. +.TP +.I --test +Decodes and prints what would be done, without touching the database or +contacting StaleMARC. +.TP +.I --verbose +Requests verbose output. +.TP +.I --version +Prints version number and exits. +.SH EXIT STATUS +Exits non-zero only on a database connection or query failure. A bounce +from which nothing could be decoded is logged to standard error and +otherwise ignored (exit status 0), so a malformed or unrelated piece of +mail delivered to the same alias cannot itself trigger further bounces. +.SH VERSION +This man page covers the version of +.I opendmarc-bounce-handler +that shipped with version @VERSION@ of +.I OpenDMARC. +.SH COPYRIGHT +Copyright (c) 2026, The Trusted Domain Project. All rights reserved. +.SH SEE ALSO +.I opendmarc-reports(8) diff --git a/reports/opendmarc-bounce-handler.in b/reports/opendmarc-bounce-handler.in new file mode 100644 index 00000000..750883a0 --- /dev/null +++ b/reports/opendmarc-bounce-handler.in @@ -0,0 +1,316 @@ +#!@PERL@ +# +# Copyright (c) 2026, The Trusted Domain Project. All rights reserved. +# +# Mail delivery agent for bounces of VERP'd DMARC reports sent by +# opendmarc-reports --verp. Meant to be invoked as the pipe target of a +# mail alias for the report envelope-sender mailbox (i.e. the local part +# of --report-email), e.g. in /etc/aliases: +# +# postmaster: "|@sbindir@/opendmarc-bounce-handler" +# +# Recovers the failing RUA/RUF address -- and, for aggregate reports, the +# domain the report concerned -- from the VERP-encoded recipient the +# bounce was addressed to, records the failing address in the +# suppressions table, and (for aggregate reports) optionally prompts +# StaleMARC to (re-)test the recovered domain's DMARC reporting +# deliverability. + +### +### Setup +### + +use strict; +use warnings; + +use DBI; +use File::Basename; +use Getopt::Long; +use HTTP::Tiny; +use POSIX; + +# general +my $progname = basename($0); +my $version = "@VERSION@"; +my $verbose = 0; +my $helponly = 0; +my $showversion = 0; +my $test = 0; + +my $address; +my $reason = "bounce"; + +# DB parameters +my $def_dbscheme = "@SQL_BACKEND@"; +my $def_dbhost = "localhost"; +my $def_dbname = "opendmarc"; +my $def_dbuser = "opendmarc"; +my $def_dbpasswd = "opendmarc"; +my $def_dbport = "3306"; +my $dbhost; +my $dbname; +my $dbuser; +my $dbpasswd; +my $dbport; + +my $dbscheme = $def_dbscheme; + +# StaleMARC +my $stale_notify = 1; +my $stale_server = "https://stalemarc.measurement.network/api"; + +my $dbi_h; +my $dbi_s; + +### +### NO user-serviceable parts beyond this point +### + +sub usage +{ + print STDERR "$progname: usage: $progname [options]\n"; + print STDERR "\t--address=tag decode this VERP tag instead of scanning stdin\n"; + print STDERR "\t--dbscheme=scheme mysql or MariaDB [$def_dbscheme]\n"; + print STDERR "\t--dbhost=host database host [$def_dbhost]\n"; + print STDERR "\t--dbname=name database name [$def_dbname]\n"; + print STDERR "\t--dbpasswd=passwd database password [$def_dbpasswd]\n"; + print STDERR "\t--dbport=port database port [$def_dbport]\n"; + print STDERR "\t--dbuser=user database user [$def_dbuser]\n"; + print STDERR "\t--help print help and exit\n"; + print STDERR "\t--reason=text suppressions.reason to record [$reason]\n"; + print STDERR "\t--stale-notify prompt StaleMARC to retest the recovered\n"; + print STDERR "\t domain (default; use --no-stale-notify\n"; + print STDERR "\t to disable)\n"; + print STDERR "\t--stale-server=url StaleMARC API base URL\n"; + print STDERR "\t--test decode and report only; don't touch the\n"; + print STDERR "\t database or contact StaleMARC\n"; + print STDERR "\t--verbose verbose output\n"; + print STDERR "\t--version print version and exit\n"; +} + +# Reverses verp_sender()'s encoding (see reports/opendmarc-reports.in). +# $tag is the local-part extension a bounce was delivered to, i.e. +# everything after the first '+' in the envelope recipient: +# +# dmarc-rua=aggregator.com (forensic, or pre-domain) +# example.com+dmarc-rua=aggregator.com (aggregate, with domain) +# +# Returns ($domain, $address); $domain is undef if none was encoded. +# +# This is a heuristic, not an exact inverse: if the failing RUA/RUF +# address's own local part contains a '+', a tag with no domain will +# contain one too, and will be misread as domain+address. That case is +# rare (report addresses are operator-chosen), and worth noting to anyone +# troubleshooting a decode that looks wrong. +sub decode_verp +{ + my ($tag) = @_; + + return (undef, undef) unless defined($tag) && length($tag); + + my ($first, $rest) = split(/\+/, $tag, 2); + + if (defined($rest) && $first !~ /=/ && $first =~ /\./ && $rest =~ /=/) + { + (my $addr = $rest) =~ s/=/\@/; + return ($first, $addr); + } + + (my $addr = $tag) =~ s/=/\@/; + return (undef, $addr); +} + +# Scans lines of a bounce message (a DSN's per-message/per-recipient +# fields, or a plain header block) for the VERP address the report was +# sent to, and returns its local-part extension (the tag decode_verp() +# expects), or undef if none was found. +sub find_tag +{ + my (@lines) = @_; + + my @candidates; + + for (@lines) + { + if (/^(?:Original-Recipient|Final-Recipient)\s*:\s*(?:rfc822;)?\s*(.+?)\s*$/i) + { + push @candidates, $1; + } + elsif (/^(?:X-Original-To|Delivered-To|To)\s*:\s*(.+?)\s*$/i) + { + push @candidates, $1; + } + } + + for my $addr (@candidates) + { + $addr =~ s/^.*.*$//; + + if ($addr =~ /^[^@+\s]+\+([^@\s]+)@/) + { + return $1; + } + } + + return undef; +} + +# parse command line arguments +my $opt_retval = &Getopt::Long::GetOptions ('address=s' => \$address, + 'dbscheme=s' => \$dbscheme, + 'dbhost=s' => \$dbhost, + 'dbname=s' => \$dbname, + 'dbpasswd=s' => \$dbpasswd, + 'dbport=s' => \$dbport, + 'dbuser=s' => \$dbuser, + 'help!' => \$helponly, + 'reason=s' => \$reason, + 'stale-notify!' => \$stale_notify, + 'stale-server=s' => \$stale_server, + 'test!' => \$test, + 'verbose!' => \$verbose, + 'version!' => \$showversion, + ); + +if (!$opt_retval || $helponly) +{ + usage(); + + if ($helponly) + { + exit(0); + } + else + { + exit(1); + } +} + +if ($showversion) +{ + print STDOUT "$progname v$version\n"; + exit(0); +} + +# apply defaults +if (!defined($dbhost)) +{ + $dbhost = defined($ENV{'OPENDMARC_DBHOST'}) ? $ENV{'OPENDMARC_DBHOST'} : $def_dbhost; +} + +if (!defined($dbname)) +{ + $dbname = defined($ENV{'OPENDMARC_DB'}) ? $ENV{'OPENDMARC_DB'} : $def_dbname; +} + +if (!defined($dbpasswd)) +{ + $dbpasswd = defined($ENV{'OPENDMARC_PASSWORD'}) ? $ENV{'OPENDMARC_PASSWORD'} : $def_dbpasswd; +} + +if (!defined($dbport)) +{ + $dbport = defined($ENV{'OPENDMARC_PORT'}) ? $ENV{'OPENDMARC_PORT'} : $def_dbport; +} + +if (!defined($dbuser)) +{ + $dbuser = defined($ENV{'OPENDMARC_USER'}) ? $ENV{'OPENDMARC_USER'} : $def_dbuser; +} + +# +# Let's go! +# + +my $tag = $address; + +if (!defined($tag)) +{ + my @lines = ; + $tag = find_tag(@lines); +} + +if (!defined($tag)) +{ + print STDERR "$progname: no VERP-encoded recipient found; nothing to do\n"; + exit(0); +} + +my ($domain, $failaddr) = decode_verp($tag); + +if (!defined($failaddr) || $failaddr !~ /\@/) +{ + print STDERR "$progname: couldn't decode a report address from '$tag'\n"; + exit(0); +} + +if ($verbose || $test) +{ + print STDERR "$progname: decoded address=$failaddr" . + (defined($domain) ? ", domain=$domain" : "") . "\n"; +} + +if ($test) +{ + exit(0); +} + +require "DBD/$dbscheme.pm"; + +my $dbi_dsn = "DBI:" . $dbscheme . ":database=" . $dbname . + ";host=" . $dbhost; +if ($dbport != $def_dbport) { + $dbi_dsn .= ";port=" . $dbport; +} + +$dbi_h = DBI->connect($dbi_dsn, $dbuser, $dbpasswd, { PrintError => 0 }); +if (!defined($dbi_h)) +{ + print STDERR "$progname: unable to connect to database: $DBI::errstr\n"; + exit(1); +} + +if ($verbose) +{ + print STDERR "$progname: connected to database\n"; +} + +$dbi_s = $dbi_h->prepare("INSERT IGNORE INTO suppressions (address, reason) VALUES (?, ?)"); +if (!$dbi_s->execute($failaddr, $reason)) +{ + print STDERR "$progname: INSERT failed: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); +} + +if ($verbose) +{ + if ($dbi_s->rows > 0) + { + print STDERR "$progname: suppressed $failaddr\n"; + } + else + { + print STDERR "$progname: $failaddr already suppressed\n"; + } +} + +$dbi_s->finish; +$dbi_h->disconnect; + +if ($stale_notify && defined($domain)) +{ + (my $enc_domain = $domain) =~ s/@/%40/g; + my $url = "$stale_server/checkDomain.php?key=$enc_domain"; + my $resp = HTTP::Tiny->new(timeout => 10)->get($url); + + if ($verbose) + { + print STDERR "$progname: StaleMARC: prompted recheck of $domain " . + "(status $resp->{status})\n"; + } +} + +exit(0); diff --git a/reports/opendmarc-reports.8.in b/reports/opendmarc-reports.8.in index 6e1df9f3..4a2ef034 100644 --- a/reports/opendmarc-reports.8.in +++ b/reports/opendmarc-reports.8.in @@ -390,16 +390,16 @@ When .I --verp is enabled, DMARC report bounces return to an encoded envelope sender that identifies the failing address (and, for aggregate reports, the -domain the report concerned). A bounce-processing script can decode -that address and insert the failing address into this table -automatically, building a locally-maintained suppression list informed -by real delivery failures. The recovered domain is also useful input -to third-party services such as StaleMARC (see -.IR --stale-check ), -which can be prompted to (re-)test a domain's DMARC reporting -deliverability by requesting its +domain the report concerned). +.B opendmarc-bounce-handler(8) +decodes that address, inserts the failing address into this table +automatically, and, for the recovered domain, optionally prompts a +third-party service such as StaleMARC (see +.IR --stale-check ) +to (re-)test that domain's DMARC reporting deliverability by requesting +its .I checkDomain -endpoint for that domain. +endpoint for it. .SH EXTERNAL DESTINATION VERIFICATION Before sending to a cross-domain .RI ( mailto: ) @@ -483,4 +483,5 @@ All rights reserved. .SH SEE ALSO .IR opendmarc(8) , .IR opendmarc.conf(5) , -.IR opendmarc-import(8) +.IR opendmarc-import(8) , +.IR opendmarc-bounce-handler(8)