package Mail::Milter::Authentication::Protocol::SMTP; use 5.20.0; use strict; use warnings; use Mail::Milter::Authentication::Pragmas; # ABSTRACT: SMTP protocol handling our $VERSION = '3.20250611'; # VERSION use Digest::MD5 qw{ md5_hex }; use Email::Date::Format qw{ email_date }; use IO::Socket::INET; use IO::Socket::UNIX; use IO::Socket; use Net::IP; sub register_metrics { return { 'mail_processed_total' => 'Number of emails processed', }; } sub get_smtp_config { my ( $self ) = @_; my $client_details = $self->get_client_details(); my $smtp_config; if ( exists( $self->{'config'}->{'smtp'}->{ $client_details } ) ) { $smtp_config = $self->{'config'}->{'smtp'}->{ $client_details }; } else { $smtp_config = $self->{'config'}->{'smtp'}; } return $smtp_config; } sub queue_type { my ( $self ) = @_; my $smtp_config = $self->get_smtp_config(); return $smtp_config->{'queue_type'} eq 'before' ? 'before' : 'after'; } sub smtp_status { my ( $self, $status ) = @_; my $smtp = $self->{'smtp'}; $PROGRAM_NAME = $Mail::Milter::Authentication::Config::IDENT . ':' . $status . '(' . $self->{'count'} . '.' . $smtp->{'count'} . ')'; } sub smtp_init { my ( $self ) = @_; return if $self->{'smtp'}->{'init_required'} == 0; my $handler = $self->{'handler'}->{'_Handler'}; my $smtp = $self->{'smtp'}; $handler->set_symbol( 'C', 'j', $smtp->{'server_name'} ); $handler->set_symbol( 'C', '{rcpt_host}', $smtp->{'server_name'} ); $smtp->{'queue_id'} = substr( uc md5_hex( "Authentication Milter Client $PID " . time() . rand(100) ) , -11 ); $handler->set_symbol( 'C', 'i', $self->smtp_queue_id() ); $smtp->{'count'}++ ; $handler->dbgout( 'SMTP Transaction count', $self->{'count'} . '.' . $smtp->{'count'} , LOG_DEBUG ); $smtp->{'init_required'} = 0; } sub protocol_process_request { my ( $self ) = @_; my $handler = $self->{'handler'}->{'_Handler'}; $handler->top_setup_callback(); my $config = $self->{ 'config' }; my $seconds = $config->{'content_timeout'} // 300; $handler->set_overall_timeout( $seconds * 1000000 ); $self->{'smtp'} = { 'fwd_helo_host' => undef, 'fwd_connect_ip' => undef, 'fwd_connect_host' => undef, 'fwd_ident' => undef, 'helo_host' => q{}, 'mail_from' => q{}, 'rcpt_to' => [], 'has_mail_from' => 0, 'has_data' => 0, 'connect_ip' => $self->{'server'}->{'peeraddr'}, 'connect_host' => $self->{'server'}->{'peeraddr'}, 'last_command' => 0, 'headers' => [], 'body' => q{}, 'using_lmtp' => 0, 'lmtp_rcpt' => [], 'init_required' => 1, 'string' => q{}, 'count' => 0, }; # If we have a UNIX connection then these will be undef, # Set them to localhost to avoid warnings later. if ( ! $self->{'smtp'}->{'connect_ip'} ) { $self->{'smtp'}->{'connect_ip'} = '127.0.0.1'; } if ( $self->{'smtp'}->{'connect_ip'} eq '127.0.0.1' ) { $self->{'smtp'}->{'connect_host'} = 'localhost'; } else { # TODO do a reverse lookup here! } my $smtp = $self->{'smtp'}; my $socket = $self->{'socket'}; my $smtp_config = $self->get_smtp_config(); $smtp->{'server_name'} = $smtp_config->{'server_name'} || 'server.example.com'; $smtp->{'smtp_timeout_in'} = $smtp_config->{'timeout_in'} || 60; $smtp->{'smtp_timeout_out'} = $smtp_config->{'timeout_out'} || 60; print $socket "220 " . $smtp->{'server_name'} . " ESMTP AuthenticationMilter\r\n"; $self->smtp_init(); COMMAND: while ( ! $smtp->{'last_command'} ) { my $command; local $SIG{'ALRM'} = sub{ die "Timeout\n" }; alarm( $smtp->{'smtp_timeout_in'} ); eval { $command = <$socket>; }; if ( my $error = $@ ) { $self->logerror( "Read Error: $error" ); last COMMAND; } alarm( 0 ); if ( ! $command ) { $self->logdebug( "receive NULL command" ); last COMMAND; } $command =~ s/\r?\n$//; my $uccommand = uc $command; $self->logdebug( "receive command $command" ); if ( exists ( $smtp_config->{ 'debug_triggers' } ) ) { my $triggers = $smtp_config->{ 'debug_triggers' }; foreach my $trigger ( @$triggers ) { if ( $command =~ /$trigger/ ) { $self->enable_extra_debugging(); } } } my $returncode = SMFIS_CONTINUE; if ( $uccommand =~ /^EHLO/ ) { $self->smtp_command_ehlo( $command ); } elsif ( $uccommand =~ /^LHLO/ ) { $self->smtp_command_lhlo( $command ); } elsif ( $uccommand =~ /^HELO/ ) { $self->smtp_command_helo( $command ); } elsif ( $uccommand =~ /^XFORWARD/ ) { $self->smtp_command_xforward( $command ); } elsif ( $uccommand =~ /^MAIL FROM:/ ) { $self->smtp_init(); $self->smtp_command_mailfrom( $command ); } elsif ( $uccommand =~ /^RCPT TO:/ ) { $self->smtp_command_rcptto( $command ); } elsif ( $uccommand =~ /^RSET/ ) { $self->smtp_command_rset( $command ); } elsif ( $uccommand =~ /^DATA/ ) { $handler->set_overall_timeout( $seconds * 1000000 ); $self->smtp_command_data( $command ); } elsif ( $uccommand =~ /^QUIT/ ){ $self->smtp_status('smtp.i.quit'); print $socket "221 2.0.0 Bye\n"; last COMMAND; } else { $self->smtp_status('smtp.i.unknown'); $self->logerror( "Unknown SMTP command: $command" ); print $socket "502 5.5.2 I don't understand\r\n"; } } $handler->clear_overall_timeout(); $self->smtp_status('smtp.close'); $self->close_destination_socket(); delete $self->{'smtp'}; } sub smtp_queue_id { my ( $self ) = @_; my $smtp = $self->{'smtp'}; my $queue_id = $smtp->{'queue_id'}; if ( $smtp->{'fwd_ident'} && $smtp->{'fwd_ident'} ne '[UNAVAILABLE]' ) { $queue_id .= '.' . $smtp->{'fwd_ident'}; } return $queue_id; } sub command_param { my ( $command, $index ) = @_; my $p = q{}; if ( length( $command ) >= $index ) { $p = substr( $command, $index ); } return $p; } # Splits the given command parameter line into a list of separate # path () and esmtp-param (KEYWORD=VALUE) strings. sub split_command_params { my ( $line ) = @_; $line =~ s/^\s+//; my $path_end = find_path_end( $line ) || length( $line ); my $path = substr( $line, 0, $path_end ); $line = substr( $line, $path_end ); $line =~ s/^\s+//; return ( $path, split( /\s+/, $line ) ); } sub find_path_end { my ( $input ) = @_; my $len = length( $input ); my @qstack; for ( my $i = 0; $i < $len; $i++ ) { my $c = substr( $input, $i, 1 ); if ( @qstack && $c eq $qstack[-1] ) { # end of a quoted string pop @qstack; } elsif ( $c eq '"' ) { # start of a "" quoted string push @qstack, '"'; } elsif ( $c eq '<' && ( !@qstack || $qstack[-1] eq '>' ) ) { # start of a <> quoted string (possibly inside of another <>) push @qstack, '>'; } elsif ( $c eq '\\' && @qstack ) { # escaped character within a quoted string $i++; } elsif ( $c eq ' ' && !@qstack ) { # found the end return $i; } } return ( @qstack ? undef : $len ); } sub smtp_command_lhlo { my ( $self, $command ) = @_; my $smtp = $self->{'smtp'}; my $socket = $self->{'socket'}; my $handler = $self->{'handler'}->{'_Handler'}; $self->smtp_status('smtp.i.lhlo'); $smtp->{'using_lmtp'} = 1; if ( $smtp->{'has_data'} ) { $self->logerror( "Out of Order SMTP command: $command" ); print $socket "501 5.5.2 Out of Order\r\n"; return; } $smtp->{'helo_host'} = command_param( $command,5 ); print $socket "250-" . $smtp->{'server_name'} . "\r\n"; if ( $self->queue_type() eq 'before' ) { print $socket "250-XFORWARD NAME ADDR HELO \r\n"; } else { print $socket "250-XFORWARD NAME ADDR HELO IDENT \r\n"; } print $socket "250-PIPELINING\r\n"; print $socket "250-ENHANCEDSTATUSCODES\r\n"; print $socket "250 8BITMIME\r\n"; } sub smtp_command_ehlo { my ( $self, $command ) = @_; my $smtp = $self->{'smtp'}; my $socket = $self->{'socket'}; my $handler = $self->{'handler'}->{'_Handler'}; $self->smtp_status('smtp.i.ehlo'); if ( $smtp->{'has_data'} ) { $self->logerror( "Out of Order SMTP command: $command" ); print $socket "501 5.5.2 Out of Order\r\n"; return; } $smtp->{'helo_host'} = command_param( $command,5 ); print $socket "250-" . $smtp->{'server_name'} . "\r\n"; if ( $self->queue_type() eq 'before' ) { print $socket "250-XFORWARD NAME ADDR HELO \r\n"; } else { print $socket "250-XFORWARD NAME ADDR HELO IDENT \r\n"; } print $socket "250-PIPELINING\r\n"; print $socket "250-ENHANCEDSTATUSCODES\r\n"; print $socket "250 8BITMIME\r\n"; } sub smtp_command_helo { my ( $self, $command ) = @_; my $smtp = $self->{'smtp'}; my $socket = $self->{'socket'}; my $handler = $self->{'handler'}->{'_Handler'}; $self->smtp_status('smtp.i.helo'); if ( $smtp->{'has_data'} ) { $self->logerror( "Out of Order SMTP command: $command" ); print $socket "501 5.5.2 Out of Order\r\n"; return; } $smtp->{'helo_host'} = command_param( $command,5 ); print $socket "250 " . $smtp->{'server_name'} . " Hi " . $smtp->{'helo_host'} . "\r\n"; } sub smtp_command_xforward { my ( $self, $command ) = @_; my $smtp = $self->{'smtp'}; my $socket = $self->{'socket'}; my $handler = $self->{'handler'}->{'_Handler'}; $self->smtp_status('smtp.i.xforward'); $self->smtp_init(); if ( $smtp->{'has_data'} ) { $self->logerror( "Out of Order SMTP command: $command" ); print $socket "503 5.5.2 Out of Order\r\n"; return; } my $xdata = command_param( $command,9 ); foreach my $entry ( split( q{ }, $xdata ) ) { my ( $key, $value ) = split( '=', $entry, 2 ); if ( $key eq 'NAME' ) { $smtp->{'fwd_connect_host'} = $value; } elsif ( $key eq 'ADDR' ) { $smtp->{'fwd_connect_ip'} = $value; } elsif ( $key eq 'HELO' ) { $smtp->{'fwd_helo_host'} = $value; } elsif ( $key eq 'IDENT' ) { if ( $self->queue_type() eq 'before' ) { $self->logerror( "XForward IDENT received in before queue mode: $key=$value" ); } else { $smtp->{'fwd_ident'} = $value; $handler->set_symbol( 'C', 'i', $self->smtp_queue_id() ); $handler->dbgout( 'Upstream ID', $value, LOG_INFO ); } } else { # NOP $self->logerror( "Unknown XForward Entry: $key=$value" ); ### log it here though } } print $socket "250 2.0.0 Ok\r\n"; } sub smtp_command_rset { my ( $self, $command ) = @_; my $smtp = $self->{'smtp'}; my $socket = $self->{'socket'}; $self->smtp_status('smtp.i.rset'); $smtp->{'mail_from'} = q{}; $smtp->{'rcpt_to'} = []; $smtp->{'headers'} = []; $smtp->{'body'} = q{}; $smtp->{'has_data'} = 0; $smtp->{'has_mail_from'} = 0; $smtp->{'fwd_connect_host'} = undef; $smtp->{'fwd_connect_ip'} = undef; $smtp->{'fwd_helo_host'} = undef; $smtp->{'fwd_ident'} = undef; $smtp->{'lmtp_rcpt'} = []; $smtp->{'string'} = q{}; $self->{'handler'}->{'_Handler'}->top_close_callback(); $smtp->{'init_required'} = 1; $self->smtp_init(); my $smtp_conf = $self->get_smtp_config(); my $handler = $self->{'handler'}->{'_Handler'}; if ( $handler->{'exit_on_close'} ) { $smtp->{'last_command'} = 1; $handler->dbgout( 'EXIT ON CLOSE REQUESTED', 'closing on RSET', LOG_INFO ); print $socket "421 4.3.2 Pipeline limit reached\r\n"; return; } if ( $smtp_conf->{'pipeline_limit'} ) { my $count = $smtp->{'count'}; my $limit = $smtp_conf->{'pipeline_limit'}; if ( $count > $limit ) { $smtp->{'last_command'} = 1; $handler->dbgout( 'SMTP Pipeline limit reached', 'closing on RSET', LOG_INFO ); print $socket "421 4.3.2 Pipeline limit reached\r\n"; return; } } print $socket "250 2.0.0 Ok\r\n"; } sub smtp_command_mailfrom { my ( $self, $command ) = @_; my $smtp = $self->{'smtp'}; my $socket = $self->{'socket'}; my $handler = $self->{'handler'}->{'_Handler'}; $self->smtp_status('smtp.i.mailfrom'); my $returncode; if ( $smtp->{'has_data'} ) { $self->logerror( "Out of Order SMTP command: $command" ); print $socket "503 5.5.2 Out of Order\r\n"; return; } if ( $smtp->{'has_mail_from'} ) { $self->logerror( "Out of Order SMTP command: $command" ); print $socket "503 5.5.1 Nested MAIL Command\r\n"; return; } # Do connect callback here, because of XFORWARD my $host = $smtp->{'fwd_connect_host'} || $smtp->{'connect_host'}; my $ip = $smtp->{'fwd_connect_ip'} || $smtp->{'connect_ip'}; my $helo = $smtp->{'fwd_helo_host'} || $smtp->{'helo_host'}; if ( substr( $ip, 0, 5 ) eq 'IPv6:' ) { $ip = substr( $ip, 5 ); } # Do connection remapping first $handler->remap_connect_callback( $host, Net::IP->new( $ip ) ); $handler->remap_helo_callback( $helo ); $self->logdebug( "Inbound IP Address " . $handler->{'ip_object'}->short() ); $returncode = $handler->top_connect_callback( $host, $handler->{'ip_object'} ); if ( $returncode == SMFIS_CONTINUE ) { $returncode = $handler->top_helo_callback( $handler->{'helo_name'} ); if ( $returncode == SMFIS_CONTINUE ) { my $envfrom = command_param( $command,10 ); $smtp->{'mail_from'} = $envfrom; $returncode = $handler->top_envfrom_callback( split_command_params( $envfrom ) ); if ( $returncode == SMFIS_CONTINUE ) { $smtp->{'has_mail_from'} = 1; print $socket "250 2.0.0 Ok\r\n"; } elsif ( my $reject_reason = $handler->get_reject_mail() ) { $handler->clear_reject_mail(); $self->loginfo ( "SMTPReject: $reject_reason" ); print $socket $reject_reason . "\r\n"; } elsif ( my $defer_reason = $handler->get_defer_mail() ) { $handler->clear_defer_mail(); $self->loginfo ( "SMTPDefer: $defer_reason" ); print $socket $defer_reason . "\r\n"; } else { print $socket "451 4.0.0 MAIL - That's not right\r\n"; } } elsif ( my $reject_reason = $handler->get_reject_mail() ) { $handler->clear_reject_mail(); $self->loginfo ( "SMTPReject: $reject_reason" ); print $socket $reject_reason . "\r\n"; } elsif ( my $defer_reason = $handler->get_defer_mail() ) { $handler->clear_defer_mail(); $self->loginfo ( "SMTPDefer: $defer_reason" ); print $socket $defer_reason . "\r\n"; } else { print $socket "451 4.0.1 HELO - That's not right\r\n"; } } elsif ( my $reject_reason = $handler->get_reject_mail() ) { $handler->clear_reject_mail(); $self->loginfo ( "SMTPReject: $reject_reason" ); print $socket $reject_reason . "\r\n"; } elsif ( my $defer_reason = $handler->get_defer_mail() ) { $handler->clear_defer_mail(); $self->loginfo ( "SMTPDefer: $defer_reason" ); print $socket $defer_reason . "\r\n"; } else { print $socket "451 4.0.2 Connection - That's not right\r\n"; } } sub smtp_command_rcptto { my ( $self, $command ) = @_; my $smtp = $self->{'smtp'}; my $socket = $self->{'socket'}; my $handler = $self->{'handler'}->{'_Handler'}; $self->smtp_status('smtp.i.rcptto'); if ( $smtp->{'has_data'} ) { $self->logerror( "Out of Order SMTP command: $command" ); print $socket "503 5.5.2 Out of Order\r\n"; return; } my $envrcpt = command_param( $command,8 ); push @{ $smtp->{'rcpt_to'} }, $envrcpt; my $returncode = $handler->top_envrcpt_callback( split_command_params( $envrcpt ) ); if ( $returncode == SMFIS_CONTINUE ) { push @{ $smtp->{'lmtp_rcpt'} }, $envrcpt; print $socket "250 2.0.0 Ok\r\n"; } elsif ( my $reject_reason = $handler->get_reject_mail() ) { $handler->clear_reject_mail(); $self->loginfo ( "SMTPReject: $reject_reason" ); print $socket $reject_reason . "\r\n"; } elsif ( my $defer_reason = $handler->get_defer_mail() ) { $handler->clear_defer_mail(); $self->loginfo ( "SMTPDefer: $defer_reason" ); print $socket $defer_reason . "\r\n"; } else { print $socket "451 4.0.3 That's not right\r\n"; } } sub smtp_command_data { my ( $self, $command ) = @_; my $smtp = $self->{'smtp'}; my $socket = $self->{'socket'}; my $handler = $self->{'handler'}->{'_Handler'}; $self->smtp_status('smtp.i.data'); my $body = q{}; my $done = 0; my $fail = 0; my @header_split; my $returncode; if ( $smtp->{'has_data'} ) { $self->logerror( "Repeated SMTP DATA command: $command" ); print $socket "503 5.5.2 One at a time please\r\n"; return; } print $socket "354 2.0.0 Send body\r\n"; local $SIG{'ALRM'} = sub{ die "Timeout\n" }; eval{ alarm( $smtp->{'smtp_timeout_in'} ); HEADERS: while ( my $dataline = <$socket> ) { $self->extra_debugging( "RAW DEBUG: ". $dataline ); alarm( 0 ); $dataline =~ s/\r?\n$//; if ( $dataline eq '.' ) { $done = 1; last HEADERS; } # Handle transparency if ( $dataline =~ /^\./ ) { $dataline = substr( $dataline, 1 ); } if ( $dataline eq q{} ) { last HEADERS; } push @header_split, $dataline; alarm( $smtp->{'smtp_timeout_in'} ); } }; if ( my $error = $@ ) { $self->logerror( "Read Error: $error" ); $done = 1; $fail = 1; } alarm( 0 ); $self->smtp_status('smtp.i.data.process'); my $value = q{}; foreach my $header_line ( @header_split ) { if ( $header_line =~ /^\s/ ) { $value .= "\r\n" . $header_line; } else { if ( $value ) { push @{ $smtp->{'headers'} } , $value; my ( $hkey, $hvalue ) = split ( ':', $value, 2 ); $hvalue =~ s/^\s+//; $hkey =~ s/^\s+//; $hkey =~ s/\s+$//; if ( ! $fail ) { my $returncode = $handler->top_header_callback( $hkey, $hvalue, $value ); if ( $returncode != SMFIS_CONTINUE ) { $fail = 1; } } } $value = $header_line; } } if ( $value ) { push @{ $smtp->{'headers'} } , $value; my ( $hkey, $hvalue ) = split ( ':', $value, 2 ); $hvalue =~ s/^\s+//; $hkey =~ s/^\s+//; $hkey =~ s/\s+$//; if ( ! $fail ) { my $returncode = $handler->top_header_callback( $hkey, $hvalue, $value ); if ( $returncode != SMFIS_CONTINUE ) { $fail = 1; } } } if ( ! $fail ) { $returncode = $handler->top_eoh_callback(); if ( $returncode != SMFIS_CONTINUE ) { $fail = 1; } } my $smtp_conf = $self->get_smtp_config(); my $chunk_limit = 1048576; # Process in chunks no larger than... if ( exists ( $smtp_conf->{ 'chunk_limit' } ) ) { $chunk_limit = $smtp_conf->{ 'chunk_limit' }; } my $temp_file; if ( exists ( $smtp_conf->{ 'temp_dir' } ) ) { # TODO use spool_dir to create this # add config option to en/disable this # allow dir to be overridden $temp_file = File::Temp->new( DIR => $smtp_conf->{ 'temp_dir' } ); } $self->smtp_status('smtp.i.body'); my $body_chunk = q{}; if ( ! $done ) { eval { alarm( $smtp->{'smtp_timeout_in'} ); DATA: while ( my $dataline = <$socket> ) { $self->extra_debugging( "RAW DEBUG: ". $dataline ); alarm( 0 ); last DATA if $dataline =~ /^\.\r\n/; # Handle transparency if ( $dataline =~ /^\./ ) { $dataline = substr( $dataline, 1 ); } if ( $temp_file ) { print $temp_file $dataline; } else { $body .= $dataline; } if ( length( $body_chunk ) + length( $dataline ) > $chunk_limit ) { $returncode = $handler->top_body_callback( $body_chunk ); if ( $returncode != SMFIS_CONTINUE ) { $fail = 1; } $body_chunk = q{}; } $body_chunk .= $dataline; alarm( $smtp->{'smtp_timeout_in'} ); } if ( ! $fail ) { $returncode = $handler->top_body_callback( $body_chunk ); if ( $returncode != SMFIS_CONTINUE ) { $fail = 1; } $body_chunk = q{}; } }; if ( my $error = $@ ) { $self->logerror( "Read Error: $error" ); $done = 1; $fail = 1; } alarm( 0 ); } if ( ! $fail ) { $returncode = $handler->top_eom_callback(); if ( $returncode != SMFIS_CONTINUE ) { $fail = 1; } } $self->smtp_status('smtp.i.data.received'); if ( ! $fail ) { if ( $temp_file ) { $smtp->{'spool'} = $temp_file; } else { $smtp->{'body'} = $body; } if ( $self->smtp_forward_to_destination() ) { if ( my $reject_reason = $handler->get_quarantine_mail() ) { $handler->metric_count( 'mail_processed_total', { 'result' => 'quarantined' } ); } else { $handler->metric_count( 'mail_processed_total', { 'result' => 'accepted' } ); } $handler->dbgout( 'Accept string', $smtp->{'string'}, LOG_INFO ); $smtp->{'has_data'} = 1; if ( $smtp->{'using_lmtp'} ) { foreach my $rcpt_to ( @{ $smtp->{'lmtp_rcpt'} } ) { print $socket "250 2.0.0 Queued as " . $self->smtp_queue_id() . "\r\n"; } } else { print $socket "250 2.0.0 Queued as " . $self->smtp_queue_id() . "\r\n"; } } else { $self->logerror( "SMTP Mail Rejected" ); my $error = '451 4.0.4 That\'s not right'; my $upstream_error = $smtp->{'string'}; if ( $upstream_error =~ /^4\d\d / ) { $handler->metric_count( 'mail_processed_total', { 'result' => 'deferred' } ); $error = $upstream_error; } elsif ( $upstream_error =~ /^5\d\d / ) { # Also pass back rejects $handler->metric_count( 'mail_processed_total', { 'result' => 'rejected' } ); $error = $upstream_error; } else { $handler->metric_count( 'mail_processed_total', { 'result' => 'deferred_error' } ); $error .= ': ' . $upstream_error; } if ( $smtp->{'using_lmtp'} ) { foreach my $rcpt_to ( @{ $smtp->{'lmtp_rcpt'} } ) { print $socket "$error\r\n"; } } else { print $socket "$error\r\n"; } } } elsif ( my $reject_reason = $handler->get_reject_mail() ) { $handler->metric_count( 'mail_processed_total', { 'result' => 'rejected' } ); $handler->clear_reject_mail(); if ( $smtp->{'using_lmtp'} ) { foreach my $rcpt_to ( @{ $smtp->{'lmtp_rcpt'} } ) { $self->loginfo ( "SMTPReject: $reject_reason" ); print $socket $reject_reason . "\r\n"; } } else { $self->loginfo ( "SMTPReject: $reject_reason" ); print $socket $reject_reason . "\r\n"; } } elsif ( my $defer_reason = $handler->get_defer_mail() ) { $handler->metric_count( 'mail_processed_total', { 'result' => 'defered' } ); $handler->clear_defer_mail(); if ( $smtp->{'using_lmtp'} ) { foreach my $rcpt_to ( @{ $smtp->{'lmtp_rcpt'} } ) { $self->loginfo ( "SMTPDefer: $defer_reason" ); print $socket $defer_reason . "\r\n"; } } else { $self->loginfo ( "SMTPDefer: $defer_reason" ); print $socket $defer_reason . "\r\n"; } } else { $handler->metric_count( 'mail_processed_total', { 'result' => 'deferred_error' } ); if ( $smtp->{'using_lmtp'} ) { foreach my $rcpt_to ( @{ $smtp->{'lmtp_rcpt'} } ) { print $socket "451 4.0.5 That's not right\r\n"; } } else { print $socket "451 4.0.6 That's not right\r\n"; } } $self->smtp_status('smtp.i.data.done'); # Reset $smtp->{'mail_from'} = q{}; $smtp->{'rcpt_to'} = []; $smtp->{'headers'} = []; $smtp->{'body'} = q{}; $smtp->{'has_data'} = 0; $smtp->{'fwd_connect_host'} = undef; $smtp->{'fwd_connect_ip'} = undef; $smtp->{'fwd_helo_host'} = undef; $smtp->{'lmtp_rcpt'} = []; $smtp->{'string'} = q{}; $self->{'handler'}->{'_Handler'}->top_close_callback(); $smtp->{'init_required'} = 1; if ( $handler->{'exit_on_close'} ) { $smtp->{'last_command'} = 1; $handler->dbgout( 'EXIT ON CLOSE REQUESTED', 'NO MORE SMTP', LOG_INFO ); } } sub smtp_insert_received_header { my ( $self ) = @_; my $smtp = $self->{'smtp'}; my $value = join ( q{}, 'from ', $smtp->{'helo_host'}, ' (', $smtp->{'connect_host'} , ' [', $smtp->{'connect_ip'}, '])', "\r\n", ' by ', $smtp->{'server_name'}, ' (Authentication Milter)', ' with ESMTP', "\r\n", ' id ', $self->smtp_queue_id(), ';', "\r\n", ' ', email_date(), ); splice @{ $smtp->{'headers'} }, 0, 0, 'Received: '. $value; } sub smtp_forward_to_destination { my ( $self ) = @_; $self->smtp_status('smtp.o'); my $smtp = $self->{'smtp'}; $self->smtp_insert_received_header(); my $smtp_conf = $self->get_smtp_config(); my $sock = $smtp->{'destination_sock'}; my $new_sock = 0; my $line; if ( $sock ) { if ( ! $sock->connected() ) { $self->logerror( "Outbound SMTP socket was disconnected by remote end" ); undef $sock; } } if ( ! $sock ) { $new_sock = 1; $self->smtp_status('smtp.o.open'); if ( $smtp_conf->{'sock_type'} eq 'inet' ) { $sock = IO::Socket::INET->new( 'Proto' => 'tcp', 'PeerAddr' => $smtp_conf->{'sock_host'}, 'PeerPort' => $smtp_conf->{'sock_port'}, ); } elsif ( $smtp_conf->{'sock_type'} eq 'unix' ) { $sock = IO::Socket::UNIX->new( 'Peer' => $smtp_conf->{'sock_path'}, ); } else { $self->logerror( 'Outbound SMTP Socket type unknown or undefined: ' . $smtp_conf->{'sock_type'} ); return 0; } if ( ! $sock ) { $self->logerror( "Could not open outbound SMTP socket: $!" ); return 0; } eval { $line = <$sock>; }; if ( my $error = $@ ) { $self->logerror( "Outbound SMTP Read Error: $error" ); return 0; } alarm( 0 ); if ( ! $line =~ /250/ ) { $self->logerror( "Unexpected SMTP response $line" ); return 0; } $smtp->{'destination_sock'} = $sock; } $self->logdebug( 'Sending envelope to destination' ); if ( $new_sock ) { $self->send_smtp_packet( $sock, 'EHLO ' . $smtp->{'server_name'}, '250' ) || return; } else { $self->send_smtp_packet( $sock, 'RSET', '250' ) || return; } if ( $smtp->{'fwd_helo_host'} ) { $self->send_smtp_packet( $sock, 'XFORWARD HELO=' . $smtp->{'fwd_helo_host'}, '250' ) || return; } if ( $smtp->{'fwd_connect_ip'} ) { $self->send_smtp_packet( $sock, 'XFORWARD ADDR=' . $smtp->{'fwd_connect_ip'}, '250' ) || return; } if ( $smtp->{'fwd_connect_host'} ) { $self->send_smtp_packet( $sock, 'XFORWARD NAME=' . $smtp->{'fwd_connect_host'}, '250' ) || return; } if ( $smtp->{'fwd_ident'} ) { $self->send_smtp_packet( $sock, 'XFORWARD IDENT=' . $smtp->{'fwd_ident'}, '250' ) || return; } $self->send_smtp_packet( $sock, 'MAIL FROM:' . $smtp->{'mail_from'}, '250' ) || return; foreach my $rcpt_to ( @{ $smtp->{'rcpt_to'} } ) { $self->send_smtp_packet( $sock, 'RCPT TO:' . $rcpt_to, '250' ) || return; } $self->logdebug( 'Sending data to destination' ); $self->send_smtp_packet( $sock, 'DATA', '354' ) || return; $self->smtp_status('smtp.o.body'); my $email = q{}; foreach my $header ( @{ $smtp->{'headers'} } ) { $email .= "$header\r\n"; } $email .= "\r\n"; my $spool = $smtp->{'spool'}; if ( $spool ) { # Handle transparency - should not be any in headers, but for completeness $email =~ s/\015\012\./\015\012\.\./g; print $sock $email; seek( $spool, 0, 0 ); while ( my $line = <$spool> ) { $line =~ s/\015?\012/\015\012/g; $line =~ s/^\./\.\./g; print $sock $line; } } else { my $body = $smtp->{'body'}; $body =~ s/\015?\012/\015\012/g; $email .= $body; # Handle transparency $email =~ s/\015\012\./\015\012\.\./g; print $sock $email; } $self->logdebug( 'Sending end to destination' ); $self->send_smtp_packet( $sock, '.', '250' ) || return; $self->logdebug( 'Sent to destination' ); $self->smtp_status('smtp.o.done'); return 1; } sub close_destination_socket { my ( $self ) = @_; my $smtp = $self->{'smtp'}; my $sock = $smtp->{'destination_sock'}; return if ! $sock; $self->send_smtp_packet( $sock, 'QUIT', '221' ) || return; $sock->close(); delete $smtp->{'destination_sock'}; } sub send_smtp_packet { my ( $self, $socket, $send, $expect ) = @_; my $smtp = $self->{'smtp'}; my $status = lc $send; $status =~ s/^([^ ]+) .*$/$1/; $status = 'dot' if $status eq '.'; $self->smtp_status('smtp.o.' . $status); print $socket "$send\r\n"; $self->smtp_status('smtp.o.' . $status . '.wait'); local $SIG{'ALRM'} = sub{ die "Timeout\n" }; alarm( $smtp->{'smtp_timeout_out'} ); my $recv; eval { $recv = <$socket>; $self->extra_debugging( "RAW DEBUG: ". $recv ); while ( $recv =~ /^\d\d\d\-/ ) { $self->smtp_status('smtp.o.' . $status . '.waitext'); $recv = <$socket>; $self->extra_debugging( "RAW DEBUG: ". $recv ); } }; if ( my $error = $@ ) { $self->logerror( "Outbound SMTP Read Error: $error" ); $smtp->{'string'} = $error; return 0; } alarm( 0 ); $self->smtp_status('smtp.o'); $smtp->{'string'} = $recv || q{}; $smtp->{'string'} =~ s/\r//g; $smtp->{'string'} =~ s/\n//g; if ( $recv =~ /^$expect/ ) { return 1; } else { $self->logerror( "SMTP Send expected $expect received $recv when sending $send" ); return 0; } } sub add_header { my ( $self, $header, $value ) = @_; my $smtp = $self->{'smtp'}; $value =~ s/\015?\012/\015\012/g; push @{ $smtp->{'headers'} } , "$header: $value"; } sub change_header { my ( $self, $header, $index, $value ) = @_; my $smtp = $self->{'smtp'}; my $header_i = 0; my $search_i = 0; my $result_i; HEADER: foreach my $header_v ( @{ $smtp->{'headers'} } ) { if ( substr( lc $header_v, 0, length($header) + 1 ) eq lc "$header:" ) { $search_i ++; if ( $search_i == $index ) { $result_i = $header_i; last HEADER; } } $header_i ++; } if ( $result_i ) { if ( $value eq q{} ) { splice @{ $smtp->{'headers'} }, $result_i, 1; } else { $value =~ s/\015?\012/\015\012/g; $smtp->{'headers'}->[ $result_i ] = "$header: $value"; #untested. } } } sub insert_header { my ( $self, $index, $key, $value ) = @_; my $smtp = $self->{'smtp'}; $value =~ s/\015?\012/\015\012/g; splice @{ $smtp->{'headers'} }, $index - 1, 0, "$key: $value"; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Mail::Milter::Authentication::Protocol::SMTP - SMTP protocol handling =head1 VERSION version 3.20250611 =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 implenmetation of email authentication standards rolled up into a single easy to use milter. =head1 FUNCTIONS =over =item I Extract parameters from a SMTP command line. =back =head1 METHODS =over =item register_metrics Return details of the metrics this module exports. =item I Process the command from the SMTP protocol stream. =item I Return the SMTP config for the given connection, or the default config if no connection specific config exists. =item I Return the smtp queue type, either before or after A before queue will not have an upstream queue id, an after queue will. =item I Send an SMTP command to the protocol stream. Expecting a response $expect. =item I Process the SMTP DATA command. =item I Process the SMTP EHLO command. =item I Process the SMTP HELO command. =item I Process the LMTP LHLO command. =item I Process the SMTP MAIL FROM command. =item I Process the SMTP RCPT TO command. =item I Process the SMTP RSET command. =item I Process the SMTP XFORWARD command. =item I Send the received SMTP transaction on to its destination with authentication results headers (etc) added. =item I QUIT and close the destination socket if open. =item I Initialise transaction data as/when required. =item I Insert a SMTP Received header into the email. =item I Return a generated Queue ID for the email. This can include the received ID from XFORWARD. =item I Add a header =item I Change a header =item I Insert a header =item I Update the process name status line =back =head1 AUTHOR Marc Bradshaw =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