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 126     126   2952 use 5.20.0;
  126         450  
3 126     126   724 use strict;
  126         655  
  126         3108  
4 126     126   962 use warnings;
  126         855  
  126         3389  
5 126     126   919 use Mail::Milter::Authentication::Pragmas;
  126         276  
  126         1183  
6             # ABSTRACT: Milter protocol handling
7             our $VERSION = '3.20230629'; # VERSION
8 126     126   30756 use Net::IP;
  126         491  
  126         403868  
9              
10             sub register_metrics {
11             return {
12 20     20 1 249 'mail_processed_total' => 'Number of emails processed',
13             };
14             }
15              
16             sub protocol_process_request {
17 33     33 1 219 my ( $self ) = @_;
18              
19 33         206 my $handler = $self->{'handler'}->{'_Handler'};
20 33         374 $handler->top_setup_callback();
21              
22             COMMAND:
23 33         211 while ( 1 ) {
24              
25             # Get packet length
26 923   50     11389 my $length = unpack('N', $self->milter_read_block(4) ) || last;
27 923 50 33     4849 $self->fatal("bad packet length $length") if ($length <= 0 || $length > 131072);
28              
29             # Get command
30 923   50     3705 my $command = $self->milter_read_block(1) || last;
31 923         6119 $self->logdebug( "receive command $command" );
32              
33             # Get data
34 923         3302 my $data = $self->milter_read_block($length - 1);
35 923 50       3364 if ( ! defined ( $data ) ) {
36 0         0 $self->fatal('EOF in stream');
37             }
38              
39 923 100       3038 last COMMAND if $command eq SMFIC_QUIT;
40 890         3167 $self->milter_process_command( $command, $data );
41              
42             }
43             }
44              
45             sub milter_process_command {
46 890     890 1 2909 my ( $self, $command, $buffer ) = @_;
47 890         4102 $self->logdebug ( "process command $command" );
48              
49 890         2356 my $handler = $self->{'handler'}->{'_Handler'};
50              
51 890         1962 my $returncode = SMFIS_CONTINUE;
52              
53 890 100       6619 if ( $command eq SMFIC_CONNECT ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    0          
    0          
54 33         421 my ( $host, $ip ) = $self->milter_process_connect( $buffer );
55 33         545 $handler->remap_connect_callback( $host, $ip );
56 33         405 $returncode = $handler->top_connect_callback( $host, $handler->{'ip_object'} );
57             }
58             elsif ( $command eq SMFIC_ABORT ) {
59 33         261 $returncode = $handler->top_abort_callback();
60             }
61             elsif ( $command eq SMFIC_BODY ) {
62 31         452 $returncode = $handler->top_body_callback( $buffer );
63             }
64             elsif ( $command eq SMFIC_MACRO ) {
65 196 50       1817 $self->fatal('SMFIC_MACRO: empty packet') unless ( $buffer =~ s/^(.)// );
66 196         881 my $code = $1;
67 196         953 my $data = $self->milter_split_buffer( $buffer );
68 196 100       1021 push ( @$data, q{} ) if (( @$data & 1 ) != 0 ); # pad last entry with empty string if odd number
69 196         1533 my %datahash = @$data;
70 196         932 foreach my $key ( keys %datahash ) {
71 328         1468 $handler->set_symbol( $code, $key, $datahash{$key} );
72             }
73 196         1040 undef $returncode;
74             }
75             elsif ( $command eq SMFIC_BODYEOB ) {
76 33         310 $returncode = $handler->top_eom_callback();
77 33 100       207 if ( $returncode == SMFIS_CONTINUE ) {
78 31         213 $handler->metric_count( 'mail_processed_total', { 'result' => 'accepted' } );
79             }
80             }
81             elsif ( $command eq SMFIC_HELO ) {
82 33         194 my $helo = $self->milter_split_buffer( $buffer );
83 33         435 $handler->remap_helo_callback( @$helo );
84 33         262 $returncode = $handler->top_helo_callback( $handler->{'helo_name'} );
85             }
86             elsif ( $command eq SMFIC_HEADER ) {
87 399         1347 my $header = $self->milter_split_buffer( $buffer );
88 399 50       1429 if ( @$header == 1 ) { push @$header , q{}; };
  0         0  
89 399 50       1894 my $original = join( $self->{'headers_include_space'} ? ':': ': ', @$header );
90 399         1152 push @$header, $original;
91 399         1609 $header->[1] =~ s/^\s+//;
92 399         1158 $header->[0] =~ s/^\s+//;
93 399         1233 $header->[0] =~ s/\s+$//;
94 399         1815 $returncode = $handler->top_header_callback( @$header );
95             }
96             elsif ( $command eq SMFIC_MAIL ) {
97 33         251 my $envfrom = $self->milter_split_buffer( $buffer );
98 33         492 $returncode = $handler->top_envfrom_callback( @$envfrom );
99             }
100             elsif ( $command eq SMFIC_EOH ) {
101 33         372 $returncode = $handler->top_eoh_callback();
102             }
103             elsif ( $command eq SMFIC_OPTNEG ) {
104 33 50       218 $self->fatal('SMFIC_OPTNEG: packet has wrong size') unless (length($buffer) == 12);
105 33         340 my ($ver, $actions, $protocol) = unpack('NNN', $buffer);
106 33 50 33     515 $self->fatal("SMFIC_OPTNEG: unknown milter protocol version $ver") unless ($ver >= 2 && $ver <= 6);
107 33         252 my $actions_reply = $self->{'callback_flags'} & $actions;
108 33         151 my $protocol_reply = $self->{'protocol'} & $protocol;
109 33         1533 $self->write_packet(SMFIC_OPTNEG,
110             pack('NNN', 2, $actions_reply, $protocol_reply)
111             );
112 33         744 undef $returncode;
113 33         200 $self->{'headers_include_space'} = ($protocol_reply & SMFIP_HDR_LEADSPC) != 0;
114             }
115             elsif ( $command eq SMFIC_RCPT ) {
116 33         193 my $envrcpt = $self->milter_split_buffer( $buffer );
117 33         410 $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         2273 my $config = $self->{'config'};
130              
131 890         2965 my $reject_reason;
132             my $defer_reason;
133 890         0 my $quarantine_reason;
134 890 100       2623 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       43 if ( $config->{'milter_quarantine'} ) {
144 0         0 $handler->clear_quarantine_mail();
145 0         0 $returncode = SMFIR_QUARANTINE;
146             }
147             else {
148 8         20 undef $quarantine_reason;
149             }
150             }
151              
152 890 100       2858 if (defined $returncode) {
153 661 50       2719 if ( $returncode eq SMFIR_QUARANTINE ) {
    100          
    100          
    50          
    0          
    0          
154             # NOP
155             }
156             elsif ( $returncode == SMFIS_CONTINUE ) {
157 637         1400 $returncode = SMFIR_CONTINUE;
158             }
159             elsif ( $returncode == SMFIS_TEMPFAIL ) {
160 23         111 $returncode = SMFIR_TEMPFAIL;
161             }
162             elsif ( $returncode == SMFIS_REJECT ) {
163 1         4 $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       2179 if ( $config->{'dryrun'} ) {
174 46 100       227 if ( $returncode ne SMFIR_CONTINUE ) {
175 23         254 $self->loginfo ( "dryrun returncode changed from $returncode to continue" );
176 23         31323 $returncode = SMFIR_CONTINUE;
177             }
178             }
179              
180 661 100       2250 if ( $command ne SMFIC_ABORT ) {
181 628 100       2459 if ( $reject_reason ) {
    50          
    50          
182 1         7 my ( $rcode, $xcode, $message ) = split( ' ', $reject_reason, 3 );
183 1 50 33     56 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         25 $handler->metric_count( 'mail_processed_total', { 'result' => 'rejected' } );
190 1         37 $self->loginfo ( "SMTPReject: $reject_reason" );
191 1         708 $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         2016 $self->write_packet($returncode);
223             }
224             }
225             }
226             }
227              
228             sub milter_process_connect {
229 33     33 1 182 my ( $self, $buffer ) = @_;
230              
231 33 50       489 unless ($buffer =~ s/^([^\0]*)\0(.)//) {
232 0         0 $self->fatal('SMFIC_CONNECT: invalid connect info');
233             }
234 33         194 my $ip;
235 33         219 my $host = $1;
236              
237 33         399 my ($port, $addr) = unpack('nZ*', $buffer);
238              
239 33 50       271 if ( substr( $addr, 0, 5 ) eq 'IPv6:' ) {
240 0         0 $addr = substr( $addr, 5 );
241             }
242              
243 33 50       334 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         114 eval {
255 33         953 $ip = Net::IP->new( $addr );
256             };
257 33 50       41117 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         233 return ( $host, $ip );
265             }
266              
267             sub milter_read_block {
268 2769     2769 1 7224 my ( $self, $len ) = @_;
269 2769         6138 my $socket = $self->{'socket'};
270 2769         4952 my $sofar = 0;
271 2769         5199 my $buffer = q{};
272 2769         6935 while ($len > $sofar) {
273 2637         10822 my $read = $socket->sysread($buffer, $len - $sofar, $sofar);
274 2637 50 33     559271 last if (!defined($read) || $read <= 0); # EOF
275 2637         8744 $sofar += $read;
276             }
277 2769         12922 return $buffer;
278             }
279              
280             sub milter_split_buffer {
281 694     694 1 1827 my ( $self, $buffer ) = @_;
282 694         5149 $buffer =~ s/\0$//; # remove trailing NUL
283 694         4108 return [ split(/\0/, $buffer) ];
284             };
285              
286             ##
287              
288             sub add_header {
289 9     9 1 23 my ( $self, $header, $value ) = @_;
290 9         97 $value =~ s/\015\012/\012/g;
291             $self->write_packet( SMFIR_ADDHEADER,
292             $header
293             . "\0"
294 9 50       82 . ($self->{'headers_include_space'} ? ' ' : '')
295             . $value
296             . "\0"
297             );
298             }
299              
300             sub change_header {
301 13     13 1 44 my ( $self, $header, $index, $value ) = @_;
302 13 50       37 $value = '' unless defined($value);
303 13         34 $value =~ s/\015\012/\012/g;
304             $self->write_packet( SMFIR_CHGHEADER,
305             pack('N', $index)
306             . $header
307             . "\0"
308 13 50       89 . ($self->{'headers_include_space'} ? ' ' : '')
309             . $value
310             . "\0"
311             );
312             }
313              
314             sub insert_header {
315 60     60 1 249 my ( $self, $index, $key, $value ) = @_;
316 60         272 $value =~ s/\015\012/\012/g;
317             $self->write_packet( SMFIR_INSHEADER,
318             pack( 'N', $index )
319             . $key
320             . "\0"
321 60 50       664 . ($self->{'headers_include_space'} ? ' ' : '')
322             . $value
323             . "\0"
324             );
325             }
326              
327             sub write_packet {
328 743     743 1 2303 my ( $self, $code, $data ) = @_;
329 743         4224 $self->logdebug ( "send command $code" );
330 743         1909 my $socket = $self->{'socket'};
331 743 100       4440 $data = q{} unless defined($data);
332 743         3042 my $len = pack('N', length($data) + 1);
333 743         4335 $socket->syswrite($len);
334 743         43694 $socket->syswrite($code);
335 743         27259 $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.20230629
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