File Coverage

blib/lib/Mail/Milter/Authentication/Protocol/Milter.pm
Criterion Covered Total %
statement 141 174 81.0
branch 66 96 68.7
condition 7 25 28.0
subroutine 15 15 100.0
pod 10 10 100.0
total 239 320 74.6


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Protocol::Milter;
2 99     99   690 use strict;
  99         283  
  99         2833  
3 99     99   538 use warnings;
  99         242  
  99         4178  
4             our $VERSION = '20191206'; # VERSION
5              
6 99     99   580 use English qw{ -no_match_vars };
  99         238  
  99         742  
7 99     99   40596 use Net::IP;
  99         245  
  99         11299  
8              
9 99     99   685 use Mail::Milter::Authentication::Constants qw{ :all };
  99         240  
  99         219619  
10              
11             sub register_metrics {
12             return {
13 16     16 1 139 'mail_processed_total' => 'Number of emails processed',
14             };
15             }
16              
17             sub protocol_process_request {
18 26     26 1 109 my ( $self ) = @_;
19              
20 26         135 my $handler = $self->{'handler'}->{'_Handler'};
21 26         279 $handler->top_setup_callback();
22              
23             COMMAND:
24 26         56 while ( 1 ) {
25              
26             # Get packet length
27 707   50     2429 my $length = unpack('N', $self->milter_read_block(4) ) || last;
28 707 50 33     3246 $self->fatal("bad packet length $length") if ($length <= 0 || $length > 131072);
29              
30             # Get command
31 707   50     2099 my $command = $self->milter_read_block(1) || last;
32 707         3725 $self->logdebug( "receive command $command" );
33              
34             # Get data
35 707         2880 my $data = $self->milter_read_block($length - 1);
36 707 50       2025 if ( ! defined ( $data ) ) {
37 0         0 $self->fatal('EOF in stream');
38             }
39              
40 707 100       2266 last COMMAND if $command eq SMFIC_QUIT;
41 681         2083 $self->milter_process_command( $command, $data );
42              
43             }
44              
45 26         83 return;
46             }
47              
48             sub milter_process_command {
49 681     681 1 1764 my ( $self, $command, $buffer ) = @_;
50 681         2849 $self->logdebug ( "process command $command" );
51              
52 681         1963 my $handler = $self->{'handler'}->{'_Handler'};
53              
54 681         1251 my $returncode = SMFIS_CONTINUE;
55              
56 681 100       4815 if ( $command eq SMFIC_CONNECT ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    0          
    0          
57 26         187 my ( $host, $ip ) = $self->milter_process_connect( $buffer );
58 26         284 $handler->remap_connect_callback( $host, $ip );
59 26         203 $returncode = $handler->top_connect_callback( $host, $handler->{'ip_object'} );
60             }
61             elsif ( $command eq SMFIC_ABORT ) {
62 26         222 $returncode = $handler->top_abort_callback();
63             }
64             elsif ( $command eq SMFIC_BODY ) {
65 26         270 $returncode = $handler->top_body_callback( $buffer );
66             }
67             elsif ( $command eq SMFIC_MACRO ) {
68 156 50       1294 $self->fatal('SMFIC_MACRO: empty packet') unless ( $buffer =~ s/^(.)// );
69 156         759 my $code = $1;
70 156         720 my $data = $self->milter_split_buffer( $buffer );
71 156 100       615 push ( @$data, q{} ) if (( @$data & 1 ) != 0 ); # pad last entry with empty string if odd number
72 156         1314 my %datahash = @$data;
73 156         626 foreach my $key ( keys %datahash ) {
74 260         1254 $handler->set_symbol( $code, $key, $datahash{$key} );
75             }
76 156         563 undef $returncode;
77             }
78             elsif ( $command eq SMFIC_BODYEOB ) {
79 26         246 $returncode = $handler->top_eom_callback();
80 26 100       113 if ( $returncode == SMFIS_CONTINUE ) {
81 24         147 $handler->metric_count( 'mail_processed_total', { 'result' => 'accepted' } );
82             }
83             }
84             elsif ( $command eq SMFIC_HELO ) {
85 26         132 my $helo = $self->milter_split_buffer( $buffer );
86 26         228 $handler->remap_helo_callback( @$helo );
87 26         123 $returncode = $handler->top_helo_callback( $handler->{'helo_name'} );
88             }
89             elsif ( $command eq SMFIC_HEADER ) {
90 291         985 my $header = $self->milter_split_buffer( $buffer );
91 291 50       1035 if ( @$header == 1 ) { push @$header , q{}; };
  0         0  
92 291         808 my $original = join( ':', @$header );
93 291         678 push @$header, $original;
94 291         890 $header->[1] =~ s/^\s+//;
95 291         649 $header->[0] =~ s/^\s+//;
96 291         674 $header->[0] =~ s/\s+$//;
97 291         1229 $returncode = $handler->top_header_callback( @$header );
98             }
99             elsif ( $command eq SMFIC_MAIL ) {
100 26         103 my $envfrom = $self->milter_split_buffer( $buffer );
101 26         380 $returncode = $handler->top_envfrom_callback( @$envfrom );
102             }
103             elsif ( $command eq SMFIC_EOH ) {
104 26         302 $returncode = $handler->top_eoh_callback();
105             }
106             elsif ( $command eq SMFIC_OPTNEG ) {
107 26 50       117 $self->fatal('SMFIC_OPTNEG: packet has wrong size') unless (length($buffer) == 12);
108 26         151 my ($ver, $actions, $protocol) = unpack('NNN', $buffer);
109 26 50 33     241 $self->fatal("SMFIC_OPTNEG: unknown milter protocol version $ver") unless ($ver >= 2 && $ver <= 6);
110 26         101 my $actions_reply = $self->{'callback_flags'} & $actions;
111 26         71 my $protocol_reply = $self->{'protocol'} & $protocol;
112 26         412 $self->write_packet(SMFIC_OPTNEG,
113             pack('NNN', 2, $actions_reply, $protocol_reply)
114             );
115 26         82 undef $returncode;
116             }
117             elsif ( $command eq SMFIC_RCPT ) {
118 26         111 my $envrcpt = $self->milter_split_buffer( $buffer );
119 26         158 $returncode = $handler->top_envrcpt_callback( @$envrcpt );
120             }
121             elsif ( $command eq SMFIC_DATA ) {
122             }
123             elsif ( $command eq SMFIC_UNKNOWN ) {
124 0         0 undef $returncode;
125             # Unknown SMTP command received
126             }
127             else {
128 0         0 $self->fatal("Unknown milter command $command");
129             }
130              
131 681         1556 my $config = $self->{'config'};
132              
133 681         1806 my $reject_reason;
134             my $defer_reason;
135 681         0 my $quarantine_reason;
136 681 100       1842 if ( $reject_reason = $handler->get_reject_mail() ) {
    50          
    100          
137 1         5 $handler->clear_reject_mail();
138 1         3 $returncode = SMFIS_REJECT;
139             }
140             elsif ( $defer_reason = $handler->get_defer_mail() ) {
141 0         0 $handler->clear_defer_mail();
142 0         0 $returncode = SMFIS_TEMPFAIL;
143             }
144             elsif ( $quarantine_reason = $handler->get_quarantine_mail() ) {
145 8 50       30 if ( $config->{'milter_quarantine'} ) {
146 0         0 $handler->clear_quarantine_mail();
147 0         0 $returncode = SMFIR_QUARANTINE;
148             }
149             else {
150 8         19 undef $quarantine_reason;
151             }
152             }
153              
154 681 100       1785 if (defined $returncode) {
155 499 100       1395 if ( $returncode == SMFIS_CONTINUE ) {
    100          
    50          
    0          
    0          
156 475         1028 $returncode = SMFIR_CONTINUE;
157             }
158             elsif ( $returncode == SMFIS_TEMPFAIL ) {
159 23         66 $returncode = SMFIR_TEMPFAIL;
160             }
161             elsif ( $returncode == SMFIS_REJECT ) {
162 1         18 $returncode = SMFIR_REJECT;
163             }
164             elsif ( $returncode == SMFIS_DISCARD ) {
165 0         0 $returncode = SMFIR_DISCARD;
166             }
167             elsif ( $returncode == SMFIS_ACCEPT ) {
168 0         0 $returncode = SMFIR_ACCEPT;
169 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'accepted' } );
170             }
171              
172 499 100       1333 if ( $config->{'dryrun'} ) {
173 46 100       166 if ( $returncode ne SMFIR_CONTINUE ) {
174 23         184 $self->loginfo ( "dryrun returncode changed from $returncode to continue" );
175 23         74 $returncode = SMFIR_CONTINUE;
176             }
177             }
178              
179 499 100       1386 if ( $command ne SMFIC_ABORT ) {
180 473 100       1815 if ( $reject_reason ) {
    50          
    50          
181 1         7 my ( $rcode, $xcode, $message ) = split( ' ', $reject_reason, 3 );
182 1 50 33     50 if ($rcode !~ /^[5]\d\d$/ || $xcode !~ /^[5]\.\d\.\d$/ || substr($rcode, 0, 1) ne substr($xcode, 0, 1)) {
      33        
183 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'deferred_error' } );
184 0         0 $self->loginfo ( "Invalid reject message $reject_reason - setting to TempFail" );
185 0         0 $self->write_packet(SMFIR_TEMPFAIL );
186             }
187             else {
188 1         10 $handler->metric_count( 'mail_processed_total', { 'result' => 'rejected' } );
189 1         42 $self->loginfo ( "SMTPReject: $reject_reason" );
190 1         8 $self->write_packet( SMFIR_REPLYCODE,
191             $reject_reason
192             . "\0"
193             );
194             }
195             }
196             elsif ( $defer_reason ) {
197 0         0 my ( $rcode, $xcode, $message ) = split( ' ', $defer_reason, 3 );
198 0 0 0     0 if ($rcode !~ /^[4]\d\d$/ || $xcode !~ /^[4]\.\d\.\d$/ || substr($rcode, 0, 1) ne substr($xcode, 0, 1)) {
      0        
199 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'deferred_error' } );
200 0         0 $self->loginfo ( "Invalid defer message $defer_reason - setting to TempFail" );
201 0         0 $self->write_packet(SMFIR_TEMPFAIL );
202             }
203             else {
204 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'deferred' } );
205 0         0 $self->loginfo ( "SMTPDefer: $reject_reason" );
206 0         0 $self->write_packet( SMFIR_REPLYCODE,
207             $defer_reason
208             . "\0"
209             );
210             }
211             }
212             elsif ( $quarantine_reason ) {
213 0         0 $handler->metric_count( 'mail_processed_total', { 'result' => 'quarantined' } );
214 0         0 $self->loginfo ( "SMTPQuarantine: $quarantine_reason" );
215 0         0 $self->write_packet( SMFIR_QUARANTINE,
216             $quarantine_reason
217             . "\0"
218             );
219             }
220             else {
221 472         1482 $self->write_packet($returncode);
222             }
223             }
224             }
225              
226 681         1899 return;
227             }
228              
229             sub milter_process_connect {
230 26     26 1 84 my ( $self, $buffer ) = @_;
231              
232 26 50       1625 unless ($buffer =~ s/^([^\0]*)\0(.)//) {
233 0         0 $self->fatal('SMFIC_CONNECT: invalid connect info');
234             }
235 26         71 my $ip;
236 26         88 my $host = $1;
237              
238 26         172 my ($port, $addr) = unpack('nZ*', $buffer);
239              
240 26 50       121 if ( substr( $addr, 0, 5 ) eq 'IPv6:' ) {
241 0         0 $addr = substr( $addr, 5 );
242             }
243              
244 26 50       157 if ( ! defined ( $addr ) ) {
    50          
245 0         0 $self->logerror('Unknown IP address format UNDEF');
246 0         0 $ip = undef;
247             # Could potentially fail here, connection is likely bad anyway.
248             }
249             elsif ( length ( $addr ) == 0 ) {
250 0         0 $self->logerror('Unknown IP address format NULL');
251 0         0 $ip = undef;
252             # Could potentially fail here, connection is likely bad anyway.
253             }
254             else {
255 26         61 eval {
256 26         631 $ip = Net::IP->new( $addr );
257             };
258 26 50       24720 if ( my $error = $@ ) {
259 0         0 $self->logerror('Unknown IP address format - ' . $addr . ' - ' . $error );
260 0         0 $ip = undef;
261             # Could potentially fail here, connection is likely bad anyway.
262             }
263             }
264              
265 26         180 return ( $host, $ip );
266             }
267              
268             sub milter_read_block {
269 2121     2121 1 4905 my ( $self, $len ) = @_;
270 2121         4296 my $socket = $self->{'socket'};
271 2121         3573 my $sofar = 0;
272 2121         3600 my $buffer = q{};
273 2121         5517 while ($len > $sofar) {
274 2019         7512 my $read = $socket->sysread($buffer, $len - $sofar, $sofar);
275 2019 50 33     316117 last if (!defined($read) || $read <= 0); # EOF
276 2019         5384 $sofar += $read;
277             }
278 2121         8621 return $buffer;
279             }
280              
281             sub milter_split_buffer {
282 525     525 1 1435 my ( $self, $buffer ) = @_;
283 525         5073 $buffer =~ s/\0$//; # remove trailing NUL
284 525         2484 return [ split(/\0/, $buffer) ];
285             };
286              
287             ##
288              
289             sub add_header {
290 4     4 1 11 my ( $self, $header, $value ) = @_;
291 4         20 $self->write_packet( SMFIR_ADDHEADER,
292             $header
293             . "\0"
294             . $value
295             . "\0"
296             );
297 4         16 return;
298             }
299              
300             sub change_header {
301 6     6 1 15 my ( $self, $header, $index, $value ) = @_;
302 6 50       15 $value = '' unless defined($value);
303 6         40 $self->write_packet( SMFIR_CHGHEADER,
304             pack('N', $index)
305             . $header
306             . "\0"
307             . $value
308             . "\0"
309             );
310 6         19 return;
311             }
312              
313             sub insert_header {
314 49     49 1 144 my ( $self, $index, $key, $value ) = @_;
315 49         538 $self->write_packet( SMFIR_INSHEADER,
316             pack( 'N', $index )
317             . $key
318             . "\0"
319             . $value
320             . "\0"
321             );
322 49         167 return;
323             }
324              
325             sub write_packet {
326 558     558 1 1578 my ( $self, $code, $data ) = @_;
327 558         2835 $self->logdebug ( "send command $code" );
328 558         1604 my $socket = $self->{'socket'};
329 558 100       3174 $data = q{} unless defined($data);
330 558         2058 my $len = pack('N', length($data) + 1);
331 558         3530 $socket->syswrite($len);
332 558         40906 $socket->syswrite($code);
333 558         37383 $socket->syswrite($data);
334 558         6656 return;
335             }
336              
337             1;
338              
339             __END__
340              
341             =pod
342              
343             =encoding UTF-8
344              
345             =head1 NAME
346              
347             Mail::Milter::Authentication::Protocol::Milter
348              
349             =head1 VERSION
350              
351             version 20191206
352              
353             =head1 SYNOPSIS
354              
355             Subclass of Net::Server::PreFork for bringing up the main server process for authentication_milter.
356              
357             Please see Net::Server docs for more detail of the server code.
358              
359             =head1 DESCRIPTION
360              
361             A Perl implementation of email authentication standards rolled up into a single easy to use milter.
362              
363             =head1 METHODS
364              
365             =over
366              
367             =item register_metrics
368              
369             Return details of the metrics this module exports.
370              
371             =item I<protocol_process_command( $command, $buffer )>
372              
373             Process the command from the milter protocol stream.
374              
375             =item I<milter_process_connect( $buffer )>
376              
377             Process a milter connect command.
378              
379             =item I<milter_read_block( $len )>
380              
381             Read $len bytes from the milter protocol stream.
382              
383             =item I<milter_split_buffer( $buffer )>
384              
385             Split the milter buffer at null
386              
387             =item I<add_header( $header, $value )>
388              
389             Write an add header packet
390              
391             =item I<change_header( $header, $index, $value )>
392              
393             Write a change header packet
394              
395             =item I<insert_header( $index, $key, $value )>
396              
397             Writa an insert header packet
398              
399             =item I<write_packet( $code, $data )>
400              
401             Write a packet to the protocol stream.
402              
403             =item I<milter_process_command( $command, $data )>
404              
405             Process the milter command $command with the data from
406             $data.
407              
408             =item I<protocol_process_request()>
409              
410             Receive a new command from the protocol stream and process it.
411              
412             =back
413              
414             =head1 DEPENDENCIES
415              
416             English
417             Net::IP
418              
419             =head1 AUTHOR
420              
421             Marc Bradshaw <marc@marcbradshaw.net>
422              
423             =head1 COPYRIGHT AND LICENSE
424              
425             This software is copyright (c) 2018 by Marc Bradshaw.
426              
427             This is free software; you can redistribute it and/or modify it under
428             the same terms as the Perl 5 programming language system itself.
429              
430             =cut