File Coverage

blib/lib/Mail/Milter/Authentication/Tester.pm
Criterion Covered Total %
statement 293 324 90.4
branch 80 128 62.5
condition 5 12 41.6
subroutine 27 28 96.4
pod 0 11 0.0
total 405 503 80.5


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Tester;
2 115     115   89207 use 5.20.0;
  115         574  
3 115     115   792 use strict;
  115         237  
  115         2198  
4 115     115   592 use warnings;
  115         450  
  115         2700  
5 115     115   595 use Mail::Milter::Authentication::Pragmas;
  115         341  
  115         744  
6             # ABSTRACT: Class used for testing
7             our $VERSION = '3.20230629'; # VERSION
8 115     115   91487 use Mail::Milter::Authentication;
  115         570  
  115         5178  
9 115     115   66654 use Mail::Milter::Authentication::Client;
  115         408  
  115         4566  
10 115     115   1085 use Mail::Milter::Authentication::Protocol::Milter;
  115         347  
  115         3145  
11 115     115   644 use Mail::Milter::Authentication::Protocol::SMTP;
  115         291  
  115         3418  
12 115     115   697 use Cwd qw{ cwd };
  115         291  
  115         6827  
13 115     115   1148 use IO::Socket::INET;
  115         357  
  115         1147  
14 115     115   66308 use IO::Socket::UNIX;
  115         447  
  115         2003  
15 115     115   196699 use Net::DNS::Resolver::Mock 1.20171219;
  115         15868  
  115         3445  
16 115     115   2202 use Test::File::Contents;
  115         33916  
  115         23793  
17 115     115   977 use Test::More;
  115         287  
  115         3595  
18              
19             our @ISA = qw{ Exporter }; ## no critic
20             our @EXPORT = qw{ start_milter stop_milter get_metrics test_metrics smtp_process smtp_process_multi milter_process smtpput send_smtp_packet smtpcat }; ## no critic
21              
22             my $base_dir = cwd();
23              
24             our $MASTER_PROCESS_PID = $$;
25              
26              
27             {
28             my $milter_pid;
29              
30             sub start_milter {
31 296     296 0 184984 my ( $prefix ) = @_;
32              
33 296 50       3461 return if $milter_pid;
34              
35 296 50       16075 if ( ! -e $prefix . '/authentication_milter.json' ) {
36 0         0 die "Could not find config";
37             }
38              
39 296         3122834 system "cp $prefix/mail-dmarc.ini .";
40              
41 296         777652 $milter_pid = fork();
42 296 50       24352 die "unable to fork: $!" unless defined($milter_pid);
43 296 100       14762 if (!$milter_pid) {
44 37         6631 $Mail::Milter::Authentication::Config::PREFIX = $prefix;
45 37         2574 $Mail::Milter::Authentication::Config::IDENT = 'test_authentication_milter_test';
46 37         13121 my $Resolver = Net::DNS::Resolver::Mock->new();
47 37         150003 $Resolver->zonefile_read( 'zonefile' );
48 37         1612541 $Mail::Milter::Authentication::Handler::TestResolver = $Resolver;
49 37         2336 Mail::Milter::Authentication::start({
50             'pid_file' => 'tmp/authentication_milter.pid',
51             'daemon' => 0,
52             });
53 0         0 die;
54             }
55              
56 259         1295056789 sleep 5;
57 259         112934 open my $pid_file, '<', 'tmp/authentication_milter.pid';
58 259         239816 $milter_pid = <$pid_file>;
59 259         8164 close $pid_file;
60 259         66029 print "Milter started at pid $milter_pid\n";
61 259         12762 return;
62             }
63              
64             sub stop_milter {
65 197 100   197 0 1052257 return if ! $milter_pid;
66 190         46356 kill( 'HUP', $milter_pid );
67 190         988769463 waitpid ($milter_pid,0);
68 190         63244 print "Milter killed at pid $milter_pid\n";
69 190         1918 undef $milter_pid;
70 190         32340 unlink 'tmp/authentication_milter.pid';
71 190         13443 unlink 'mail-dmarc.ini';
72 190         2514 return;
73             }
74              
75             END {
76 115 100   115   9091148 return if $MASTER_PROCESS_PID != $$;
77 7         77 stop_milter();
78             }
79             }
80              
81             sub get_metrics {
82 190     190 0 4481 my ( $path ) = @_;
83              
84 190         10335 my $sock = IO::Socket::UNIX->new(
85             'Peer' => $path,
86             );
87              
88 190         189163 print $sock "GET /metrics HTTP/1.0\n\n";
89              
90 190         1969 my $data = {};
91              
92 190         12440307 while ( my $line = <$sock> ) {
93 570         4396 chomp $line;
94 570 100       6317 last if $line eq q{};
95             }
96 190         469218 while ( my $line = <$sock> ) {
97 22231         49680 chomp $line;
98 22231 100       83725 next if $line =~ /^#/;
99 14447         62586 $line =~ /^(.*)\{(.*)\} (.*)$/;
100 14447         41160 my $count_id = $1;
101 14447         33626 my $labels = $2;
102 14447         29901 my $count = $3;
103 14447         223889 $data->{ $count_id . '{' . $labels . '}' } = $count;
104             }
105              
106 190         10639 return $data;
107             }
108              
109             sub test_metrics {
110 190     190 0 756088 my ( $expected ) = @_;
111              
112             # Sleep for 5 to allow server to catch up on metrics
113 190         950048443 sleep 5;
114              
115             subtest $expected => sub {
116              
117 190     190   858333 my $metrics = get_metrics( 'tmp/authentication_milter_test_metrics.sock' );
118 190         21374 my $j = JSON::XS->new();
119              
120 190 50       22138 if ( -e $expected ) {
121              
122 190         15334 open my $InF, '<', $expected;
123 190         170381 my @content = <$InF>;
124 190         6336 close $InF;
125 190         34888 my $data = $j->decode( join( q{}, @content ) );
126              
127 190         4288 plan tests => scalar keys %$data;
128              
129 190         414957 foreach my $key ( sort keys %$data ) {
130 11737 100       6430171 if ( $key =~ /seconds_total/ ) {
    50          
    100          
    100          
131 6090         46394 is( $metrics->{ $key } > 0, $data->{ $key } > 0, "Metrics $expected $key" );
132             }
133             elsif ( $key =~ /microseconds_sum/ ) {
134 0         0 is( $metrics->{ $key } > 0, $data->{ $key } > 0, "Metrics $expected $key" );
135             }
136             elsif ( $key =~ /authmilter_forked_children_total/ ) {
137 190         2698 is( $metrics->{ $key } > 0, $data->{ $key } > 0, "Metrics $expected $key" );
138             }
139             elsif ( $key =~ /authmilter_processes_/) {
140 380         3969 is( $metrics->{ $key } > -1, $data->{ $key } > -1, "Metrics $expected $key" );
141             }
142             else {
143 5077         30570 is( $metrics->{ $key }, $data->{ $key }, "Metrics $expected $key" );
144             }
145             }
146              
147             }
148             else {
149 0         0 fail( 'Metrics data does not exist' );
150             }
151              
152 190 50       109104 if ( $ENV{'WRITE_METRICS'} ) {
153 0         0 foreach my $key ( sort keys %$metrics ) {
154 0 0       0 if ( $key =~ /seconds_total/ ) {
    0          
    0          
    0          
155 0 0       0 $metrics->{ $key } = 123456 if $metrics->{ $key } > 0;
156             }
157             elsif ( $key =~ /microseconds_sum/ ) {
158 0 0       0 $metrics->{ $key } = 123456 if $metrics->{ $key } > 0;
159             }
160             elsif ( $key =~ /authmilter_forked_children_total/ ) {
161 0 0       0 $metrics->{ $key } = 123456 if $metrics->{ $key } > 0;
162             }
163             elsif ( $key =~ /authmilter_processes_/) {
164 0 0       0 $metrics->{ $key } = 123456 if $metrics->{ $key } > -1;
165             }
166             }
167 0         0 open my $OutF, '>', $expected;
168 0         0 $j->pretty();
169 0         0 $j->canonical();
170 0         0 print $OutF $j->encode( $metrics );
171 0         0 close $OutF;
172             }
173              
174 190         34801 };
175             }
176              
177             sub smtp_process {
178 953     953 0 3324321 my ( $args ) = @_;
179              
180 953 50       42741 if ( ! -e $args->{'prefix'} . '/authentication_milter.json' ) {
181 0         0 die "Could not find config " . $args->{'prefix'};
182             }
183 953 50       30733 if ( ! -e 'data/source/' . $args->{'source'} ) {
184 0         0 die "Could not find source";
185             }
186              
187             my $catargs = {
188             'sock_type' => 'unix',
189             'sock_path' => 'tmp/authentication_milter_smtp_out.sock',
190             'remove' => [10,11],
191 953         16960 'output' => 'tmp/result/' . $args->{'dest'},
192             };
193 953         755431 unlink 'tmp/authentication_milter_smtp_out.sock';
194 953         13632 my $cat_pid;
195 953 100       13985 if ( ! $args->{'no_cat'} ) {
196 932         10052 $cat_pid = smtpcat( $catargs );
197 898         1796233039 sleep 2;
198             }
199              
200             my $return = smtpput({
201             'sock_type' => 'unix',
202             'sock_path' => 'tmp/authentication_milter_test.sock',
203             'mailer_name' => 'test.module',
204             'connect_ip' => [ $args->{'ip'} ],
205             'connect_name' => [ $args->{'name'} ],
206             'helo_host' => [ $args->{'name'} ],
207             'mail_from' => [ $args->{'from'} ],
208             'rcpt_to' => [ $args->{'to'} ],
209             'mail_file' => [ 'data/source/' . $args->{'source'} ],
210 919         442944 'eom_expect' => $args->{'eom_expect'},
211             });
212              
213 919 100       27481 if ( ! $args->{'no_cat'} ) {
214 898         3742190110 waitpid( $cat_pid,0 );
215 898         63491 files_eq_or_diff( 'data/example/' . $args->{'dest'}, 'tmp/result/' . $args->{'dest'}, 'smtp ' . $args->{'desc'} );
216             }
217             else {
218 21         1071 is( $return, 1, 'SMTP Put Returned ok' );
219             }
220             }
221              
222             sub smtp_process_multi {
223 97     97 0 169087 my ( $args ) = @_;
224              
225 97 50       3500 if ( ! -e $args->{'prefix'} . '/authentication_milter.json' ) {
226 0         0 die "Could not find config";
227             }
228              
229             # Hardcoded lines to remove in subsequent messages
230             # If you change the source email then change the awk
231             # numbers here too.
232             # This could be better!
233              
234             my $catargs = {
235             'sock_type' => 'unix',
236             'sock_path' => 'tmp/authentication_milter_smtp_out.sock',
237             'remove' => $args->{'filter'},
238 97         2393 'output' => 'tmp/result/' . $args->{'dest'},
239             };
240 97         6285 unlink 'tmp/authentication_milter_smtp_out.sock';
241 97         1560 my $cat_pid = smtpcat( $catargs );
242 95         190022476 sleep 2;
243              
244 95         21120 my $putargs = {
245             'sock_type' => 'unix',
246             'sock_path' => 'tmp/authentication_milter_test.sock',
247             'mailer_name' => 'test.module',
248             'connect_ip' => [],
249             'connect_name' => [],
250             'helo_host' => [],
251             'mail_from' => [],
252             'rcpt_to' => [],
253             'mail_file' => [],
254             };
255              
256 95         2474 foreach my $item ( @{$args->{'ip'}} ) {
  95         3620  
257 426         1761 push @{$putargs->{'connect_ip'}}, $item;
  426         6215  
258             }
259 95         1292 foreach my $item ( @{$args->{'name'}} ) {
  95         2321  
260 426         2992 push @{$putargs->{'connect_name'}}, $item;
  426         8815  
261             }
262 95         1621 foreach my $item ( @{$args->{'name'}} ) {
  95         904  
263 426         1572 push @{$putargs->{'helo_host'}}, $item;
  426         3883  
264             }
265 95         1052 foreach my $item ( @{$args->{'from'}} ) {
  95         2003  
266 426         1476 push @{$putargs->{'mail_from'}}, $item;
  426         3779  
267             }
268 95         907 foreach my $item ( @{$args->{'to'}} ) {
  95         1147  
269 426         1286 push @{$putargs->{'rcpt_to'}}, $item;
  426         3450  
270             }
271 95         285 foreach my $item ( @{$args->{'source'}} ) {
  95         1284  
272 426         1524 push @{$putargs->{'mail_file'}}, 'data/source/' . $item;
  426         4455  
273             }
274             #warn 'Testing ' . $args->{'source'} . ' > ' . $args->{'dest'} . "\n";
275              
276 95         2529 smtpput( $putargs );
277              
278 95         387357662 waitpid( $cat_pid,0 );
279              
280 95         6143 files_eq_or_diff( 'data/example/' . $args->{'dest'}, 'tmp/result/' . $args->{'dest'}, 'smtp ' . $args->{'desc'} );
281             }
282              
283             sub milter_process {
284 886     886 0 2819140 my ( $args ) = @_;
285              
286 886 50       42046 if ( ! -e $args->{'prefix'} . '/authentication_milter.json' ) {
287 0         0 die "Could not find config";
288             }
289 886 50       41337 if ( ! -e 'data/source/' . $args->{'source'} ) {
290 0         0 die "Could not find source";
291             }
292              
293             client({
294             'prefix' => $args->{'prefix'},
295             'mailer_name' => 'test.module',
296             'mail_file' => 'data/source/' . $args->{'source'},
297             'connect_ip' => $args->{'ip'},
298             'connect_name' => $args->{'name'},
299             'helo_host' => $args->{'name'},
300             'mail_from' => $args->{'from'},
301             'rcpt_to' => $args->{'to'},
302 886         43423 'output' => 'tmp/result/' . $args->{'dest'},
303             });
304              
305 853         218188 files_eq_or_diff( 'data/example/' . $args->{'dest'}, 'tmp/result/' . $args->{'dest'}, 'milter ' . $args->{'desc'} );
306             }
307              
308             sub smtpput {
309 1016     1016 0 4029905 my ( $args ) = @_;
310              
311 1016         23011 my $mailer_name = $args->{'mailer_name'};
312              
313 1016         13187 my $mail_file_a = $args->{'mail_file'};
314 1016         17023 my $mail_from_a = $args->{'mail_from'};
315 1016         16122 my $rcpt_to_a = $args->{'rcpt_to'};
316 1016         9245 my $x_name_a = $args->{'connect_name'};
317 1016         29357 my $x_addr_a = $args->{'connect_ip'};
318 1016         6000 my $x_helo_a = $args->{'helo_host'};
319              
320 1016         7664 my $sock_type = $args->{'sock_type'};
321 1016         16789 my $sock_path = $args->{'sock_path'};
322 1016         7920 my $sock_host = $args->{'sock_host'};
323 1016         5383 my $sock_port = $args->{'sock_port'};
324              
325 1016   100     41302 my $eom_expect = $args->{'eom_expect'} || '250';
326              
327 1016         8562 my $sock;
328 1016 50       40306 if ( $sock_type eq 'inet' ) {
    50          
329 0   0     0 $sock = IO::Socket::INET->new(
330             'Proto' => 'tcp',
331             'PeerAddr' => $sock_host,
332             'PeerPort' => $sock_port,
333             ) || die "could not open outbound SMTP socket: $!";
334             }
335             elsif ( $sock_type eq 'unix' ) {
336 1016   50     93612 $sock = IO::Socket::UNIX->new(
337             'Peer' => $sock_path,
338             ) || die "could not open outbound SMTP socket: $!";
339             }
340              
341 1016         8729986 my $line = <$sock>;
342              
343 1016 50       28157 if ( ! $line =~ /250/ ) {
344 0         0 die "Unexpected SMTP response $line";
345             }
346              
347 1016 50       23440 send_smtp_packet( $sock, 'EHLO ' . $mailer_name, '250' ) || die;
348              
349 1016         6297 my $first_time = 1;
350              
351 1016         11212 while ( @$mail_from_a ) {
352              
353 1302 100       9640 if ( ! $first_time ) {
354 286 100       1914 if ( ! send_smtp_packet( $sock, 'RSET', '250' ) ) {
355 47         1269 $sock->close();
356 47         3572 return;
357             };
358             }
359 1255         6899 $first_time = 0;
360              
361 1255         8672 my $mail_file = shift @$mail_file_a;
362 1255         6483 my $mail_from = shift @$mail_from_a;
363 1255         7184 my $rcpt_to = shift @$rcpt_to_a;
364 1255         6925 my $x_name = shift @$x_name_a;
365 1255         5391 my $x_addr = shift @$x_addr_a;
366 1255         5643 my $x_helo = shift @$x_helo_a;
367              
368 1255         17940 my $mail_data = q{};
369              
370 1255 50       10358 if ( $mail_file eq '-' ) {
371 0         0 while ( my $l = <> ) {
372 0         0 $mail_data .= $l;
373             }
374             }
375             else {
376 1255 50       59697 if ( ! -e $mail_file ) {
377 0         0 die "Mail file $mail_file does not exist";
378             }
379 1255         125128 open my $inf, '<', $mail_file;
380 1255         280323 my @all = <$inf>;
381 1255         28687 $mail_data = join( q{}, @all );
382 1255         40513 close $inf;
383             }
384              
385 1255         104114 $mail_data =~ s/\015?\012/\015\012/g;
386             # Handle transparency
387 1255         14709 $mail_data =~ s/\015\012\./\015\012\.\./g;
388              
389 1255 50       30782 send_smtp_packet( $sock, 'XFORWARD NAME=' . $x_name, '250' ) || die;
390 1255 50       25095 send_smtp_packet( $sock, 'XFORWARD ADDR=' . $x_addr, '250' ) || die;
391 1255 50       13413 send_smtp_packet( $sock, 'XFORWARD HELO=' . $x_helo, '250' ) || die;
392              
393 1255 50       28124 send_smtp_packet( $sock, 'MAIL FROM:' . $mail_from, '250' ) || die;
394 1255 50       15581 send_smtp_packet( $sock, 'RCPT TO:' . $rcpt_to, '250' ) || die;
395 1255 50       14162 send_smtp_packet( $sock, 'DATA', '354' ) || die;
396              
397 1255         71816 print $sock $mail_data;
398 1255         21120 print $sock "\r\n";
399              
400 1255 50       9608 send_smtp_packet( $sock, '.', $eom_expect ) || return 0;
401              
402             }
403              
404 969 50       8119 send_smtp_packet( $sock, 'QUIT', '221' ) || return 0;
405 969         15158 $sock->close();
406              
407 969         104754 return 1;
408             }
409              
410             sub send_smtp_packet {
411 11056     11056 0 108408 my ( $socket, $send, $expect ) = @_;
412 11056         532430 print $socket "$send\r\n";
413 11056         369636265 my $recv = <$socket>;
414 11056 50       116832 $recv = '' if !defined $recv;
415 11056         122408 while ( $recv =~ /^\d\d\d\-/ ) {
416 4056         1594374 $recv = <$socket>;
417             }
418 11056 100       427940 if ( $recv =~ /^$expect/ ) {
419 11009         137969 return 1;
420             }
421             else {
422 47         3055 $recv =~ s/\r?\n?$//;
423 47         1692 $send =~ s/\r?\n?$//;
424 47         5029 warn "SMTP Send expected \"$expect\" received \"$recv\" when sending \"$send\"\n";
425 47         987 return 0;
426             }
427             }
428              
429             sub smtpcat {
430 1033     1033 0 13429 my ( $args ) = @_;
431              
432 1033         3571187 my $cat_pid = fork();
433 1033 50       62690 die "unable to fork: $!" unless defined($cat_pid);
434 1033 100       102319 return $cat_pid if $cat_pid;
435              
436 38         6057 my $sock_type = $args->{'sock_type'};
437 38         2884 my $sock_path = $args->{'sock_path'};
438 38         2724 my $sock_host = $args->{'sock_host'};
439 38         2096 my $sock_port = $args->{'sock_port'};
440              
441 38         2668 my $remove = $args->{'remove'};
442 38         1974 my $output = $args->{'output'};
443              
444 38         1568 my @out_lines;
445              
446             my $sock;
447 38 50       6777 if ( $sock_type eq 'inet' ) {
    50          
448 0   0     0 $sock = IO::Socket::INET->new(
449             'Listen' => 5,
450             'LocalHost' => $sock_host,
451             'LocalPort' => $sock_port,
452             'Protocol' => 'tcp',
453             ) || die "could not open socket: $!";
454             }
455             elsif ( $sock_type eq 'unix' ) {
456 38   50     9321 $sock = IO::Socket::UNIX->new(
457             'Listen' => 5,
458             'Local' => $sock_path,
459             ) || die "could not open socket: $!";
460             }
461              
462 38         66296 my $accept = $sock->accept();
463              
464 38         86281429 print $accept "220 smtp.cat ESMTP Test\r\n";
465              
466 38     0   6290 local $SIG{'ALRM'} = sub{ die "Timeout\n" };
  0         0  
467 38         1707 alarm( 60 );
468              
469 38         857 my $quit = 0;
470 38         1408 while ( ! $quit ) {
471 353   50     2360429 my $command = <$accept> || { $quit = 1 };
472 353         4400 alarm( 60 );
473              
474 353 50       14581 if ( $command =~ /^HELO/ ) {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
475 0         0 push @out_lines, $command;
476 0         0 print $accept "250 HELO Ok\r\n";
477             }
478             elsif ( $command =~ /^EHLO/ ) {
479 38         1562 push @out_lines, $command;
480 38         2685 print $accept "250 EHLO Ok\r\n";
481             }
482             elsif ( $command =~ /^MAIL/ ) {
483 45         901 push @out_lines, $command;
484 45         2821 print $accept "250 MAIL Ok\r\n";
485             }
486             elsif ( $command =~ /^XFORWARD/ ) {
487 135         1409 push @out_lines, $command;
488 135         7717 print $accept "250 XFORWARD Ok\r\n";
489             }
490             elsif ( $command =~ /^RCPT/ ) {
491 45         808 push @out_lines, $command;
492 45         2558 print $accept "250 RCPT Ok\r\n";
493             }
494             elsif ( $command =~ /^RSET/ ) {
495 7         140 push @out_lines, $command;
496 7         378 print $accept "250 RSET Ok\r\n";
497             }
498             elsif ( $command =~ /^DATA/ ) {
499 45         939 push @out_lines, $command;
500 45         2126 print $accept "354 Send\r\n";
501             DATA:
502 45         16852 while ( my $line = <$accept> ) {
503 3049         18470 alarm( 60 );
504 3049         21505 push @out_lines, $line;
505 3049 100       9815 last DATA if $line eq ".\r\n";
506             # Handle transparency
507 3004 100       16893 if ( $line =~ /^\./ ) {
508 28         218 $line = substr( $line, 1 );
509             }
510             }
511 45         1868 print $accept "250 DATA Ok\r\n";
512             }
513             elsif ( $command =~ /^QUIT/ ) {
514 38         604 push @out_lines, $command;
515 38         2646 print $accept "221 Bye\r\n";
516 38         843 $quit = 1;
517             }
518             else {
519 0         0 push @out_lines, $command;
520 0         0 print $accept "250 Unknown Ok\r\n";
521             }
522             }
523              
524 38         7850 open my $file, '>', $output;
525 38         402 my $i = 0;
526 38         748 foreach my $line ( @out_lines ) {
527 3402         5762 $i++;
528 3402 100       6795 $line = "############\n" if grep { $i == $_ } @$remove;
  8644         21349  
529 3402         8511 print $file $line;
530             }
531 38         5213 close $file;
532              
533 38         1798 $accept->close();
534 38         2871 $sock->close();
535              
536 38         25178 exit 0;
537             }
538              
539             sub client {
540 886     886 0 7795 my ( $args ) = @_;
541 886         2827791 my $pid = fork();
542 886 50       50715 die "unable to fork: $!" unless defined($pid);
543 886 100       28723 if ( ! $pid ) {
544              
545 33         5784 my $output = $args->{'output'};
546 33         3390 delete $args->{'output'};
547              
548 33         3976 $Mail::Milter::Authentication::Config::PREFIX = $args->{'prefix'};
549 33         2995 delete $args->{'prefix'};
550 33         2267 $args->{'testing'} = 1;
551              
552 33         8238 my $client = Mail::Milter::Authentication::Client->new( $args );
553              
554 33         1199 $client->process();
555              
556 33         16097 open my $file, '>', $output;
557 33         526 print $file $client->result();
558 33         4561 close $file;
559 33         3493 exit 0;
560              
561             }
562 853         4743642945 waitpid( $pid, 0 );
563             }
564              
565             1;
566              
567             __END__
568              
569             =pod
570              
571             =encoding UTF-8
572              
573             =head1 NAME
574              
575             Mail::Milter::Authentication::Tester - Class used for testing
576              
577             =head1 VERSION
578              
579             version 3.20230629
580              
581             =head1 AUTHOR
582              
583             Marc Bradshaw <marc@marcbradshaw.net>
584              
585             =head1 COPYRIGHT AND LICENSE
586              
587             This software is copyright (c) 2020 by Marc Bradshaw.
588              
589             This is free software; you can redistribute it and/or modify it under
590             the same terms as the Perl 5 programming language system itself.
591              
592             =cut