File Coverage

blib/lib/Mail/Milter/Authentication/Client.pm
Criterion Covered Total %
statement 195 208 93.7
branch 33 50 66.0
condition 16 31 51.6
subroutine 16 16 100.0
pod 8 8 100.0
total 268 313 85.6


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Client;
2              
3 92     92   685 use strict;
  92         142  
  92         2697  
4 92     92   463 use warnings;
  92         224  
  92         3548  
5             our $VERSION = '20191206'; # VERSION
6              
7              
8 92     92   49761 use Data::Dumper;
  92         511659  
  92         5520  
9 92     92   733 use Digest::MD5 qw{ md5_base64 };
  92         182  
  92         3439  
10 92     92   38974 use Email::Simple;
  92         378417  
  92         2743  
11 92     92   681 use English qw{ -no_match_vars };
  92         186  
  92         781  
12              
13 92     92   79168 use Mail::Milter::Authentication::Net::Milter;
  92         231  
  92         3077  
14 92     92   685 use Mail::Milter::Authentication::Config qw{ get_config };
  92         187  
  92         160747  
15              
16              
17             sub new {
18 26     26 1 849 my ( $class, $args ) = @_;
19              
20 26   33     2523 $class = ref($class) || $class;
21 26         648 my $self = {};
22              
23 26         1799 my $config = get_config();
24             {
25 26   50     265 my $connection = $config->{'connection'} || die('No connection details given');
  26         225  
26 26         735 $connection =~ /^([^:]+):([^:@]+)(?:@([^:@]+|\[[0-9a-f:\.]+\]))?$/;
27 26         989 my $type = $1;
28 26         713 my $path = $2;
29 26   50     1080 my $host = $3 || q{};
30 26 50       515 if ( $type eq 'inet' ) {
    50          
31 0         0 $self->{'type'} = 'tcp';
32 0         0 $self->{'port'} = $path;
33 0         0 $self->{'path'} = $host;
34             }
35             elsif ( $type eq 'unix' ) {
36 26         584 $self->{'type'} = 'unix';
37 26         424 $self->{'port'} = 10;
38 26         486 $self->{'path'} = $path;
39             }
40             else {
41 0         0 die 'Invalid connection';
42             }
43             }
44              
45 26 50       247 if ( $config->{'protocol'} ne 'milter' ) {
46 0         0 die 'Client only works with milter protocol mode';
47             }
48              
49 26         2037 $self->{'mailer_string'} = 'Testfix 1.00.0';
50 26   50     650 $self->{'mailer_name'} = $args->{'mailer_name'} || 'test.mta.example.com';
51              
52 26   50     388 $self->{'connect_ip'} = $args->{'connect_ip'} || '66.111.4.147';
53 26   50     378 $self->{'connect_name'} = $args->{'connect_name'} || 'test.example.com';
54 26   50     656 $self->{'connect_port'} = $args->{'connect_port'} || '123456';
55 26   50     208 $self->{'connect_type'} = $args->{'connect_type'} || 'tcp4';
56              
57 26   50     361 $self->{'helo_host'} = $args->{'helo_host'} || 'test.host.example.com';
58 26   100     477 $self->{'mail_from'} = $args->{'mail_from'} || '';
59 26   50     436 $self->{'rcpt_to'} = $args->{'rcpt_to'} || 'test@to.example.com';
60              
61             # Generate a unique Queue ID
62 26         1500 $self->{'queue_id'} = md5_base64( "Authentication Milter Client $PID " . time() );
63              
64 26         573 $self->{'mail_file'} = $args->{'mail_file'};
65 26         324 $self->{'mail_data'} = $args->{'mail_data'};
66 26 0 33     558 if ( ! $self->{'mail_file'} && ! $self->{'mail_data'} ) {
67 0         0 die 'No mail file or data supplied';
68             }
69              
70 26         175 $self->{'testing'} = $args->{'testing'};
71              
72 26         1305 $self->{'milter'} = Mail::Milter::Authentication::Net::Milter->new();
73              
74 26         322 bless($self,$class);
75 26         234 return $self;
76             }
77              
78              
79             sub r { ## no critic [Subroutines::RequireArgUnpacking]
80 473     473 1 1067 my $self = shift;
81 473         1550 my @results = @_;
82             RESULT:
83 473         1628 foreach my $result ( @results ) {
84 532         1192 my $action = $result->{'action'};
85 532 100       1371 if ( $action eq 'continue' ) {
    100          
    100          
    100          
    50          
86 472         1392 next RESULT;
87             }
88             elsif ( $action eq 'insert' ) {
89 49         137 my $value = $result->{'value'};
90 49         168 my $header = $result->{'header'};
91 49         91 my $index = $result->{'index'};
92 49         236 $self->insert_header( $index, $header, $value );
93             # warn "INSERT HEADER $header at position $index\n$value\n\n";
94             }
95             elsif ( $action eq 'replace' ) {
96 6         13 my $value = $result->{'value'};
97 6         10 my $header = $result->{'header'};
98 6         19 my $index = $result->{'index'};
99 6         15 $self->replace_header( $index, $header, $value );
100             # warn "REPLACE HEADER $header at position $index\n$value\n\n";
101             }
102             elsif ( $action eq 'add' ) {
103 4         8 my $value = $result->{'value'};
104 4         9 my $header = $result->{'header'};
105 4         20 $self->add_header( $header, $value );
106             # warn "ADD HEADER $header\n$value\n\n";
107             }
108             elsif ( $action eq 'reject' ) {
109 1   50     5 my $value = $result->{'value'} || q{};
110 1         15 $value =~ s/\0/ /g;
111 1 50       15 if ( $self->{'testing'} ) {
112 1         8 $self->{'rejected'} = "Message rejected with code : $value";
113             }
114             else {
115 0         0 die "Message rejected with code : $value\n";
116             }
117             }
118             else {
119 0         0 warn "Unknown Action\n";
120 0         0 warn Dumper $result;
121             }
122             }
123 473         1839 return;
124             }
125              
126              
127             sub insert_header {
128 49     49 1 221 my ( $self, $index, $header, $value ) = @_;
129 49         144 my @process_header = @{ $self->{'header_pairs'} };
  49         578  
130 49         113 my @header_pairs;
131 49         96 my $i = 1;
132 49         228 while ( @process_header ) {
133 509         770 my $key = shift @process_header;
134 509         783 my $evalue = shift @process_header;
135 509 100       1029 if ( $i == $index ) {
136 49         151 push @header_pairs, $header;
137 49         104 push @header_pairs, $value;
138             }
139 509         1004 push @header_pairs, $key;
140 509         794 push @header_pairs, $evalue;
141 509         1043 $i++;
142             }
143 49         198 $self->{'header_pairs'} = \@header_pairs;
144 49         381 return;
145             }
146              
147              
148             sub replace_header {
149 6     6 1 14 my ( $self, $index, $header, $value ) = @_;
150              
151 6         12 my @process_header = @{ $self->{'header_pairs'} };
  6         40  
152 6         9 my @header_pairs;
153 6         11 my $i = 1;
154 6         16 while ( @process_header ) {
155 105         180 my $key = shift @process_header;
156 105         162 my $evalue = shift @process_header;
157 105 100       198 if ( lc $key eq lc $header ) {
158 13 100       26 if ( $i == $index ) {
159 6 50       24 if ( $value eq q{} ) {
160             # NOP
161             }
162             else {
163 0         0 push @header_pairs, $key;
164 0         0 push @header_pairs, $value;
165             }
166             }
167             else {
168 7         14 push @header_pairs, $key;
169 7         13 push @header_pairs, $evalue;
170             }
171 13         29 $i++;
172             }
173             else {
174 92         172 push @header_pairs, $key;
175 92         190 push @header_pairs, $evalue;
176             }
177             }
178 6         23 $self->{'header_pairs'} = \@header_pairs;
179 6         23 return;
180             }
181              
182              
183             sub add_header {
184 4     4 1 10 my ( $self, $header, $value ) = @_;
185 4         8 my @header_pairs = @{ $self->{'header_pairs'} };
  4         33  
186 4         12 push @header_pairs, $header;
187 4         18 push @header_pairs, $value;
188 4         14 $self->{'header_pairs'} = \@header_pairs;
189 4         18 return;
190             }
191              
192              
193             sub load_mail {
194 26     26 1 258 my ( $self ) = @_;
195              
196 26         98 my $mail_data;
197 26 50       958 if ( $self->{'mail_file'} ) {
    0          
198 26         1511 open my $inf, '<', $self->{'mail_file'};
199 26         1439 my @mail_content = <$inf>;
200 26         323 close $inf;
201 26         424 $mail_data = join( q{}, @mail_content );
202             }
203             elsif ( $self->{'mail_data'} ) {
204 0         0 $mail_data = $self->{'mail_data'};
205             }
206              
207 26         92 my @header_pairs;
208             my @header_split;
209              
210             HEADERS:
211 26         1186 foreach my $dataline ( split ( "\n", $mail_data ) ) {
212 684         1165 $dataline =~ s/\r?\n$//;
213             # Handle transparency
214 684 50       1306 if ( $dataline =~ /^\./ ) {
215 0         0 $dataline = substr( $dataline, 1 );
216             }
217 684 100       1213 if ( $dataline eq q{} ) {
218 26         131 last HEADERS;
219             }
220 658         1363 push @header_split, $dataline;
221             }
222              
223 26         110 my $value = q{};
224 26         154 foreach my $header_line ( @header_split ) {
225 658 100       1556 if ( $header_line =~ /^\s/ ) {
226 367         818 $value .= "\r\n" . $header_line;
227             }
228             else {
229 291 100       701 if ( $value ) {
230 265         787 my ( $hkey, $hvalue ) = split ( ':', $value, 2 );
231 265 50       1085 $hvalue =~ s/^ // if defined $hvalue;
232 265         605 push @header_pairs , $hkey;
233 265         491 push @header_pairs , $hvalue;
234             }
235 291         680 $value = $header_line;
236             }
237             }
238 26 50       159 if ( $value ) {
239 26         111 my ( $hkey, $hvalue ) = split ( ':', $value, 2 );
240 26 50       848 $hvalue =~ s/^ // if defined $hvalue;
241 26         132 push @header_pairs , $hkey;
242 26         92 push @header_pairs , $hvalue;
243             }
244              
245 26         1192 my $message_object = Email::Simple->new( $mail_data );
246 26         24970 $self->{'message_object'} = $message_object;
247 26         288 $self->{'header_pairs'} = \@header_pairs;
248 26         188 return;
249             }
250              
251              
252             sub process {
253 26     26 1 129 my ( $self ) = @_;
254              
255 26         272 $self->load_mail();
256 26         159 my $milter = $self->{'milter'};
257              
258 26         668 $milter->open( $self->{'path'}, $self->{'port'}, $self->{'type'} );
259 26         456 $milter->protocol_negotiation(
260             SMFIF_ADDHDRS => 1,
261             SMFIF_CHGBODY => 0,
262             SMFIF_ADDRCPT => 0,
263             SMFIF_DELRCPT => 0,
264             SMFIF_CHGHDRS => 1,
265             SMFIP_NOCONNECT => 0,
266             SMFIP_NOHELO => 0,
267             SMFIP_NOMAIL => 0,
268             SMFIP_NORCPT => 0,
269             SMFIP_NOBODY => 0,
270             SMFIP_NOHDRS => 0,
271             SMFIP_NOEOH => 0,
272             );
273              
274             $milter->send_macros(
275             'v' => $self->{'mailer_string'},
276             'j' => $self->{'mailer_name'},
277 26         986 '{daemon_name}' => $self->{'$mailer_name'},
278             );
279              
280             $self->r( $milter->send_connect(
281             $self->{'connect_name'},
282             $self->{'connect_type'},
283             $self->{'connect_port'},
284 26         421 $self->{'connect_ip'},
285             ));
286              
287 26         231 $self->r( $milter->send_helo( $self->{'helo_host'} ));
288              
289             $milter->send_macros(
290             '{mail_mailer}' => 'smtp',
291             '{mail_addr}' => $self->{'mail_from'},
292 26         228 '{mail_host}' => $self->{'helo_host'},
293             );
294 26         175 $self->r( $milter->send_mail_from( $self->{'mail_from'} ));
295              
296             $milter->send_macros(
297             '{rcpt_mailer}' => 'local',
298             '{rcpt_addr}' => $self->{'rcpt_to'},
299 26         244 '{rcpt_host}' => $self->{'helo_host'},
300             );
301 26         168 $self->r( $milter->send_rcpt_to( $self->{'rcpt_to'} ));
302              
303 26         88 my @process_header = @{ $self->{'header_pairs'} };
  26         489  
304 26         169 while ( @process_header ) {
305 291         676 my $key = shift @process_header;
306 291         628 my $value = shift @process_header;
307 291         1019 $self->r( $milter->send_header( $key, $value ));
308             }
309              
310 26         228 $milter->send_macros( 'i' => $self->{'queue_id'} );
311 26         153 $self->r( $milter->send_end_headers());
312              
313 26         375 my $body = $self->{'message_object'}->body();
314 26         658 $milter->send_macros( 'i' => $self->{'queue_id'} );
315 26         492 $self->r( $milter->send_body( $body ));
316              
317 26         204 $milter->send_macros( 'i' => $self->{'queue_id'} );
318 26         283 $self->r( $milter->send_end_body());
319              
320 26         263 $milter->send_abort();
321              
322 26         156 $milter->send_quit();
323              
324 26         1522 my $header_string = q{};
325             {
326 26         73 my @process_header = @{ $self->{'header_pairs'} };
  26         79  
  26         265  
327 26         203 while ( @process_header ) {
328 338         529 my $key = shift @process_header;
329 338         513 my $value = shift @process_header;
330 338 50       673 $value = '' unless defined $value;
331 338         1020 $header_string .= "$key: $value\015\012";
332             }
333 26         474 my $header_obj = Email::Simple::Header->new( $header_string );
334 26         16662 $self->{'message_object'}->header_obj_set( $header_obj );
335             }
336              
337 26         1576 $self->{'result'} = $self->{'message_object'}->as_string();
338 26         4528 return;
339             }
340              
341              
342             sub result {
343 26     26 1 127 my ( $self ) = @_;
344 26 50 66     199 return $self->{'rejected'} if $self->{'rejected'} && $self->{'testing'};
345 25         517 return $self->{'result'};
346             }
347              
348             1;
349              
350             __END__
351              
352             =pod
353              
354             =encoding UTF-8
355              
356             =head1 NAME
357              
358             Mail::Milter::Authentication::Client
359              
360             =head1 VERSION
361              
362             version 20191206
363              
364             =head1 SYNOPSIS
365              
366             Connect to the Authentication Milter and pass it email, returning the result.
367              
368             =head1 DESCRIPTION
369              
370             Client to the Authentication Milter
371              
372             =head1 CONSTRUCTOR
373              
374             =head2 I<new( $args )>
375              
376             Instantiate a new Client object
377              
378             my $client = Mail::Milter::Authentication::Client->new({
379             'mailer_name' => 'test.mta.yoga.fastmail.com',
380             'connect_ip' => '66.111.4.148',
381             'connect_name' => 'test.fastmail.com',
382             'connect_port' => '54321',
383             'connect_type' => 'tcp4',
384             'helo_host' => 'test.helo.fastmail.com',
385             'mail_from' => 'test@marc.fastmail.com',
386             'rcpt_to' => 'marc@yoga',
387             'mail_data' => $email_content,
388             'mail_file' => '/path/to/email.txt',
389             });
390              
391             =head1 METHODS
392              
393             =head2 I<r()>
394              
395             Private method, do not call this directly
396              
397             =head2 I<insert_header()>
398              
399             Private method, do not call this directly
400              
401             =head2 I<replace_header()>
402              
403             Private method, do not call this directly
404              
405             =head2 I<add_header()>
406              
407             Private method, do not call this directly
408              
409             =head2 I<load_mail()>
410              
411             Private method, do not call this directly
412              
413             =head2 I<process()>
414              
415             Send the email to the milter and process the result.
416              
417             =head2 I<result()>
418              
419             Return the result of the milter run
420              
421             =head2 Arguments
422              
423             =over
424              
425             =item mailer_name
426              
427             The name (fqdn) of the MTA
428              
429             =item connect_ip
430              
431             The IP address of the host connecting to the mailer.
432              
433             =item connect_name
434              
435             The name of the host connecting to the mailer.
436              
437             =item connect_port
438              
439             The port of the connection to the mailer.
440              
441             =item connect_type
442              
443             The type of connection to the mailer (eg tcp4).
444              
445             =item helo_host
446              
447             The string passed in the HELO stage of the SMTP transaction.
448              
449             =item mail_from
450              
451             The string passed in the MAIL FROM stage of the SMTP transaction.
452              
453             =item rcpt_to
454              
455             The string passed in the RCPT TO stage of the SMTP transaction.
456              
457             =item mail_data
458              
459             The EMail body as a string.
460              
461             =item mail_file
462              
463             The EMail body can also be passed as a filename.
464              
465             =back
466              
467             =head1 Net::Milter
468              
469             This project includes a modified copy of Net::Milter which is
470             imported into the Mail::Milter::Authentication::Net::Milter
471             namespace.
472              
473             The included module has been modified to support all of the
474             features required by Authentication Milter.
475              
476             If these required features are ever merged back into Net::Milter
477             then we may just use it instead, however at this point the
478             modified version does the job.
479              
480             =head1 AUTHOR
481              
482             Marc Bradshaw <marc@marcbradshaw.net>
483              
484             =head1 COPYRIGHT AND LICENSE
485              
486             This software is copyright (c) 2018 by Marc Bradshaw.
487              
488             This is free software; you can redistribute it and/or modify it under
489             the same terms as the Perl 5 programming language system itself.
490              
491             =cut