File Coverage

blib/lib/Mail/Milter/Authentication/Protocol/Milter.pm
Criterion Covered Total %
statement 138 171 80.7
branch 71 106 66.9
condition 7 25 28.0
subroutine 15 15 100.0
pod 10 10 100.0
total 241 327 73.7


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Protocol::Milter;
2 127     127   3022 use 5.20.0;
  127         451  
3 127     127   759 use strict;
  127         268  
  127         2848  
4 127     127   583 use warnings;
  127         416  
  127         3455  
5 127     127   762 use Mail::Milter::Authentication::Pragmas;
  127         697  
  127         1062  
6             # ABSTRACT: Milter protocol handling
7             our $VERSION = '3.20230911'; # VERSION
8 127     127   30903 use Net::IP;
  127         330  
  127         395666  
9              
10             sub register_metrics {
11             return {
12 20     20 1 376 'mail_processed_total' => 'Number of emails processed',
13             };
14             }
15              
16             sub protocol_process_request {
17 33     33 1 133 my ( $self ) = @_;
18              
19 33         206 my $handler = $self->{'handler'}->{'_Handler'};
20 33         371 $handler->top_setup_callback();
21              
22             COMMAND:
23 33         203 while ( 1 ) {
24              
25             # Get packet length
26 923   50     11308 my $length = unpack('N', $self->milter_read_block(4) ) || last;
27 923 50 33     4870 $self->fatal("bad packet length $length") if ($length <= 0 || $length > 131072);
28              
29             # Get command
30 923   50     3393 my $command = $self->milter_read_block(1) || last;
31 923         5745 $self->logdebug( "receive command $command" );
32              
33             # Get data
34 923         2921 my $data = $self->milter_read_block($length - 1);
35 923 50       2943 if ( ! defined ( $data ) ) {
36 0         0 $self->fatal('EOF in stream');
37             }
38              
39 923 100       2845 last COMMAND if $command eq SMFIC_QUIT;
40 890         3708 $self->milter_process_command( $command, $data );
41              
42             }
43             }
44              
45             sub milter_process_command {
46 890     890 1 2745 my ( $self, $command, $buffer ) = @_;
47 890         4423 $self->logdebug ( "process command $command" );
48              
49 890         2267 my $handler = $self->{'handler'}->{'_Handler'};
50              
51 890         2186 my $returncode = SMFIS_CONTINUE;
52              
53 890 100       6383 if ( $command eq SMFIC_CONNECT ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    0          
    0          
54 33         366 my ( $host, $ip ) = $self->milter_process_connect( $buffer );
55 33         458 $handler->remap_connect_callback( $host, $ip );
56 33         518 $returncode = $handler->top_connect_callback( $host, $handler->{'ip_object'} );
57             }
58             elsif ( $command eq SMFIC_ABORT ) {
59 33         425 $returncode = $handler->top_abort_callback();
60             }
61             elsif ( $command eq SMFIC_BODY ) {
62 31         445 $returncode = $handler->top_body_callback( $buffer );
63             }
64             elsif ( $command eq SMFIC_MACRO ) {
65 196 50       1718 $self->fatal('SMFIC_MACRO: empty packet') unless ( $buffer =~ s/^(.)// );
66 196         978 my $code = $1;
67 196         996 my $data = $self->milter_split_buffer( $buffer );
68 196 100       800 push ( @$data, q{} ) if (( @$data & 1 ) != 0 ); # pad last entry with empty string if odd number
69 196         1458 my %datahash = @$data;
70 196         713 foreach my $key ( keys %datahash ) {
71 328         1376 $handler->set_symbol( $code, $key, $datahash{$key} );
72             }
73 196         941 undef $returncode;
74             }
75             elsif ( $command eq SMFIC_BODYEOB ) {
76 33         386 $returncode = $handler->top_eom_callback();
77 33 100       203 if ( $returncode == SMFIS_CONTINUE ) {
78 31         203 $handler->metric_count( 'mail_processed_total', { 'result' => 'accepted' } );
79             }
80             }
81             elsif ( $command eq SMFIC_HELO ) {
82 33         193 my $helo = $self->milter_split_buffer( $buffer );
83 33         451 $handler->remap_helo_callback( @$helo );
84 33         203 $returncode = $handler->top_helo_callback( $handler->{'helo_name'} );
85             }
86             elsif ( $command eq SMFIC_HEADER ) {
87 399         1335 my $header = $self->milter_split_buffer( $buffer );
88 399 50       1360 if ( @$header == 1 ) { push @$header , q{}; };
  0         0  
89 399 50       1813 my $original = join( $self->{'headers_include_space'} ? ':': ': ', @$header );
90 399         1176 push @$header, $original;
91 399         1496 $header->[1] =~ s/^\s+//;
92 399         989 $header->[0] =~ s/^\s+//;
93 399         1207 $header->[0] =~ s/\s+$//;
94 399         1782 $returncode = $handler->top_header_callback( @$header );
95             }
96             elsif ( $command eq SMFIC_MAIL ) {
97 33         179 my $envfrom = $self->milter_split_buffer( $buffer );
98 33         434 $returncode = $handler->top_envfrom_callback( @$envfrom );
99             }
100             elsif ( $command eq SMFIC_EOH ) {
101 33         227 $returncode = $handler->top_eoh_callback();
102             }
103             elsif ( $command eq SMFIC_OPTNEG ) {
104 33 50       216 $self->fatal('SMFIC_OPTNEG: packet has wrong size') unless (length($buffer) == 12);
105 33         237 my ($ver, $actions, $protocol) = unpack('NNN', $buffer);
106 33 50 33     449 $self->fatal("SMFIC_OPTNEG: unknown milter protocol version $ver") unless ($ver >= 2 && $ver <= 6);
107 33         266 my $actions_reply = $self->{'callback_flags'} & $actions;
108 33         144 my $protocol_reply = $self->{'protocol'} & $protocol;
109 33         887 $self->write_packet(SMFIC_OPTNEG,
110             pack('NNN', 2, $actions_reply, $protocol_reply)
111             );
112 33         668 undef $returncode;
113 33         217 $self->{'headers_include_space'} = ($protocol_reply & SMFIP_HDR_LEADSPC) != 0;
114             }
115             elsif ( $command eq SMFIC_RCPT ) {
116 33         139 my $envrcpt = $self->milter_split_buffer( $buffer );
117 33         300 $returncode = $handler->top_envrcpt_callback( @$envrcpt );
118             }
119             elsif ( $command eq SMFIC_DATA ) {
120             }
121             elsif ( $command eq SMFIC_UNKNOWN ) {
122 0         0 undef $returncode;
123             # Unknown SMTP command received
124             }
125             else {
126 0         0 $self->fatal("Unknown milter command $command");
127             }
128              
129 890         2358 my $config = $self->{'config'};
130              
131 890         2734 my $reject_reason;
132             my $defer_reason;
133 890         0 my $quarantine_reason;
134 890 100       2665 if ( $reject_reason = $handler->get_reject_mail() ) {
    50          
    100          
135 1         6 $handler->clear_reject_mail();
136 1         3 $returncode = SMFIS_REJECT;
137             }
138             elsif ( $defer_reason = $handler->get_defer_mail() ) {
139 0         0 $handler->clear_defer_mail();
140 0         0 $returncode = SMFIS_TEMPFAIL;
141             }
142             elsif ( $quarantine_reason = $handler->get_quarantine_mail() ) {
143 8 50       48 if ( $config->{'milter_quarantine'} ) {
144 0         0 $handler->clear_quarantine_mail();
145 0         0 $returncode = SMFIR_QUARANTINE;
146             }
147             else {
148 8         22 undef $quarantine_reason;
149             }
150             }
151              
152 890 100       2782 if (defined $returncode) {
153 661 50       2608 if ( $returncode eq SMFIR_QUARANTINE ) {
    100          
    100          
    50          
    0          
    0          
154             # NOP
155             }
156             elsif ( $returncode == SMFIS_CONTINUE ) {
157 637         1344 $returncode = SMFIR_CONTINUE;
158             }
159             elsif ( $returncode == SMFIS_TEMPFAIL ) {
160 23         121 $returncode = SMFIR_TEMPFAIL;
161             }
162             elsif ( $returncode == SMFIS_REJECT ) {
163 1         3 $returncode = SMFIR_REJECT;
164             }
165             elsif ( $returncode == SMFIS_DISCARD ) {
166 0         0 $returncode = SMFIR_DISCARD;
167             }
168             elsif ( $returncode == SMFIS_ACCEPT ) {
169 0         0 $returncode = SMFIR_ACCEPT;
170 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'accepted' } );
171             }
172              
173 661 100       2049 if ( $config->{'dryrun'} ) {
174 46 100       195 if ( $returncode ne SMFIR_CONTINUE ) {
175 23         295 $self->loginfo ( "dryrun returncode changed from $returncode to continue" );
176 23         32274 $returncode = SMFIR_CONTINUE;
177             }
178             }
179              
180 661 100       2065 if ( $command ne SMFIC_ABORT ) {
181 628 100       2402 if ( $reject_reason ) {
    50          
    50          
182 1         5 my ( $rcode, $xcode, $message ) = split( ' ', $reject_reason, 3 );
183 1 50 33     54 if ($rcode !~ /^[5]\d\d$/ || $xcode !~ /^[5]\.\d\.\d$/ || substr($rcode, 0, 1) ne substr($xcode, 0, 1)) {
      33        
184 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'deferred_error' } );
185 0         0 $self->loginfo ( "Invalid reject message $reject_reason - setting to TempFail" );
186 0         0 $self->write_packet(SMFIR_TEMPFAIL );
187             }
188             else {
189 1         11 $handler->metric_count( 'mail_processed_total', { 'result' => 'rejected' } );
190 1         16 $self->loginfo ( "SMTPReject: $reject_reason" );
191 1         652 $self->write_packet( SMFIR_REPLYCODE,
192             $reject_reason
193             . "\0"
194             );
195             }
196             }
197             elsif ( $defer_reason ) {
198 0         0 my ( $rcode, $xcode, $message ) = split( ' ', $defer_reason, 3 );
199 0 0 0     0 if ($rcode !~ /^[4]\d\d$/ || $xcode !~ /^[4]\.\d\.\d$/ || substr($rcode, 0, 1) ne substr($xcode, 0, 1)) {
      0        
200 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'deferred_error' } );
201 0         0 $self->loginfo ( "Invalid defer message $defer_reason - setting to TempFail" );
202 0         0 $self->write_packet(SMFIR_TEMPFAIL );
203             }
204             else {
205 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'deferred' } );
206 0         0 $self->loginfo ( "SMTPDefer: $defer_reason" );
207 0         0 $self->write_packet( SMFIR_REPLYCODE,
208             $defer_reason
209             . "\0"
210             );
211             }
212             }
213             elsif ( $quarantine_reason ) {
214 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'quarantined' } );
215 0         0 $self->loginfo ( "SMTPQuarantine: $quarantine_reason" );
216 0         0 $self->write_packet( SMFIR_QUARANTINE,
217             $quarantine_reason
218             . "\0"
219             );
220             }
221             else {
222 627         2182 $self->write_packet($returncode);
223             }
224             }
225             }
226             }
227              
228             sub milter_process_connect {
229 33     33 1 223 my ( $self, $buffer ) = @_;
230              
231 33 50       484 unless ($buffer =~ s/^([^\0]*)\0(.)//) {
232 0         0 $self->fatal('SMFIC_CONNECT: invalid connect info');
233             }
234 33         124 my $ip;
235 33         186 my $host = $1;
236              
237 33         311 my ($port, $addr) = unpack('nZ*', $buffer);
238              
239 33 50       285 if ( substr( $addr, 0, 5 ) eq 'IPv6:' ) {
240 0         0 $addr = substr( $addr, 5 );
241             }
242              
243 33 50       305 if ( ! defined ( $addr ) ) {
    50          
244 0         0 $self->logerror('Unknown IP address format UNDEF');
245 0         0 $ip = undef;
246             # Could potentially fail here, connection is likely bad anyway.
247             }
248             elsif ( length ( $addr ) == 0 ) {
249 0         0 $self->logerror('Unknown IP address format NULL');
250 0         0 $ip = undef;
251             # Could potentially fail here, connection is likely bad anyway.
252             }
253             else {
254 33         125 eval {
255 33         892 $ip = Net::IP->new( $addr );
256             };
257 33 50       44020 if ( my $error = $@ ) {
258 0         0 $self->logerror('Unknown IP address format - ' . $addr . ' - ' . $error );
259 0         0 $ip = undef;
260             # Could potentially fail here, connection is likely bad anyway.
261             }
262             }
263              
264 33         227 return ( $host, $ip );
265             }
266              
267             sub milter_read_block {
268 2769     2769 1 7303 my ( $self, $len ) = @_;
269 2769         6469 my $socket = $self->{'socket'};
270 2769         4535 my $sofar = 0;
271 2769         5424 my $buffer = q{};
272 2769         6924 while ($len > $sofar) {
273 2637         10769 my $read = $socket->sysread($buffer, $len - $sofar, $sofar);
274 2637 50 33     500327 last if (!defined($read) || $read <= 0); # EOF
275 2637         8417 $sofar += $read;
276             }
277 2769         13019 return $buffer;
278             }
279              
280             sub milter_split_buffer {
281 694     694 1 1899 my ( $self, $buffer ) = @_;
282 694         4995 $buffer =~ s/\0$//; # remove trailing NUL
283 694         4136 return [ split(/\0/, $buffer) ];
284             };
285              
286             ##
287              
288             sub add_header {
289 9     9 1 28 my ( $self, $header, $value ) = @_;
290 9         63 $value =~ s/\015\012/\012/g;
291             $self->write_packet( SMFIR_ADDHEADER,
292             $header
293             . "\0"
294 9 50       83 . ($self->{'headers_include_space'} ? ' ' : '')
295             . $value
296             . "\0"
297             );
298             }
299              
300             sub change_header {
301 13     13 1 34 my ( $self, $header, $index, $value ) = @_;
302 13 50       37 $value = '' unless defined($value);
303 13         32 $value =~ s/\015\012/\012/g;
304             $self->write_packet( SMFIR_CHGHEADER,
305             pack('N', $index)
306             . $header
307             . "\0"
308 13 50       128 . ($self->{'headers_include_space'} ? ' ' : '')
309             . $value
310             . "\0"
311             );
312             }
313              
314             sub insert_header {
315 60     60 1 185 my ( $self, $index, $key, $value ) = @_;
316 60         265 $value =~ s/\015\012/\012/g;
317             $self->write_packet( SMFIR_INSHEADER,
318             pack( 'N', $index )
319             . $key
320             . "\0"
321 60 50       643 . ($self->{'headers_include_space'} ? ' ' : '')
322             . $value
323             . "\0"
324             );
325             }
326              
327             sub write_packet {
328 743     743 1 4446 my ( $self, $code, $data ) = @_;
329 743         4188 $self->logdebug ( "send command $code" );
330 743         1864 my $socket = $self->{'socket'};
331 743 100       2376 $data = q{} unless defined($data);
332 743         3072 my $len = pack('N', length($data) + 1);
333 743         4336 $socket->syswrite($len);
334 743         44558 $socket->syswrite($code);
335 743         37392 $socket->syswrite($data);
336             }
337              
338             1;
339              
340             __END__
341              
342             =pod
343              
344             =encoding UTF-8
345              
346             =head1 NAME
347              
348             Mail::Milter::Authentication::Protocol::Milter - Milter protocol handling
349              
350             =head1 VERSION
351              
352             version 3.20230911
353              
354             =head1 SYNOPSIS
355              
356             Subclass of Net::Server::PreFork for bringing up the main server process for authentication_milter.
357              
358             Please see Net::Server docs for more detail of the server code.
359              
360             =head1 DESCRIPTION
361              
362             A Perl implementation of email authentication standards rolled up into a single easy to use milter.
363              
364             =head1 METHODS
365              
366             =over
367              
368             =item register_metrics
369              
370             Return details of the metrics this module exports.
371              
372             =item I<protocol_process_command( $command, $buffer )>
373              
374             Process the command from the milter protocol stream.
375              
376             =item I<milter_process_connect( $buffer )>
377              
378             Process a milter connect command.
379              
380             =item I<milter_read_block( $len )>
381              
382             Read $len bytes from the milter protocol stream.
383              
384             =item I<milter_split_buffer( $buffer )>
385              
386             Split the milter buffer at null
387              
388             =item I<add_header( $header, $value )>
389              
390             Write an add header packet
391              
392             =item I<change_header( $header, $index, $value )>
393              
394             Write a change header packet
395              
396             =item I<insert_header( $index, $key, $value )>
397              
398             Writa an insert header packet
399              
400             =item I<write_packet( $code, $data )>
401              
402             Write a packet to the protocol stream.
403              
404             =item I<milter_process_command( $command, $data )>
405              
406             Process the milter command $command with the data from
407             $data.
408              
409             =item I<protocol_process_request()>
410              
411             Receive a new command from the protocol stream and process it.
412              
413             =back
414              
415             =head1 AUTHOR
416              
417             Marc Bradshaw <marc@marcbradshaw.net>
418              
419             =head1 COPYRIGHT AND LICENSE
420              
421             This software is copyright (c) 2020 by Marc Bradshaw.
422              
423             This is free software; you can redistribute it and/or modify it under
424             the same terms as the Perl 5 programming language system itself.
425              
426             =cut