File Coverage

blib/lib/Mail/Milter/Authentication/Client.pm
Criterion Covered Total %
statement 193 208 92.7
branch 34 52 65.3
condition 16 31 51.6
subroutine 16 16 100.0
pod 8 8 100.0
total 267 315 84.7


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