File Coverage

blib/lib/Mail/Milter/Authentication/Tester/HandlerTester.pm
Criterion Covered Total %
statement 177 183 96.7
branch 37 50 74.0
condition 11 14 78.5
subroutine 28 31 90.3
pod 20 20 100.0
total 273 298 91.6


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Tester::HandlerTester;
2 13     13   77229 use 5.20.0;
  13         115  
3 13     13   74 use strict;
  13         29  
  13         260  
4 13     13   62 use warnings;
  13         26  
  13         297  
5 13     13   4738 use Mail::Milter::Authentication::Pragmas;
  13         52  
  13         102  
6             # ABSTRACT: Class for testing handlers
7             our $VERSION = '3.20230911'; # VERSION
8 13     13   11228 use Mail::Milter::Authentication;
  13         76  
  13         1300  
9 13     13   130 use Mail::Milter::Authentication::Protocol::Milter;
  13         38  
  13         410  
10 13     13   96 use Mail::Milter::Authentication::Protocol::SMTP;
  13         50  
  13         378  
11 13     13   6359 use Net::DNS::Resolver::Mock;
  13         76322  
  13         586  
12 13     13   471 use Net::IP;
  13         48  
  13         38175  
13              
14             sub _build_config_smtp {
15 17     17   60 my ( $self, $handler_config ) = @_;
16              
17 17         331 my $config = {
18              
19             '_is_test' => 1,
20             'debug' => 1,
21             'dryrun' => 0,
22             'logtoerr' => 1,
23             'error_log' => 'tmp/smtp.err',
24             'connection' => 'unix:tmp/authentication_milter_test.sock',
25             'umask' => '0000',
26             'connect_timeout' => 55,
27             'command_timeout' => 55,
28             'content_timeout' => 595,
29             'tempfail_on_error' => 1,
30             'tempfail_on_error_authenticated' => 1,
31             'tempfail_on_error_local' => 1,
32             'tempfail_on_error_trusted' => 1,
33              
34             '#metric_connection' => 'unix:tmp/authentication_milter_test_metrics.sock',
35             '#metric_umask' => '0000',
36              
37             'protocol' => 'smtp',
38             'smtp' => {
39             'sock_type' => 'unix',
40             'sock_path' => 'tmp/authentication_milter_smtp_out.sock',
41             'pipeline_limit' => '4',
42             },
43              
44             'handlers' => $handler_config,
45              
46             };
47              
48 17         152 return $config;
49             }
50              
51             sub _build_config_milter {
52 4     4   14 my ( $self, $handler_config ) = @_;
53              
54 4         60 my $config = {
55              
56             '_is_test' => 1,
57             'debug' => 1,
58             'dryrun' => 0,
59             'logtoerr' => 1,
60             'error_log' => 'tmp/milter.err',
61             'connection' => 'unix:tmp/authentication_milter_test.sock',
62             'umask' => '0000',
63             'connect_timeout' => 55,
64             'command_timeout' => 55,
65             'content_timeout' => 595,
66             'tempfail_on_error' => 1,
67             'tempfail_on_error_authenticated' => 1,
68             'tempfail_on_error_local' => 1,
69             'tempfail_on_error_trusted' => 1,
70              
71             '#metric_connection' => 'unix:tmp/authentication_milter_test_metrics.sock',
72             '#metric_umask' => '0000',
73              
74             'protocol' => 'milter',
75              
76             'handlers' => $handler_config,
77              
78             };
79              
80 4         28 return $config;
81             }
82              
83             sub new {
84 21     21 1 690412 my ( $class, $args ) = @_;
85 21         74 my $self = {};
86 21         67 bless $self, $class;
87              
88 21         134 $self->{ 'snapshots' } = {};
89              
90 21         92 foreach my $arg ( qw{ prefix zonefile zonedata } ) {
91 63 100       266 $self->{ $arg } = $args->{ $arg } if exists $args->{ $arg };
92             }
93              
94 21 50       108 croak 'prefix must be supplies' if ! exists $self->{ 'prefix' };
95 21 50 66     199 croak 'zonefile or zonedata cannot both be supplied' if ( exists $self->{ 'zonefile' } ) && ( exists $self->{ 'zonedata' });
96 21 50 66     199 $self->{ 'zonedata' } = q{} if ( ! exists $self->{ 'zonefile' } ) && ( ! exists $self->{ 'zonedata' });
97              
98 21   100     119 my $protocol = $args->{ 'protocol' } // 'smtp';
99              
100 21 50       119 if ( exists( $args->{ 'handler_config' } ) ) {
101 21 100       100 if ( $protocol eq 'smtp' ) {
102 17         93 set_config( $self->_build_config_smtp( $args->{ 'handler_config' } ) );
103             }
104             else {
105 4         23 set_config( $self->_build_config_milter( $args->{ 'handler_config' } ) );
106             }
107             }
108              
109 21         116 $Mail::Milter::Authentication::Config::PREFIX = $self->{ 'prefix' };
110 21         132 Mail::Milter::Authentication::Config::setup_config;
111              
112 21         123 my $config = get_config();
113              
114 21         379 my $Resolver = Net::DNS::Resolver::Mock->new();
115 21 100       9862 $Resolver->zonefile_read( $self->{ 'zonefile' } ) if exists $self->{ 'zonefile' };
116 21 100       212181 $Resolver->zonefile_parse( $self->{ 'zonedata' } ) if exists $self->{ 'zonedata' };
117 21         25072 $Mail::Milter::Authentication::Handler::TestResolver = $Resolver;
118              
119             # Setup a new authentication milter object
120 21         440 my $authmilter = Mail::Milter::Authentication->new();
121 21         459 $authmilter->{'metric'} = Mail::Milter::Authentication::Metric->new();
122 21         101 $authmilter->{'config'} = $config;
123              
124             # if ( $protocol eq 'smtp' ) {
125 21         447 push @Mail::Milter::Authentication::ISA, 'Mail::Milter::Authentication::Protocol::SMTP';
126             #}
127             #else {
128             #push @Mail::Milter::Authentication::ISA, 'Mail::Milter::Authentication::Protocol::Milter';
129             #}
130              
131             # Setup a fake server object
132 21         219 $authmilter->{ 'server' }->{ 'ppid' } = $PID;
133              
134             # Load handlers
135 21         53 foreach my $name ( @{$config->{'load_handlers'}} ) {
  21         81  
136 44         300 $authmilter->load_handler( $name );
137              
138 44         550 my $package = "Mail::Milter::Authentication::Handler::$name";
139 44         492 my $object = $package->new( $authmilter );
140 44 100       394 if ( $object->can( 'pre_loop_setup' ) ) {
141 11         57 $object->pre_loop_setup();
142             }
143 44 50       7293 if ( $object->can( 'register_metrics' ) ) {
144 44         231 $authmilter->{'metric'}->register_metrics( $object->register_metrics() );
145             }
146              
147             }
148              
149             # Init handlers
150              
151 21         71 my $callbacks_list = {};
152 21         55 my $callbacks = {};
153 21         49 my $handler = {};
154 21         56 my $object = {};
155 21         55 my $object_maker = {};
156 21         277 my $count = 0;
157              
158 21         67 $authmilter->{'callbacks_list'} = $callbacks_list;
159 21         53 $authmilter->{'callbacks'} = $callbacks;
160 21         58 $authmilter->{'count'} = $count;
161 21         52 $authmilter->{'handler'} = $handler;
162 21         75 $authmilter->{'object'} = $object;
163 21         61 $authmilter->{'object_maker'} = $object_maker;
164              
165 21         110 $authmilter->setup_handlers();
166              
167 21         84 $self->{ 'authmilter' } = $authmilter;
168              
169 21         118 $self->handler()->top_setup_callback();
170              
171 21         174 $self->snapshot( '_new' );
172              
173 21         183 return $self;
174             }
175              
176             sub snapshot {
177 31     31 1 149 my ( $self, $name ) = @_;
178 31         5850 my $snapshot = clone( $self->{ 'authmilter' } );
179 31         345 $self->{ 'snapshots' }->{ $name } = $snapshot;
180             }
181              
182             sub switch {
183 234     234 1 166274 my ( $self, $name ) = @_;
184 234 50       1647 croak 'unknown snapshot' if ! exists ( $self->{ 'snapshots' }->{ $name } );
185 234         49858 my $snapshot = clone( $self->{ 'snapshots' }->{ $name } );
186 234         2294 $self->{ 'authmilter' } = $snapshot;
187             }
188              
189             sub handler {
190 3519     3519 1 6448 my ( $self ) = @_;
191 3519         34731 return $self->{ 'authmilter' }->{ 'handler' }->{ '_Handler' };
192             }
193              
194             sub connect { ## no critic
195 194     194 1 816 my ( $self, $name, $ip ) = @_;
196 194         568 my $authmilter = $self->{ 'authmilter' };
197 194   50     515 my $ip_obj = eval{ Net::IP->new( $ip ) } // undef;
  194         2127  
198             # An undef here should not make it through to handlers, however
199             # for testing we will allow this.
200 194         209715 $self->handler()->remap_connect_callback( $name, $ip_obj );
201 194         604 return $self->handler()->top_connect_callback( $name, $self->handler()->{ 'ip_object' } );
202             }
203              
204             sub helo {
205 188     188 1 683 my ( $self, $helo ) = @_;
206 188         585 $self->handler()->remap_helo_callback( $helo );
207 188         587 return $self->handler()->top_helo_callback( $self->handler()->{ 'helo_name' } );
208             }
209              
210             sub mailfrom {
211 187     187 1 551 my ( $self, $from ) = @_;
212 187         571 return $self->handler()->top_envfrom_callback( $from );
213             }
214              
215             sub rcptto {
216 186     186 1 572 my ( $self, $to ) = @_;
217 186         571 return $self->handler()->top_envrcpt_callback( $to );
218             }
219              
220             sub header {
221 723     723 1 1979 my ( $self, $key, $value, $original ) = @_;
222 723 50       2477 $original = "$key: $value" if ! defined $original;
223 723         1926 return $self->handler()->top_header_callback( $key, $value, $original );
224             }
225              
226             sub end_of_headers {
227 184     184 1 503 my ( $self ) = @_;
228 184         492 return $self->handler()->top_eoh_callback();
229             }
230              
231             sub body {
232 184     184 1 649 my ( $self, $body ) = @_;
233 184         629 return $self->handler()->top_body_callback( $body );
234             }
235              
236             sub end_of_message {
237 184     184 1 547 my ( $self ) = @_;
238 184         586 return $self->handler()->top_eom_callback();
239             }
240              
241             sub close { ## no critic
242 12     12 1 46583 my ( $self ) = @_;
243 12         67 return $self->handler()->top_close_callback();
244             }
245              
246             sub abort {
247 0     0 1 0 my ( $self ) = @_;
248 0         0 return $self->handler()->top_abort_callback();
249             }
250              
251             sub addheader {
252 183     183 1 573 my ( $self ) = @_;
253 183         626 return $self->handler()->top_addheader_callback();
254             }
255              
256             sub run {
257 193     193 1 2662816 my ( $self, $args ) = @_;
258              
259 193         1074 $self->switch( '_new' );
260              
261 193         662 my $returncode;
262 193         1386 $returncode = $self->connect( $args->{ 'connect_name' }, $args->{ 'connect_ip' } );
263 193 100       1039 die 'connect' if ( $returncode != SMFIS_CONTINUE );
264 187         974 $returncode = $self->helo( $args->{ 'helo' } );
265 187 100       947 die 'helo' if ( $returncode != SMFIS_CONTINUE );
266 186         863 $returncode = $self->mailfrom( $args->{ 'mailfrom' } );
267 186 100       991 die 'mailfrom' if ( $returncode != SMFIS_CONTINUE );
268 185         526 foreach my $rcptto ( @{ $args->{ 'rcptto' } } ) {
  185         862  
269 185         692 $returncode = $self->rcptto( $rcptto );
270 185 100       984 die 'rcptto ' . $rcptto if ( $returncode != SMFIS_CONTINUE );
271             }
272              
273 184         709 my $body = $args->{ 'body' };
274 184         3122 $body =~ s/\r?\n/\n/g;
275              
276 184         1383 my @lines = split( /\n/, $body );
277              
278             # Process headers
279 184         531 my $buffer = q{};
280 184         845 while ( my $line = shift @lines ) {
281 1366         2545 chomp $line;
282 1366 50       2963 last if $line eq q{};
283              
284 1366 100       4055 if ( $line =~ /^\s/ ) {
285 644         1902 $buffer .= "\n" . $line;
286             }
287             else {
288 722 100       1746 if ( $buffer ) {
289 538         2403 my ( $key, $value ) = split( ':', $buffer, 2 );
290 538         1683 $key =~ s/\s+$//;
291 538         2388 $value =~ s/^\s+//;
292 538         1809 $returncode = $self->header( $key, $value );
293 538 100       1703 die "header $key: $value" if ( $returncode != SMFIS_CONTINUE );
294             }
295 721         2838 $buffer = $line;
296             }
297              
298             }
299 183 50       959 if ( $buffer ) {
300 183         1028 my ( $key, $value ) = split( ':', $buffer, 2 );
301 183         809 $key =~ s/\s+$//;
302 183         1018 $value =~ s/^\s+//;
303 183         598 $returncode = $self->header( $key, $value );
304 183 50       986 die "header $key: $value" if ( $returncode != SMFIS_CONTINUE );
305             }
306              
307 183         812 $returncode = $self->end_of_headers();
308 183 50       978 die 'eoh' if ( $returncode != SMFIS_CONTINUE );
309              
310 183         1340 $returncode = $self->body( join( "\n", @lines) . "\n" );
311 183 50       973 die 'body' if ( $returncode != SMFIS_CONTINUE );
312              
313 183         933 $returncode = $self->end_of_message();
314 183 50       820 die 'body' if ( $returncode != SMFIS_CONTINUE );
315              
316 183         926 $self->addheader();
317             # $self->close();
318             }
319              
320             sub get_return {
321 0     0 1 0 my ( $self ) = @_;
322 0         0 return $self->handler()->get_return();
323             }
324              
325             sub get_reject_mail {
326 0     0 1 0 my ( $self ) = @_;
327 0         0 return $self->handler()->get_reject_mail();
328             }
329              
330             sub servername {
331 254     254 1 4587 my ( $self ) = @_;
332 254         1046 return 'handlertester.test.authmilter.org';
333             }
334              
335             sub get_authresults_header {
336 254     254 1 176355 my ( $self ) = @_;
337             # Build a Mail::AuthenticationReslts object
338 254   100     614 my $c_auth_headers = eval{ clone( $self->handler()->{ 'c_auth_headers'}->{'Authentication-Results'} ) } // [];
  254         881  
339 254   100     767 my $auth_headers = eval{ clone( $self->handler()->{ 'auth_headers'}->{'Authentication-Results'} ) } // [];
  254         751  
340 254         775 my @added_ar_headers = ( @{ $c_auth_headers }, @{ $auth_headers } );
  254         721  
  254         703  
341 254         1284 my $header = Mail::AuthenticationResults::Header->new()->set_value( Mail::AuthenticationResults::Header::AuthServID->new()->set_value( $self->servername() ) );
342 254         8770 foreach my $ar_header ( @added_ar_headers ) {
343 514         11775 eval{ $ar_header->orphan(); }; # Remove parent for testing.
  514         1571  
344 514         4562 $header->add_child( $ar_header );
345             }
346 254         11552 return $header;
347             }
348              
349             1;
350              
351             __END__
352              
353             =pod
354              
355             =encoding UTF-8
356              
357             =head1 NAME
358              
359             Mail::Milter::Authentication::Tester::HandlerTester - Class for testing handlers
360              
361             =head1 VERSION
362              
363             version 3.20230911
364              
365             =head1 SYNOPSIS
366              
367             Emulates an Authentication Milter environment with methods for testing Handlers.
368              
369             Can snapshot and restore state at any point.
370              
371             =head1 DESCRIPTION
372              
373             Make testing of Authentication Milter Handler modules easier.
374              
375             =head1 NAME
376              
377             Mail::Milter::Authentication::Tester::HandlerTester - Test harness for testing Authentication Milter Handlers
378              
379             =head1 CONSTRUCTOR
380              
381             =over
382              
383             =item new( $args )
384              
385             Instantiate a new HandlerTester object.
386              
387             $args is a hashref with the following entries.
388              
389             =over
390              
391             =item prefix
392              
393             Required
394              
395             The Prefix path containing the authentication milter config file(s). This should contain
396             all configuration files required for your test, the main authentication_milter.json file
397             can be overridden by the handler_config option (see below).
398              
399             This location should, for example, contain a valid mail-dmarc.ini for any tests using
400             the DMARC handler.
401              
402             =item handler_config
403              
404             If present, the config will be built from a generic default SMTP environment, with the given
405             HASHREF substituted as the Handler configuration. This eliminates the need to have a config file
406             for each handler configuration you wish to test.
407              
408             =item zonedata
409              
410             The zonefile data for use with Net::DNS::Resolver::Mock
411              
412             =item zonefile
413              
414             A zonefile for use with Net::DNS::Resolver::Mock
415              
416             =back
417              
418             =back
419              
420             =head1 METHODS
421              
422             =over
423              
424             =item snapshot( $name )
425              
426             Save a snapshot with the given name
427              
428             =item switch( $name )
429              
430             Restore state from the given snapshot
431              
432             =item handler()
433              
434             Returns the Handler object
435              
436             =item connect( $name, $ip )
437              
438             Call the connect callbacks with the given data.
439              
440             Returns the value of get_return()
441              
442             =item helo( $name )
443              
444             Call the helo callbacks with the given data.
445              
446             Returns the value of get_return();
447              
448             =item mailfrom( $email )
449              
450             Call the envfrom callbacks with the given data.
451              
452             Returns the value of get_return();
453              
454             =item rcptto( $email )
455              
456             Call the envrcpt callbacks with the given data.
457              
458             Returns the value of get_return();
459              
460             =item header( $key, $value )
461              
462             Call the header callbacks with the given data.
463              
464             Returns the value of get_return()
465              
466             =item end_of_headers()
467              
468             Call the end_of_headers callbacks.
469              
470             Returns the value of get_return()
471              
472             =item body( $body_chunk )
473              
474             Call the body callbacks with the given data.
475              
476             Returns the value of get_return()
477              
478             =item end_of_message()
479              
480             Call the eom callbacks.
481              
482             Returns the value of get_return()
483              
484             =item close()
485              
486             Call the close callbacks.
487              
488             Returns the value of get_return()
489              
490             =item abort()
491              
492             Call the abort callbacks.
493              
494             =item addheader()
495              
496             Call the addheader callbacks.
497              
498             =item run( $args )
499              
500             Run with a given set of data as defined in $args hashref.
501              
502             Dies if the mail would be rejected.
503              
504             Arguments of $args are.
505              
506             =over
507              
508             =item connect_name
509              
510             The name of the connecting server.
511              
512             =item connect_ip
513              
514             The ip address of the connecting server.
515              
516             =item helo
517              
518             The helo string.
519              
520             =item mailfrom
521              
522             The envelope MAILFROM address.
523              
524             =item rcptto
525              
526             Arrayref of the envelope RCPTTO addresses.
527              
528             =item body
529              
530             The email body.
531              
532             =back
533              
534             =item get_return()
535              
536             Returns the value of get_return() from the current handler object.
537              
538             =item get_reject_mail()
539              
540             Returns the value of get_reject_mail() from the current handler object.
541              
542             =item servername()
543              
544             Returns a dummy authservid servername.
545              
546             =item get_authresults_header()
547              
548             Returns a Mail::AuthenticationResults::Header object representing the authentication results
549             header which would be added to the message.
550              
551             =back
552              
553             =head1 DEPENDENCIES
554              
555             Carp
556             Clone
557             Mail::AuthenticationResults::Header
558             Mail::AuthenticationResults::Header::AuthServID
559             Mail::Milter::Authentication
560             Mail::Milter::Authentication::Protocol::Milter
561             Mail::Milter::Authentication::Protocol::SMTP
562             Mail::Milter::Authentication::Config
563             Module::Load
564             Net::DNS::Resolver::Mock
565              
566             =head1 AUTHORS
567              
568             Marc Bradshaw E<lt>marc@marcbradshaw.netE<gt>
569              
570             =head1 COPYRIGHT
571              
572             Copyright 2018
573              
574             This library is free software; you may redistribute it and/or
575             modify it under the same terms as Perl itself.
576              
577             =head1 AUTHOR
578              
579             Marc Bradshaw <marc@marcbradshaw.net>
580              
581             =head1 COPYRIGHT AND LICENSE
582              
583             This software is copyright (c) 2020 by Marc Bradshaw.
584              
585             This is free software; you can redistribute it and/or modify it under
586             the same terms as the Perl 5 programming language system itself.
587              
588             =cut