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   3409 use 5.20.0;
  115         676  
3 115     115   1023 use strict;
  115         345  
  115         2369  
4 115     115   526 use warnings;
  115         236  
  115         3503  
5 115     115   644 use Mail::Milter::Authentication::Pragmas;
  115         347  
  115         870  
6             # ABSTRACT: Client for connecting back to the authmilter server
7             our $VERSION = '3.20230629'; # VERSION
8 115     115   98591 use Mail::Milter::Authentication::Net::Milter;
  115         465  
  115         3581  
9 115     115   76423 use Data::Dumper;
  115         763302  
  115         7434  
10 115     115   928 use Digest::MD5 qw{ md5_base64 };
  115         402  
  115         5396  
11 115     115   869 use Email::Simple;
  115         456  
  115         323665  
12              
13              
14              
15             sub new {
16 33     33 1 1939 my ( $class, $args ) = @_;
17              
18 33   33     6270 $class = ref($class) || $class;
19 33         1779 my $self = {};
20              
21 33         2954 my $config = get_config();
22             {
23 33   50     230 my $connection = $config->{'connection'} || die('No connection details given');
  33         1281  
24 33         1386 $connection =~ /^([^:]+):([^:@]+)(?:@([^:@]+|\[[0-9a-f:\.]+\]))?$/;
25 33         1518 my $type = $1;
26 33         1236 my $path = $2;
27 33   50     1749 my $host = $3 || q{};
28 33 50       1789 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         880 $self->{'type'} = 'unix';
35 33         860 $self->{'port'} = 10;
36 33         1449 $self->{'path'} = $path;
37             }
38             else {
39 0         0 die 'Invalid connection';
40             }
41             }
42              
43 33 50       946 if ( $config->{'protocol'} ne 'milter' ) {
44 0         0 die 'Client only works with milter protocol mode';
45             }
46              
47 33         1399 $self->{'mailer_string'} = 'Testfix 1.00.0';
48 33   50     1809 $self->{'mailer_name'} = $args->{'mailer_name'} || 'test.mta.example.com';
49              
50 33   50     1178 $self->{'connect_ip'} = $args->{'connect_ip'} || '66.111.4.147';
51 33   50     698 $self->{'connect_name'} = $args->{'connect_name'} || 'test.example.com';
52 33   50     2182 $self->{'connect_port'} = $args->{'connect_port'} || '123456';
53 33   50     942 $self->{'connect_type'} = $args->{'connect_type'} || 'tcp4';
54              
55 33   50     950 $self->{'helo_host'} = $args->{'helo_host'} || 'test.host.example.com';
56 33   100     1283 $self->{'mail_from'} = $args->{'mail_from'} || '';
57 33   50     1215 $self->{'rcpt_to'} = $args->{'rcpt_to'} || 'test@to.example.com';
58              
59             # Generate a unique Queue ID
60 33         3223 $self->{'queue_id'} = md5_base64( "Authentication Milter Client $PID " . time() );
61              
62 33         1117 $self->{'mail_file'} = $args->{'mail_file'};
63 33         517 $self->{'mail_data'} = $args->{'mail_data'};
64 33 0 33     579 if ( ! $self->{'mail_file'} && ! $self->{'mail_data'} ) {
65 0         0 die 'No mail file or data supplied';
66             }
67              
68 33         531 $self->{'testing'} = $args->{'testing'};
69              
70 33         3335 $self->{'milter'} = Mail::Milter::Authentication::Net::Milter->new();
71              
72 33         990 bless($self,$class);
73 33         462 return $self;
74             }
75              
76              
77             sub r { ## no critic [Subroutines::RequireArgUnpacking]
78 628     628 1 2120 my $self = shift;
79 628         2687 my @results = @_;
80             RESULT:
81 628         2708 foreach my $result ( @results ) {
82 710         2506 my $action = $result->{'action'};
83 710 100       2971 if ( $action eq 'continue' ) {
    100          
    100          
    100          
    50          
84 627         5111 next RESULT;
85             }
86             elsif ( $action eq 'insert' ) {
87 60         297 my $value = $result->{'value'};
88 60         374 my $header = $result->{'header'};
89 60         213 my $index = $result->{'index'};
90 60         631 $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         37 my $value = $result->{'value'};
95 13         60 my $header = $result->{'header'};
96 13         32 my $index = $result->{'index'};
97 13         46 $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         19 my $value = $result->{'value'};
102 9         20 my $header = $result->{'header'};
103 9         32 $self->add_header( $header, $value );
104             # warn "ADD HEADER $header\n$value\n\n";
105             }
106             elsif ( $action eq 'reject' ) {
107 1   50     4 my $value = $result->{'value'} || q{};
108 1         17 $value =~ s/\0/ /g;
109 1 50       7 if ( $self->{'testing'} ) {
110 1         8 $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 349 my ( $self, $index, $header, $value ) = @_;
126 60         188 my @process_header = @{ $self->{'header_pairs'} };
  60         1151  
127 60         253 my @header_pairs;
128 60         279 my $i = 1;
129 60         491 while ( @process_header ) {
130 677         1593 my $key = shift @process_header;
131 677         1471 my $evalue = shift @process_header;
132 677 100       1949 if ( $i == $index ) {
133 60         484 push @header_pairs, $header;
134 60         259 push @header_pairs, $value;
135             }
136 677         1748 push @header_pairs, $key;
137 677         1566 push @header_pairs, $evalue;
138 677         1909 $i++;
139             }
140 60         1043 $self->{'header_pairs'} = \@header_pairs;
141             }
142              
143              
144             sub replace_header {
145 13     13 1 43 my ( $self, $index, $header, $value ) = @_;
146              
147 13         49 my @process_header = @{ $self->{'header_pairs'} };
  13         215  
148 13         41 my @header_pairs;
149 13         53 my $i = 1;
150 13         61 while ( @process_header ) {
151 276         476 my $key = shift @process_header;
152 276         460 my $evalue = shift @process_header;
153 276 100       593 if ( lc $key eq lc $header ) {
154 62 100       166 if ( $i == $index ) {
155 13 50       42 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         106 push @header_pairs, $key;
165 49         88 push @header_pairs, $evalue;
166             }
167 62         153 $i++;
168             }
169             else {
170 214         402 push @header_pairs, $key;
171 214         571 push @header_pairs, $evalue;
172             }
173             }
174 13         125 $self->{'header_pairs'} = \@header_pairs;
175             }
176              
177              
178             sub add_header {
179 9     9 1 24 my ( $self, $header, $value ) = @_;
180 9         20 my @header_pairs = @{ $self->{'header_pairs'} };
  9         69  
181 9         34 push @header_pairs, $header;
182 9         29 push @header_pairs, $value;
183 9         81 $self->{'header_pairs'} = \@header_pairs;
184             }
185              
186              
187             sub load_mail {
188 33     33 1 326 my ( $self ) = @_;
189              
190 33         261 my $mail_data;
191 33 50       1907 if ( $self->{'mail_file'} ) {
    0          
192 33         3190 open my $inf, '<', $self->{'mail_file'};
193 33         13021 my @mail_content = <$inf>;
194 33         758 close $inf;
195 33         805 $mail_data = join( q{}, @mail_content );
196             }
197             elsif ( $self->{'mail_data'} ) {
198 0         0 $mail_data = $self->{'mail_data'};
199             }
200              
201 33         196 my @header_pairs;
202             my @header_split;
203              
204             HEADERS:
205 33         3677 foreach my $dataline ( split ( /\r?\n/, $mail_data ) ) {
206             # Handle transparency
207 1197 50       3835 if ( $dataline =~ /^\./ ) {
208 0         0 $dataline = substr( $dataline, 1 );
209             }
210 1197 100       3593 if ( $dataline eq q{} ) {
211 31         285 last HEADERS;
212             }
213 1166         8808 push @header_split, $dataline;
214             }
215              
216 33         715 my $value = q{};
217 33         338 foreach my $header_line ( @header_split ) {
218 1166 100       3739 if ( $header_line =~ /^\s/ ) {
219 767         2273 $value .= "\r\n" . $header_line;
220             }
221             else {
222 399 100       1029 if ( $value ) {
223 366         1597 my ( $hkey, $hvalue ) = split ( ':', $value, 2 );
224 366 50       2322 $hvalue =~ s/^ // if defined $hvalue;
225 366         1060 push @header_pairs , $hkey;
226 366         844 push @header_pairs , $hvalue;
227             }
228 399         1118 $value = $header_line;
229             }
230             }
231 33 50       296 if ( $value ) {
232 33         246 my ( $hkey, $hvalue ) = split ( ':', $value, 2 );
233 33 50       507 $hvalue =~ s/^ // if defined $hvalue;
234 33         307 push @header_pairs , $hkey;
235 33         167 push @header_pairs , $hvalue;
236             }
237              
238 33         3136 my $message_object = Email::Simple->new( $mail_data );
239 33         56798 $self->{'message_object'} = $message_object;
240 33         698 $self->{'header_pairs'} = \@header_pairs;
241             }
242              
243              
244             sub process {
245 33     33 1 14959 my ( $self ) = @_;
246              
247 33         1000 $self->load_mail();
248 33         179 my $milter = $self->{'milter'};
249              
250 33         831 $milter->open( $self->{'path'}, $self->{'port'}, $self->{'type'} );
251 33         1112 $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         1778 '{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         773 $self->{'connect_ip'},
277             ));
278              
279 33         584 $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         507 '{mail_host}' => $self->{'helo_host'},
285             );
286 33         442 $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         479 '{rcpt_host}' => $self->{'helo_host'},
292             );
293 33         798 $self->r( $milter->send_rcpt_to( $self->{'rcpt_to'} ));
294              
295 33         215 my @process_header = @{ $self->{'header_pairs'} };
  33         1013  
296 33         342 while ( @process_header ) {
297 399         1889 my $key = shift @process_header;
298 399         1231 my $value = shift @process_header;
299 399         2228 $self->r( $milter->send_header( $key, $value ));
300             }
301              
302 33         530 $milter->send_macros( 'i' => $self->{'queue_id'} );
303 33         344 $self->r( $milter->send_end_headers());
304              
305 33         750 my $body = $self->{'message_object'}->body();
306              
307 33         1017 my $chunk_size = 50000;
308 33         349 while ($body) {
309 31         108 my $body_chunk;
310 31 50       615 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         200 $body_chunk = $body;
316 31         682 $body = '';
317             }
318 31         656 $milter->send_macros( 'i' => $self->{'queue_id'} );
319 31         390 $self->r( $milter->send_body( $body_chunk ));
320             }
321              
322 33         457 $milter->send_macros( 'i' => $self->{'queue_id'} );
323 33         346 $self->r( $milter->send_end_body());
324              
325 33         624 $milter->send_abort();
326              
327 33         337 $milter->send_quit();
328              
329 33         2338 my $header_string = q{};
330             {
331 33         149 my @process_header = @{ $self->{'header_pairs'} };
  33         133  
  33         548  
332 33         361 while ( @process_header ) {
333 455         991 my $key = shift @process_header;
334 455         864 my $value = shift @process_header;
335 455 50       1279 $value = '' unless defined $value;
336 455         2295 $header_string .= "$key: $value\015\012";
337             }
338 33         854 my $header_obj = Email::Simple::Header->new( $header_string );
339 33         37760 $self->{'message_object'}->header_obj_set( $header_obj );
340             }
341              
342 33         3576 $self->{'result'} = $self->{'message_object'}->as_string();
343             }
344              
345              
346             sub result {
347 33     33 1 205 my ( $self ) = @_;
348 33 50 66     426 return $self->{'rejected'} if $self->{'rejected'} && $self->{'testing'};
349 32         911 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.20230629
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