Add clearerr(stdin) in -bem code (needed for Darwin).
[exim.git] / src / src / exipick.src
1 #!PERL_COMMAND
2 # $Cambridge: exim/src/src/exipick.src,v 1.13 2006/09/19 20:01:13 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 = "20060919.0";
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 sub get_matching_vars {
631   my $self = shift;
632   my $e    = shift;
633
634   if ($e =~ /^\^/) {
635     my @r = ();
636     foreach my $v (keys %{$self->{_vars}}) { push(@r, $v) if ($v =~ /$e/); }
637     return(@r);
638   } else {
639     return($e);
640   }
641 }
642
643 # accepts a variable with or without leading '$' or trailing ':'
644 sub get_var {
645   my $self = shift;
646   my $var  = lc(shift);
647
648   $var =~ s/^\$//;
649   $var =~ s/:$//;
650
651   $self->_parse_body()
652       if ($var eq 'message_body' && !$self->{_vars}{message_body});
653
654   chomp($self->{_vars}{$var});
655   return $self->{_vars}{$var};
656 }
657
658 sub _parse_body {
659   my $self = shift;
660   my $f    = $self->{_path} . '/' . $self->{_message} . '-D';
661
662   open(I, "<$f") || return($self->_error("Couldn't open $f: $!"));
663   chomp($_ = <I>);
664   return(0) if ($self->{_message}.'-D' ne $_);
665
666   $self->{_vars}{message_body} = join('', <I>);
667   close(I);
668   $self->{_vars}{message_body} =~ s/\n/ /g;
669   $self->{_vars}{message_body} =~ s/\000/ /g;
670   return(1);
671 }
672
673 sub _parse_header {
674   my $self = shift;
675   my $f    = $self->{_path} . '/' . $self->{_message} . '-H';
676
677   if (!open(I, "<$f")) {
678     # assume message went away and silently ignore
679     $self->{_delivered} = 1;
680     return(1);
681   }
682
683   chomp($_ = <I>);
684   return(0) if ($self->{_message}.'-H' ne $_);
685   $self->{_vars}{message_id}       = $self->{_message};
686   $self->{_vars}{message_exim_id}  = $self->{_message};
687
688   # line 2
689   chomp($_ = <I>);
690   return(0) if (!/^(.+)\s(\-?\d+)\s(\-?\d+)$/);
691   $self->{_vars}{originator_login} = $1;
692   $self->{_vars}{originator_uid}   = $2;
693   $self->{_vars}{originator_gid}   = $3;
694
695   # line 3
696   chomp($_ = <I>);
697   return(0) if (!/^<(.*)>$/);
698   $self->{_vars}{sender_address}   = $1;
699   $self->{_vars}{sender_address_domain} = $1;
700   $self->{_vars}{sender_address_local_part} = $1;
701   $self->{_vars}{sender_address_domain} =~ s/^.*\@//;
702   $self->{_vars}{sender_address_local_part} =~ s/^(.*)\@.*$/$1/;
703
704   # line 4
705   chomp($_ = <I>);
706   return(0) if (!/^(\d+)\s(\d+)$/);
707   $self->{_vars}{received_time}    = $1;
708   $self->{_vars}{warning_count}    = $2;
709   $self->{_vars}{message_age}      = time() - $self->{_vars}{received_time};
710
711   while (<I>) {
712     chomp();
713     if (/^(-\S+)\s*(.*$)/) {
714       my $tag = $1;
715       my $arg = $2;
716       if ($tag eq '-acl') {
717         my $t;
718         return(0) if ($arg !~ /^(\d+)\s(\d+)$/);
719         if ($1 < $Exim::SpoolFile::ACL_C_MAX_LEGACY) {
720           $t = "acl_c$1";
721         } else {
722           $t = "acl_m" . ($1 - $Exim::SpoolFile::ACL_C_MAX_LEGACY);
723         }
724         read(I, $self->{_vars}{$t}, $2+1) || return(0);
725         chomp($self->{_vars}{$t});
726       } elsif ($tag eq '-aclc') {
727         #return(0) if ($arg !~ /^(\d+)\s(\d+)$/);
728         return(0) if ($arg !~ /^(\S+)\s(\d+)$/);
729         my $t = "acl_c$1";
730         read(I, $self->{_vars}{$t}, $2+1) || return(0);
731         chomp($self->{_vars}{$t});
732       } elsif ($tag eq '-aclm') {
733         #return(0) if ($arg !~ /^(\d+)\s(\d+)$/);
734         return(0) if ($arg !~ /^(\S+)\s(\d+)$/);
735         my $t = "acl_m$1";
736         read(I, $self->{_vars}{$t}, $2+1) || return(0);
737         chomp($self->{_vars}{$t});
738       } elsif ($tag eq '-local') {
739         $self->{_vars}{sender_local} = 1;
740       } elsif ($tag eq '-localerror') {
741         $self->{_vars}{local_error_message} = 1;
742       } elsif ($tag eq '-local_scan') {
743         $self->{_vars}{local_scan_data} = $arg;
744       } elsif ($tag eq '-spam_score_int') {
745         $self->{_vars}{spam_score_int} = $arg;
746         $self->{_vars}{spam_score}     = $arg / 10;
747       } elsif ($tag eq '-bmi_verdicts') {
748         $self->{_vars}{bmi_verdicts} = $arg;
749       } elsif ($tag eq '-host_lookup_deferred') {
750         $self->{_vars}{host_lookup_deferred} = 1;
751       } elsif ($tag eq '-host_lookup_failed') {
752         $self->{_vars}{host_lookup_failed} = 1;
753       } elsif ($tag eq '-body_linecount') {
754         $self->{_vars}{body_linecount} = $arg;
755       } elsif ($tag eq '-body_zerocount') {
756         $self->{_vars}{body_zerocount} = $arg;
757       } elsif ($tag eq '-frozen') {
758         $self->{_vars}{deliver_freeze} = 1;
759         $self->{_vars}{deliver_frozen_at} = $arg;
760       } elsif ($tag eq '-allow_unqualified_recipient') {
761         $self->{_vars}{allow_unqualified_recipient} = 1;
762       } elsif ($tag eq '-allow_unqualified_sender') {
763         $self->{_vars}{allow_unqualified_sender} = 1;
764       } elsif ($tag eq '-deliver_firsttime') {
765         $self->{_vars}{deliver_firsttime} = 1;
766         $self->{_vars}{first_delivery} = 1;
767       } elsif ($tag eq '-manual_thaw') {
768         $self->{_vars}{deliver_manual_thaw} = 1;
769         $self->{_vars}{manually_thawed} = 1;
770       } elsif ($tag eq '-auth_id') {
771         $self->{_vars}{authenticated_id} = $arg;
772       } elsif ($tag eq '-auth_sender') {
773         $self->{_vars}{authenticated_sender} = $arg;
774       } elsif ($tag eq '-sender_set_untrusted') {
775         $self->{_vars}{sender_set_untrusted} = 1;
776       } elsif ($tag eq '-tls_certificate_verified') {
777         $self->{_vars}{tls_certificate_verified} = 1;
778       } elsif ($tag eq '-tls_cipher') {
779         $self->{_vars}{tls_cipher} = $arg;
780       } elsif ($tag eq '-tls_peerdn') {
781         $self->{_vars}{tls_peerdn} = $arg;
782       } elsif ($tag eq '-host_address') {
783         $self->{_vars}{sender_host_port} = $self->_get_host_and_port(\$arg);
784         $self->{_vars}{sender_host_address} = $arg;
785       } elsif ($tag eq '-interface_address') {
786         $self->{_vars}{interface_port} = $self->_get_host_and_port(\$arg);
787         $self->{_vars}{interface_address} = $arg;
788       } elsif ($tag eq '-active_hostname') {
789         $self->{_vars}{smtp_active_hostname} = $arg;
790       } elsif ($tag eq '-host_auth') {
791         $self->{_vars}{sender_host_authenticated} = $arg;
792       } elsif ($tag eq '-host_name') {
793         $self->{_vars}{sender_host_name} = $arg;
794       } elsif ($tag eq '-helo_name') {
795         $self->{_vars}{sender_helo_name} = $arg;
796       } elsif ($tag eq '-ident') {
797         $self->{_vars}{sender_ident} = $arg;
798       } elsif ($tag eq '-received_protocol') {
799         $self->{_vars}{received_protocol} = $arg;
800       } elsif ($tag eq '-N') {
801         $self->{_vars}{dont_deliver} = 1;
802       } else {
803         # unrecognized tag, save it for reference
804         $self->{$tag} = $arg;
805       }
806     } else {
807       last;
808     }
809   }
810
811   # when we drop out of the while loop, we have the first line of the
812   # delivered tree in $_
813   do {
814     if ($_ eq 'XX') {
815       ; # noop
816     } elsif ($_ =~ s/^[YN][YN]\s+//) {
817       $self->{_del_tree}{$_} = 1;
818     } else {
819       return(0);
820     }
821     chomp($_ = <I>);
822   } while ($_ !~ /^\d+$/);
823
824   $self->{_numrecips} = $_;
825   $self->{_vars}{recipients_count} = $self->{_numrecips};
826   for (my $i = 0; $i < $self->{_numrecips}; $i++) {
827     chomp($_ = <I>);
828     return(0) if (/^$/);
829     my $addr = '';
830     if (/^(.*)\s\d+,(\d+),\d+$/) {
831       #print STDERR "exim3 type (untested): $_\n";
832       $self->{_recips}{$1} = { pno => $2 };
833       $addr = $1;
834     } elsif (/^(.*)\s(\d+)$/) {
835       #print STDERR "exim4 original type (untested): $_\n";
836       $self->{_recips}{$1} = { pno => $2 };
837       $addr = $1;
838     } elsif (/^(.*)\s(.*)\s(\d+),(\d+)#1$/) {
839       #print STDERR "exim4 new type #1 (untested): $_\n";
840       return($self->_error("incorrect format: $_")) if (length($2) != $3);
841       $self->{_recips}{$1} = { pno => $4, errors_to => $2 };
842       $addr = $1;
843     } elsif (/^.*#(\d+)$/) {
844       #print STDERR "exim4 #$1 style (unimplemented): $_\n";
845       $self->_error("exim4 #$1 style (unimplemented): $_");
846     } else {
847       #print STDERR "default type: $_\n";
848       $self->{_recips}{$_} = {};
849       $addr = $_;
850     }
851     $self->{_udel_tree}{$addr} = 1 if (!$self->{_del_tree}{$addr});
852   }
853   $self->{_vars}{recipients}         = join(', ', keys(%{$self->{_recips}}));
854   $self->{_vars}{recipients_del}     = join(', ', keys(%{$self->{_del_tree}}));
855   $self->{_vars}{recipients_undel}   = join(', ', keys(%{$self->{_udel_tree}}));
856   $self->{_vars}{recipients_undel_count} = scalar(keys(%{$self->{_udel_tree}}));
857   $self->{_vars}{recipients_del_count}   = 0;
858   foreach my $r (keys %{$self->{_del_tree}}) {
859     next if (!$self->{_recips}{$r});
860     $self->{_vars}{recipients_del_count}++;
861   }
862
863   # blank line
864   $_ = <I>;
865   return(0) if (!/^$/);
866
867   # start reading headers
868   while (read(I, $_, 3) == 3) {
869     my $t = getc(I);
870     return(0) if (!length($t));
871     while ($t =~ /^\d$/) {
872       $_ .= $t;
873       $t  = getc(I);
874     }
875     # ok, right here $t contains the header flag and $_ contains the number of
876     # bytes to read.  If we ever use the header flag, grab it here.
877     $self->{_vars}{message_size} += $_ if ($t ne '*');
878     $t = getc(I); # strip the space out of the file
879     my $bytes = $_;
880     return(0) if (read(I, $_, $bytes) != $bytes);
881     $self->{_vars}{message_linecount} += (tr/\n//) if ($t ne '*');
882
883     # build the $header_ variable, following exim's rules (sort of)
884     my($v,$d) = split(/:/, $_, 2);
885     $v = "header_" . lc($v);
886     $d =~ s/^\s+//;
887     $d =~ s/\s+$//;
888     $self->{_vars}{$v} .= "$d\n";
889     $self->{_vars}{received_count}++ if ($v eq 'header_received');
890     # push header onto $message_headers var, following exim's rules
891     $self->{_vars}{message_headers} .= $_;
892   }
893   close(I);
894   # remove trailing newline from $message_headers
895   chomp($self->{_vars}{message_headers});
896
897   if (length($self->{_vars}{"header_reply-to"}) > 0) {
898     $self->{_vars}{reply_address} = $self->{_vars}{"header_reply-to"};
899   } else {
900     $self->{_vars}{reply_address} = $self->{_vars}{header_from};
901   }
902
903   $self->{_vars}{message_body_size} =
904       (stat($self->{_path}.'/'.$self->{_message}.'-D'))[7] - 19;
905   if ($self->{_vars}{message_body_size} < 0) {
906     $self->{_vars}{message_size} = 0;
907   } else {
908     $self->{_vars}{message_size} += $self->{_vars}{message_body_size} + 1;
909   }
910
911   $self->{_vars}{message_linecount} += $self->{_vars}{body_linecount};
912
913   my $i = $self->{_vars}{message_size};
914   if ($i == 0)          { $i = ""; }
915   elsif ($i < 1024)     { $i = sprintf("%d",    $i);                    }
916   elsif ($i < 10240)    { $i = sprintf("%.1fK", $i / 1024);             }
917   elsif ($i < 1048576)  { $i = sprintf("%dK",   ($i+512)/1024);         }
918   elsif ($i < 10485760) { $i = sprintf("%.1fM", $i/1048576);            }
919   else                  { $i = sprintf("%dM",   ($i + 524288)/1048576); }
920   $self->{_vars}{shown_message_size} = $i;
921
922   return(1);
923 }
924
925 # mimic exim's host_extract_port function - receive a ref to a scalar,
926 # strip it of port, return port
927 sub _get_host_and_port {
928   my $self = shift;
929   my $host = shift; # scalar ref, be careful
930
931   if ($$host =~ /^\[([^\]]+)\](?:\:(\d+))?$/) {
932     $$host = $1;
933     return($2 || 0);
934   } elsif ($$host =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:\.(\d+))?$/) {
935     $$host = $1;
936     return($2 || 0);
937   } elsif ($$host =~ /^([\d\:]+)(?:\.(\d+))?$/) {
938     $$host = $1;
939     return($2 || 0);
940   }
941   # implicit else
942   return(0);
943 }
944
945 # honoring all formatting preferences, return a scalar variable of the
946 # information for the single message matching what exim -bp would show.
947 # We can print later if we want.
948 sub format_message {
949   my $self = shift;
950   my $o    = '';
951   return if ($self->{_delivered});
952
953   # define any vars we want to print out for this message.  The requests
954   # can be regexps, and the defined vars can change for each message, so we
955   # have to build this list for each message
956   my @vars = ();
957   if (@{$self->{_show_vars}}) {
958     my %t = ();
959     foreach my $e (@{$self->{_show_vars}}) {
960       foreach my $v ($self->get_matching_vars($e)) {
961         next if ($t{$v}); $t{$v}++; push(@vars, $v);
962       }
963     }
964   }
965
966   if ($self->{_output_idonly}) {
967     $o .= $self->{_message};
968     foreach my $v (@vars) {
969       $o .= " $v='" . $self->get_var($v) . "'";
970     }
971     $o .= "\n";
972     return $o;
973   }
974
975   if ($self->{_output_long} || $self->{_output_flatq}) {
976     my $i = int($self->{_vars}{message_age} / 60);
977     if ($i > 90) {
978       $i = int(($i+30)/60);
979       if ($i > 72) { $o .= sprintf "%2dd ", int(($i+12)/24); }
980       else { $o .= sprintf "%2dh ", $i; }
981     } else { $o .= sprintf "%2dm ", $i; }
982
983     if ($self->{_output_flatq} && @vars) {
984         $o .= join(';', map { "$_='".$self->get_var($_)."'" } (@vars)
985                   );
986     } else {
987       $o .= sprintf "%5s", $self->{_vars}{shown_message_size};
988     }
989     $o .= " ";
990   }
991   $o .= "$self->{_message} ";
992   $o .= "From: " if ($self->{_output_brief});
993   $o .= "<$self->{_vars}{sender_address}>";
994
995   if ($self->{_output_long}) {
996     $o .= " ($self->{_vars}{originator_login})"
997         if ($self->{_vars}{sender_set_untrusted});
998
999     # XXX exim contains code here to print spool format errors
1000     $o .= " *** frozen ***" if ($self->{_vars}{deliver_freeze});
1001     $o .= "\n";
1002
1003     foreach my $v (@vars) {
1004       $o .= sprintf "  %25s = '%s'\n", $v, $self->get_var($v);
1005     }
1006
1007     foreach my $r (keys %{$self->{_recips}}) {
1008       next if ($self->{_del_tree}{$r} && $self->{_undelivered_only});
1009       $o .= sprintf "        %s %s\n", $self->{_del_tree}{$r} ? "D" : " ", $r;
1010     }
1011     if ($self->{_show_generated}) {
1012       foreach my $r (keys %{$self->{_del_tree}}) {
1013         next if ($self->{_recips}{$r});
1014         $o .= sprintf "       +D %s\n", $r;
1015       }
1016     }
1017   } elsif ($self->{_output_brief}) {
1018     my @r = ();
1019     foreach my $r (keys %{$self->{_recips}}) {
1020       next if ($self->{_del_tree}{$r});
1021       push(@r, $r);
1022     }
1023     $o .= " To: " . join(';', @r);
1024     if (scalar(@vars)) {
1025       $o .= " Vars: ".join(';',map { "$_='".$self->get_var($_)."'" } (@vars));
1026     }
1027   } elsif ($self->{_output_flatq}) {
1028     $o .= " *** frozen ***" if ($self->{_vars}{deliver_freeze});
1029     my @r = ();
1030     foreach my $r (keys %{$self->{_recips}}) {
1031       next if ($self->{_del_tree}{$r});
1032       push(@r, $r);
1033     }
1034     $o .= " " . join(' ', @r);
1035   }
1036
1037   $o .= "\n";
1038   return($o);
1039 }
1040
1041 sub print_message {
1042   my $self = shift;
1043   my $fh   = shift || \*STDOUT;
1044   return if ($self->{_delivered});
1045
1046   print $fh $self->format_message();
1047 }
1048
1049 sub dump {
1050   my $self = shift;
1051
1052   foreach my $k (sort keys %$self) {
1053     my $r = ref($self->{$k});
1054     if ($r eq 'ARRAY') {
1055       printf "%20s <<EOM\n", $k;
1056       print @{$self->{$k}}, "EOM\n";
1057     } elsif ($r eq 'HASH') {
1058       printf "%20s <<EOM\n", $k;
1059       foreach (sort keys %{$self->{$k}}) {
1060         printf "%20s %s\n", $_, $self->{$k}{$_};
1061       }
1062       print "EOM\n";
1063     } else {
1064       printf "%20s %s\n", $k, $self->{$k};
1065     }
1066   }
1067 }
1068
1069 } # BEGIN
1070
1071 sub ext_usage {
1072   if ($ARGV[0] =~ /^--help$/i) {
1073     require Config;
1074     $ENV{PATH} .= ":" unless $ENV{PATH} eq "";
1075     $ENV{PATH} = "$ENV{PATH}$Config::Config{'installscript'}";
1076     #exec("perldoc", "-F", "-U", $0) || exit 1;
1077     $< = $> = 1 if ($> == 0 || $< == 0);
1078     exec("perldoc", $0) || exit 1;
1079     # make parser happy
1080     %Config::Config = ();
1081   } elsif ($ARGV[0] =~ /^--version$/i) {
1082     print "$p_name version $p_version\n\n$p_cp\n";
1083   } else {
1084     return;
1085   }
1086
1087   exit(0);
1088 }
1089
1090 __END__
1091
1092 =head1 NAME
1093
1094 exipick - selectively display messages from an Exim queue
1095
1096 =head1 SYNOPSIS
1097
1098 exipick [<options>] [<criterion> [<criterion> ...]]
1099
1100 =head1 DESCRIPTION
1101
1102 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.
1103
1104 =head1 QUICK START
1105
1106 Delete every frozen message from queue:
1107     exipick -zi | xargs exim -Mrm
1108
1109 Show only messages which have not yet been virus scanned:
1110     exipick '$received_protocol ne virus-scanned'
1111
1112 Run the queue in a semi-random order:
1113     exipick -i --random | xargs exim -M
1114
1115 Show the count and total size of all messages which either originated from localhost or have a received protocol of 'local':
1116     exipick --or --size --bpc \
1117             '$sender_host_address eq 127.0.0.1' \
1118             '$received_protocol eq local'
1119
1120 Display all messages received on the MSA port, ordered first by the sender's email domain and then by the size of the emails:
1121     exipick --sort sender_address_domain,message_size \
1122             '$interface_port == 587'
1123
1124 Display only messages whose every recipient is in the example.com domain, also listing the IP address of the sending host:
1125     exipick --show-vars sender_host_address \
1126             '$each_recipients = example.com'
1127
1128 Same as above, but show values for all defined variables starting with sender_ and the number of recipients:
1129     exipick --show-vars ^sender_,recipients_count \
1130             '$each_recipients = example.com'
1131
1132 =head1 OPTIONS
1133
1134 =over 4
1135
1136 =item --and
1137
1138 Display messages matching all criteria (default)
1139
1140 =item -b
1141
1142 Display messages in brief format (exiqgrep)
1143
1144 =item -bp
1145
1146 Display messages in standard mailq format (default)
1147
1148 =item -bpa
1149
1150 Same as -bp, show generated addresses also (exim)
1151
1152 =item -bpc
1153
1154 Show a count of matching messages (exim)
1155
1156 =item -bpr
1157
1158 Same as '-bp --unsorted' (exim)
1159
1160 =item -bpra
1161
1162 Same as '-bpr --unsorted' (exim)
1163
1164 =item -bpru
1165
1166 Same as '-bpu --unsorted' (exim)
1167
1168 =item -bpu
1169
1170 Same as -bp, but only show undelivered messages (exim)
1171
1172 =item -c
1173
1174 Show a count of matching messages (exiqgrep)
1175
1176 =item --caseful
1177
1178 Make operators involving '=' honor case
1179
1180 =item -f <regexp>
1181
1182 Same as '$sender_address = <regexp>' (exiqgrep)
1183
1184 =item --flatq
1185
1186 Use a single-line output format
1187
1188 =item --freeze <cache file>
1189
1190 Save queue information in an quickly retrievable format
1191
1192 =item --help
1193
1194 Display this output
1195
1196 =item -i
1197
1198 Display only the message IDs (exiqgrep)
1199
1200 =item -l
1201
1202 Same as -bp (exiqgrep)
1203
1204 =item --not
1205
1206 Negate all tests.
1207
1208 =item -o <seconds>
1209
1210 Same as '$message_age > <seconds>' (exiqgrep)
1211
1212 =item --or
1213
1214 Display messages matching any criteria
1215
1216 =item -R
1217
1218 Same as --reverse (exiqgrep)
1219
1220 =item -r <regexp>
1221
1222 Same as '$recipients = <regexp>' (exiqgrep)
1223
1224 =item --random
1225
1226 Display messages in random order
1227
1228 =item --reverse
1229
1230 Display messages in reverse order
1231
1232 =item -s <string>
1233
1234 Same as '$shown_message_size eq <string>' (exiqgrep)
1235
1236 =item --spool <path>
1237
1238 Set the path to the exim spool to use
1239
1240 =item --show-rules
1241
1242 Show the internal representation of each criterion specified
1243
1244 =item --show-tests
1245
1246 Show the result of each criterion on each message
1247
1248 =item --show-vars <variable>[,<variable>...]
1249
1250 Show the value for <variable> for each displayed message.  <variable> will be a regular expression if it begins with a circumflex.
1251
1252 =item --size
1253
1254 Show the total bytes used by each displayed message
1255
1256 =item --thaw <cache file>
1257
1258 Read queue information cached from a previous --freeze run
1259
1260 =item --sort <variable>[,<variable>...]
1261
1262 Display matching messages sorted according to <variable>
1263
1264 =item --unsorted
1265
1266 Do not apply any sorting to output
1267
1268 =item --version
1269
1270 Display the version of this command
1271
1272 =item -x
1273
1274 Same as '!$deliver_freeze' (exiqgrep)
1275
1276 =item -y
1277
1278 Same as '$message_age < <seconds>' (exiqgrep)
1279
1280 =item -z
1281
1282 Same as '$deliver_freeze' (exiqgrep)
1283
1284 =back
1285
1286 =head1 CRITERIA
1287
1288 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.
1289
1290 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.
1291
1292 See the VARIABLES and OPERATORS sections below for more details
1293
1294 =head1 OPERATORS
1295
1296 =over 4
1297
1298 =item BOOLEAN
1299
1300 Boolean variables are checked simply by being true or false.  There is no real operator except negation.  Examples of valid boolean tests:
1301   '$deliver_freeze'
1302   '!$deliver_freeze'
1303
1304 =item NUMERIC
1305
1306 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:
1307   '$message_age >= 3d'
1308   '$local_interface == 587'
1309   '$message_size < 30K'
1310
1311 =item STRING
1312
1313 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:
1314   '$received_protocol eq esmtp'
1315   '$sender_address = example.com'
1316   '$each_recipients =~ /^a[a-z]{2,3}@example.com$/'
1317
1318 =item NEGATION
1319
1320 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.
1321
1322 =back
1323
1324 =head1 VARIABLES
1325
1326 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.
1327
1328 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.
1329
1330   Identifiers
1331     B - Boolean variables
1332     S - String variables
1333     N - Numeric variables
1334     . - Standard variable matching Exim's content definition
1335     # - Standard variable, contents differ from Exim's definition
1336     + - Non-standard variable
1337
1338 =over 4
1339
1340 =item S . $acl_c0-$acl_c9, $acl_m0-$acl_m9
1341
1342 User definable variables.
1343
1344 =item B + $allow_unqualified_recipient
1345
1346 TRUE if unqualified recipient addresses are permitted in header lines.
1347
1348 =item B + $allow_unqualified_sender
1349
1350 TRUE if unqualified sender addresses are permitted in header lines.
1351
1352 =item S . $authenticated_id
1353
1354 Optional saved information from authenticators, or the login name of the calling process for locally submitted messages.
1355
1356 =item S . $authenticated_sender
1357
1358 The value of AUTH= param for smtp messages, or a generated value from the calling processes login and qualify domain for locally submitted messages.
1359
1360 =item S + $bmi_verdicts
1361
1362 The verdict string provided by a Brightmail content scan
1363
1364 =item N . $body_linecount
1365
1366 The number of lines in the message's body.
1367
1368 =item N . $body_zerocount
1369
1370 The number of binary zero bytes in the message's body.
1371
1372 =item B + $deliver_freeze
1373
1374 TRUE if the message is currently frozen.
1375
1376 =item N + $deliver_frozen_at
1377
1378 The epoch time at which message was frozen.
1379
1380 =item B + $dont_deliver
1381
1382 TRUE if, under normal circumstances, Exim will not try to deliver the message.
1383
1384 =item S + $each_recipients
1385
1386 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.
1387
1388 =item S + $each_recipients_del
1389
1390 Like $each_recipients, but for $recipients_del
1391
1392 =item S + $each_recipients_undel
1393
1394 Like $each_recipients, but for $recipients_undel
1395
1396 =item B . $first_delivery
1397
1398 TRUE if the message has never been deferred.
1399
1400 =item S # $header_*
1401
1402 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.
1403
1404 =item B . $host_lookup_deferred
1405
1406 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.
1407
1408 =item B . $host_lookup_failed
1409
1410 TRUE if there was an attempt to look up the host's name from its IP address, but the attempt returned a negative result.
1411
1412 =item S . $interface_address
1413
1414 The address of the local IP interface for network-originated messages.
1415
1416 =item N . $interface_port
1417
1418 The local port number if network-originated messages.
1419
1420 =item B + $local_error_message
1421
1422 TRUE if the message is a locally-generated error message.
1423
1424 =item S . $local_scan_data
1425
1426 The text returned by the local_scan() function when a message is received.
1427
1428 =item B . $manually_thawed
1429
1430 TRUE when the message has been manually thawed.
1431
1432 =item N . $message_age
1433
1434 The number of seconds since the message was received.
1435
1436 =item S # $message_body
1437
1438 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.
1439
1440 =item N . $message_body_size
1441
1442 The size of the body in bytes.
1443
1444 =item S . $message_exim_id, $message_id
1445
1446 The unique message id that is used by Exim to identify the message.  $message_id is deprecated as of Exim 4.53.
1447
1448 =item S . $message_headers
1449
1450 A concatenation of all the header lines except for lines added by routers or transports.
1451
1452 =item N . $message_linecount
1453
1454 The number of lines in the entire message (body and headers).
1455
1456 =item N . $message_size
1457
1458 The size of the message in bytes.
1459
1460 =item N . $originator_gid
1461
1462 The group id under which the process that called Exim was running as when the message was received.
1463
1464 =item S + $originator_login
1465
1466 The login of the process which called Exim.
1467
1468 =item N . $originator_uid
1469
1470 The user id under which the process that called Exim was running as when the message was received.
1471
1472 =item N . $received_count
1473
1474 The number of Received: header lines in the message.
1475
1476 =item S . $received_protocol
1477
1478 The name of the protocol by which the message was received.
1479
1480 =item N . $received_time
1481
1482 The epoch time at which the message was received.
1483
1484 =item S # $recipients
1485
1486 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.
1487
1488 =item N . $recipients_count
1489
1490 The number of envelope recipients for the message.
1491
1492 =item S + $recipients_del
1493
1494 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.
1495
1496 =item N + $recipients_del_count
1497
1498 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.
1499
1500 =item S + $recipients_undel
1501
1502 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.
1503
1504 =item N + $recipients_undel_count
1505
1506 The number of envelope recipients for the message which have not yet been delivered.
1507
1508 =item S . $reply_address
1509
1510 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.
1511
1512 =item S . $sender_address
1513
1514 The sender's address that was received in the message's envelope.  For bounce messages, the value of this variable is the empty string.
1515
1516 =item S . $sender_address_domain
1517
1518 The domain part of $sender_address.
1519
1520 =item S . $sender_address_local_part
1521
1522 The local part of $sender_address.
1523
1524 =item S . $sender_helo_name
1525
1526 The HELO or EHLO value supplied for smtp or bsmtp messages.
1527
1528 =item S . $sender_host_address
1529
1530 The remote host's IP address.
1531
1532 =item S . $sender_host_authenticated
1533
1534 The name of the authenticator driver which successfully authenticated the client from which the message was received.
1535
1536 =item S . $sender_host_name
1537
1538 The remote host's name as obtained by looking up its IP address.
1539
1540 =item N . $sender_host_port
1541
1542 The port number that was used on the remote host for network-originated messages.
1543
1544 =item S . $sender_ident
1545
1546 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.
1547
1548 =item B + $sender_local
1549
1550 TRUE if the message was locally generated.
1551
1552 =item B + $sender_set_untrusted
1553
1554 TRUE if the envelope sender of this message was set by an untrusted local caller.
1555
1556 =item S + $shown_message_size
1557
1558 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.
1559
1560 =item S . $smtp_active_hostname
1561
1562 The value of the active host name when the message was received, as specified by the "smtp_active_hostname" option.
1563
1564 =item S . $spam_score
1565
1566 The spam score of the message, for example '3.4' or '30.5'.  (Requires exiscan or WITH_CONTENT_SCAN)
1567
1568 =item S . $spam_score_int
1569
1570 The spam score of the message, multiplied by ten, as an integer value.  For instance '34' or '305'.  (Requires exiscan or WITH_CONTENT_SCAN)
1571
1572 =item B . $tls_certificate_verified
1573
1574 TRUE if a TLS certificate was verified when the message was received.
1575
1576 =item S . $tls_cipher
1577
1578 The cipher suite that was negotiated for encrypted SMTP connections.
1579
1580 =item S . $tls_peerdn
1581
1582 The value of the Distinguished Name of the certificate if Exim is configured to request one
1583
1584 =item N + $warning_count
1585
1586 The number of delay warnings which have been sent for this message.
1587
1588 =back
1589
1590 =head1 CONTACT
1591
1592 =over 4
1593
1594 =item EMAIL: proj-exipick@jetmore.net
1595
1596 =item HOME: jetmore.org/john/code/#exipick
1597
1598 =back
1599
1600 =cut