File Coverage

blib/lib/Mail/Milter/Authentication/Tester.pm
Criterion Covered Total %
statement 299 330 90.6
branch 79 126 62.7
condition 5 12 41.6
subroutine 28 29 96.5
pod 0 11 0.0
total 411 508 80.9


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Tester;
2 92     92   15583246 use strict;
  92         2434  
  92         2297  
3 92     92   459 use warnings;
  92         140  
  92         5544  
4             our $VERSION = '20191206'; # VERSION
5              
6             our @ISA = qw{ Exporter }; ## no critic
7             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
8              
9 92     92   1641 use Net::DNS::Resolver::Mock 1.20171219;
  92         165853  
  92         2643  
10 92     92   538 use Test::More;
  92         143  
  92         516  
11 92     92   22621 use Test::File::Contents;
  92         8970  
  92         6102  
12              
13 92     92   423 use Cwd qw{ cwd };
  92         188  
  92         4384  
14 92     92   549 use IO::Socket::INET;
  92         140  
  92         3823  
15 92     92   68188 use IO::Socket::UNIX;
  92         98  
  92         1701  
16 92     92   118549 use JSON;
  92         804276  
  92         421  
17 92     92   46951 use Module::Load;
  92         69320  
  92         511  
18              
19 92     92   43764 use Mail::Milter::Authentication;
  92         410  
  92         3903  
20 92     92   44687 use Mail::Milter::Authentication::Client;
  92         240  
  92         4546  
21 92     92   723 use Mail::Milter::Authentication::Config;
  92         188  
  92         3339  
22 92     92   591 use Mail::Milter::Authentication::Protocol::Milter;
  92         563  
  92         2742  
23 92     92   545 use Mail::Milter::Authentication::Protocol::SMTP;
  92         227  
  92         290229  
24              
25             my $base_dir = cwd();
26              
27             our $MASTER_PROCESS_PID = $$;
28              
29              
30             {
31             my $milter_pid;
32              
33             sub start_milter {
34 180     180 0 126289 my ( $prefix ) = @_;
35              
36 180 50       1410 return if $milter_pid;
37              
38 180 50       4216 if ( ! -e $prefix . '/authentication_milter.json' ) {
39 0         0 die "Could not find config";
40             }
41              
42 180         1126182 system "cp $prefix/mail-dmarc.ini .";
43              
44 180         328210 $milter_pid = fork();
45 180 50       15642 die "unable to fork: $!" unless defined($milter_pid);
46 180 100       7850 if (!$milter_pid) {
47 28         5245 $Mail::Milter::Authentication::Config::PREFIX = $prefix;
48 28         2354 $Mail::Milter::Authentication::Config::IDENT = 'test_authentication_milter_test';
49 28         7420 my $Resolver = Net::DNS::Resolver::Mock->new();
50 28         56698 $Resolver->zonefile_read( 'zonefile' );
51 28         847732 $Mail::Milter::Authentication::Handler::TestResolver = $Resolver;
52 28         1778 Mail::Milter::Authentication::start({
53             'pid_file' => 'tmp/authentication_milter.pid',
54             'daemon' => 0,
55             });
56 0         0 die;
57             }
58              
59 152         760026557 sleep 5;
60 152         45855 open my $pid_file, '<', 'tmp/authentication_milter.pid';
61 152         52903 $milter_pid = <$pid_file>;
62 152         3464 close $pid_file;
63 152         21343 print "Milter started at pid $milter_pid\n";
64 152         8637 return;
65             }
66              
67             sub stop_milter {
68 104 100   104 0 1170 return if ! $milter_pid;
69 97         10596 kill( 'HUP', $milter_pid );
70 97         318131875 waitpid ($milter_pid,0);
71 97         18930 print "Milter killed at pid $milter_pid\n";
72 97         593 undef $milter_pid;
73 97         15037 unlink 'tmp/authentication_milter.pid';
74 97         5732 unlink 'mail-dmarc.ini';
75 97         956 return;
76             }
77              
78             END {
79 92 100   92   5599286 return if $MASTER_PROCESS_PID != $$;
80 7         41 stop_milter();
81             }
82             }
83              
84             sub get_metrics {
85 97     97 0 1488 my ( $path ) = @_;
86              
87 97         3285 my $sock = IO::Socket::UNIX->new(
88             'Peer' => $path,
89             );
90              
91 97         48779 print $sock "GET /metrics HTTP/1.0\n\n";
92              
93 97         602 my $data = {};
94              
95 97         224682 while ( my $line = <$sock> ) {
96 291         1307 chomp $line;
97 291 100       1848 last if $line eq q{};
98             }
99 97         406079 while ( my $line = <$sock> ) {
100 8676         15329 chomp $line;
101 8676 100       23148 next if $line =~ /^#/;
102 5516         18134 $line =~ /^(.*)\{(.*)\} (.*)$/;
103 5516         12202 my $count_id = $1;
104 5516         10617 my $labels = $2;
105 5516         9039 my $count = $3;
106 5516         50382 $data->{ $count_id . '{' . $labels . '}' } = $count;
107             }
108              
109 97         3174 return $data;
110             }
111              
112             sub test_metrics {
113 97     97 0 5000 my ( $expected ) = @_;
114              
115             # Sleep for 5 to allow server to catch up on metrics
116 97         485015074 sleep 5;
117              
118             subtest $expected => sub {
119              
120 97     97   242731 my $metrics = get_metrics( 'tmp/authentication_milter_test_metrics.sock' );
121 97         4446 my $j = JSON->new();
122              
123 97 50       3263 if ( -e $expected ) {
124              
125 97         5371 open my $InF, '<', $expected;
126 97         6413 my @content = <$InF>;
127 97         1186 close $InF;
128 97         12869 my $data = $j->decode( join( q{}, @content ) );
129              
130 97         1736 plan tests => scalar keys %$data;
131              
132 97         121422 foreach my $key ( sort keys %$data ) {
133 5458 100       2999967 if ( $key =~ /seconds_total/ ) {
    50          
    100          
    100          
134 2638         14535 is( $metrics->{ $key } > 0, $data->{ $key } > 0, "Metrics $key" );
135             }
136             elsif ( $key =~ /microseconds_sum/ ) {
137 0         0 is( $metrics->{ $key } > 0, $data->{ $key } > 0, "Metrics $key" );
138             }
139             elsif ( $key =~ /authmilter_forked_children_total/ ) {
140 97         1194 is( $metrics->{ $key } > 0, $data->{ $key } > 0, "Metrics $key" );
141             }
142             elsif ( $key =~ /authmilter_processes_/) {
143 194         1778 is( $metrics->{ $key } > -1, $data->{ $key } > -1, "Metrics $key" );
144             }
145             else {
146 2529         10836 is( $metrics->{ $key }, $data->{ $key }, "Metrics $key" );
147             }
148             }
149              
150             }
151             else {
152 0         0 fail( 'Metrics data does not exist' );
153             }
154              
155 97 50       49407 if ( $ENV{'WRITE_METRICS'} ) {
156 0         0 foreach my $key ( sort keys %$metrics ) {
157 0 0       0 if ( $key =~ /seconds_total/ ) {
    0          
    0          
    0          
158 0 0       0 $metrics->{ $key } = 123456 if $metrics->{ $key } > 0;
159             }
160             elsif ( $key =~ /microseconds_sum/ ) {
161 0 0       0 $metrics->{ $key } = 123456 if $metrics->{ $key } > 0;
162             }
163             elsif ( $key =~ /authmilter_forked_children_total/ ) {
164 0 0       0 $metrics->{ $key } = 123456 if $metrics->{ $key } > 0;
165             }
166             elsif ( $key =~ /authmilter_processes_/) {
167 0 0       0 $metrics->{ $key } = 123456 if $metrics->{ $key } > -1;
168             }
169             }
170 0         0 open my $OutF, '>', $expected;
171 0         0 $j->pretty();
172 0         0 $j->canonical();
173 0         0 print $OutF $j->encode( $metrics );
174 0         0 close $OutF;
175             }
176              
177 97         10908 };
178              
179 97         421634 return;
180             }
181              
182             sub smtp_process {
183 638     638 0 45750 my ( $args ) = @_;
184              
185 638 50       22466 if ( ! -e $args->{'prefix'} . '/authentication_milter.json' ) {
186 0         0 die "Could not find config " . $args->{'prefix'};
187             }
188 638 50       12534 if ( ! -e 'data/source/' . $args->{'source'} ) {
189 0         0 die "Could not find source";
190             }
191              
192             my $catargs = {
193             'sock_type' => 'unix',
194             'sock_path' => 'tmp/authentication_milter_smtp_out.sock',
195             'remove' => [10,11],
196 638         11024 'output' => 'tmp/result/' . $args->{'dest'},
197             };
198 638         57298 unlink 'tmp/authentication_milter_smtp_out.sock';
199 638         3135 my $cat_pid;
200 638 100       3849 if ( ! $args->{'no_cat'} ) {
201 627         4571 $cat_pid = smtpcat( $catargs );
202 600         1200111942 sleep 2;
203             }
204              
205             my $return = smtpput({
206             'sock_type' => 'unix',
207             'sock_path' => 'tmp/authentication_milter_test.sock',
208             'mailer_name' => 'test.module',
209             'connect_ip' => [ $args->{'ip'} ],
210             'connect_name' => [ $args->{'name'} ],
211             'helo_host' => [ $args->{'name'} ],
212             'mail_from' => [ $args->{'from'} ],
213             'rcpt_to' => [ $args->{'to'} ],
214             'mail_file' => [ 'data/source/' . $args->{'source'} ],
215 611         183331 'eom_expect' => $args->{'eom_expect'},
216             });
217              
218 611 100       13540 if ( ! $args->{'no_cat'} ) {
219 600         1623942409 waitpid( $cat_pid,0 );
220 600         32471 files_eq_or_diff( 'data/example/' . $args->{'dest'}, 'tmp/result/' . $args->{'dest'}, 'smtp ' . $args->{'desc'} );
221             }
222             else {
223 11         605 is( $return, 1, 'SMTP Put Returned ok' );
224             }
225              
226 611         1216077 return;
227             }
228              
229             sub smtp_process_multi {
230 77     77 0 10083 my ( $args ) = @_;
231              
232 77 50       3246 if ( ! -e $args->{'prefix'} . '/authentication_milter.json' ) {
233 0         0 die "Could not find config";
234             }
235              
236             # Hardcoded lines to remove in subsequent messages
237             # If you change the source email then change the awk
238             # numbers here too.
239             # This could be better!
240              
241             my $catargs = {
242             'sock_type' => 'unix',
243             'sock_path' => 'tmp/authentication_milter_smtp_out.sock',
244             'remove' => $args->{'filter'},
245 77         2331 'output' => 'tmp/result/' . $args->{'dest'},
246             };
247 77         5783 unlink 'tmp/authentication_milter_smtp_out.sock';
248 77         1359 my $cat_pid = smtpcat( $catargs );
249 75         150013109 sleep 2;
250              
251 75         13810 my $putargs = {
252             'sock_type' => 'unix',
253             'sock_path' => 'tmp/authentication_milter_test.sock',
254             'mailer_name' => 'test.module',
255             'connect_ip' => [],
256             'connect_name' => [],
257             'helo_host' => [],
258             'mail_from' => [],
259             'rcpt_to' => [],
260             'mail_file' => [],
261             };
262              
263 75         1264 foreach my $item ( @{$args->{'ip'}} ) {
  75         2175  
264 336         1054 push @{$putargs->{'connect_ip'}}, $item;
  336         3594  
265             }
266 75         750 foreach my $item ( @{$args->{'name'}} ) {
  75         1312  
267 336         1319 push @{$putargs->{'connect_name'}}, $item;
  336         1690  
268             }
269 75         338 foreach my $item ( @{$args->{'name'}} ) {
  75         978  
270 336         978 push @{$putargs->{'helo_host'}}, $item;
  336         2186  
271             }
272 75         188 foreach my $item ( @{$args->{'from'}} ) {
  75         983  
273 336         1015 push @{$putargs->{'mail_from'}}, $item;
  336         1126  
274             }
275 75         415 foreach my $item ( @{$args->{'to'}} ) {
  75         976  
276 336         1052 push @{$putargs->{'rcpt_to'}}, $item;
  336         3712  
277             }
278 75         712 foreach my $item ( @{$args->{'source'}} ) {
  75         937  
279 336         1659 push @{$putargs->{'mail_file'}}, 'data/source/' . $item;
  336         3371  
280             }
281             #warn 'Testing ' . $args->{'source'} . ' > ' . $args->{'dest'} . "\n";
282              
283 75         2825 smtpput( $putargs );
284              
285 75         206466023 waitpid( $cat_pid,0 );
286              
287 75         5855 files_eq_or_diff( 'data/example/' . $args->{'dest'}, 'tmp/result/' . $args->{'dest'}, 'smtp ' . $args->{'desc'} );
288              
289 75         187251 return;
290             }
291              
292             sub milter_process {
293 590     590 0 35222 my ( $args ) = @_;
294              
295 590 50       19415 if ( ! -e $args->{'prefix'} . '/authentication_milter.json' ) {
296 0         0 die "Could not find config";
297             }
298 590 50       13500 if ( ! -e 'data/source/' . $args->{'source'} ) {
299 0         0 die "Could not find source";
300             }
301              
302             client({
303             'prefix' => $args->{'prefix'},
304             'mailer_name' => 'test.module',
305             'mail_file' => 'data/source/' . $args->{'source'},
306             'connect_ip' => $args->{'ip'},
307             'connect_name' => $args->{'name'},
308             'helo_host' => $args->{'name'},
309             'mail_from' => $args->{'from'},
310             'rcpt_to' => $args->{'to'},
311 590         17776 'output' => 'tmp/result/' . $args->{'dest'},
312             });
313              
314 564         90867 files_eq_or_diff( 'data/example/' . $args->{'dest'}, 'tmp/result/' . $args->{'dest'}, 'milter ' . $args->{'desc'} );
315              
316 564         1081804 return;
317             }
318              
319             sub smtpput {
320 688     688 0 4007202 my ( $args ) = @_;
321              
322 688         5605 my $mailer_name = $args->{'mailer_name'};
323              
324 688         5985 my $mail_file_a = $args->{'mail_file'};
325 688         3248 my $mail_from_a = $args->{'mail_from'};
326 688         5063 my $rcpt_to_a = $args->{'rcpt_to'};
327 688         3291 my $x_name_a = $args->{'connect_name'};
328 688         2835 my $x_addr_a = $args->{'connect_ip'};
329 688         4176 my $x_helo_a = $args->{'helo_host'};
330              
331 688         2353 my $sock_type = $args->{'sock_type'};
332 688         2051 my $sock_path = $args->{'sock_path'};
333 688         2643 my $sock_host = $args->{'sock_host'};
334 688         1910 my $sock_port = $args->{'sock_port'};
335              
336 688   100     19709 my $eom_expect = $args->{'eom_expect'} || '250';
337              
338 688         2791 my $sock;
339 688 50       8212 if ( $sock_type eq 'inet' ) {
    50          
340 0   0     0 $sock = IO::Socket::INET->new(
341             'Proto' => 'tcp',
342             'PeerAddr' => $sock_host,
343             'PeerPort' => $sock_port,
344             ) || die "could not open outbound SMTP socket: $!";
345             }
346             elsif ( $sock_type eq 'unix' ) {
347 688   50     48034 $sock = IO::Socket::UNIX->new(
348             'Peer' => $sock_path,
349             ) || die "could not open outbound SMTP socket: $!";
350             }
351              
352 688         3691327 my $line = <$sock>;
353              
354 688 50       12149 if ( ! $line =~ /250/ ) {
355 0         0 die "Unexpected SMTP response $line";
356             }
357              
358 688 50       6840 send_smtp_packet( $sock, 'EHLO ' . $mailer_name, '250' ) || die;
359              
360 688         2619 my $first_time = 1;
361              
362 688         3594 while ( @$mail_from_a ) {
363              
364 914 100       5543 if ( ! $first_time ) {
365 226 100       1321 if ( ! send_smtp_packet( $sock, 'RSET', '250' ) ) {
366 37         1369 $sock->close();
367 37         3182 return;
368             };
369             }
370 877         3164 $first_time = 0;
371              
372 877         9539 my $mail_file = shift @$mail_file_a;
373 877         3659 my $mail_from = shift @$mail_from_a;
374 877         2594 my $rcpt_to = shift @$rcpt_to_a;
375 877         2847 my $x_name = shift @$x_name_a;
376 877         3993 my $x_addr = shift @$x_addr_a;
377 877         2939 my $x_helo = shift @$x_helo_a;
378              
379 877         4295 my $mail_data = q{};
380              
381 877 50       6031 if ( $mail_file eq '-' ) {
382 0         0 while ( my $l = <> ) {
383 0         0 $mail_data .= $l;
384             }
385             }
386             else {
387 877 50       35017 if ( ! -e $mail_file ) {
388 0         0 die "Mail file $mail_file does not exist";
389             }
390 877         66653 open my $inf, '<', $mail_file;
391 877         71297 my @all = <$inf>;
392 877         13235 $mail_data = join( q{}, @all );
393 877         18192 close $inf;
394             }
395              
396 877         48839 $mail_data =~ s/\015?\012/\015\012/g;
397             # Handle transparency
398 877         6647 $mail_data =~ s/\015\012\./\015\012\.\./g;
399              
400 877 50       6803 send_smtp_packet( $sock, 'XFORWARD NAME=' . $x_name, '250' ) || die;
401 877 50       5387 send_smtp_packet( $sock, 'XFORWARD ADDR=' . $x_addr, '250' ) || die;
402 877 50       5717 send_smtp_packet( $sock, 'XFORWARD HELO=' . $x_helo, '250' ) || die;
403              
404 877 50       7040 send_smtp_packet( $sock, 'MAIL FROM:' . $mail_from, '250' ) || die;
405 877 50       18808 send_smtp_packet( $sock, 'RCPT TO:' . $rcpt_to, '250' ) || die;
406 877 50       5559 send_smtp_packet( $sock, 'DATA', '354' ) || die;
407              
408 877         37797 print $sock $mail_data;
409 877         10680 print $sock "\r\n";
410              
411 877 50       5973 send_smtp_packet( $sock, '.', $eom_expect ) || return 0;
412              
413             }
414              
415 651 50       3903 send_smtp_packet( $sock, 'QUIT', '221' ) || return 0;
416 651         17223 $sock->close();
417              
418 651         68470 return 1;
419             }
420              
421             sub send_smtp_packet {
422 7704     7704 0 65443 my ( $socket, $send, $expect ) = @_;
423 7704         271268 print $socket "$send\r\n";
424 7704         599072533 my $recv = <$socket>;
425 7704         115571 while ( $recv =~ /^\d\d\d\-/ ) {
426 2744         603625 $recv = <$socket>;
427             }
428 7704 100       224566 if ( $recv =~ /^$expect/ ) {
429 7667         62941 return 1;
430             }
431             else {
432 37         3626 warn "SMTP Send expected $expect received $recv when sending $send";
433 37         1073 return 0;
434             }
435             }
436              
437             sub smtpcat {
438 708     708 0 11947 my ( $args ) = @_;
439              
440 708         1368946 my $cat_pid = fork();
441 708 50       31605 die "unable to fork: $!" unless defined($cat_pid);
442 708 100       58770 return $cat_pid if $cat_pid;
443              
444 31         3461 my $sock_type = $args->{'sock_type'};
445 31         1719 my $sock_path = $args->{'sock_path'};
446 31         1096 my $sock_host = $args->{'sock_host'};
447 31         918 my $sock_port = $args->{'sock_port'};
448              
449 31         788 my $remove = $args->{'remove'};
450 31         796 my $output = $args->{'output'};
451              
452 31         738 my @out_lines;
453              
454             my $sock;
455 31 50       2816 if ( $sock_type eq 'inet' ) {
    50          
456 0   0     0 $sock = IO::Socket::INET->new(
457             'Listen' => 5,
458             'LocalHost' => $sock_host,
459             'LocalPort' => $sock_port,
460             'Protocol' => 'tcp',
461             ) || die "could not open socket: $!";
462             }
463             elsif ( $sock_type eq 'unix' ) {
464 31   50     4714 $sock = IO::Socket::UNIX->new(
465             'Listen' => 5,
466             'Local' => $sock_path,
467             ) || die "could not open socket: $!";
468             }
469              
470 31         35941 my $accept = $sock->accept();
471              
472 31         72656249 print $accept "220 smtp.cat ESMTP Test\r\n";
473              
474 31     0   3815 local $SIG{'ALRM'} = sub{ die "Timeout\n" };
  0         0  
475 31         800 alarm( 60 );
476              
477 31         526 my $quit = 0;
478 31         682 while ( ! $quit ) {
479 297   50     15443491 my $command = <$accept> || { $quit = 1 };
480 297         2860 alarm( 60 );
481              
482 297 50       7396 if ( $command =~ /^HELO/ ) {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
483 0         0 push @out_lines, $command;
484 0         0 print $accept "250 HELO Ok\r\n";
485             }
486             elsif ( $command =~ /^EHLO/ ) {
487 31         480 push @out_lines, $command;
488 31         1520 print $accept "250 EHLO Ok\r\n";
489             }
490             elsif ( $command =~ /^MAIL/ ) {
491 38         266 push @out_lines, $command;
492 38         1580 print $accept "250 MAIL Ok\r\n";
493             }
494             elsif ( $command =~ /^XFORWARD/ ) {
495 114         757 push @out_lines, $command;
496 114         4059 print $accept "250 XFORWARD Ok\r\n";
497             }
498             elsif ( $command =~ /^RCPT/ ) {
499 38         307 push @out_lines, $command;
500 38         1488 print $accept "250 RCPT Ok\r\n";
501             }
502             elsif ( $command =~ /^RSET/ ) {
503 7         80 push @out_lines, $command;
504 7         334 print $accept "250 RSET Ok\r\n";
505             }
506             elsif ( $command =~ /^DATA/ ) {
507 38         294 push @out_lines, $command;
508 38         1497 print $accept "354 Send\r\n";
509             DATA:
510 38         11701 while ( my $line = <$accept> ) {
511 2327         11301 alarm( 60 );
512 2327         10579 push @out_lines, $line;
513 2327 100       6551 last DATA if $line eq ".\r\n";
514             # Handle transparency
515 2289 100       12316 if ( $line =~ /^\./ ) {
516 28         148 $line = substr( $line, 1 );
517             }
518             }
519 38         1615 print $accept "250 DATA Ok\r\n";
520             }
521             elsif ( $command =~ /^QUIT/ ) {
522 31         685 push @out_lines, $command;
523 31         1508 print $accept "221 Bye\r\n";
524 31         510 $quit = 1;
525             }
526             else {
527 0         0 push @out_lines, $command;
528 0         0 print $accept "250 Unknown Ok\r\n";
529             }
530             }
531              
532 31         5448 open my $file, '>', $output;
533 31         247 my $i = 0;
534 31         568 foreach my $line ( @out_lines ) {
535 2624         3830 $i++;
536 2624 100       4705 $line = "############\n" if grep { $i == $_ } @$remove;
  7088         14048  
537 2624         6028 print $file $line;
538             }
539 31         3335 close $file;
540              
541 31         919 $accept->close();
542 31         2325 $sock->close();
543              
544 31         16475 exit 0;
545             }
546              
547             sub client {
548 590     590 0 2291 my ( $args ) = @_;
549 590         1009141 my $pid = fork();
550 590 50       24719 die "unable to fork: $!" unless defined($pid);
551 590 100       10322 if ( ! $pid ) {
552              
553 26         2493 my $output = $args->{'output'};
554 26         1609 delete $args->{'output'};
555              
556 26         1629 $Mail::Milter::Authentication::Config::PREFIX = $args->{'prefix'};
557 26         785 delete $args->{'prefix'};
558 26         1201 $args->{'testing'} = 1;
559              
560 26         3518 my $client = Mail::Milter::Authentication::Client->new( $args );
561              
562 26         539 $client->process();
563              
564 26         4238 open my $file, '>', $output;
565 26         240 print $file $client->result();
566 26         2628 close $file;
567 26         804 exit 0;
568              
569             }
570 564         2413870924 waitpid( $pid, 0 );
571 564         22243 return;
572             }
573              
574             1;
575              
576             __END__
577              
578             =pod
579              
580             =encoding UTF-8
581              
582             =head1 NAME
583              
584             Mail::Milter::Authentication::Tester
585              
586             =head1 VERSION
587              
588             version 20191206
589              
590             =head1 AUTHOR
591              
592             Marc Bradshaw <marc@marcbradshaw.net>
593              
594             =head1 COPYRIGHT AND LICENSE
595              
596             This software is copyright (c) 2018 by Marc Bradshaw.
597              
598             This is free software; you can redistribute it and/or modify it under
599             the same terms as the Perl 5 programming language system itself.
600              
601             =cut