The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

use 5.20.0;
use strict;
# ABSTRACT: Milter protocol handling
our $VERSION = '3.20210514'; # VERSION
use Net::IP;
sub register_metrics {
return {
'mail_processed_total' => 'Number of emails processed',
};
}
sub protocol_process_request {
my ( $self ) = @_;
my $handler = $self->{'handler'}->{'_Handler'};
$handler->top_setup_callback();
COMMAND:
while ( 1 ) {
# Get packet length
my $length = unpack('N', $self->milter_read_block(4) ) || last;
$self->fatal("bad packet length $length") if ($length <= 0 || $length > 131072);
# Get command
my $command = $self->milter_read_block(1) || last;
$self->logdebug( "receive command $command" );
# Get data
my $data = $self->milter_read_block($length - 1);
if ( ! defined ( $data ) ) {
$self->fatal('EOF in stream');
}
last COMMAND if $command eq SMFIC_QUIT;
$self->milter_process_command( $command, $data );
}
}
sub milter_process_command {
my ( $self, $command, $buffer ) = @_;
$self->logdebug ( "process command $command" );
my $handler = $self->{'handler'}->{'_Handler'};
my $returncode = SMFIS_CONTINUE;
if ( $command eq SMFIC_CONNECT ) {
my ( $host, $ip ) = $self->milter_process_connect( $buffer );
$handler->remap_connect_callback( $host, $ip );
$returncode = $handler->top_connect_callback( $host, $handler->{'ip_object'} );
}
elsif ( $command eq SMFIC_ABORT ) {
$returncode = $handler->top_abort_callback();
}
elsif ( $command eq SMFIC_BODY ) {
$returncode = $handler->top_body_callback( $buffer );
}
elsif ( $command eq SMFIC_MACRO ) {
$self->fatal('SMFIC_MACRO: empty packet') unless ( $buffer =~ s/^(.)// );
my $code = $1;
my $data = $self->milter_split_buffer( $buffer );
push ( @$data, q{} ) if (( @$data & 1 ) != 0 ); # pad last entry with empty string if odd number
my %datahash = @$data;
foreach my $key ( keys %datahash ) {
$handler->set_symbol( $code, $key, $datahash{$key} );
}
undef $returncode;
}
elsif ( $command eq SMFIC_BODYEOB ) {
$returncode = $handler->top_eom_callback();
if ( $returncode == SMFIS_CONTINUE ) {
$handler->metric_count( 'mail_processed_total', { 'result' => 'accepted' } );
}
}
elsif ( $command eq SMFIC_HELO ) {
my $helo = $self->milter_split_buffer( $buffer );
$handler->remap_helo_callback( @$helo );
$returncode = $handler->top_helo_callback( $handler->{'helo_name'} );
}
elsif ( $command eq SMFIC_HEADER ) {
my $header = $self->milter_split_buffer( $buffer );
if ( @$header == 1 ) { push @$header , q{}; };
my $original = join( $self->{'headers_include_space'} ? ':': ': ', @$header );
push @$header, $original;
$header->[1] =~ s/^\s+//;
$header->[0] =~ s/^\s+//;
$header->[0] =~ s/\s+$//;
$returncode = $handler->top_header_callback( @$header );
}
elsif ( $command eq SMFIC_MAIL ) {
my $envfrom = $self->milter_split_buffer( $buffer );
$returncode = $handler->top_envfrom_callback( @$envfrom );
}
elsif ( $command eq SMFIC_EOH ) {
$returncode = $handler->top_eoh_callback();
}
elsif ( $command eq SMFIC_OPTNEG ) {
$self->fatal('SMFIC_OPTNEG: packet has wrong size') unless (length($buffer) == 12);
my ($ver, $actions, $protocol) = unpack('NNN', $buffer);
$self->fatal("SMFIC_OPTNEG: unknown milter protocol version $ver") unless ($ver >= 2 && $ver <= 6);
my $actions_reply = $self->{'callback_flags'} & $actions;
my $protocol_reply = $self->{'protocol'} & $protocol;
$self->write_packet(SMFIC_OPTNEG,
pack('NNN', 2, $actions_reply, $protocol_reply)
);
undef $returncode;
$self->{'headers_include_space'} = ($protocol_reply & SMFIP_HDR_LEADSPC) != 0;
}
elsif ( $command eq SMFIC_RCPT ) {
my $envrcpt = $self->milter_split_buffer( $buffer );
$returncode = $handler->top_envrcpt_callback( @$envrcpt );
}
elsif ( $command eq SMFIC_DATA ) {
}
elsif ( $command eq SMFIC_UNKNOWN ) {
undef $returncode;
# Unknown SMTP command received
}
else {
$self->fatal("Unknown milter command $command");
}
my $config = $self->{'config'};
my $reject_reason;
my $defer_reason;
my $quarantine_reason;
if ( $reject_reason = $handler->get_reject_mail() ) {
$handler->clear_reject_mail();
$returncode = SMFIS_REJECT;
}
elsif ( $defer_reason = $handler->get_defer_mail() ) {
$handler->clear_defer_mail();
$returncode = SMFIS_TEMPFAIL;
}
elsif ( $quarantine_reason = $handler->get_quarantine_mail() ) {
if ( $config->{'milter_quarantine'} ) {
$handler->clear_quarantine_mail();
$returncode = SMFIR_QUARANTINE;
}
else {
undef $quarantine_reason;
}
}
if (defined $returncode) {
if ( $returncode eq SMFIR_QUARANTINE ) {
# NOP
}
elsif ( $returncode == SMFIS_CONTINUE ) {
$returncode = SMFIR_CONTINUE;
}
elsif ( $returncode == SMFIS_TEMPFAIL ) {
$returncode = SMFIR_TEMPFAIL;
}
elsif ( $returncode == SMFIS_REJECT ) {
$returncode = SMFIR_REJECT;
}
elsif ( $returncode == SMFIS_DISCARD ) {
$returncode = SMFIR_DISCARD;
}
elsif ( $returncode == SMFIS_ACCEPT ) {
$returncode = SMFIR_ACCEPT;
$handler->metric_count( 'mail_processed_total', { 'result' => 'accepted' } );
}
if ( $config->{'dryrun'} ) {
if ( $returncode ne SMFIR_CONTINUE ) {
$self->loginfo ( "dryrun returncode changed from $returncode to continue" );
$returncode = SMFIR_CONTINUE;
}
}
if ( $command ne SMFIC_ABORT ) {
if ( $reject_reason ) {
my ( $rcode, $xcode, $message ) = split( ' ', $reject_reason, 3 );
if ($rcode !~ /^[5]\d\d$/ || $xcode !~ /^[5]\.\d\.\d$/ || substr($rcode, 0, 1) ne substr($xcode, 0, 1)) {
$handler->metric_count( 'mail_processed_total', { 'result' => 'deferred_error' } );
$self->loginfo ( "Invalid reject message $reject_reason - setting to TempFail" );
$self->write_packet(SMFIR_TEMPFAIL );
}
else {
$handler->metric_count( 'mail_processed_total', { 'result' => 'rejected' } );
$self->loginfo ( "SMTPReject: $reject_reason" );
$self->write_packet( SMFIR_REPLYCODE,
$reject_reason
. "\0"
);
}
}
elsif ( $defer_reason ) {
my ( $rcode, $xcode, $message ) = split( ' ', $defer_reason, 3 );
if ($rcode !~ /^[4]\d\d$/ || $xcode !~ /^[4]\.\d\.\d$/ || substr($rcode, 0, 1) ne substr($xcode, 0, 1)) {
$handler->metric_count( 'mail_processed_total', { 'result' => 'deferred_error' } );
$self->loginfo ( "Invalid defer message $defer_reason - setting to TempFail" );
$self->write_packet(SMFIR_TEMPFAIL );
}
else {
$handler->metric_count( 'mail_processed_total', { 'result' => 'deferred' } );
$self->loginfo ( "SMTPDefer: $reject_reason" );
$self->write_packet( SMFIR_REPLYCODE,
$defer_reason
. "\0"
);
}
}
elsif ( $quarantine_reason ) {
$handler->metric_count( 'mail_processed_total', { 'result' => 'quarantined' } );
$self->loginfo ( "SMTPQuarantine: $quarantine_reason" );
$self->write_packet( SMFIR_QUARANTINE,
$quarantine_reason
. "\0"
);
}
else {
$self->write_packet($returncode);
}
}
}
}
sub milter_process_connect {
my ( $self, $buffer ) = @_;
unless ($buffer =~ s/^([^\0]*)\0(.)//) {
$self->fatal('SMFIC_CONNECT: invalid connect info');
}
my $ip;
my $host = $1;
my ($port, $addr) = unpack('nZ*', $buffer);
if ( substr( $addr, 0, 5 ) eq 'IPv6:' ) {
$addr = substr( $addr, 5 );
}
if ( ! defined ( $addr ) ) {
$self->logerror('Unknown IP address format UNDEF');
$ip = undef;
# Could potentially fail here, connection is likely bad anyway.
}
elsif ( length ( $addr ) == 0 ) {
$self->logerror('Unknown IP address format NULL');
$ip = undef;
# Could potentially fail here, connection is likely bad anyway.
}
else {
eval {
$ip = Net::IP->new( $addr );
};
if ( my $error = $@ ) {
$self->logerror('Unknown IP address format - ' . $addr . ' - ' . $error );
$ip = undef;
# Could potentially fail here, connection is likely bad anyway.
}
}
return ( $host, $ip );
}
sub milter_read_block {
my ( $self, $len ) = @_;
my $socket = $self->{'socket'};
my $sofar = 0;
my $buffer = q{};
while ($len > $sofar) {
my $read = $socket->sysread($buffer, $len - $sofar, $sofar);
last if (!defined($read) || $read <= 0); # EOF
$sofar += $read;
}
return $buffer;
}
sub milter_split_buffer {
my ( $self, $buffer ) = @_;
$buffer =~ s/\0$//; # remove trailing NUL
return [ split(/\0/, $buffer) ];
};
##
sub add_header {
my ( $self, $header, $value ) = @_;
$value =~ s/\015\012/\012/g;
$self->write_packet( SMFIR_ADDHEADER,
$header
. "\0"
. ($self->{'headers_include_space'} ? ' ' : '')
. $value
. "\0"
);
}
sub change_header {
my ( $self, $header, $index, $value ) = @_;
$value = '' unless defined($value);
$value =~ s/\015\012/\012/g;
$self->write_packet( SMFIR_CHGHEADER,
pack('N', $index)
. $header
. "\0"
. ($self->{'headers_include_space'} ? ' ' : '')
. $value
. "\0"
);
}
sub insert_header {
my ( $self, $index, $key, $value ) = @_;
$value =~ s/\015\012/\012/g;
$self->write_packet( SMFIR_INSHEADER,
pack( 'N', $index )
. $key
. "\0"
. ($self->{'headers_include_space'} ? ' ' : '')
. $value
. "\0"
);
}
sub write_packet {
my ( $self, $code, $data ) = @_;
$self->logdebug ( "send command $code" );
my $socket = $self->{'socket'};
$data = q{} unless defined($data);
my $len = pack('N', length($data) + 1);
$socket->syswrite($len);
$socket->syswrite($code);
$socket->syswrite($data);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Mail::Milter::Authentication::Protocol::Milter - Milter protocol handling
=head1 VERSION
version 3.20210514
=head1 SYNOPSIS
Subclass of Net::Server::PreFork for bringing up the main server process for authentication_milter.
Please see Net::Server docs for more detail of the server code.
=head1 DESCRIPTION
A Perl implementation of email authentication standards rolled up into a single easy to use milter.
=head1 METHODS
=over
=item register_metrics
Return details of the metrics this module exports.
=item I<protocol_process_command( $command, $buffer )>
Process the command from the milter protocol stream.
=item I<milter_process_connect( $buffer )>
Process a milter connect command.
=item I<milter_read_block( $len )>
Read $len bytes from the milter protocol stream.
=item I<milter_split_buffer( $buffer )>
Split the milter buffer at null
=item I<add_header( $header, $value )>
Write an add header packet
=item I<change_header( $header, $index, $value )>
Write a change header packet
=item I<insert_header( $index, $key, $value )>
Writa an insert header packet
=item I<write_packet( $code, $data )>
Write a packet to the protocol stream.
=item I<milter_process_command( $command, $data )>
Process the milter command $command with the data from
$data.
=item I<protocol_process_request()>
Receive a new command from the protocol stream and process it.
=back
=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