exipick 20060721.2
[exim.git] / src / src / exipick.src
1 #!PERL_COMMAND
2 # $Cambridge: exim/src/src/exipick.src,v 1.12 2006/07/21 16:48:43 jetmore Exp $
3
4 # This variable should be set by the building process to Exim's spool directory.
5 my $spool = 'SPOOL_DIRECTORY';
6
7 # use 'exipick --help' to view documentation for this program.
8 # Documentation also viewable online at
9 #       http://www.exim.org/eximwiki/ToolExipickManPage
10
11 use strict;
12 use Getopt::Long;
13
14 my($p_name)   = $0 =~ m|/?([^/]+)$|;
15 my $p_version = "20060721.2";
16 my $p_usage   = "Usage: $p_name [--help|--version] (see --help for details)";
17 my $p_cp      = <<EOM;
18         Copyright (c) 2003-2006 John Jetmore <jj33\@pobox.com>
19
20     This program is free software; you can redistribute it and/or modify
21     it under the terms of the GNU General Public License as published by
22     the Free Software Foundation; either version 2 of the License, or
23     (at your option) any later version.
24
25     This program is distributed in the hope that it will be useful,
26     but WITHOUT ANY WARRANTY; without even the implied warranty of
27     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
28     GNU General Public License for more details.
29
30     You should have received a copy of the GNU General Public License
31     along with this program; if not, write to the Free Software
32     Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
33 EOM
34 ext_usage(); # before we do anything else, check for --help
35
36 $| = 1; # unbuffer STDOUT
37
38 Getopt::Long::Configure("bundling_override");
39 GetOptions(
40   'spool:s'     => \$G::spool,      # exim spool dir
41   'bp'          => \$G::mailq_bp,   # List the queue (noop - default)
42   'bpa'         => \$G::mailq_bpa,  # ... with generated address as well
43   'bpc'         => \$G::mailq_bpc,  # ... but just show a count of messages
44   'bpr'         => \$G::mailq_bpr,  # ... do not sort
45   'bpra'        => \$G::mailq_bpra, # ... with generated addresses, unsorted
46   'bpru'        => \$G::mailq_bpru, # ... only undelivered addresses, unsorted
47   'bpu'         => \$G::mailq_bpu,  # ... only undelivered addresses
48   'and'         => \$G::and,        # 'and' the criteria (default)
49   'or'          => \$G::or,         # 'or' the criteria
50   'f:s'         => \$G::qgrep_f,    # from regexp
51   'r:s'         => \$G::qgrep_r,    # recipient regexp
52   's:s'         => \$G::qgrep_s,    # match against size field
53   'y:s'         => \$G::qgrep_y,    # message younger than (secs)
54   'o:s'         => \$G::qgrep_o,    # message older than (secs)
55   'z'           => \$G::qgrep_z,    # frozen only
56   'x'           => \$G::qgrep_x,    # non-frozen only
57   'c'           => \$G::qgrep_c,    # display match count
58   'l'           => \$G::qgrep_l,    # long format (default)
59   'i'           => \$G::qgrep_i,    # message ids only
60   'b'           => \$G::qgrep_b,    # brief format
61   'size'        => \$G::size_only,  # sum the size of the matching msgs
62   'not'         => \$G::negate,     # flip every test
63   'R|reverse'   => \$G::reverse,    # reverse output (-R is qgrep option)
64   'sort:s'      => \@G::sort,       # allow you to choose variables to sort by
65   'freeze:s'    => \$G::freeze,     # freeze data in this file
66   'thaw:s'      => \$G::thaw,       # thaw data from this file
67   'unsorted'    => \$G::unsorted,   # unsorted, regardless of output format
68   'random'      => \$G::random,     # (poorly) randomize evaluation order
69   'flatq'       => \$G::flatq,      # brief format
70   'caseful'     => \$G::caseful,    # in '=' criteria, respect case
71   'caseless'    => \$G::caseless,   #   ...ignore case (default)
72   'show-vars:s' => \$G::show_vars,  # display the contents of these vars
73   'show-rules'  => \$G::show_rules, # display compiled match rules
74   'show-tests'  => \$G::show_tests  # display tests as applied to each message
75 ) || exit(1);
76
77 # if both freeze and thaw specified, only thaw as it is less desctructive
78 $G::freeze = undef               if ($G::freeze && $G::thaw);
79 freeze_start()                   if ($G::freeze);
80 thaw_start()                     if ($G::thaw);
81
82 # massage sort options (make '$var,Var:' be 'var','var')
83 for (my $i = scalar(@G::sort)-1; $i >= 0; $i--) {
84   $G::sort[$i] = lc($G::sort[$i]);
85   $G::sort[$i] =~ s/[\$:\s]//g;
86   if ((my @vars = split(/,/, $G::sort[$i])) > 1) {
87     $G::sort[$i] = $vars[0]; shift(@vars); # replace current slot w/ first var
88     splice(@G::sort, $i+1, 0, @vars);      # add other vars after current pos
89   }
90 }
91 push(@G::sort, "message_exim_id") if (@G::sort);
92 die "empty value provided to --sort not allowed, exiting\n"
93     if (grep /^\s*$/, @G::sort);
94
95 # massage the qgrep options into standard criteria
96 push(@ARGV, "\$sender_address     =~ /$G::qgrep_f/") if ($G::qgrep_f);
97 push(@ARGV, "\$recipients         =~ /$G::qgrep_r/") if ($G::qgrep_r);
98 push(@ARGV, "\$shown_message_size eq $G::qgrep_s")   if ($G::qgrep_s);
99 push(@ARGV, "\$message_age        <  $G::qgrep_y")   if ($G::qgrep_y);
100 push(@ARGV, "\$message_age        >  $G::qgrep_o")   if ($G::qgrep_o);
101 push(@ARGV, "\$deliver_freeze")                      if ($G::qgrep_z);
102 push(@ARGV, "!\$deliver_freeze")                     if ($G::qgrep_x);
103
104 $G::mailq_bp        = $G::mailq_bp;        # shut up -w
105 $G::and             = $G::and;             # shut up -w
106 $G::msg_ids         = {};                  # short circuit when crit is only MID
107 $G::caseless        = $G::caseful ? 0 : 1; # nocase by default, case if both
108 @G::recipients_crit = ();                  # holds per-recip criteria
109 $spool              = $G::spool if ($G::spool);
110 my $count_only      = 1 if ($G::mailq_bpc  || $G::qgrep_c);
111 my $unsorted        = 1 if ($G::mailq_bpr  || $G::mailq_bpra ||
112                             $G::mailq_bpru || $G::unsorted);
113 my $msg             = $G::thaw ? thaw_message_list()
114                                : get_all_msgs($spool, $unsorted,
115                                               $G::reverse, $G::random);
116 die "Problem accessing thaw file\n" if ($G::thaw && !$msg);
117 my $crit            = process_criteria(\@ARGV);
118 my $e               = Exim::SpoolFile->new();
119 my $tcount          = 0 if ($count_only);  # holds count of all messages
120 my $mcount          = 0 if ($count_only);  # holds count of matching messages
121 my $total_size      = 0 if ($G::size_only);
122 $e->set_undelivered_only(1)      if ($G::mailq_bpru || $G::mailq_bpu);
123 $e->set_show_generated(1)        if ($G::mailq_bpra || $G::mailq_bpa);
124 $e->output_long()                if ($G::qgrep_l);
125 $e->output_idonly()              if ($G::qgrep_i);
126 $e->output_brief()               if ($G::qgrep_b);
127 $e->output_flatq()               if ($G::flatq);
128 $e->set_show_vars($G::show_vars) if ($G::show_vars);
129 $e->set_spool($spool);
130
131 MSG:
132 foreach my $m (@$msg) {
133   next if (scalar(keys(%$G::msg_ids)) && !$G::or
134                                       && !$G::msg_ids->{$m->{message}});
135   if ($G::thaw) {
136     my $data = thaw_data();
137     if (!$e->restore_state($data)) {
138       warn "Couldn't thaw $data->{_message}: ".$e->error()."\n";
139       next MSG;
140     }
141   } else {
142     if (!$e->parse_message($m->{message}, $m->{path})) {
143       warn "Couldn't parse $m->{message}: ".$e->error()."\n";
144       next MSG;
145     }
146   }
147   $tcount++;
148   my $match = 0;
149   my @local_crit = ();
150   foreach my $c (@G::recipients_crit) {              # handle each_recip* vars
151     foreach my $addr (split(/, /, $e->get_var($c->{var}))) {
152       my %t = ( 'cmp' => $c->{cmp}, 'var' => $c->{var} );
153       $t{cmp} =~ s/"?\$var"?/'$addr'/;
154       push(@local_crit, \%t);
155     }
156   }
157   if ($G::show_tests) { print $e->get_var('message_exim_id'), "\n"; }
158   CRITERIA:
159   foreach my $c (@$crit, @local_crit) {
160     my $var = $e->get_var($c->{var});
161     my $ret = eval($c->{cmp});
162     if ($G::show_tests) {
163       printf "  %25s =  '%s'\n  %25s => $ret\n",$c->{var},$var,$c->{cmp},$ret;
164     }
165     if ($@) {
166       print STDERR "Error in eval '$c->{cmp}': $@\n";
167       next MSG;
168     } elsif ($ret) {
169       $match = 1;
170       if ($G::or) { last CRITERIA; }
171       else        { next CRITERIA; }
172     } else { # no match
173       if ($G::or) { next CRITERIA; }
174       else        { next MSG;      }
175     }
176   }
177
178   # skip this message if any criteria were supplied and it didn't match
179   next MSG if ((scalar(@$crit) || scalar(@local_crit)) && !$match);
180
181   if ($count_only || $G::size_only) {
182     $mcount++;
183     $total_size += $e->get_var('message_size');
184   } else {
185     if (@G::sort) {
186       # if we are defining criteria to sort on, save the message here.  If
187       # we don't save here and do the sort later, we have a chicken/egg
188       # problem
189       push(@G::to_print, { vars => {}, output => "" });
190       foreach my $var (@G::sort) {
191         # save any values we want to sort on.  I don't like doing the internal
192         # struct access here, but calling get_var a bunch can be _slow_ =(
193         $G::sort_type{$var} ||= '<=>';
194         $G::to_print[-1]{vars}{$var} = $e->{_vars}{$var};
195         $G::sort_type{$var} = 'cmp' if ($G::to_print[-1]{vars}{$var} =~ /\D/);
196       }
197       $G::to_print[-1]{output} = $e->format_message();
198     } else {
199       print $e->format_message();
200     }
201   }
202
203   if ($G::freeze) {
204     freeze_data($e->get_state());
205     push(@G::frozen_msgs, $m);
206   }
207 }
208
209 if (@G::to_print) {
210   msg_sort(\@G::to_print, \@G::sort, $G::reverse);
211   foreach my $msg (@G::to_print) {
212     print $msg->{output};
213   }
214 }
215
216 if ($G::qgrep_c) {
217   print "$mcount matches out of $tcount messages" .
218         ($G::size_only ? " ($total_size)" : "") . "\n";
219 } elsif ($G::mailq_bpc) {
220   print "$mcount" .  ($G::size_only ? " ($total_size)" : "") . "\n";
221 } elsif ($G::size_only) {
222   print "$total_size\n";
223 }
224
225 if ($G::freeze) {
226   freeze_message_list(\@G::frozen_msgs);
227   freeze_end();
228 } elsif ($G::thaw) {
229   thaw_end();
230 }
231
232 exit;
233
234 # sender_address_domain,shown_message_size
235 sub msg_sort {
236   my $msgs    = shift;
237   my $vars    = shift;
238   my $reverse = shift;
239
240   my @pieces = ();
241   foreach my $v (@G::sort) {
242     push(@pieces, "\$a->{vars}{\"$v\"} $G::sort_type{$v} \$b->{vars}{\"$v\"}");
243   }
244   my $sort_str = join(" || ", @pieces);
245
246   @$msgs = sort { eval $sort_str } (@$msgs);
247   @$msgs = reverse(@$msgs) if ($reverse);
248 }
249
250 sub try_load {
251   my $mod = shift;
252
253   eval("use $mod");
254   return $@ ? 0 : 1;
255 }
256
257 # FREEZE FILE FORMAT:
258 # message_data_bytes
259 # message_data
260 # <...>
261 # EOM
262 # message_list
263 # message_list_bytes <- 10 bytes, zero-packed, plus \n
264
265 sub freeze_start {
266   eval("use Storable");
267   die "Storable module not found: $@\n" if ($@);
268   open(O, ">$G::freeze") || die "Can't open freeze file $G::freeze: $!\n";
269   $G::freeze_handle = \*O;
270 }
271
272 sub freeze_end {
273   close($G::freeze_handle);
274 }
275
276 sub thaw_start {
277   eval("use Storable");
278   die "Storable module not found: $@\n" if ($@);
279   open(I, "<$G::thaw") || die "Can't open freeze file $G::thaw: $!\n";
280   $G::freeze_handle = \*I;
281 }
282
283 sub thaw_end {
284   close($G::freeze_handle);
285 }
286
287 sub freeze_data {
288   my $h = Storable::freeze($_[0]);
289   print $G::freeze_handle length($h)+1, "\n$h\n";
290 }
291
292 sub freeze_message_list {
293   my $h = Storable::freeze($_[0]);
294   my $l = length($h) + 1;
295   printf $G::freeze_handle "EOM\n$l\n$h\n%010d\n", $l+11+length($l)+1;
296 }
297
298 sub thaw_message_list {
299   my $orig_pos = tell($G::freeze_handle);
300   seek($G::freeze_handle, -11, 2);
301   chomp(my $bytes = <$G::freeze_handle>);
302   seek($G::freeze_handle, $bytes * -1, 2);
303   my $obj = thaw_data();
304   seek($G::freeze_handle, 0, $orig_pos);
305   return($obj);
306 }
307
308 sub thaw_data {
309   my $obj;
310   chomp(my $bytes = <$G::freeze_handle>);
311   return(undef) if (!$bytes || $bytes eq 'EOM');
312   my $read = read(I, $obj, $bytes);
313   die "Format error in thaw file (expected $bytes bytes, got $read)\n"
314       if ($bytes != $read);
315   chomp($obj);
316   return(Storable::thaw($obj));
317 }
318
319 sub process_criteria {
320   my $a = shift;
321   my @c = ();
322   my $e = 0;
323
324   foreach (@$a) {
325     foreach my $t ('@') { s/$t/\\$t/g; }
326     if (/^(.*?)\s+(<=|>=|==|!=|<|>)\s+(.*)$/) {
327       #print STDERR "found as integer\n";
328       my $v = $1; my $o = $2; my $n = $3;
329       if    ($n =~ /^([\d\.]+)M$/)  { $n = $1 * 1024 * 1024; }
330       elsif ($n =~ /^([\d\.]+)K$/)  { $n = $1 * 1024; }
331       elsif ($n =~ /^([\d\.]+)B?$/) { $n = $1; }
332       elsif ($n =~ /^([\d\.]+)d$/)  { $n = $1 * 60 * 60 * 24; }
333       elsif ($n =~ /^([\d\.]+)h$/)  { $n = $1 * 60 * 60; }
334       elsif ($n =~ /^([\d\.]+)m$/)  { $n = $1 * 60; }
335       elsif ($n =~ /^([\d\.]+)s?$/) { $n = $1; }
336       else {
337         print STDERR "Expression $_ did not parse: numeric comparison with ",
338                      "non-number\n";
339         $e = 1;
340         next;
341       }
342       #push(@c, { var => lc($v), cmp => "(\$var $o $n) ? 1 : 0" });
343       push(@c, { var => lc($v), cmp => "(\$var $o $n)" });
344     } elsif (/^(.*?)\s+(=~|!~)\s+(.*)$/) {
345       #print STDERR "found as string regexp\n";
346       push(@c, { var => lc($1), cmp => "(\"\$var\" $2 $3)" });
347     } elsif (/^(.*?)\s+=\s+(.*)$/) {
348       #print STDERR "found as bare string regexp\n";
349       my $case = $G::caseful ? '' : 'i';
350       push(@c, { var => lc($1), cmp => "(\"\$var\" =~ /$2/$case)" });
351       # quote special characters in perl text string
352       #foreach my $t ('@') { $c[-1]{cmp} =~ s/$t/\\$t/g; }
353     } elsif (/^(.*?)\s+(eq|ne)\s+(.*)$/) {
354       #print STDERR "found as string cmp\n";
355       my $var = lc($1); my $op = $2; my $val = $3;
356       $val =~ s|^(['"])(.*)\1$|$2|;
357       push(@c, { var => $var, cmp => "(\"\$var\" $op \"$val\")" });
358       if (($var eq 'message_id' || $var eq 'message_exim_id') && $op eq "eq") {
359         #print STDERR "short circuit @c[-1]->{cmp} $val\n";
360         $G::msg_ids->{$val} = 1;
361       }
362       #foreach my $t ('@') { $c[-1]{cmp} =~ s/$t/\\$t/g; }
363     } elsif (/^(\S+)$/) {
364       #print STDERR "found as boolean\n";
365       push(@c, { var => lc($1), cmp => "(\$var)" });
366     } else {
367       print STDERR "Expression $_ did not parse\n";
368       $e = 1;
369     }
370     # assign the results of the cmp test here (handle "!" negation)
371     # also handle global --not negation
372     if ($c[-1]{var} =~ s|^!||) {
373       $c[-1]{cmp} .= $G::negate ? " ? 1 : 0" : " ? 0 : 1";
374     } else {
375       $c[-1]{cmp} .= $G::negate ? " ? 0 : 1" : " ? 1 : 0";
376     }
377     # support the each_* psuedo variables.  Steal the criteria off of the
378     # queue for special processing later
379     if ($c[-1]{var} =~ /^each_(recipients(_(un)?del)?)$/) {
380       my $var = $1;
381       push(@G::recipients_crit,pop(@c));
382       $G::recipients_crit[-1]{var} = $var; # remove each_ from the variable
383     }
384   }
385
386   exit(1) if ($e);
387
388   if ($G::show_rules) { foreach (@c) { print "$_->{var}\t$_->{cmp}\n"; } }
389
390   return(\@c);
391 }
392
393 sub get_all_msgs {
394   my $d = shift() . '/input';
395   my $u = shift; # don't sort
396   my $r = shift; # right before returning, reverse order
397   my $o = shift; # if true, randomize list order before returning
398   my @m = ();
399
400   opendir(D, "$d") || die "Couldn't opendir $d: $!\n";
401   foreach my $e (grep !/^\./, readdir(D)) {
402     if ($e =~ /^[a-zA-Z0-9]$/) {
403       opendir(DD, "$d/$e") || next;
404       foreach my $f (grep !/^\./, readdir(DD)) {
405         push(@m, { message => $1, path => "$d/$e" }) if ($f =~ /^(.{16})-H$/);
406       }
407       closedir(DD);
408     } elsif ($e =~ /^(.{16})-H$/) {
409       push(@m, { message => $1, path => $d });
410     }
411   }
412   closedir(D);
413
414   if ($o) {
415     my $c = scalar(@m);
416     # loop twice to pretend we're doing a good job of mixing things up
417     for (my $i = 0; $i < 2 * $c; $i++) {
418       my $rand = int(rand($c));
419       ($m[$i % $c],$m[$rand]) = ($m[$rand],$m[$i % $c]);
420     }
421   } elsif (!$u) {
422     @m = sort { $a->{message} cmp $b->{message} } @m;
423   }
424   @m = reverse(@m) if ($r);
425
426   return(\@m);
427 }
428
429 BEGIN {
430
431 package Exim::SpoolFile;
432
433 # versions 4.61 and higher will not need these variables anymore, but they
434 # are left for handling legacy installs
435 $Exim::SpoolFile::ACL_C_MAX_LEGACY = 10;
436 #$Exim::SpoolFile::ACL_M_MAX _LEGACY= 10;
437
438 sub new {
439   my $class = shift;
440   my $self  = {};
441   bless($self, $class);
442
443   $self->{_spool_dir}        = '';
444   $self->{_undelivered_only} = 0;
445   $self->{_show_generated}   = 0;
446   $self->{_output_long}      = 1;
447   $self->{_output_idonly}    = 0;
448   $self->{_output_brief}     = 0;
449   $self->{_output_flatq}     = 0;
450   $self->{_show_vars}        = [];
451
452   $self->_reset();
453   return($self);
454 }
455
456 sub output_long {
457   my $self = shift;
458
459   $self->{_output_long}      = 1;
460   $self->{_output_idonly}    = 0;
461   $self->{_output_brief}     = 0;
462   $self->{_output_flatq}     = 0;
463 }
464
465 sub output_idonly {
466   my $self = shift;
467
468   $self->{_output_long}      = 0;
469   $self->{_output_idonly}    = 1;
470   $self->{_output_brief}     = 0;
471   $self->{_output_flatq}     = 0;
472 }
473
474 sub output_brief {
475   my $self = shift;
476
477   $self->{_output_long}      = 0;
478   $self->{_output_idonly}    = 0;
479   $self->{_output_brief}     = 1;
480   $self->{_output_flatq}     = 0;
481 }
482
483 sub output_flatq {
484   my $self = shift;
485
486   $self->{_output_long}      = 0;
487   $self->{_output_idonly}    = 0;
488   $self->{_output_brief}     = 0;
489   $self->{_output_flatq}     = 1;
490 }
491
492 sub set_show_vars {
493   my $self = shift;
494   my $s    = shift;
495
496   foreach my $v (split(/\s*,\s*/, $s)) {
497     push(@{$self->{_show_vars}}, $v);
498   }
499 }
500
501 sub set_show_generated {
502   my $self = shift;
503   $self->{_show_generated} = shift;
504 }
505
506 sub set_undelivered_only {
507   my $self = shift;
508   $self->{_undelivered_only} = shift;
509 }
510
511 sub error {
512   my $self = shift;
513   return $self->{_error};
514 }
515
516 sub _error {
517   my $self = shift;
518   $self->{_error} = shift;
519   return(undef);
520 }
521
522 sub _reset {
523   my $self = shift;
524
525   $self->{_error}       = '';
526   $self->{_delivered}   = 0;
527   $self->{_message}     = '';
528   $self->{_path}        = '';
529   $self->{_vars}        = {};
530
531   $self->{_numrecips}   = 0;
532   $self->{_udel_tree}   = {};
533   $self->{_del_tree}    = {};
534   $self->{_recips}      = {};
535
536   return($self);
537 }
538
539 sub parse_message {
540   my $self = shift;
541
542   $self->_reset();
543   $self->{_message} = shift || return(0);
544   $self->{_path}    = shift; # optional path to message
545   return(0) if (!$self->{_spool_dir});
546   if (!$self->{_path} && !$self->_find_path()) {
547     # assume the message was delivered from under us and ignore
548     $self->{_delivered} = 1;
549     return(1);
550   }
551   $self->_parse_header() || return(0);
552
553   return(1);
554 }
555
556 # take the output of get_state() and set up a message internally like
557 # parse_message (except from a saved data struct, not by parsing the
558 # files on disk).
559 sub restore_state {
560   my $self = shift;
561   my $h    = shift;
562
563   return(1) if ($h->{_delivered});
564   $self->_reset();
565   $self->{_message} = $h->{_message} || return(0);
566   return(0) if (!$self->{_spool_dir});
567
568   $self->{_path}      = $h->{_path};
569   $self->{_vars}      = $h->{_vars};
570   $self->{_numrecips} = $h->{_numrecips};
571   $self->{_udel_tree} = $h->{_udel_tree};
572   $self->{_del_tree}  = $h->{_del_tree};
573   $self->{_recips}    = $h->{_recips};
574
575   $self->{_vars}{message_age} = time() - $self->{_vars}{received_time};
576   return(1);
577 }
578
579 # This returns the state data for a specific message in a format that can
580 # be later frozen back in to regain state
581 #
582 # after calling this function, this specific state is not expect to be
583 # reused.  That's because we're returning direct references to specific
584 # internal structures.  We're also modifying the structure ourselves
585 # by deleting certain internal message variables.
586 sub get_state {
587   my $self = shift;
588   my $h    = {};    # this is the hash ref we'll be returning.
589
590   $h->{_delivered} = $self->{_delivered};
591   $h->{_message}   = $self->{_message};
592   $h->{_path}      = $self->{_path};
593   $h->{_vars}      = $self->{_vars};
594   $h->{_numrecips} = $self->{_numrecips};
595   $h->{_udel_tree} = $self->{_udel_tree};
596   $h->{_del_tree}  = $self->{_del_tree};
597   $h->{_recips}    = $self->{_recips};
598
599   # delete some internal variables that we will rebuild later if needed
600   delete($h->{_vars}{message_body});
601   delete($h->{_vars}{message_age});
602
603   return($h);
604 }
605
606 # keep this sub as a feature if we ever break this module out, but do away
607 # with its use in exipick (pass it in from caller instead)
608 sub _find_path {
609   my $self = shift;
610
611   return(0) if (!$self->{_message});
612   return(0) if (!$self->{_spool_dir});
613
614   # test split spool first on the theory that people concerned about
615   # performance will have split spool set =).
616   foreach my $f (substr($self->{_message}, 5, 1).'/', '') {
617     if (-f "$self->{_spool_dir}/input/$f$self->{_message}-H") {
618       $self->{_path} = $self->{_spool_dir} . "/input/$f";
619       return(1);
620     }
621   }
622   return(0);
623 }
624
625 sub set_spool {
626   my $self = shift;
627   $self->{_spool_dir} = shift;
628 }
629
630 # accepts a variable with or without leading '$' or trailing ':'
631 sub get_var {
632   my $self = shift;
633   my $var  = lc(shift);
634
635   $var =~ s/^\$//;
636   $var =~ s/:$//;
637
638   $self->_parse_body()
639       if ($var eq 'message_body' && !$self->{_vars}{message_body});
640
641   chomp($self->{_vars}{$var});
642   return $self->{_vars}{$var};
643 }
644
645 sub _parse_body {
646   my $self = shift;
647   my $f    = $self->{_path} . '/' . $self->{_message} . '-D';
648
649   open(I, "<$f") || return($self->_error("Couldn't open $f: $!"));
650   chomp($_ = <I>);
651   return(0) if ($self->{_message}.'-D' ne $_);
652
653   $self->{_vars}{message_body} = join('', <I>);
654   close(I);
655   $self->{_vars}{message_body} =~ s/\n/ /g;
656   $self->{_vars}{message_body} =~ s/\000/ /g;
657   return(1);
658 }
659
660 sub _parse_header {
661   my $self = shift;
662   my $f    = $self->{_path} . '/' . $self->{_message} . '-H';
663
664   if (!open(I, "<$f")) {
665     # assume message went away and silently ignore
666     $self->{_delivered} = 1;
667     return(1);
668   }
669
670   chomp($_ = <I>);
671   return(0) if ($self->{_message}.'-H' ne $_);
672   $self->{_vars}{message_id}       = $self->{_message};
673   $self->{_vars}{message_exim_id}  = $self->{_message};
674
675   # line 2
676   chomp($_ = <I>);
677   return(0) if (!/^(.+)\s(\-?\d+)\s(\-?\d+)$/);
678   $self->{_vars}{originator_login} = $1;
679   $self->{_vars}{originator_uid}   = $2;
680   $self->{_vars}{originator_gid}   = $3;
681
682   # line 3
683   chomp($_ = <I>);
684   return(0) if (!/^<(.*)>$/);
685   $self->{_vars}{sender_address}   = $1;
686   $self->{_vars}{sender_address_domain} = $1;
687   $self->{_vars}{sender_address_local_part} = $1;
688   $self->{_vars}{sender_address_domain} =~ s/^.*\@//;
689   $self->{_vars}{sender_address_local_part} =~ s/^(.*)\@.*$/$1/;
690
691   # line 4
692   chomp($_ = <I>);
693   return(0) if (!/^(\d+)\s(\d+)$/);
694   $self->{_vars}{received_time}    = $1;
695   $self->{_vars}{warning_count}    = $2;
696   $self->{_vars}{message_age}      = time() - $self->{_vars}{received_time};
697
698   while (<I>) {
699     chomp();
700     if (/^(-\S+)\s*(.*$)/) {
701       my $tag = $1;
702       my $arg = $2;
703       if ($tag eq '-acl') {
704         my $t;
705         return(0) if ($arg !~ /^(\d+)\s(\d+)$/);
706         if ($1 < $Exim::SpoolFile::ACL_C_MAX_LEGACY) {
707           $t = "acl_c$1";
708         } else {
709           $t = "acl_m" . ($1 - $Exim::SpoolFile::ACL_C_MAX_LEGACY);
710         }
711         read(I, $self->{_vars}{$t}, $2+1) || return(0);
712         chomp($self->{_vars}{$t});
713       } elsif ($tag eq '-aclc') {
714         return(0) if ($arg !~ /^(\d+)\s(\d+)$/);
715         my $t = "acl_c$1";
716         read(I, $self->{_vars}{$t}, $2+1) || return(0);
717         chomp($self->{_vars}{$t});
718       } elsif ($tag eq '-aclm') {
719         return(0) if ($arg !~ /^(\d+)\s(\d+)$/);
720         my $t = "acl_m$1";
721         read(I, $self->{_vars}{$t}, $2+1) || return(0);
722         chomp($self->{_vars}{$t});
723       } elsif ($tag eq '-local') {
724         $self->{_vars}{sender_local} = 1;
725       } elsif ($tag eq '-localerror') {
726         $self->{_vars}{local_error_message} = 1;
727       } elsif ($tag eq '-local_scan') {
728         $self->{_vars}{local_scan_data} = $arg;
729       } elsif ($tag eq '-spam_score_int') {
730         $self->{_vars}{spam_score_int} = $arg;
731         $self->{_vars}{spam_score}     = $arg / 10;
732       } elsif ($tag eq '-bmi_verdicts') {
733         $self->{_vars}{bmi_verdicts} = $arg;
734       } elsif ($tag eq '-host_lookup_deferred') {
735         $self->{_vars}{host_lookup_deferred} = 1;
736       } elsif ($tag eq '-host_lookup_failed') {
737         $self->{_vars}{host_lookup_failed} = 1;
738       } elsif ($tag eq '-body_linecount') {
739         $self->{_vars}{body_linecount} = $arg;
740       } elsif ($tag eq '-body_zerocount') {
741         $self->{_vars}{body_zerocount} = $arg;
742       } elsif ($tag eq '-frozen') {
743         $self->{_vars}{deliver_freeze} = 1;
744         $self->{_vars}{deliver_frozen_at} = $arg;
745       } elsif ($tag eq '-allow_unqualified_recipient') {
746         $self->{_vars}{allow_unqualified_recipient} = 1;
747       } elsif ($tag eq '-allow_unqualified_sender') {
748         $self->{_vars}{allow_unqualified_sender} = 1;
749       } elsif ($tag eq '-deliver_firsttime') {
750         $self->{_vars}{deliver_firsttime} = 1;
751         $self->{_vars}{first_delivery} = 1;
752       } elsif ($tag eq '-manual_thaw') {
753         $self->{_vars}{deliver_manual_thaw} = 1;
754         $self->{_vars}{manually_thawed} = 1;
755       } elsif ($tag eq '-auth_id') {
756         $self->{_vars}{authenticated_id} = $arg;
757       } elsif ($tag eq '-auth_sender') {
758         $self->{_vars}{authenticated_sender} = $arg;
759       } elsif ($tag eq '-sender_set_untrusted') {
760         $self->{_vars}{sender_set_untrusted} = 1;
761       } elsif ($tag eq '-tls_certificate_verified') {
762         $self->{_vars}{tls_certificate_verified} = 1;
763       } elsif ($tag eq '-tls_cipher') {
764         $self->{_vars}{tls_cipher} = $arg;
765       } elsif ($tag eq '-tls_peerdn') {
766         $self->{_vars}{tls_peerdn} = $arg;
767       } elsif ($tag eq '-host_address') {
768         $self->{_vars}{sender_host_port} = $self->_get_host_and_port(\$arg);
769         $self->{_vars}{sender_host_address} = $arg;
770       } elsif ($tag eq '-interface_address') {
771         $self->{_vars}{interface_port} = $self->_get_host_and_port(\$arg);
772         $self->{_vars}{interface_address} = $arg;
773       } elsif ($tag eq '-active_hostname') {
774         $self->{_vars}{smtp_active_hostname} = $arg;
775       } elsif ($tag eq '-host_auth') {
776         $self->{_vars}{sender_host_authenticated} = $arg;
777       } elsif ($tag eq '-host_name') {
778         $self->{_vars}{sender_host_name} = $arg;
779       } elsif ($tag eq '-helo_name') {
780         $self->{_vars}{sender_helo_name} = $arg;
781       } elsif ($tag eq '-ident') {
782         $self->{_vars}{sender_ident} = $arg;
783       } elsif ($tag eq '-received_protocol') {
784         $self->{_vars}{received_protocol} = $arg;
785       } elsif ($tag eq '-N') {
786         $self->{_vars}{dont_deliver} = 1;
787       } else {
788         # unrecognized tag, save it for reference
789         $self->{$tag} = $arg;
790       }
791     } else {
792       last;
793     }
794   }
795
796   # when we drop out of the while loop, we have the first line of the
797   # delivered tree in $_
798   do {
799     if ($_ eq 'XX') {
800       ; # noop
801     } elsif ($_ =~ s/^[YN][YN]\s+//) {
802       $self->{_del_tree}{$_} = 1;
803     } else {
804       return(0);
805     }
806     chomp($_ = <I>);
807   } while ($_ !~ /^\d+$/);
808
809   $self->{_numrecips} = $_;
810   $self->{_vars}{recipients_count} = $self->{_numrecips};
811   for (my $i = 0; $i < $self->{_numrecips}; $i++) {
812     chomp($_ = <I>);
813     return(0) if (/^$/);
814     my $addr = '';
815     if (/^(.*)\s\d+,(\d+),\d+$/) {
816       #print STDERR "exim3 type (untested): $_\n";
817       $self->{_recips}{$1} = { pno => $2 };
818       $addr = $1;
819     } elsif (/^(.*)\s(\d+)$/) {
820       #print STDERR "exim4 original type (untested): $_\n";
821       $self->{_recips}{$1} = { pno => $2 };
822       $addr = $1;
823     } elsif (/^(.*)\s(.*)\s(\d+),(\d+)#1$/) {
824       #print STDERR "exim4 new type #1 (untested): $_\n";
825       return($self->_error("incorrect format: $_")) if (length($2) != $3);
826       $self->{_recips}{$1} = { pno => $4, errors_to => $2 };
827       $addr = $1;
828     } elsif (/^.*#(\d+)$/) {
829       #print STDERR "exim4 #$1 style (unimplemented): $_\n";
830       $self->_error("exim4 #$1 style (unimplemented): $_");
831     } else {
832       #print STDERR "default type: $_\n";
833       $self->{_recips}{$_} = {};
834       $addr = $_;
835     }
836     $self->{_udel_tree}{$addr} = 1 if (!$self->{_del_tree}{$addr});
837   }
838   $self->{_vars}{recipients}         = join(', ', keys(%{$self->{_recips}}));
839   $self->{_vars}{recipients_del}     = join(', ', keys(%{$self->{_del_tree}}));
840   $self->{_vars}{recipients_undel}   = join(', ', keys(%{$self->{_udel_tree}}));
841   $self->{_vars}{recipients_undel_count} = scalar(keys(%{$self->{_udel_tree}}));
842   $self->{_vars}{recipients_del_count}   = 0;
843   foreach my $r (keys %{$self->{_del_tree}}) {
844     next if (!$self->{_recips}{$r});
845     $self->{_vars}{recipients_del_count}++;
846   }
847
848   # blank line
849   $_ = <I>;
850   return(0) if (!/^$/);
851
852   # start reading headers
853   while (read(I, $_, 3) == 3) {
854     my $t = getc(I);
855     return(0) if (!length($t));
856     while ($t =~ /^\d$/) {
857       $_ .= $t;
858       $t  = getc(I);
859     }
860     # ok, right here $t contains the header flag and $_ contains the number of
861     # bytes to read.  If we ever use the header flag, grab it here.
862     $self->{_vars}{message_size} += $_ if ($t ne '*');
863     $t = getc(I); # strip the space out of the file
864     my $bytes = $_;
865     return(0) if (read(I, $_, $bytes) != $bytes);
866     $self->{_vars}{message_linecount} += (tr/\n//) if ($t ne '*');
867
868     # build the $header_ variable, following exim's rules (sort of)
869     my($v,$d) = split(/:/, $_, 2);
870     $v = "header_" . lc($v);
871     $d =~ s/^\s+//;
872     $d =~ s/\s+$//;
873     $self->{_vars}{$v} .= "$d\n";
874     $self->{_vars}{received_count}++ if ($v eq 'header_received');
875     # push header onto $message_headers var, following exim's rules
876     $self->{_vars}{message_headers} .= $_;
877   }
878   close(I);
879   # remove trailing newline from $message_headers
880   chomp($self->{_vars}{message_headers});
881
882   if (length($self->{_vars}{"header_reply-to"}) > 0) {
883     $self->{_vars}{reply_address} = $self->{_vars}{"header_reply-to"};
884   } else {
885     $self->{_vars}{reply_address} = $self->{_vars}{header_from};
886   }
887
888   $self->{_vars}{message_body_size} =
889       (stat($self->{_path}.'/'.$self->{_message}.'-D'))[7] - 19;
890   if ($self->{_vars}{message_body_size} < 0) {
891     $self->{_vars}{message_size} = 0;
892   } else {
893     $self->{_vars}{message_size} += $self->{_vars}{message_body_size} + 1;
894   }
895
896   $self->{_vars}{message_linecount} += $self->{_vars}{body_linecount};
897
898   my $i = $self->{_vars}{message_size};
899   if ($i == 0)          { $i = ""; }
900   elsif ($i < 1024)     { $i = sprintf("%d",    $i);                    }
901   elsif ($i < 10240)    { $i = sprintf("%.1fK", $i / 1024);             }
902   elsif ($i < 1048576)  { $i = sprintf("%dK",   ($i+512)/1024);         }
903   elsif ($i < 10485760) { $i = sprintf("%.1fM", $i/1048576);            }
904   else                  { $i = sprintf("%dM",   ($i + 524288)/1048576); }
905   $self->{_vars}{shown_message_size} = $i;
906
907   return(1);
908 }
909
910 # mimic exim's host_extract_port function - receive a ref to a scalar,
911 # strip it of port, return port
912 sub _get_host_and_port {
913   my $self = shift;
914   my $host = shift; # scalar ref, be careful
915
916   if ($$host =~ /^\[([^\]]+)\](?:\:(\d+))?$/) {
917     $$host = $1;
918     return($2 || 0);
919   } elsif ($$host =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:\.(\d+))?$/) {
920     $$host = $1;
921     return($2 || 0);
922   } elsif ($$host =~ /^([\d\:]+)(?:\.(\d+))?$/) {
923     $$host = $1;
924     return($2 || 0);
925   }
926   # implicit else
927   return(0);
928 }
929
930 # honoring all formatting preferences, return a scalar variable of the
931 # information for the single message matching what exim -bp would show.
932 # We can print later if we want.
933 sub format_message {
934   my $self = shift;
935   my $o    = '';
936   return if ($self->{_delivered});
937
938   if ($self->{_output_idonly}) {
939     $o .= $self->{_message};
940     foreach my $v (@{$self->{_show_vars}}) {
941       $o .= " $v='" . $self->get_var($v) . "'";
942     }
943     $o .= "\n";
944     return $o;
945   }
946
947   if ($self->{_output_long} || $self->{_output_flatq}) {
948     my $i = int($self->{_vars}{message_age} / 60);
949     if ($i > 90) {
950       $i = int(($i+30)/60);
951       if ($i > 72) { $o .= sprintf "%2dd ", int(($i+12)/24); }
952       else { $o .= sprintf "%2dh ", $i; }
953     } else { $o .= sprintf "%2dm ", $i; }
954
955     if ($self->{_output_flatq} && $self->{_show_vars}) {
956         $o .= join(';', map { "$_='".$self->get_var($_)."'" }
957                         (@{$self->{_show_vars}})
958                   );
959     } else {
960       $o .= sprintf "%5s", $self->{_vars}{shown_message_size};
961     }
962     $o .= " ";
963   }
964   $o .= "$self->{_message} ";
965   $o .= "From: " if ($self->{_output_brief});
966   $o .= "<$self->{_vars}{sender_address}>";
967
968   if ($self->{_output_long}) {
969     $o .= " ($self->{_vars}{originator_login})"
970         if ($self->{_vars}{sender_set_untrusted});
971
972     # XXX exim contains code here to print spool format errors
973     $o .= " *** frozen ***" if ($self->{_vars}{deliver_freeze});
974     $o .= "\n";
975
976     foreach my $v (@{$self->{_show_vars}}) {
977       $o .= sprintf "  %25s = '%s'\n", $v, $self->get_var($v);
978     }
979
980     foreach my $r (keys %{$self->{_recips}}) {
981       next if ($self->{_del_tree}{$r} && $self->{_undelivered_only});
982       $o .= sprintf "        %s %s\n", $self->{_del_tree}{$r} ? "D" : " ", $r;
983     }
984     if ($self->{_show_generated}) {
985       foreach my $r (keys %{$self->{_del_tree}}) {
986         next if ($self->{_recips}{$r});
987         $o .= sprintf "       +D %s\n", $r;
988       }
989     }
990   } elsif ($self->{_output_brief}) {
991     my @r = ();
992     foreach my $r (keys %{$self->{_recips}}) {
993       next if ($self->{_del_tree}{$r});
994       push(@r, $r);
995     }
996     $o .= " To: " . join(';', @r);
997     if ($self->{_show_vars} && scalar(@{$self->{_show_vars}})) {
998       $o .= " Vars: " . join(';', map { "$_='".$self->get_var($_)."'" }
999                                   (@{$self->{_show_vars}})
1000                             );
1001     }
1002   } elsif ($self->{_output_flatq}) {
1003     $o .= " *** frozen ***" if ($self->{_vars}{deliver_freeze});
1004     my @r = ();
1005     foreach my $r (keys %{$self->{_recips}}) {
1006       next if ($self->{_del_tree}{$r});
1007       push(@r, $r);
1008     }
1009     $o .= " " . join(' ', @r);
1010   }
1011
1012   $o .= "\n";
1013   return($o);
1014 }
1015
1016 sub print_message {
1017   my $self = shift;
1018   my $fh   = shift || \*STDOUT;
1019   return if ($self->{_delivered});
1020
1021   print $fh $self->format_message();
1022 }
1023
1024 sub dump {
1025   my $self = shift;
1026
1027   foreach my $k (sort keys %$self) {
1028     my $r = ref($self->{$k});
1029     if ($r eq 'ARRAY') {
1030       printf "%20s <<EOM\n", $k;
1031       print @{$self->{$k}}, "EOM\n";
1032     } elsif ($r eq 'HASH') {
1033       printf "%20s <<EOM\n", $k;
1034       foreach (sort keys %{$self->{$k}}) {
1035         printf "%20s %s\n", $_, $self->{$k}{$_};
1036       }
1037       print "EOM\n";
1038     } else {
1039       printf "%20s %s\n", $k, $self->{$k};
1040     }
1041   }
1042 }
1043
1044 } # BEGIN
1045
1046 sub ext_usage {
1047   if ($ARGV[0] =~ /^--help$/i) {
1048     require Config;
1049     $ENV{PATH} .= ":" unless $ENV{PATH} eq "";
1050     $ENV{PATH} = "$ENV{PATH}$Config::Config{'installscript'}";
1051     #exec("perldoc", "-F", "-U", $0) || exit 1;
1052     $< = $> = 1 if ($> == 0 || $< == 0);
1053     exec("perldoc", $0) || exit 1;
1054     # make parser happy
1055     %Config::Config = ();
1056   } elsif ($ARGV[0] =~ /^--version$/i) {
1057     print "$p_name version $p_version\n\n$p_cp\n";
1058   } else {
1059     return;
1060   }
1061
1062   exit(0);
1063 }
1064
1065 __END__
1066
1067 =head1 NAME
1068
1069 exipick - selectively display messages from an Exim queue
1070
1071 =head1 SYNOPSIS
1072
1073 exipick [<options>] [<criterion> [<criterion> ...]]
1074
1075 =head1 DESCRIPTION
1076
1077 exipick is a tool to display messages in an Exim queue.  It is very similar to exiqgrep and is, in fact, a drop in replacement for exiqgrep.  exipick allows you to select messages to be displayed using any piece of data stored in an Exim spool file.  Matching messages can be displayed in a variety of formats.
1078
1079 =head1 QUICK START
1080
1081 Delete every frozen message from queue:
1082     exipick -zi | xargs exim -Mrm
1083
1084 Show only messages which have not yet been virus scanned:
1085     exipick '$received_protocol ne virus-scanned'
1086
1087 Run the queue in a semi-random order:
1088     exipick -i --random | xargs exim -M
1089
1090 Show the count and total size of all messages which either originated from localhost or have a received protocol of 'local':
1091     exipick --or --size --bpc \
1092             '$sender_host_address eq 127.0.0.1' \
1093             '$received_protocol eq local'
1094
1095 Display all messages received on the MSA port, ordered first by the sender's email domain and then by the size of the emails:
1096     exipick --sort sender_address_domain,message_size \
1097             '$interface_port == 587'
1098
1099 Display only messages whose every recipient is in the example.com domain, also listing the IP address of the sending host:
1100     exipick --show-vars sender_host_address \
1101             '$each_recipients = example.com'
1102
1103 =head1 OPTIONS
1104
1105 =over 4
1106
1107 =item --and
1108
1109 Display messages matching all criteria (default)
1110
1111 =item -b
1112
1113 Display messages in brief format (exiqgrep)
1114
1115 =item -bp
1116
1117 Display messages in standard mailq format (default)
1118
1119 =item -bpa
1120
1121 Same as -bp, show generated addresses also (exim)
1122
1123 =item -bpc
1124
1125 Show a count of matching messages (exim)
1126
1127 =item -bpr
1128
1129 Same as '-bp --unsorted' (exim)
1130
1131 =item -bpra
1132
1133 Same as '-bpr --unsorted' (exim)
1134
1135 =item -bpru
1136
1137 Same as '-bpu --unsorted' (exim)
1138
1139 =item -bpu
1140
1141 Same as -bp, but only show undelivered messages (exim)
1142
1143 =item -c
1144
1145 Show a count of matching messages (exiqgrep)
1146
1147 =item --caseful
1148
1149 Make operators involving '=' honor case
1150
1151 =item -f <regexp>
1152
1153 Same as '$sender_address = <regexp>' (exiqgrep)
1154
1155 =item --flatq
1156
1157 Use a single-line output format
1158
1159 =item --freeze <cache file>
1160
1161 Save queue information in an quickly retrievable format
1162
1163 =item --help
1164
1165 Display this output
1166
1167 =item -i
1168
1169 Display only the message IDs (exiqgrep)
1170
1171 =item -l
1172
1173 Same as -bp (exiqgrep)
1174
1175 =item --not
1176
1177 Negate all tests.
1178
1179 =item -o <seconds>
1180
1181 Same as '$message_age > <seconds>' (exiqgrep)
1182
1183 =item --or
1184
1185 Display messages matching any criteria
1186
1187 =item -R
1188
1189 Same as --reverse (exiqgrep)
1190
1191 =item -r <regexp>
1192
1193 Same as '$recipients = <regexp>' (exiqgrep)
1194
1195 =item --random
1196
1197 Display messages in random order
1198
1199 =item --reverse
1200
1201 Display messages in reverse order
1202
1203 =item -s <string>
1204
1205 Same as '$shown_message_size eq <string>' (exiqgrep)
1206
1207 =item --spool <path>
1208
1209 Set the path to the exim spool to use
1210
1211 =item --show-rules
1212
1213 Show the internal representation of each criterion specified
1214
1215 =item --show-tests
1216
1217 Show the result of each criterion on each message
1218
1219 =item --show-vars <variable>[,<variable>...]
1220
1221 Show the value for <variable> for each displayed message
1222
1223 =item --size
1224
1225 Show the total bytes used by each displayed message
1226
1227 =item --thaw <cache file>
1228
1229 Read queue information cached from a previous --freeze run
1230
1231 =item --sort <variable>[,<variable>...]
1232
1233 Display matching messages sorted according to <variable>
1234
1235 =item --unsorted
1236
1237 Do not apply any sorting to output
1238
1239 =item --version
1240
1241 Display the version of this command
1242
1243 =item -x
1244
1245 Same as '!$deliver_freeze' (exiqgrep)
1246
1247 =item -y
1248
1249 Same as '$message_age < <seconds>' (exiqgrep)
1250
1251 =item -z
1252
1253 Same as '$deliver_freeze' (exiqgrep)
1254
1255 =back
1256
1257 =head1 CRITERIA
1258
1259 Exipick decides which messages to display by applying a test against each message.  The rules take the general form of 'VARIABLE OPERATOR VALUE'.  For example, '$message_age > 60'.  When exipick is deciding which messages to display, it checks the $message_age variable for each message.  If a message's age is greater than 60, the message will be displayed.  If the message's age is 60 or less seconds, it will not be displayed.
1260
1261 Multiple criteria can be used.  The order they are specified does not matter.  By default all criteria must evaluate to true for a message to be displayed.  If the --or option is used, a message is displayed as long as any of the criteria evaluate to true.
1262
1263 See the VARIABLES and OPERATORS sections below for more details
1264
1265 =head1 OPERATORS
1266
1267 =over 4
1268
1269 =item BOOLEAN
1270
1271 Boolean variables are checked simply by being true or false.  There is no real operator except negation.  Examples of valid boolean tests:
1272   '$deliver_freeze'
1273   '!$deliver_freeze'
1274
1275 =item NUMERIC
1276
1277 Valid comparisons are <, <=, >, >=, ==, and !=.  Numbers can be integers or floats.  Any number in a test suffixed with d, h, m, s, M, K, or B will be mulitplied by 86400, 3600, 60, 1, 1048576, 1024, or 1 respectively.  Examples of valid numeric tests:
1278   '$message_age >= 3d'
1279   '$local_interface == 587'
1280   '$message_size < 30K'
1281
1282 =item STRING
1283
1284 The string operators are =, eq, ne, =~, and !~.  With the exception of '=', the operators all match the functionality of the like-named perl operators.  eq and ne match a string exactly.  !~, =~, and = apply a perl regular expression to a string.  The '=' operator behaves just like =~ but you are not required to place // around the regular expression.  Examples of valid string tests:
1285   '$received_protocol eq esmtp'
1286   '$sender_address = example.com'
1287   '$each_recipients =~ /^a[a-z]{2,3}@example.com$/'
1288
1289 =item NEGATION
1290
1291 There are many ways to negate tests, each having a reason for existing.  Many tests can be negated using native operators.  For instance, >1 is the opposite of <=1 and eq and ne are opposites.  In addition, each individual test can be negated by adding a ! at the beginning of the test.  For instance, '!$acl_m1 =~ /^DENY$/' is the same as '$acl_m1 !~ /^DENY$/'.  Finally, every test can be specified by using the command line argument --not.  This is functionally equivilant to adding a ! to the beginning of every test.
1292
1293 =back
1294
1295 =head1 VARIABLES
1296
1297 With a few exceptions the available variables match Exim's internal expansion variables in both name and exact contents.  There are a few notable additions and format deviations which are noted below.  Although a brief explanation is offered below, Exim's spec.txt should be consulted for full details.  It is important to remember that not every variable will be defined for every message.  For example, $sender_host_port is not defined for messages not received from a remote host.
1298
1299 Internally, all variables are represented as strings, meaning any operator will work on any variable.  This means that '$sender_host_name > 4' is a legal criterion, even if it does not produce meaningful results.  Variables in the list below are marked with a 'type' to help in choosing which types of operators make sense to use.
1300
1301   Identifiers
1302     B - Boolean variables
1303     S - String variables
1304     N - Numeric variables
1305     . - Standard variable matching Exim's content definition
1306     # - Standard variable, contents differ from Exim's definition
1307     + - Non-standard variable
1308
1309 =over 4
1310
1311 =item S . $acl_c0-$acl_c9, $acl_m0-$acl_m9
1312
1313 User definable variables.
1314
1315 =item B + $allow_unqualified_recipient
1316
1317 TRUE if unqualified recipient addresses are permitted in header lines.
1318
1319 =item B + $allow_unqualified_sender
1320
1321 TRUE if unqualified sender addresses are permitted in header lines.
1322
1323 =item S . $authenticated_id
1324
1325 Optional saved information from authenticators, or the login name of the calling process for locally submitted messages.
1326
1327 =item S . $authenticated_sender
1328
1329 The value of AUTH= param for smtp messages, or a generated value from the calling processes login and qualify domain for locally submitted messages.
1330
1331 =item S + $bmi_verdicts
1332
1333 The verdict string provided by a Brightmail content scan
1334
1335 =item N . $body_linecount
1336
1337 The number of lines in the message's body.
1338
1339 =item N . $body_zerocount
1340
1341 The number of binary zero bytes in the message's body.
1342
1343 =item B + $deliver_freeze
1344
1345 TRUE if the message is currently frozen.
1346
1347 =item N + $deliver_frozen_at
1348
1349 The epoch time at which message was frozen.
1350
1351 =item B + $dont_deliver
1352
1353 TRUE if, under normal circumstances, Exim will not try to deliver the message.
1354
1355 =item S + $each_recipients
1356
1357 This is a psuedo variable which allows you to apply a test against each address in $recipients individually.  Whereas '$recipients =~ /@aol.com/' will match if any recipient address contains aol.com, '$each_recipients =~ /@aol.com$/' will only be true if every recipient matches that pattern.  Note that this obeys --and or --or being set.  Using it with --or is very similar to just matching against $recipients, but with the added benefit of being able to use anchors at the beginning and end of each recipient address.
1358
1359 =item S + $each_recipients_del
1360
1361 Like $each_recipients, but for $recipients_del
1362
1363 =item S + $each_recipients_undel
1364
1365 Like $each_recipients, but for $recipients_undel
1366
1367 =item B . $first_delivery
1368
1369 TRUE if the message has never been deferred.
1370
1371 =item S # $header_*
1372
1373 The value of the same named message header.  These variables are really closer to Exim's rheader_* variables, with the exception that leading and trailing space is removed.
1374
1375 =item B . $host_lookup_deferred
1376
1377 TRUE if there was an attempt to look up the host's name from its IP address, but an error occurred that during the attempt.
1378
1379 =item B . $host_lookup_failed
1380
1381 TRUE if there was an attempt to look up the host's name from its IP address, but the attempt returned a negative result.
1382
1383 =item S . $interface_address
1384
1385 The address of the local IP interface for network-originated messages.
1386
1387 =item N . $interface_port
1388
1389 The local port number if network-originated messages.
1390
1391 =item B + $local_error_message
1392
1393 TRUE if the message is a locally-generated error message.
1394
1395 =item S . $local_scan_data
1396
1397 The text returned by the local_scan() function when a message is received.
1398
1399 =item B . $manually_thawed
1400
1401 TRUE when the message has been manually thawed.
1402
1403 =item N . $message_age
1404
1405 The number of seconds since the message was received.
1406
1407 =item S # $message_body
1408
1409 The message's body.  Unlike Exim's variable of the same name, this variable contains the entire message body.  Newlines and nulls are replaced by spaces.
1410
1411 =item N . $message_body_size
1412
1413 The size of the body in bytes.
1414
1415 =item S . $message_exim_id, $message_id
1416
1417 The unique message id that is used by Exim to identify the message.  $message_id is deprecated as of Exim 4.53.
1418
1419 =item S . $message_headers
1420
1421 A concatenation of all the header lines except for lines added by routers or transports.
1422
1423 =item N . $message_linecount
1424
1425 The number of lines in the entire message (body and headers).
1426
1427 =item N . $message_size
1428
1429 The size of the message in bytes.
1430
1431 =item N . $originator_gid
1432
1433 The group id under which the process that called Exim was running as when the message was received.
1434
1435 =item S + $originator_login
1436
1437 The login of the process which called Exim.
1438
1439 =item N . $originator_uid
1440
1441 The user id under which the process that called Exim was running as when the message was received.
1442
1443 =item N . $received_count
1444
1445 The number of Received: header lines in the message.
1446
1447 =item S . $received_protocol
1448
1449 The name of the protocol by which the message was received.
1450
1451 =item N . $received_time
1452
1453 The epoch time at which the message was received.
1454
1455 =item S # $recipients
1456
1457 The list of envelope recipients for a message.  Unlike Exim's version, this variable always contains every recipient of the message.  The recipients are seperated by a comma and a space.  See also $each_recipients.
1458
1459 =item N . $recipients_count
1460
1461 The number of envelope recipients for the message.
1462
1463 =item S + $recipients_del
1464
1465 The list of delivered envelope recipients for a message.  This non-standard variable is in the same format as $recipients and contains the list of already-delivered recipients including any generated addresses.  See also $each_recipients_del.
1466
1467 =item N + $recipients_del_count
1468
1469 The number of envelope recipients for the message which have already been delivered.  Note that this is the count of original recipients to which the message has been delivered.  It does not include generated addresses so it is possible that this number will be less than the number of addresses in the $recipients_del string.
1470
1471 =item S + $recipients_undel
1472
1473 The list of undelivered envelope recipients for a message.  This non-standard variable is in the same format as $recipients and contains the list of undelivered recipients.  See also $each_recipients_undel.
1474
1475 =item N + $recipients_undel_count
1476
1477 The number of envelope recipients for the message which have not yet been delivered.
1478
1479 =item S . $reply_address
1480
1481 The contents of the Reply-To: header line if one exists and it is not empty, or otherwise the contents of the From: header line.
1482
1483 =item S . $sender_address
1484
1485 The sender's address that was received in the message's envelope.  For bounce messages, the value of this variable is the empty string.
1486
1487 =item S . $sender_address_domain
1488
1489 The domain part of $sender_address.
1490
1491 =item S . $sender_address_local_part
1492
1493 The local part of $sender_address.
1494
1495 =item S . $sender_helo_name
1496
1497 The HELO or EHLO value supplied for smtp or bsmtp messages.
1498
1499 =item S . $sender_host_address
1500
1501 The remote host's IP address.
1502
1503 =item S . $sender_host_authenticated
1504
1505 The name of the authenticator driver which successfully authenticated the client from which the message was received.
1506
1507 =item S . $sender_host_name
1508
1509 The remote host's name as obtained by looking up its IP address.
1510
1511 =item N . $sender_host_port
1512
1513 The port number that was used on the remote host for network-originated messages.
1514
1515 =item S . $sender_ident
1516
1517 The identification received in response to an RFC 1413 request for remote messages, the login name of the user that called Exim for locally generated messages.
1518
1519 =item B + $sender_local
1520
1521 TRUE if the message was locally generated.
1522
1523 =item B + $sender_set_untrusted
1524
1525 TRUE if the envelope sender of this message was set by an untrusted local caller.
1526
1527 =item S + $shown_message_size
1528
1529 This non-standard variable contains the formatted size string.  That is, for a message whose $message_size is 66566 bytes, $shown_message_size is 65K.
1530
1531 =item S . $smtp_active_hostname
1532
1533 The value of the active host name when the message was received, as specified by the "smtp_active_hostname" option.
1534
1535 =item S . $spam_score
1536
1537 The spam score of the message, for example '3.4' or '30.5'.  (Requires exiscan or WITH_CONTENT_SCAN)
1538
1539 =item S . $spam_score_int
1540
1541 The spam score of the message, multiplied by ten, as an integer value.  For instance '34' or '305'.  (Requires exiscan or WITH_CONTENT_SCAN)
1542
1543 =item B . $tls_certificate_verified
1544
1545 TRUE if a TLS certificate was verified when the message was received.
1546
1547 =item S . $tls_cipher
1548
1549 The cipher suite that was negotiated for encrypted SMTP connections.
1550
1551 =item S . $tls_peerdn
1552
1553 The value of the Distinguished Name of the certificate if Exim is configured to request one
1554
1555 =item N + $warning_count
1556
1557 The number of delay warnings which have been sent for this message.
1558
1559 =back
1560
1561 =head1 CONTACT
1562
1563 =over 4
1564
1565 =item EMAIL: proj-exipick@jetmore.net
1566
1567 =item HOME: jetmore.org/john/code/#exipick
1568
1569 =back
1570
1571 =cut