Mail-Milter-Authentication/lib/Mail/Milter/Authentication/Handler/XGoogleDKIM.pm
package Mail::Milter::Authentication::Handler::XGoogleDKIM;
use 5.20.0;
use strict;
use warnings;
use Mail::Milter::Authentication::Pragmas;
# ABSTRACT: Handler class for Google specific DKIM
our $VERSION = '4.20250811'; # VERSION
use base 'Mail::Milter::Authentication::Handler';
use Mail::DKIM 1.20200824;
use Mail::DKIM::DNS;
use Mail::DKIM::Verifier;
sub default_config {
return {
'hide_none' => 0,
};
}
sub grafana_rows {
my ( $self ) = @_;
my @rows;
push @rows, $self->get_json( 'XGoogleDKIM_metrics' );
return \@rows;
}
sub register_metrics {
return {
'xgoogledkim_total' => 'The number of emails processed for X-Google-DKIM',
};
}
sub envfrom_callback {
my ( $self, $env_from ) = @_;
$self->{'failmode'} = 0;
$self->{'headers'} = [];
$self->{'has_dkim'} = 0;
$self->{'carry'} = q{};
$self->destroy_object('xgdkim');
}
sub header_callback {
my ( $self, $header, $value, $original ) = @_;
return if ( $self->{'failmode'} );
my $EOL = "\015\012";
my $dkim_chunk = $original . $EOL;
$dkim_chunk =~ s/\015?\012/$EOL/g;
if ( lc($header) eq 'dkim-signature' ) {
$dkim_chunk = 'X-Orig-' . $dkim_chunk;
}
if ( lc($header) eq 'domainkey-signature' ) {
$dkim_chunk = 'X-Orig-' . $dkim_chunk;
}
push @{$self->{'headers'}} , $dkim_chunk;
# Add Google signatures to the mix.
# Is this wise?
if ( $header eq 'X-Google-DKIM-Signature' ) {
my $x_dkim_chunk = 'DKIM-Signature: ' . $value . $EOL;
$x_dkim_chunk =~ s/\015?\012/$EOL/g;
push @{$self->{'headers'}} , $x_dkim_chunk;
$self->{'has_dkim'} = 1;
my ($domain) = $value =~ /d=([^;]*);/;
my ($selector) = $value =~ /s=([^;]*);/;
my $resolver = $self->get_object('resolver');
if ( defined $selector && defined $domain ) {
my $lookup = $selector.'._domainkey.'.$domain;
eval{ $resolver->bgsend( $lookup, 'TXT' ) };
$self->handle_exception( $@ );
$self->dbgout( 'DNSEarlyLookup', "$lookup TXT", LOG_DEBUG );
}
}
}
sub eoh_callback {
my ($self) = @_;
return if ( $self->{'failmode'} );
my $config = $self->handler_config();
if ( $self->{'has_dkim'} == 0 ) {
$self->metric_count( 'xgoogledkim_total', { 'result' => 'none' } );
$self->dbgout( 'XGoogleDKIMResult', 'No X-Google-DKIM headers', LOG_DEBUG );
if ( !( $config->{'hide_none'} ) ) {
my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( 'none' );
$header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'no signatures found' ) );
$self->add_auth_header( $header );
}
delete $self->{'headers'};
}
else {
my $dkim;
eval {
$dkim = Mail::DKIM::Verifier->new();
my $resolver = $self->get_object('resolver');
Mail::DKIM::DNS::resolver($resolver);
$self->set_object('xgdkim', $dkim, 1);
};
if ( my $error = $@ ) {
$self->handle_exception( $error );
$self->log_error( 'XGoogleDKIM Setup Error ' . $error );
$self->{'failmode'} = 1;
$self->_check_error( $error );
$self->metric_count( 'xgoogledkim_total', { 'result' => 'error' } );
delete $self->{'headers'};
return;
}
eval {
$dkim->PRINT( join q{},
@{ $self->{'headers'} },
"\015\012",
);
};
if ( my $error = $@ ) {
$self->handle_exception( $error );
$self->log_error( 'XGoogleDKIM Headers Error ' . $error );
$self->{'failmode'} = 1;
$self->_check_error( $error );
$self->metric_count( 'xgoogledkim_total', { 'result' => 'error' } );
}
delete $self->{'headers'};
}
$self->{'carry'} = q{};
}
sub body_callback {
my ( $self, $body_chunk ) = @_;
return if ( $self->{'failmode'} );
return if ( $self->{'has_dkim'} == 0 );
my $EOL = "\015\012";
my $dkim_chunk;
if ( $self->{'carry'} ne q{} ) {
$dkim_chunk = $self->{'carry'} . $body_chunk;
$self->{'carry'} = q{};
}
else {
$dkim_chunk = $body_chunk;
}
if ( substr( $dkim_chunk, -1 ) eq "\015" ) {
$self->{'carry'} = "\015";
$dkim_chunk = substr( $dkim_chunk, 0, -1 );
}
$dkim_chunk =~ s/\015?\012/$EOL/g;
my $dkim = $self->get_object('xgdkim');
eval {
$dkim->PRINT( $dkim_chunk );
};
if ( my $error = $@ ) {
$self->handle_exception( $error );
$self->log_error( 'XGoogleDKIM Body Error ' . $error );
$self->{'failmode'} = 1;
$self->_check_error( $error );
$self->metric_count( 'xgoogledkim_total', { 'result' => 'error' } );
}
}
sub eom_callback {
my ($self) = @_;
return if ( $self->{'has_dkim'} == 0 );
return if ( $self->{'failmode'} );
my $config = $self->handler_config();
my $dkim = $self->get_object('xgdkim');
eval {
$dkim->PRINT( $self->{'carry'} );
$dkim->CLOSE();
my $dkim_result = $dkim->result;
my $dkim_result_detail = $dkim->result_detail;
$self->metric_count( 'xgoogledkim_total', { 'result' => $dkim_result } );
$self->dbgout( 'XGoogleDKIMResult', $dkim_result_detail, LOG_DEBUG );
if ( !$dkim->signatures() ) {
if ( !( $config->{'hide_none'} && $dkim_result eq 'none' ) ) {
my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( $dkim_result );
$header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'no signatures found' ) );
$self->add_auth_header( $header );
}
}
foreach my $signature ( $dkim->signatures() ) {
my $otype = ref $signature;
my $type =
$otype eq 'Mail::DKIM::DkSignature' ? 'domainkeys'
: $otype eq 'Mail::DKIM::Signature' ? 'dkim'
: 'dkim';
$self->dbgout( 'XGoogleDKIMSignatureType', $type, LOG_DEBUG );
$self->dbgout( 'XGoogleDKIMSignatureIdentity', $signature->identity, LOG_DEBUG );
$self->dbgout( 'XGoogleDKIMSignatureResult', $signature->result_detail, LOG_DEBUG );
my $signature_result = $signature->result();
my $signature_result_detail = $signature->result_detail();
if ( $signature_result eq 'invalid' ) {
if ( $signature_result_detail =~ /DNS query timeout for (.*) at / ) {
my $timeout_domain = $1;
$self->log_error( "TIMEOUT DETECTED: in XGoogleDKIM result: $timeout_domain" );
$signature_result_detail = "DNS query timeout for $timeout_domain";
}
}
my $result_comment = q{};
if ( $signature_result ne 'pass' and $signature_result ne 'none' ) {
$signature_result_detail =~ /$signature_result \((.*)\)/;
if ( $1 ) {
$result_comment = $1 . ', ';
}
}
if (
!(
$config->{'hide_none'} && $signature_result eq 'none'
)
)
{
my $key_data = q{};
eval {
my $key = $signature->get_public_key();
$key_data = $key->size() . '-bit ' . $key->type() . ' key';
};
my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( $signature_result );
$header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( $result_comment . $key_data ) );
$header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.d' )->safe_set_value( $signature->domain() ) );
$header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.i' )->safe_set_value( $signature->identity() ) );
$header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.b' )->safe_set_value( substr( $signature->data(), 0, 8 ) ) );
$self->add_auth_header($header);
}
}
};
if ( my $error = $@ ) {
$self->handle_exception( $error );
# Also in DMARC module
$self->log_error( 'XGoogleDKIM EOM Error ' . $error );
$self->{'failmode'} = 1;
$self->_check_error( $error );
$self->metric_count( 'xgoogledkim_total', { 'result' => 'error' } );
return;
}
}
sub close_callback {
my ( $self ) = @_;
delete $self->{'failmode'};
delete $self->{'headers'};
delete $self->{'body'};
delete $self->{'carry'};
delete $self->{'has_dkim'};
$self->destroy_object('xgdkim');
}
sub _check_error {
my ( $self, $error ) = @_;
if ( $error =~ /^DNS error: query timed out/
or $error =~ /^DNS query timeout/
){
$self->log_error( 'Temp XGoogleDKIM Error - ' . $error );
my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( 'temperror' );
$header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'dns timeout' ) );
$self->add_auth_header( $header );
}
elsif ( $error =~ /^no domain to fetch policy for$/
or $error =~ /^policy syntax error$/
or $error =~ /^empty domain label/
or $error =~ /^invalid name /
){
$self->log_error( 'Perm XGoogleDKIM Error - ' . $error );
my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( 'perlerror' );
$header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'syntax or domain error' ) );
$self->add_auth_header( $header );
}
else {
$self->exit_on_close( 'Unexpected XGoogleDKIM Error - ' . $error );
my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-google-dkim' )->safe_set_value( 'temperror' );
$self->add_auth_header( $header );
# Fill these in as they occur, but for unknowns err on the side of caution
# and tempfail/exit
$self->tempfail_on_error();
}
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Mail::Milter::Authentication::Handler::XGoogleDKIM - Handler class for Google specific DKIM
=head1 VERSION
version 4.20250811
=head1 DESCRIPTION
Module for validation of X-Google-DKIM signatures.
=head1 CONFIGURATION
"XGoogleDKIM" : { | Config for the X-Google-DKIM Module
"hide_none" : 0, | Hide auth line if the result is 'none'
},
=head1 AUTHOR
Marc Bradshaw <marc@marcbradshaw.net>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2020 by Marc Bradshaw.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut