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   90470 use 5.20.0;
  115         410  
3 115     115   631 use strict;
  115         231  
  115         2305  
4 115     115   736 use warnings;
  115         284  
  115         2715  
5 115     115   1170 use Mail::Milter::Authentication::Pragmas;
  115         230  
  115         636  
6             # ABSTRACT: Class used for testing
7             our $VERSION = '3.20230911'; # VERSION
8 115     115   95117 use Mail::Milter::Authentication;
  115         520  
  115         7039  
9 115     115   73533 use Mail::Milter::Authentication::Client;
  115         465  
  115         4483  
10 115     115   1256 use Mail::Milter::Authentication::Protocol::Milter;
  115         240  
  115         3058  
11 115     115   855 use Mail::Milter::Authentication::Protocol::SMTP;
  115         345  
  115         3855  
12 115     115   588 use Cwd qw{ cwd };
  115         290  
  115         7484  
13 115     115   1035 use IO::Socket::INET;
  115         309  
  115         1664  
14 115     115   69887 use IO::Socket::UNIX;
  115         345  
  115         998  
15 115     115   197040 use Net::DNS::Resolver::Mock 1.20171219;
  115         16768  
  115         3984  
16 115     115   2510 use Test::File::Contents;
  115         35184  
  115         22774  
17 115     115   958 use Test::More;
  115         347  
  115         6084  
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 179906 my ( $prefix ) = @_;
32              
33 296 50       1995 return if $milter_pid;
34              
35 296 50       16630 if ( ! -e $prefix . '/authentication_milter.json' ) {
36 0         0 die "Could not find config";
37             }
38              
39 296         3211147 system "cp $prefix/mail-dmarc.ini .";
40              
41 296         767379 $milter_pid = fork();
42 296 50       32792 die "unable to fork: $!" unless defined($milter_pid);
43 296 100       18939 if (!$milter_pid) {
44 37         4676 $Mail::Milter::Authentication::Config::PREFIX = $prefix;
45 37         2736 $Mail::Milter::Authentication::Config::IDENT = 'test_authentication_milter_test';
46 37         12947 my $Resolver = Net::DNS::Resolver::Mock->new();
47 37         160340 $Resolver->zonefile_read( 'zonefile' );
48 37         1685056 $Mail::Milter::Authentication::Handler::TestResolver = $Resolver;
49 37         2726 Mail::Milter::Authentication::start({
50             'pid_file' => 'tmp/authentication_milter.pid',
51             'daemon' => 0,
52             });
53 0         0 die;
54             }
55              
56 259         1295060839 sleep 5;
57 259         110299 open my $pid_file, '<', 'tmp/authentication_milter.pid';
58 259         221417 $milter_pid = <$pid_file>;
59 259         10010 close $pid_file;
60 259         63578 print "Milter started at pid $milter_pid\n";
61 259         14240 return;
62             }
63              
64             sub stop_milter {
65 197 100   197 0 1068968 return if ! $milter_pid;
66 190         12415 kill( 'HUP', $milter_pid );
67 190         955770702 waitpid ($milter_pid,0);
68 190         40985 print "Milter killed at pid $milter_pid\n";
69 190         1644 undef $milter_pid;
70 190         34906 unlink 'tmp/authentication_milter.pid';
71 190         11493 unlink 'mail-dmarc.ini';
72 190         2598 return;
73             }
74              
75             END {
76 115 100   115   8993808 return if $MASTER_PROCESS_PID != $$;
77 7         67 stop_milter();
78             }
79             }
80              
81             sub get_metrics {
82 190     190 0 4370 my ( $path ) = @_;
83              
84 190         8472 my $sock = IO::Socket::UNIX->new(
85             'Peer' => $path,
86             );
87              
88 190         146418 print $sock "GET /metrics HTTP/1.0\n\n";
89              
90 190         1552 my $data = {};
91              
92 190         12512159 while ( my $line = <$sock> ) {
93 570         3242 chomp $line;
94 570 100       4863 last if $line eq q{};
95             }
96 190         509657 while ( my $line = <$sock> ) {
97 22513         62926 chomp $line;
98 22513 100       93150 next if $line =~ /^#/;
99 14729         73324 $line =~ /^(.*)\{(.*)\} (.*)$/;
100 14729         48778 my $count_id = $1;
101 14729         41079 my $labels = $2;
102 14729         34591 my $count = $3;
103 14729         254519 $data->{ $count_id . '{' . $labels . '}' } = $count;
104             }
105              
106 190         10693 return $data;
107             }
108              
109             sub test_metrics {
110 190     190 0 762771 my ( $expected ) = @_;
111              
112             # Sleep for 5 to allow server to catch up on metrics
113 190         950044552 sleep 5;
114              
115             subtest $expected => sub {
116              
117 190     190   865909 my $metrics = get_metrics( 'tmp/authentication_milter_test_metrics.sock' );
118 190         9491 my $j = JSON::XS->new();
119              
120 190 50       22290 if ( -e $expected ) {
121              
122 190         17048 open my $InF, '<', $expected;
123 190         188270 my @content = <$InF>;
124 190         4759 close $InF;
125 190         38903 my $data = $j->decode( join( q{}, @content ) );
126              
127 190         5028 plan tests => scalar keys %$data;
128              
129 190         444754 foreach my $key ( sort keys %$data ) {
130 11737 100       6006936 if ( $key =~ /seconds_total/ ) {
    50          
    100          
    100          
131 6090         44806 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         2674 is( $metrics->{ $key } > 0, $data->{ $key } > 0, "Metrics $expected $key" );
138             }
139             elsif ( $key =~ /authmilter_processes_/) {
140 380         3923 is( $metrics->{ $key } > -1, $data->{ $key } > -1, "Metrics $expected $key" );
141             }
142             else {
143 5077         31684 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       103093 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         35741 };
175             }
176              
177             sub smtp_process {
178 953     953 0 3008254 my ( $args ) = @_;
179              
180 953 50       40653 if ( ! -e $args->{'prefix'} . '/authentication_milter.json' ) {
181 0         0 die "Could not find config " . $args->{'prefix'};
182             }
183 953 50       29056 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         19170 'output' => 'tmp/result/' . $args->{'dest'},
192             };
193 953         223800 unlink 'tmp/authentication_milter_smtp_out.sock';
194 953         10830 my $cat_pid;
195 953 100       9222 if ( ! $args->{'no_cat'} ) {
196 932         7169 $cat_pid = smtpcat( $catargs );
197 898         1796252327 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         389718 'eom_expect' => $args->{'eom_expect'},
211             });
212              
213 919 100       15891 if ( ! $args->{'no_cat'} ) {
214 898         3534976403 waitpid( $cat_pid,0 );
215 898         69986 files_eq_or_diff( 'data/example/' . $args->{'dest'}, 'tmp/result/' . $args->{'dest'}, 'smtp ' . $args->{'desc'} );
216             }
217             else {
218 21         1806 is( $return, 1, 'SMTP Put Returned ok' );
219             }
220             }
221              
222             sub smtp_process_multi {
223 97     97 0 150704 my ( $args ) = @_;
224              
225 97 50       3893 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         4644 'output' => 'tmp/result/' . $args->{'dest'},
239             };
240 97         6047 unlink 'tmp/authentication_milter_smtp_out.sock';
241 97         2050 my $cat_pid = smtpcat( $catargs );
242 95         190027188 sleep 2;
243              
244 95         28851 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         2037 foreach my $item ( @{$args->{'ip'}} ) {
  95         5684  
257 426         2328 push @{$putargs->{'connect_ip'}}, $item;
  426         6675  
258             }
259 95         1001 foreach my $item ( @{$args->{'name'}} ) {
  95         3266  
260 426         3358 push @{$putargs->{'connect_name'}}, $item;
  426         5256  
261             }
262 95         2692 foreach my $item ( @{$args->{'name'}} ) {
  95         2467  
263 426         2364 push @{$putargs->{'helo_host'}}, $item;
  426         10835  
264             }
265 95         3430 foreach my $item ( @{$args->{'from'}} ) {
  95         2033  
266 426         2139 push @{$putargs->{'mail_from'}}, $item;
  426         4386  
267             }
268 95         426 foreach my $item ( @{$args->{'to'}} ) {
  95         2001  
269 426         1230 push @{$putargs->{'rcpt_to'}}, $item;
  426         3593  
270             }
271 95         1001 foreach my $item ( @{$args->{'source'}} ) {
  95         1527  
272 426         2748 push @{$putargs->{'mail_file'}}, 'data/source/' . $item;
  426         7959  
273             }
274             #warn 'Testing ' . $args->{'source'} . ' > ' . $args->{'dest'} . "\n";
275              
276 95         4837 smtpput( $putargs );
277              
278 95         371628741 waitpid( $cat_pid,0 );
279              
280 95         8462 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 3229107 my ( $args ) = @_;
285              
286 886 50       58549 if ( ! -e $args->{'prefix'} . '/authentication_milter.json' ) {
287 0         0 die "Could not find config";
288             }
289 886 50       37433 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         40760 'output' => 'tmp/result/' . $args->{'dest'},
303             });
304              
305 853         233261 files_eq_or_diff( 'data/example/' . $args->{'dest'}, 'tmp/result/' . $args->{'dest'}, 'milter ' . $args->{'desc'} );
306             }
307              
308             sub smtpput {
309 1016     1016 0 4022544 my ( $args ) = @_;
310              
311 1016         19392 my $mailer_name = $args->{'mailer_name'};
312              
313 1016         15808 my $mail_file_a = $args->{'mail_file'};
314 1016         21863 my $mail_from_a = $args->{'mail_from'};
315 1016         6730 my $rcpt_to_a = $args->{'rcpt_to'};
316 1016         15803 my $x_name_a = $args->{'connect_name'};
317 1016         14616 my $x_addr_a = $args->{'connect_ip'};
318 1016         23921 my $x_helo_a = $args->{'helo_host'};
319              
320 1016         20150 my $sock_type = $args->{'sock_type'};
321 1016         18541 my $sock_path = $args->{'sock_path'};
322 1016         5176 my $sock_host = $args->{'sock_host'};
323 1016         4214 my $sock_port = $args->{'sock_port'};
324              
325 1016   100     65729 my $eom_expect = $args->{'eom_expect'} || '250';
326              
327 1016         6455 my $sock;
328 1016 50       29554 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     93343 $sock = IO::Socket::UNIX->new(
337             'Peer' => $sock_path,
338             ) || die "could not open outbound SMTP socket: $!";
339             }
340              
341 1016         8283449 my $line = <$sock>;
342              
343 1016 50       37697 if ( ! $line =~ /250/ ) {
344 0         0 die "Unexpected SMTP response $line";
345             }
346              
347 1016 50       36841 send_smtp_packet( $sock, 'EHLO ' . $mailer_name, '250' ) || die;
348              
349 1016         9485 my $first_time = 1;
350              
351 1016         8369 while ( @$mail_from_a ) {
352              
353 1302 100       8244 if ( ! $first_time ) {
354 286 100       1716 if ( ! send_smtp_packet( $sock, 'RSET', '250' ) ) {
355 47         1645 $sock->close();
356 47         4841 return;
357             };
358             }
359 1255         5347 $first_time = 0;
360              
361 1255         7156 my $mail_file = shift @$mail_file_a;
362 1255         7047 my $mail_from = shift @$mail_from_a;
363 1255         6380 my $rcpt_to = shift @$rcpt_to_a;
364 1255         6643 my $x_name = shift @$x_name_a;
365 1255         5909 my $x_addr = shift @$x_addr_a;
366 1255         4717 my $x_helo = shift @$x_helo_a;
367              
368 1255         18778 my $mail_data = q{};
369              
370 1255 50       10095 if ( $mail_file eq '-' ) {
371 0         0 while ( my $l = <> ) {
372 0         0 $mail_data .= $l;
373             }
374             }
375             else {
376 1255 50       58635 if ( ! -e $mail_file ) {
377 0         0 die "Mail file $mail_file does not exist";
378             }
379 1255         131282 open my $inf, '<', $mail_file;
380 1255         256070 my @all = <$inf>;
381 1255         39126 $mail_data = join( q{}, @all );
382 1255         39071 close $inf;
383             }
384              
385 1255         115002 $mail_data =~ s/\015?\012/\015\012/g;
386             # Handle transparency
387 1255         13055 $mail_data =~ s/\015\012\./\015\012\.\./g;
388              
389 1255 50       10929 send_smtp_packet( $sock, 'XFORWARD NAME=' . $x_name, '250' ) || die;
390 1255 50       15764 send_smtp_packet( $sock, 'XFORWARD ADDR=' . $x_addr, '250' ) || die;
391 1255 50       21862 send_smtp_packet( $sock, 'XFORWARD HELO=' . $x_helo, '250' ) || die;
392              
393 1255 50       12250 send_smtp_packet( $sock, 'MAIL FROM:' . $mail_from, '250' ) || die;
394 1255 50       13992 send_smtp_packet( $sock, 'RCPT TO:' . $rcpt_to, '250' ) || die;
395 1255 50       11169 send_smtp_packet( $sock, 'DATA', '354' ) || die;
396              
397 1255         72197 print $sock $mail_data;
398 1255         21099 print $sock "\r\n";
399              
400 1255 50       9755 send_smtp_packet( $sock, '.', $eom_expect ) || return 0;
401              
402             }
403              
404 969 50       6852 send_smtp_packet( $sock, 'QUIT', '221' ) || return 0;
405 969         26058 $sock->close();
406              
407 969         109871 return 1;
408             }
409              
410             sub send_smtp_packet {
411 11056     11056 0 126934 my ( $socket, $send, $expect ) = @_;
412 11056         524418 print $socket "$send\r\n";
413 11056         380427543 my $recv = <$socket>;
414 11056 50       116153 $recv = '' if !defined $recv;
415 11056         125980 while ( $recv =~ /^\d\d\d\-/ ) {
416 4056         1566723 $recv = <$socket>;
417             }
418 11056 100       423751 if ( $recv =~ /^$expect/ ) {
419 11009         125034 return 1;
420             }
421             else {
422 47         3995 $recv =~ s/\r?\n?$//;
423 47         1410 $send =~ s/\r?\n?$//;
424 47         5264 warn "SMTP Send expected \"$expect\" received \"$recv\" when sending \"$send\"\n";
425 47         705 return 0;
426             }
427             }
428              
429             sub smtpcat {
430 1033     1033 0 14090 my ( $args ) = @_;
431              
432 1033         3438466 my $cat_pid = fork();
433 1033 50       65263 die "unable to fork: $!" unless defined($cat_pid);
434 1033 100       125307 return $cat_pid if $cat_pid;
435              
436 38         5884 my $sock_type = $args->{'sock_type'};
437 38         2841 my $sock_path = $args->{'sock_path'};
438 38         2705 my $sock_host = $args->{'sock_host'};
439 38         1975 my $sock_port = $args->{'sock_port'};
440              
441 38         1811 my $remove = $args->{'remove'};
442 38         2443 my $output = $args->{'output'};
443              
444 38         1854 my @out_lines;
445              
446             my $sock;
447 38 50       6039 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     7727 $sock = IO::Socket::UNIX->new(
457             'Listen' => 5,
458             'Local' => $sock_path,
459             ) || die "could not open socket: $!";
460             }
461              
462 38         68533 my $accept = $sock->accept();
463              
464 38         86582356 print $accept "220 smtp.cat ESMTP Test\r\n";
465              
466 38     0   6773 local $SIG{'ALRM'} = sub{ die "Timeout\n" };
  0         0  
467 38         1365 alarm( 60 );
468              
469 38         704 my $quit = 0;
470 38         766 while ( ! $quit ) {
471 353   50     2396284 my $command = <$accept> || { $quit = 1 };
472 353         4261 alarm( 60 );
473              
474 353 50       14552 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         1241 push @out_lines, $command;
480 38         2379 print $accept "250 EHLO Ok\r\n";
481             }
482             elsif ( $command =~ /^MAIL/ ) {
483 45         1034 push @out_lines, $command;
484 45         2246 print $accept "250 MAIL Ok\r\n";
485             }
486             elsif ( $command =~ /^XFORWARD/ ) {
487 135         1408 push @out_lines, $command;
488 135         7089 print $accept "250 XFORWARD Ok\r\n";
489             }
490             elsif ( $command =~ /^RCPT/ ) {
491 45         862 push @out_lines, $command;
492 45         2167 print $accept "250 RCPT Ok\r\n";
493             }
494             elsif ( $command =~ /^RSET/ ) {
495 7         89 push @out_lines, $command;
496 7         362 print $accept "250 RSET Ok\r\n";
497             }
498             elsif ( $command =~ /^DATA/ ) {
499 45         690 push @out_lines, $command;
500 45         2733 print $accept "354 Send\r\n";
501             DATA:
502 45         16633 while ( my $line = <$accept> ) {
503 3049         18759 alarm( 60 );
504 3049         22058 push @out_lines, $line;
505 3049 100       10756 last DATA if $line eq ".\r\n";
506             # Handle transparency
507 3004 100       17633 if ( $line =~ /^\./ ) {
508 28         283 $line = substr( $line, 1 );
509             }
510             }
511 45         2598 print $accept "250 DATA Ok\r\n";
512             }
513             elsif ( $command =~ /^QUIT/ ) {
514 38         979 push @out_lines, $command;
515 38         2633 print $accept "221 Bye\r\n";
516 38         699 $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         7525 open my $file, '>', $output;
525 38         922 my $i = 0;
526 38         623 foreach my $line ( @out_lines ) {
527 3402         5523 $i++;
528 3402 100       6235 $line = "############\n" if grep { $i == $_ } @$remove;
  8644         18828  
529 3402         8075 print $file $line;
530             }
531 38         5214 close $file;
532              
533 38         1898 $accept->close();
534 38         3754 $sock->close();
535              
536 38         23531 exit 0;
537             }
538              
539             sub client {
540 886     886 0 5907 my ( $args ) = @_;
541 886         3270583 my $pid = fork();
542 886 50       54576 die "unable to fork: $!" unless defined($pid);
543 886 100       31028 if ( ! $pid ) {
544              
545 33         5459 my $output = $args->{'output'};
546 33         3392 delete $args->{'output'};
547              
548 33         3676 $Mail::Milter::Authentication::Config::PREFIX = $args->{'prefix'};
549 33         2582 delete $args->{'prefix'};
550 33         2802 $args->{'testing'} = 1;
551              
552 33         9093 my $client = Mail::Milter::Authentication::Client->new( $args );
553              
554 33         932 $client->process();
555              
556 33         16166 open my $file, '>', $output;
557 33         529 print $file $client->result();
558 33         4514 close $file;
559 33         2322 exit 0;
560              
561             }
562 853         4644940485 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.20230911
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