File Coverage

blib/lib/Test/MTA/Exim4.pm
Criterion Covered Total %
statement 242 270 89.6
branch 74 134 55.2
condition 28 86 32.5
subroutine 26 29 89.6
pod 17 17 100.0
total 387 536 72.2


line stmt bran cond sub pod time code
1             package Test::MTA::Exim4;
2              
3 5     5   353889 use warnings;
  5         44  
  5         174  
4 5     5   28 use strict;
  5         8  
  5         125  
5 5     5   166 use 5.006;
  5         28  
6 5     5   36 use base qw(Class::Accessor::Fast);
  5         7  
  5         2638  
7 5     5   17700 use IPC::Cmd qw[can_run run];
  5         313348  
  5         359  
8 5     5   66 use Test::Builder;
  5         12  
  5         18178  
9              
10             our $VERSION = '0.06'; # 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 423 my ( $proto, $fields ) = @_;
24 4   33     33 my ($class) = ref $proto || $proto;
25              
26             # copy fields into self (without checking) and bless
27 4 50       16 my $self = defined($fields) ? { %{$fields} } : {};
  4         20  
28 4         15 bless( $self, $class );
29              
30             # set some defaults if not already in place
31             $self->{exim_path}
32             ||= $ENV{DEFAULT_EXIM_PATH}
33 4   0     55 || can_run('exim4')
      33        
34             || can_run('exim')
35             || '/usr/sbin/exim';
36 4   33     31 $self->{config_file} ||= $ENV{DEFAULT_EXIM_CONFIG_FILE};
37 4   33     50 $self->{test} ||= Test::Builder->new;
38 4   50     66 $self->{timeout} ||= 5;
39              
40             # check that underlying IPC::Cmd has sufficient capabilities
41 4 50       28 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             can_run( $self->{exim_path} )
46 4 50       62086 || $self->_croak('No runnable exim binary found');
47              
48             # reset internal state
49 4         431049 $self->reset;
50              
51 4         19 return $self;
52             }
53              
54             # ------------------------------------------------------------------------
55              
56              
57             sub reset {
58 4     4 1 13 my $self = shift;
59              
60 4         77 $self->{_state} = { config => {} };
61              
62 4         41 return $self;
63             }
64              
65             # ------------------------------------------------------------------------
66              
67              
68             sub config_ok {
69 4     4 1 2191 my $self = shift;
70 4         10 my $msg = shift;
71              
72 4         21 $self->_run_exim_bv;
73              
74             # pad the msg if not specified
75             $msg ||= sprintf( 'config %s is valid',
76 4   0     110 ( $self->{_state}{exim_config_file} || $self->{config_file} || '(unknown)' ) );
      33        
77              
78 4 50       398 $self->test->ok( $self->{_state}{config}{ok}, $msg ) || $self->_diag;
79             }
80              
81             # ------------------------------------------------------------------------
82              
83              
84             sub exim_version {
85 2     2 1 1035 my $self = shift;
86              
87 2         8 $self->_run_exim_bv;
88              
89 2         43 return $self->{_state}{exim_version};
90             }
91              
92             # ------------------------------------------------------------------------
93              
94              
95             sub exim_build {
96 1     1 1 5 my $self = shift;
97              
98 1         5 $self->_run_exim_bv;
99              
100 1         16 return $self->{_state}{exim_build};
101             }
102              
103             # ------------------------------------------------------------------------
104              
105              
106             sub has_option {
107 1     1 1 787 my $self = shift;
108 1         10 my $option = shift;
109 1         3 my $msg = shift;
110              
111 1         11 $self->_run_exim_bv;
112 1 50       6 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
113 1         13 $self->_run_exim_bp;
114              
115             # pad the msg if not specified
116 1   33     57 $msg ||= sprintf( 'Checking for existence of %s option', $option );
117              
118 1 50       150 $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 1336 my $self = shift;
127 1         17 my $option = shift;
128 1         6 my $msg = shift;
129              
130 1         7 $self->_run_exim_bv;
131 1 50       18 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
132 1         50 $self->_run_exim_bp;
133              
134             # pad the msg if not specified
135 1   33     30 $msg ||= sprintf( 'Checking for lack of existence of %s option', $option );
136              
137 1 50       71 $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 2475 my $self = shift;
146 3         21 my $option = shift;
147 3         10 my $value = shift;
148 3         9 my $msg = shift;
149              
150 3         32 $self->_run_exim_bv;
151 3 50       14 $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     64 $msg ||= sprintf( 'Checking for %s option', $option );
156              
157 3 50       138 $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 4584 my $self = shift;
206 3         22 my $string = shift;
207 3         13 my $expect = shift;
208 3         5 my $msg = shift;
209              
210 3         13 $self->_run_exim_bv;
211 3 50       17 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
212              
213             # pad the msg if not specified
214 3   33     63 $msg ||= sprintf( "Checking expansion of '%s'", $string );
215              
216 3         18 my $got = $self->_run_exim_be($string);
217 3         19 chomp $got;
218 3 50       346 $self->test->is_eq( $got, $expect, $msg )
219             || $self->_diag;
220             }
221              
222             # ------------------------------------------------------------------------
223              
224              
225             sub has_capability {
226 15     15 1 5366 my $self = shift;
227 15         37 my $type = shift;
228 15         48 my $what = shift;
229 15         31 my $msg = shift;
230              
231 15         50 $self->_run_exim_bv;
232 15 50       49 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
233 15 50       34 $self->_croak('Capability requires a type') unless ($type);
234 15 50       39 $self->_croak('Capability requires a thing to check') unless ($what);
235              
236             # pad the msg if not specified
237 15   33     130 $msg ||= sprintf( 'Checking for %s/%s capability', $type, $what );
238              
239 15 50 33     485 $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 764 my $self = shift;
248 2         6 my $type = shift;
249 2         12 my $what = shift;
250 2         6 my $msg = shift;
251              
252 2         9 $self->_run_exim_bv;
253 2 50       20 $self->_croak('Invalid exim config') unless ( $self->{_state}{config}{ok} );
254 2 50       8 $self->_croak('Capability requires a type') unless ($type);
255 2 50       6 $self->_croak('Capability requires a thing to check') unless ($what);
256              
257             # pad the msg if not specified
258 2   33     37 $msg ||= sprintf( 'Checking for lack of %s/%s capability', $type, $what );
259              
260 2 50 33     69 $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 2342 my $self = shift;
269 3         14 my $addr = shift;
270 3         6 my $msg = shift;
271              
272 3 50       15 $self->_croak('Requires an address') unless ($addr);
273              
274             # run the check
275 3         18 my $res = $self->_run_exim_bt($addr);
276              
277             # pad the msg if not specified
278 3   33     28 $msg ||= sprintf( 'Can route to %s', $addr );
279              
280             # OK if there are no undeliverables and there are deliverables
281 3 50 33     242 $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 3993 my $self = shift;
290 5         33 my $addr = shift;
291 5         14 my $target = shift;
292 5         12 my $msg = shift;
293              
294 5 50       25 $self->_croak('Requires an address') unless ($addr);
295 5 50       19 $self->_croak('Requires a target description') unless ($target);
296              
297             # if target is a hash, wrap it in an array
298 5 100       40 $target = [$target] if ( ref($target) eq 'HASH' );
299 5 50       24 $self->_croak('target should be an arrayref')
300             unless ( ref($target) eq 'ARRAY' );
301              
302             # run the check
303 5         19 my $res = $self->_run_exim_bt($addr);
304              
305             # pad the msg if not specified
306 5   33     86 $msg ||= sprintf( 'Can route to %s', $addr );
307              
308             # check we get the right number of things back
309             my $count_ok =
310 5         10 ( scalar( keys %{ $res->{addresses} } ) == scalar( @{$target} ) );
  5         23  
  5         18  
311 5         13 my $count = scalar( @{$target} );
  5         13  
312 5         14 my $addr_count_ok = 0;
313 5         8 my $addresses = { %{ $res->{addresses} } }; #copy address info
  5         72  
314              
315             # only do these tests if the count matches the rules
316 5 50       26 if ($count_ok) {
317 5         8 foreach my $targetspec ( @{$target} ) {
  5         17  
318 11 50       48 $self->_croak('target spec should be hashref')
319             unless ( ref($targetspec) eq 'HASH' );
320 11         21 foreach my $addr ( keys %{$addresses} ) {
  11         102  
321 11         61 my $thisone = 1;
322 11         20 foreach my $key ( keys %{$targetspec} ) {
  11         29  
323 19 50 33     121 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         21 $addr_count_ok++;
331 11         25 last;
332             }
333             }
334             }
335             }
336              
337             # return test status
338 5 50 33     449 $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 765 my $self = shift;
347 1         4 my $addr = shift;
348 1         6 my $msg = shift;
349              
350 1 50       4 $self->_croak('Requires an address') unless ($addr);
351              
352             # run the check
353 1         3 my $res = $self->_run_exim_bt($addr);
354              
355             # pad the msg if not specified
356 1   33     27 $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     85 $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 750 my $self = shift;
368 1         8 my $addr = shift;
369 1         6 my $msg = shift;
370              
371 1 50       4 $self->_croak('Requires an address') unless ($addr);
372              
373             # run the check
374 1         4 my $res = $self->_run_exim_bt($addr);
375              
376             # pad the msg if not specified
377 1   33     26 $msg ||= sprintf( 'Undeliverable to %s', $addr );
378              
379             # OK if there are no deliverables and there are undeliverables
380 1 50 33     93 $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   37 my $self = shift;
392 18         120 my @args = @_;
393              
394             # we always put the config file as the first argument if we have one
395             unshift @args, ( '-C' . $self->{config_file} )
396 18 50       57 if ( $self->{config_file} );
397              
398             # run command
399             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 18         156 );
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       222055 $full_buf = [ map { ( split( /\r?\n/, $_ ) ) } @{ $full_buf || [] } ];
  18         529  
  18         231  
412 18 50       128 $stdout_buf = [ map { ( split( /\r?\n/, $_ ) ) } @{ $stdout_buf || [] } ];
  18         507  
  18         175  
413 18 50       165 $stderr_buf = [ map { ( split( /\r?\n/, $_ ) ) } @{ $stderr_buf || [] } ];
  0         0  
  18         136  
414              
415 18         103 $self->{_state}{last_error} = $error_code;
416 18         231 $self->{_state}{last_output} = $full_buf;
417              
418 18         326 return ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf );
419             }
420              
421             # ------------------------------------------------------------------------
422              
423              
424             sub _run_exim_bv {
425 32     32   83 my $self = shift;
426              
427             # we only want to run this once per session
428 32 100       161 return if ( $self->{_state}{checked}++ );
429              
430             # run command
431 4         19 my ( $success, undef, undef, $stdout_buf, undef ) = $self->_run_exim_command('-bV');
432              
433             # parse things out if command worked
434 4 50       65 if ($success) {
435 4         37 $self->{_state}{config}{ok} = 1;
436 4         22 foreach ( @{$stdout_buf} ) {
  4         30  
437 48         128 chomp;
438 48 100       514 if (/^Exim\s+version\s+([0-9\.]+)\s+#(\d+)/) {
    100          
    100          
439 4         92 $self->{_state}{exim_version} = $1;
440 4         79 $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         72 my $type = lc($1);
458 20         81 my $res = lc($2);
459 20         76 $type =~ tr/a-z/_/cs;
460 20         93 $type =~ s/s$//; # strip trailing s
461 20         67 $res =~ tr|a-z0-9_ /||cd;
462 20         764 $self->{_state}{config}{$type}{$_} = 1 foreach ( split( /[\s\/]/, $res ) );
463             }
464             elsif (/Configuration file is (.*)/) {
465 4         56 $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             $self->_croak('No exim version number found')
472 4 50       67 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   20 my $self = shift;
484              
485             # we only want to run this once per session
486 5 100       21 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         11 $self->{_state}{option} = {};
491              
492             # run command
493 1         9 my ( $success, undef, undef, $stdout_buf, undef ) = $self->_run_exim_command('-bP');
494              
495             # parse things out if command worked
496 1 50       29 if ($success) {
497 1         10 foreach ( @{$stdout_buf} ) {
  1         17  
498 27         92 chomp;
499 27 100       195 if (/^(no_)?(\w+)(?: = (.*))?$/) {
500 26         129 my ( $negate, $option, $value ) = ( $1, $2, $3 );
501 26 100       269 $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   7 my $self = shift;
513 3         6 my $string = shift;
514              
515             # run command
516 3         15 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     173 return $success && join( "\n", @$stdout_buf, '' );
521             }
522              
523             # ------------------------------------------------------------------------
524              
525              
526             sub _run_exim_bt {
527 10     10   44 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       36 $self->_run_exim_bv unless ( $self->{_state}{config}{ok} );
533             $self->_croak('No exim version number found')
534 10 50       54 unless ( $self->{_state}{config}{ok} );
535              
536 10         33 my @options = ('-bt');
537 10 50       24 push( @options, '-f', $sender ) if ( defined($sender) );
538 10         19 push( @options, '--', $address );
539              
540             # run command - use a -- divider to prevent funkiness in the address
541 10         59 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         60 my @lines = @{$stdout_buf};
  10         67  
547 10         220 my $result = {
548             all_ok => $success,
549             deliverable => 0,
550             undeliverable => 0,
551             total => 0,
552             addresses => {}
553             };
554 10         76 while ( scalar(@lines) ) {
555 22         57 my $line = shift @lines;
556 22 50       106 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       67 if ( $line =~ /^(.*) is undeliverable(.*)$/ ) {
564 2         28 $result->{undeliverable}++;
565 2         13 $result->{total}++;
566 2         53 $result->{addresses}{$1} = { ok => 0, reason => $2, address => $1 };
567 2         15 next;
568             }
569 20         46 $result->{deliverable}++;
570 20         33 $result->{total}++;
571 20         138 my $res = { ok => 1, discarded => 0, data => [] };
572 20 100       110 if ( $line =~ /^(.*) -\> (.*)$/ ) {
    100          
573 8         50 $res->{address} = $1;
574 8         32 $res->{target} = $2;
575 8         53 $result->{addresses}{$1} = $res;
576             }
577             elsif ( $line =~ /^(.*) is discarded$/ ) {
578 2         36 $res->{address} = $1;
579 2         5 $res->{discarded} = 1;
580 2         19 $result->{addresses}{$1} = $res;
581             }
582             else {
583 10         35 $res->{address} = $line;
584 10         65 $result->{addresses}{$line} = $res;
585             }
586              
587             # mop up subsequent lines
588 20   100     177 while ( scalar(@lines) && ( $lines[0] =~ /^\s/ ) ) {
589 28         62 $line = shift @lines;
590 28 50       178 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         60 $res->{transport} = $1;
596             }
597             elsif ( $line =~ /^\s+router = (.*), transport = (.*)/ ) {
598 10         64 $res->{router} = $1;
599 10         64 $res->{transport} = $2;
600             }
601             else {
602 10         16 push( @{ $res->{data} }, $line );
  10         60  
603             }
604             }
605             }
606              
607 10         98 return $result;
608             }
609              
610             # ------------------------------------------------------------------------
611              
612              
613             sub _diag {
614 0     0     my $self = shift;
615              
616             $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 0 0         : [ $self->{_state}{last_output} ]
625             }
626             )
627             )
628             );
629             }
630              
631             # ------------------------------------------------------------------------
632              
633              
634             1; # End of Test::MTA::Exim4
635              
636             __END__