File Coverage

blib/lib/Test/MTA/Exim4.pm
Criterion Covered Total %
statement 243 271 89.6
branch 74 134 55.2
condition 29 86 33.7
subroutine 26 29 89.6
pod 17 17 100.0
total 389 537 72.4


line stmt bran cond sub pod time code
1             package Test::MTA::Exim4;
2              
3 5     5   190377 use warnings;
  5         542  
  5         159  
4 5     5   44 use strict;
  5         8  
  5         190  
5 5     5   119 use 5.006;
  5         18  
  5         220  
6 5     5   26 use base qw(Class::Accessor::Fast);
  5         8  
  5         5001  
7 5     5   25972 use IPC::Cmd qw[can_run run];
  5         494116  
  5         396  
8 5     5   60 use Test::Builder;
  5         13  
  5         18436  
9              
10             our $VERSION = '0.05'; # VERSION
11             our $AUTHORITY = 'cpan:NIGELM'; # AUTHORITY
12              
13             __PACKAGE__->mk_accessors(qw[ debug]);
14             __PACKAGE__->mk_ro_accessors(qw[exim_path config_file test timeout]);
15              
16             # ABSTRACT: Test Anything interface for testing Exim4 configurations
17              
18              
19             # ------------------------------------------------------------------------
20              
21              
22             sub new {
23 4     4 1 91 my ( $proto, $fields ) = @_;
24 4   33     40 my ($class) = ref $proto || $proto;
25              
26             # copy fields into self (without checking) and bless
27 4 50       19 my $self = defined($fields) ? { %{$fields} } : {};
  4         25  
28 4         61 bless( $self, $class );
29              
30             # set some defaults if not already in place
31 4   0     61 $self->{exim_path}
      33        
32             ||= $ENV{DEFAULT_EXIM_PATH}
33             || can_run('exim4')
34             || can_run('exim')
35             || '/usr/sbin/exim';
36 4   33     45 $self->{config_file} ||= $ENV{DEFAULT_EXIM_CONFIG_FILE};
37 4   33     60 $self->{test} ||= Test::Builder->new;
38 4   50     76 $self->{timeout} ||= 5;
39              
40             # check that underlying IPC::Cmd has sufficient capabilities
41 4 50       41 IPC::Cmd->can_capture_buffer
42             || $self->_croak('IPC::Cmd cannot capture buffers on this system - testing will fail');
43              
44             # check that exim is there and runnable
45 4 50       80863 can_run( $self->{exim_path} )
46             || $self->_croak('No runnable exim binary found');
47              
48             # reset internal state
49 4         1848431 $self->reset;
50              
51 4         21 return $self;
52             }
53              
54             # ------------------------------------------------------------------------
55              
56              
57             sub reset {
58 4     4 1 12 my $self = shift;
59              
60 4         25 $self->{_state} = { config => {} };
61              
62 4         13 return $self;
63             }
64              
65             # ------------------------------------------------------------------------
66              
67              
68             sub config_ok {
69 4     4 1 2522 my $self = shift;
70 4         11 my $msg = shift;
71              
72 4         19 $self->_run_exim_bv;
73              
74             # pad the msg if not specified
75 4   50     175 $msg ||= sprintf( 'config %s is valid',
      33        
76             ( $self->{_state}{exim_config_file} || $self->{config_file} || '(unknown)' ) );
77              
78 4 50       56 $self->test->ok( $self->{_state}{config}{ok}, $msg ) || $self->_diag;
79             }
80              
81             # ------------------------------------------------------------------------
82              
83              
84             sub exim_version {
85 2     2 1 1082 my $self = shift;
86              
87 2         6 $self->_run_exim_bv;
88              
89 2         34 return $self->{_state}{exim_version};
90             }
91              
92             # ------------------------------------------------------------------------
93              
94              
95             sub exim_build {
96 1     1 1 2 my $self = shift;
97              
98 1         3 $self->_run_exim_bv;
99              
100 1         6 return $self->{_state}{exim_build};
101             }
102              
103             # ------------------------------------------------------------------------
104              
105              
106             sub has_option {
107 1     1 1 614 my $self = shift;
108 1         2 my $option = shift;
109 1         2 my $msg = shift;
110              
111 1         5 $self->_run_exim_bv;
112 1 50       11 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
113 1         6 $self->_run_exim_bp;
114              
115             # pad the msg if not specified
116 1   33     33 $msg ||= sprintf( 'Checking for existence of %s option', $option );
117              
118 1 50       33 $self->test->ok( exists $self->{_state}{option}{$option}, $msg )
119             || $self->_diag;
120             }
121              
122             # ------------------------------------------------------------------------
123              
124              
125             sub has_not_option {
126 1     1 1 540 my $self = shift;
127 1         2 my $option = shift;
128 1         3 my $msg = shift;
129              
130 1         3 $self->_run_exim_bv;
131 1 50       8 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
132 1         4 $self->_run_exim_bp;
133              
134             # pad the msg if not specified
135 1   33     12 $msg ||= sprintf( 'Checking for lack of existence of %s option', $option );
136              
137 1 50       7 $self->test->ok( !exists $self->{_state}{option}{$option}, $msg )
138             || $self->_diag;
139             }
140              
141             # ------------------------------------------------------------------------
142              
143              
144             sub option_is {
145 3     3 1 1178 my $self = shift;
146 3         12 my $option = shift;
147 3         6 my $value = shift;
148 3         4 my $msg = shift;
149              
150 3         10 $self->_run_exim_bv;
151 3 50       13 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
152 3         11 $self->_run_exim_bp;
153              
154             # pad the msg if not specified
155 3   33     24 $msg ||= sprintf( 'Checking for %s option', $option );
156              
157 3 50       11 $self->test->is_eq( $self->{_state}{option}{$option}, $value, $msg )
158             || $self->_diag;
159             }
160              
161             # ------------------------------------------------------------------------
162              
163              
164             sub option_is_true {
165 0     0 1 0 my $self = shift;
166 0         0 my $option = shift;
167 0         0 my $msg = shift;
168              
169 0         0 $self->_run_exim_bv;
170 0 0       0 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
171 0         0 $self->_run_exim_bp;
172              
173             # pad the msg if not specified
174 0   0     0 $msg ||= sprintf( 'Checking for %s option', $option );
175              
176 0 0       0 my $value = $option =~ s/^no_// ? undef : 1;
177 0 0       0 $self->test->is_eq( $self->{_state}{option}{$option}, $value, $msg )
178             || $self->_diag;
179             }
180              
181             # ------------------------------------------------------------------------
182              
183              
184             sub option_is_false {
185 0     0 1 0 my $self = shift;
186 0         0 my $option = shift;
187 0         0 my $msg = shift;
188              
189 0         0 $self->_run_exim_bv;
190 0 0       0 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
191 0         0 $self->_run_exim_bp;
192              
193             # pad the msg if not specified
194 0   0     0 $msg ||= sprintf( 'Checking for %s option', $option );
195              
196 0 0       0 my $value = $option =~ s/^no_// ? 1 : undef;
197 0 0       0 $self->test->is_eq( $self->{_state}{option}{$option}, $value, $msg )
198             || $self->_diag;
199             }
200              
201             # ------------------------------------------------------------------------
202              
203              
204             sub expansion_is {
205 3     3 1 4686 my $self = shift;
206 3         15 my $string = shift;
207 3         10 my $expect = shift;
208 3         6 my $msg = shift;
209              
210 3         27 $self->_run_exim_bv;
211 3 50       24 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
212              
213             # pad the msg if not specified
214 3   33     34 $msg ||= sprintf( "Checking expansion of '%s'", $string );
215              
216 3         12 my $got = $self->_run_exim_be($string);
217 3         16 chomp $got;
218 3 50       97 $self->test->is_eq( $got, $expect, $msg )
219             || $self->_diag;
220             }
221              
222             # ------------------------------------------------------------------------
223              
224              
225             sub has_capability {
226 15     15 1 9859 my $self = shift;
227 15         35 my $type = shift;
228 15         37 my $what = shift;
229 15         19 my $msg = shift;
230              
231 15         43 $self->_run_exim_bv;
232 15 50       51 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
233 15 50       39 $self->_croak('Capability requires a type') unless ($type);
234 15 50       31 $self->_croak('Capability requires a thing to check') unless ($what);
235              
236             # pad the msg if not specified
237 15   33     109 $msg ||= sprintf( 'Checking for %s/%s capability', $type, $what );
238              
239 15 50 33     64 $self->test->ok( ( $self->{_state}{config}{$type} && $self->{_state}{config}{$type}{$what} ), $msg )
240             || $self->_diag;
241             }
242              
243             # ------------------------------------------------------------------------
244              
245              
246             sub has_not_capability {
247 2     2 1 916 my $self = shift;
248 2         8 my $type = shift;
249 2         4 my $what = shift;
250 2         4 my $msg = shift;
251              
252 2         6 $self->_run_exim_bv;
253 2 50       10 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
254 2 50       7 $self->_croak('Capability requires a type') unless ($type);
255 2 50       5 $self->_croak('Capability requires a thing to check') unless ($what);
256              
257             # pad the msg if not specified
258 2   33     21 $msg ||= sprintf( 'Checking for lack of %s/%s capability', $type, $what );
259              
260 2 50 33     31 $self->test->ok( ( $self->{_state}{config}{$type} && !$self->{_state}{config}{$type}{$what} ), $msg )
261             || $self->_diag;
262             }
263              
264             # ------------------------------------------------------------------------
265              
266              
267             sub routes_ok {
268 3     3 1 3995 my $self = shift;
269 3         18 my $addr = shift;
270 3         5 my $msg = shift;
271              
272 3 50       28 $self->_croak('Requires an address') unless ($addr);
273              
274             # run the check
275 3         14 my $res = $self->_run_exim_bt($addr);
276              
277             # pad the msg if not specified
278 3   33     49 $msg ||= sprintf( 'Can route to %s', $addr );
279              
280             # OK if there are no undeliverables and there are deliverables
281 3 50 33     6006 $self->test->ok( ( $res->{deliverable} && !$res->{undeliverable} ), $msg )
282             || $self->_diag;
283             }
284              
285             # ------------------------------------------------------------------------
286              
287              
288             sub routes_as_ok {
289 5     5 1 4591 my $self = shift;
290 5         18 my $addr = shift;
291 5         11 my $target = shift;
292 5         191 my $msg = shift;
293              
294 5 50       18 $self->_croak('Requires an address') unless ($addr);
295 5 50       18 $self->_croak('Requires a target description') unless ($target);
296              
297             # if target is a hash, wrap it in an array
298 5 100       39 $target = [$target] if ( ref($target) eq 'HASH' );
299 5 50       22 $self->_croak('target should be an arrayref')
300             unless ( ref($target) eq 'ARRAY' );
301              
302             # run the check
303 5         29 my $res = $self->_run_exim_bt($addr);
304              
305             # pad the msg if not specified
306 5   33     79 $msg ||= sprintf( 'Can route to %s', $addr );
307              
308             # check we get the right number of things back
309 5         23 my $count_ok =
310 5         7 ( scalar( keys %{ $res->{addresses} } ) == scalar( @{$target} ) );
  5         11  
311 5         13 my $count = scalar( @{$target} );
  5         11  
312 5         12 my $addr_count_ok = 0;
313 5         10 my $addresses = { %{ $res->{addresses} } }; #copy address info
  5         21  
314              
315             # only do these tests if the count matches the rules
316 5 50       27 if ($count_ok) {
317 5         8 foreach my $targetspec ( @{$target} ) {
  5         13  
318 11 50       44 $self->_croak('target spec should be hashref')
319             unless ( ref($targetspec) eq 'HASH' );
320 11         14 foreach my $addr ( keys %{$addresses} ) {
  11         29  
321 11         14 my $thisone = 1;
322 11         14 foreach my $key ( keys %{$targetspec} ) {
  11         31  
323 19 50 33     274 unless ( exists( $addresses->{$addr}{$key} )
324             && ( $addresses->{$addr}{$key} eq $targetspec->{$key} ) ) {
325 0         0 $thisone = 0;
326 0         0 last;
327             }
328             }
329 11 50       25 if ($thisone) {
330 11         13 $addr_count_ok++;
331 11         28 last;
332             }
333             }
334             }
335             }
336              
337             # return test status
338 5 50 33     57 $self->test->ok( ( $count_ok && ( $addr_count_ok == $count ) ), $msg )
339             || $self->_diag;
340             }
341              
342             # ------------------------------------------------------------------------
343              
344              
345             sub discards_ok {
346 1     1 1 9511 my $self = shift;
347 1         10 my $addr = shift;
348 1         3 my $msg = shift;
349              
350 1 50       7 $self->_croak('Requires an address') unless ($addr);
351              
352             # run the check
353 1         12 my $res = $self->_run_exim_bt($addr);
354              
355             # pad the msg if not specified
356 1   33     17 $msg ||= sprintf( 'Discard for %s', $addr );
357              
358             # OK if there is a total of one address and it was discarded
359 1 50 33     16 $self->test->ok( ( ( $res->{total} == 1 ) && ( values %{ $res->{addresses} } )[0]->{discarded} ), $msg )
360             || $self->_diag;
361             }
362              
363             # ------------------------------------------------------------------------
364              
365              
366             sub undeliverable_ok {
367 1     1 1 782 my $self = shift;
368 1         6 my $addr = shift;
369 1         6 my $msg = shift;
370              
371 1 50       8 $self->_croak('Requires an address') unless ($addr);
372              
373             # run the check
374 1         10 my $res = $self->_run_exim_bt($addr);
375              
376             # pad the msg if not specified
377 1   33     30 $msg ||= sprintf( 'Undeliverable to %s', $addr );
378              
379             # OK if there are no deliverables and there are undeliverables
380 1 50 33     19 $self->test->ok( ( $res->{undeliverable} && !$res->{deliverable} ), $msg )
381             || $self->_diag;
382             }
383              
384             # ------------------------------------------------------------------------
385              
386              
387             # ------------------------------------------------------------------------
388              
389              
390             sub _run_exim_command {
391 18     18   64 my $self = shift;
392 18         51 my @args = @_;
393              
394             # we always put the config file as the first argument if we have one
395 18 50       80 unshift @args, ( '-C' . $self->{config_file} )
396             if ( $self->{config_file} );
397              
398             # run command
399 18         186 my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) = run(
400             command => [ $self->{exim_path}, @args ],
401             verbose => $self->{debug},
402              
403             ## TODO timeout appears to have a nasty interaction which
404             ## causes the tests to fail, plus hang after the run
405             #timeout => $self->{timeout}
406             );
407              
408             # as documented in IPC::Cmd, the buffer returns are an arrayref
409             # unexpectedly, that array has a single element with a slurped string
410             # so we reprocess into a one line per element form
411 18 50       444303 $full_buf = [ map { ( split( /\r?\n/, $_ ) ) } @{ $full_buf || [] } ];
  18         1443  
  18         442  
412 18 50       73 $stdout_buf = [ map { ( split( /\r?\n/, $_ ) ) } @{ $stdout_buf || [] } ];
  18         349  
  18         149  
413 18 50       92 $stderr_buf = [ map { ( split( /\r?\n/, $_ ) ) } @{ $stderr_buf || [] } ];
  0         0  
  18         197  
414              
415 18         146 $self->{_state}{last_error} = $error_code;
416 18         554 $self->{_state}{last_output} = $full_buf;
417              
418 18         453 return ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf );
419             }
420              
421             # ------------------------------------------------------------------------
422              
423              
424             sub _run_exim_bv {
425 32     32   53 my $self = shift;
426              
427             # we only want to run this once per session
428 32 100       300 return if ( $self->{_state}{checked}++ );
429              
430             # run command
431 4         48 my ( $success, undef, undef, $stdout_buf, undef ) = $self->_run_exim_command('-bV');
432              
433             # parse things out if command worked
434 4 50       40 if ($success) {
435 4         93 $self->{_state}{config}{ok} = 1;
436 4         17 foreach ( @{$stdout_buf} ) {
  4         19  
437 48         84 chomp;
438 48 100       588 if (/^Exim\s+version\s+([0-9\.]+)\s+#(\d+)/) {
    100          
    100          
439 4         79 $self->{_state}{exim_version} = $1;
440 4         68 $self->{_state}{exim_build} = $2;
441             }
442             elsif (
443             m{ ^
444             ( support \s+ for | # pick one of these
445             lookups | # in $1
446             authenticators |
447             routers |
448             transports
449             )
450             (?: \s+ \( [a-z\s-]+ \) )? # optional load type for 4.74+ - discarded
451             # eg (built-in)
452             : \s* # followed by a colon
453             (.*) # and the rest of the line in $2
454             $
455             }ix
456             ) {
457 20         93 my $type = lc($1);
458 20         65 my $res = lc($2);
459 20         66 $type =~ tr/a-z/_/cs;
460 20         96 $type =~ s/s$//; # strip trailing s
461 20         65 $res =~ tr|a-z0-9_ /||cd;
462 20         835 $self->{_state}{config}{$type}{$_} = 1 foreach ( split( /[\s\/]/, $res ) );
463             }
464             elsif (/Configuration file is (.*)/) {
465 4         152 $self->{_state}{exim_config_file} = $1;
466             }
467             }
468              
469             # we do sanity checks here - currently croak on these, which might
470             # be too drastic!
471 4 50       38 $self->_croak('No exim version number found')
472             unless ( $self->{_state}{exim_version} );
473             }
474             else {
475 0         0 $self->{_state}{config}{ok} = 0;
476             }
477             }
478              
479             # ------------------------------------------------------------------------
480              
481              
482             sub _run_exim_bp {
483 5     5   8 my $self = shift;
484              
485             # we only want to run this once per session
486 5 100       18 return if ( exists $self->{_state}{option} );
487              
488             # initialize the option hash, because if we don't get anything parseable
489             # back the first time it probably won't succeed subsequently
490 1         7 $self->{_state}{option} = {};
491              
492             # run command
493 1         4 my ( $success, undef, undef, $stdout_buf, undef ) = $self->_run_exim_command('-bP');
494              
495             # parse things out if command worked
496 1 50       14 if ($success) {
497 1         6 foreach ( @{$stdout_buf} ) {
  1         3  
498 27         32 chomp;
499 27 100       133 if (/^(no_)?(\w+)(?: = (.*))?$/) {
500 26         76 my ( $negate, $option, $value ) = ( $1, $2, $3 );
501 26 100       152 $self->{_state}{option}{$option} =
    100          
502             $negate ? undef : defined $value ? $value : 1;
503             }
504             }
505             }
506             }
507              
508             # ------------------------------------------------------------------------
509              
510              
511             sub _run_exim_be {
512 3     3   6 my $self = shift;
513 3         6 my $string = shift;
514              
515             # run command
516 3         10 my ( $success, undef, undef, $stdout_buf, undef ) = $self->_run_exim_command( '-be', $string );
517              
518             # parse things out if command worked
519              
520 3   66     125 return $success && join( "\n", @$stdout_buf, '' );
521             }
522              
523             # ------------------------------------------------------------------------
524              
525              
526             sub _run_exim_bt {
527 10     10   22 my $self = shift;
528 10         18 my $address = shift;
529 10         16 my $sender = shift;
530              
531             # check for sanity... make sure we have a valid binary + config
532 10 50       58 $self->_run_exim_bv unless ( $self->{_state}{config}{ok} );
533 10 50       122 $self->_croak('No exim version number found')
534             unless ( $self->{_state}{config}{ok} );
535              
536 10         38 my @options = ('-bt');
537 10 50       30 push( @options, '-f', $sender ) if ( defined($sender) );
538 10         22 push( @options, '--', $address );
539              
540             # run command - use a -- divider to prevent funkiness in the address
541 10         43 my ( $success, undef, undef, $stdout_buf, undef ) = $self->_run_exim_command(@options);
542              
543             # as exim uses the exit value to signify how well things worked, and
544             # IPC::Cmd obscures this somewhat, we are just going to ignore it!
545             # and parse the output to see what happened...
546 10         64 my @lines = @{$stdout_buf};
  10         47  
547 10         231 my $result = {
548             all_ok => $success,
549             deliverable => 0,
550             undeliverable => 0,
551             total => 0,
552             addresses => {}
553             };
554 10         58 while ( scalar(@lines) ) {
555 22         38 my $line = shift @lines;
556 22 50       289 next if ( $line =~ /^\s*$/ );
557              
558             # this line should be one of:-
559             # is undeliverable
560             # is discarded
561             # -> + more info on next lines
562             # + more info on next lines
563 22 100       91 if ( $line =~ /^(.*) is undeliverable(.*)$/ ) {
564 2         13 $result->{undeliverable}++;
565 2         11 $result->{total}++;
566 2         67 $result->{addresses}{$1} = { ok => 0, reason => $2, address => $1 };
567 2         12 next;
568             }
569 20         31 $result->{deliverable}++;
570 20         32 $result->{total}++;
571 20         138 my $res = { ok => 1, discarded => 0, data => [] };
572 20 100       144 if ( $line =~ /^(.*) -\> (.*)$/ ) {
    100          
573 8         58 $res->{address} = $1;
574 8         33 $res->{target} = $2;
575 8         61 $result->{addresses}{$1} = $res;
576             }
577             elsif ( $line =~ /^(.*) is discarded$/ ) {
578 2         34 $res->{address} = $1;
579 2         13 $res->{discarded} = 1;
580 2         18 $result->{addresses}{$1} = $res;
581             }
582             else {
583 10         40 $res->{address} = $line;
584 10         31 $result->{addresses}{$line} = $res;
585             }
586              
587             # mop up subsequent lines
588 20   100     210 while ( scalar(@lines) && ( $lines[0] =~ /^\s/ ) ) {
589 28         46 $line = shift @lines;
590 28 50       261 if ( $line =~ /^\s+\<-- (.*)/ ) {
    100          
    100          
591 0   0     0 $res->{original} ||= [];
592 0         0 push( @{ $res->{original} }, $1 );
  0         0  
593             }
594             elsif ( $line =~ /^\s+transport = (.*)/ ) {
595 8         86 $res->{transport} = $1;
596             }
597             elsif ( $line =~ /^\s+router = (.*), transport = (.*)/ ) {
598 10         60 $res->{router} = $1;
599 10         90 $res->{transport} = $2;
600             }
601             else {
602 10         9 push( @{ $res->{data} }, $line );
  10         62  
603             }
604             }
605             }
606              
607 10         116 return $result;
608             }
609              
610             # ------------------------------------------------------------------------
611              
612              
613             sub _diag {
614 0     0     my $self = shift;
615              
616 0 0         $self->test->diag(
617             sprintf(
618             "Error: %s\nOutput: %s\n",
619             $self->{_state}{last_error},
620             join(
621             ' ',
622 0           @{ ( ref( $self->{_state}{last_output} ) eq 'ARRAY' )
623             ? $self->{_state}{last_output}
624             : [ $self->{_state}{last_output} ]
625             }
626             )
627             )
628             );
629             }
630              
631             # ------------------------------------------------------------------------
632              
633              
634             1; # End of Test::MTA::Exim4
635              
636             __END__