File Coverage

lib/QMail/QueueHandler.pm
Criterion Covered Total %
statement 29 462 6.2
branch 4 142 2.8
condition 0 12 0.0
subroutine 9 56 16.0
pod 23 25 92.0
total 65 697 9.3


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             QMail::QueueHandler - Module to manage QMail message queues
4              
5             =head1 DESCRIPTION
6              
7             This is all the code behind the qmHandle command line program.
8              
9             =head1 SYNOPSIS
10              
11             use QMail::QueueHandler;
12              
13             QMail::QueueHandler->new->run;
14              
15             =cut
16              
17             package QMail::QueueHandler;
18              
19 1     1   73452 use Moose;
  1         482207  
  1         8  
20              
21 1     1   8685 use Term::ANSIColor;
  1         9693  
  1         79  
22 1     1   2019 use Getopt::Std;
  1         49  
  1         58  
23 1     1   8 use File::Basename;
  1         1  
  1         6032  
24              
25             our $VERSION = '2.0.4';
26             my $me = basename $0;
27              
28             # Where qmail stores all of its files
29             has queue => (
30             is => 'ro',
31             isa => 'Str',
32             default => '/var/qmail/queue/',
33             );
34              
35             # Which todo format do we have?
36             has bigtodo => (
37             is => 'ro',
38             isa => 'Bool',
39             lazy => 1,
40             default => sub { -d $_[0]->queue . 'todo/0' },
41             );
42              
43             # Various commands that we use
44             has commands => (
45             is => 'ro',
46             isa => 'HashRef',
47             default => sub {
48             {
49             start => '/sbin/service qmail start',
50             stop => '/sbin/service qmail stop',
51             pid => '/sbin/pidof qmail-send',
52             };
53             },
54             );
55              
56             # Colours for output.
57             # Default is non-coloured. These values can be changed in parse_args.
58             has colours => (
59             is => 'ro',
60             isa => 'HashRef',
61             default => sub {
62             {
63             msg => '',
64             stat => '',
65             end => '',
66             };
67             },
68             );
69              
70             # Are we showing a summary?
71             has summary => (
72             is => 'ro',
73             isa => 'Bool',
74             default => 0,
75             );
76              
77             # Are we supposed to be deleting things?
78             has deletions => (
79             is => 'rw',
80             isa => 'Bool',
81             );
82              
83             # What actions are we carrying out.
84             # Each element in this array is another array.
85             # The first element in these second level arrays is a code ref.
86             # The other elements are arguments to be passed to the code ref.
87             has actions => (
88             is => 'ro',
89             traits => ['Array'],
90             isa => 'ArrayRef',
91             default => sub { [] },
92             handles => {
93             add_action => 'push',
94             all_actions => 'elements',
95             },
96             );
97              
98             # Do we need to restart QMail once we have finished?
99             has restart => (
100             is => 'rw',
101             isa => 'Bool',
102             default => 0,
103             );
104              
105             # List of messages to delete
106             has to_delete => (
107             is => 'rw',
108             traits => ['Array'],
109             isa => 'ArrayRef',
110             default => sub { [] },
111             handles => {
112             add_to_delete => 'push',
113             all_to_delete => 'elements',
114             to_delete_count => 'count',
115             },
116             );
117              
118             before add_to_delete => sub {
119             my $self = shift;
120             my ($msg_id) = @_;
121              
122             warn "Message [$msg_id] queued for deletion.\n";
123             };
124              
125             # List of messages to flag
126             has to_flag => (
127             is => 'rw',
128             traits => ['Array'],
129             isa => 'ArrayRef',
130             default => sub { [] },
131             handles => {
132             add_to_flag => 'push',
133             all_to_flag => 'elements',
134             },
135             );
136              
137             # Hash containing details of the messages in the queue
138             has msglist => (
139             is => 'rw',
140             isa => 'HashRef',
141             lazy_build => 1,
142             );
143              
144             sub BUILD {
145 1     1 0 2 my $self = shift;
146              
147             # Get command line options
148 1         5 $self->parse_args;
149             }
150              
151             =head1 METHODS
152              
153             =head2 run()
154              
155             Main driver method.
156              
157             =cut
158              
159             sub run {
160 0     0 1 0 my $self = shift;
161 0         0 my @args = @_;
162              
163             # (Possibly) stop qmail
164 0         0 $self->stop_qmail;
165              
166             # Execute actions
167 0         0 foreach my $action ( $self->all_actions ) {
168 0         0 my $sub = shift @$action; # First element is the sub
169 0         0 $self->$sub(@$action); # Others the arguments, if any
170             }
171              
172             # If we have planned deletions, then do them.
173 0 0       0 if ( $self->to_delete_count ) {
174 0         0 $self->trash_msgs;
175             }
176              
177             # If we stopped qmail, then restart it
178 0         0 $self->start_qmail;
179             }
180              
181             sub _get_todo {
182 0     0   0 my $self = shift;
183 0         0 my ($todohash, $msglist) = @_;
184              
185 0         0 my $queue = $self->queue;
186              
187 0         0 opendir( my $tododir, "${queue}todo" );
188              
189 0 0       0 if ( $self->bigtodo ) {
190 0         0 foreach my $todofile ( grep { !/\./ } readdir $tododir ) {
  0         0  
191 0         0 $todohash->{$todofile} = $todofile;
192             }
193             }
194             else {
195 0         0 foreach my $tododir ( grep { !/\./ } readdir $tododir ) {
  0         0  
196 0         0 opendir( my $subdir, "${queue}todo/$tododir" );
197 0         0 foreach my $todofile (
198 0         0 grep { !/\./ }
199 0         0 map { "$tododir/$_" } readdir $subdir
200             ) {
201 0         0 $msglist->{$todofile}{'todo'} = $todofile;
202             }
203             }
204             }
205 0         0 closedir $tododir;
206             }
207              
208             sub _get_info {
209 0     0   0 my $self = shift;
210 0         0 my ($dir, $msglist) = @_;
211              
212 0         0 my $queue = $self->queue;
213              
214 0         0 opendir( my $infosubdir, "${queue}info/$dir" );
215              
216 0         0 foreach my $infofile (
217 0         0 grep { !/\./ }
218 0         0 map { "$dir/$_" } readdir $infosubdir
219             ) {
220 0         0 $msglist->{$infofile}{sender} = 'S';
221             }
222              
223 0         0 close $infosubdir;
224             }
225              
226             sub _get_local {
227 0     0   0 my $self = shift;
228 0         0 my ($dir, $msglist) = @_;
229              
230 0         0 my $queue = $self->queue;
231              
232 0         0 opendir( my $localsubdir, "${queue}local/$dir" );
233              
234 0         0 foreach my $localfile (
235 0         0 grep { !/\./ }
236 0         0 map { "$dir/$_" } readdir $localsubdir
237             ) {
238 0         0 $msglist->{$localfile}{local} = 'L';
239             }
240              
241 0         0 close $localsubdir;
242             }
243              
244             sub _get_remote {
245 0     0   0 my $self = shift;
246 0         0 my ($dir, $msglist) = @_;
247              
248 0         0 my $queue = $self->queue;
249              
250 0         0 opendir( my $remotesubdir, "${queue}remote/$dir" );
251              
252 0         0 foreach my $remotefile (
253 0         0 grep { !/\./ }
254 0         0 map { "$dir/$_" } readdir $remotesubdir
255             ) {
256 0         0 $msglist->{$remotefile}{remote} = 'R';
257             }
258              
259 0         0 close $remotesubdir;
260             }
261              
262             sub _get_subdir {
263 0     0   0 my $self = shift;
264 0         0 my ($dir, $msglist, $todohash, $bouncehash) = @_;
265              
266 0         0 my $queue = $self->queue;
267              
268 0         0 opendir( my $subdir, "${queue}mess/$dir" );
269              
270 0         0 foreach my $file (
271 0         0 grep { !/\./ }
272 0         0 map { "$dir/$_" } readdir $subdir
273             ) {
274 0         0 my $msgno = ( split( /\//, $file ) )[1];
275 0 0       0 if ( $bouncehash->{$msgno} ) {
276 0         0 $msglist->{$file}{bounce} = 'B';
277             }
278 0 0       0 if ( $self->bigtodo ) {
279 0 0       0 if ( $todohash->{$msgno} ) {
280 0         0 $msglist->{$file}{todo} = $msgno;
281             }
282             }
283             }
284              
285 0         0 closedir $subdir;
286             }
287              
288             sub _build_msglist {
289 0     0   0 my $self = shift;
290              
291 0         0 my $queue = $self->queue;
292              
293 0         0 my ( $todohash, $bouncehash );
294 0         0 my $msglist = {};
295              
296 0         0 $self->_get_todo($todohash, $msglist);
297              
298 0         0 opendir( my $bouncedir, "${queue}bounce" );
299 0         0 foreach my $bouncefile ( grep { !/\./ } readdir $bouncedir ) {
  0         0  
300 0         0 $bouncehash->{$bouncefile} = 'B';
301             }
302 0         0 closedir $bouncedir;
303              
304 0         0 opendir( my $messdir, "${queue}mess" );
305              
306 0         0 foreach my $dir ( grep { !/\./ } readdir $messdir ) {
  0         0  
307 0         0 $self->_get_info($dir);
308 0         0 $self->_get_local($dir);
309 0         0 $self->_get_remote($dir);
310 0         0 $self->_get_subdir($dir);
311             }
312 0         0 closedir $messdir;
313              
314 0         0 return $msglist;
315             }
316              
317             =head2 parse_args()
318              
319             Parse the command line arguments and set any required attributes.
320              
321             =cut
322              
323             sub parse_args {
324 1     1 1 2 my $self = shift;
325              
326 1 50       7 @ARGV or $self->usage;
327              
328 1         2 my %opt;
329              
330             my %option = (
331             # (Attempt to) send all queued messages
332             a => {
333             arg => 0,
334             code => sub {
335 0     0   0 $self->add_action( [ \&send_msgs ] );
336             },
337             },
338             # List message queues
339             l => {
340             arg => 0,
341             code => sub {
342 0     0   0 $self->add_action( [ \&list_msg, 'A' ] );
343             },
344             },
345             # List local message queue
346             L => {
347             arg => 0,
348             code => sub {
349 0     0   0 $self->add_action( [ \&list_msg, 'L' ] );
350             },
351             },
352             # List remote message queue
353             R => {
354             arg => 0,
355             code => sub {
356 0     0   0 $self->add_action( [ \&list_msg, 'R' ] );
357             },
358             },
359             # List message numbers only
360             N => {
361             arg => 0,
362             code => sub {
363 0     0   0 $self->summary(1);
364             },
365             },
366             # Coloured output
367             c => {
368             arg => 0,
369             code => sub {
370 0     0   0 @{ $self->colours }{qw[msg stat end]} = (
  0         0  
371             color('bold bright_blue'),
372             color('bold bright_red'),
373             color('reset'),
374             );
375             },
376             },
377             # Show statistics of queues
378             s => {
379             arg => 0,
380             code => sub {
381 0     0   0 $self->add_action( [ \&stats ] );
382             },
383             },
384             # Display message with given number
385             m => {
386             arg => 1,
387             code => sub {
388 0     0   0 $self->add_action( [ \&view_msg, @_ ] );
389             },
390             },
391             # Delete messages from given sender
392             f => {
393             arg => 1,
394             code => sub {
395 0     0   0 $self->add_action( [ \&del_msg_from_sender, @_ ] );
396 0         0 $self->deletions(1);
397             },
398             },
399             # Delete messages from given sender (regex match)
400             F => {
401             arg => 1,
402             code => sub {
403 0     0   0 $self->add_action( [ \&del_msg_from_sender_r, @_ ] );
404 0         0 $self->deletions(1);
405             },
406             },
407             # Delete message with given number
408             d => {
409             arg => 1,
410             code => sub {
411 0     0   0 $self->add_action( [ \&del_msg, @_ ] );
412 0         0 $self->deletions(1);
413             },
414             },
415             # Delete messages with matching subject
416             S => {
417             arg => 1,
418             code => sub {
419 0     0   0 $self->add_action( [ \&del_msg_subj, @_ ] );
420 0         0 $self->deletions(1);
421             },
422             },
423             # Delete messages with matching header (case insensitive)
424             h => {
425             arg => 1,
426             code => sub {
427 0     0   0 $self->add_action( [ \&del_msg_header_r, @_, 1 ] );
428 0         0 $self->deletions(1);
429             },
430             },
431             # Delete messages with matching body (case insensitive)
432             b => {
433             arg => 1,
434             code => sub {
435 0     0   0 $self->add_action( [ \&del_msg_body_r, @_, 1 ] );
436 0         0 $self->deletions(1);
437             },
438             },
439             # Delete messages with matching header (case sensitive)
440             H => {
441             arg => 1,
442             code => sub {
443 0     0   0 $self->add_action( [ \&del_msg_header_r, @_, 0 ] );
444 0         0 $self->deletions(1);
445             },
446             },
447             # Delete messages with matching body (case sensitive)
448             B => {
449             arg => 1,
450             code => sub {
451 0     0   0 $self->add_action( [ \&del_msg_body_r, @_, 0 ] );
452 0         0 $self->deletions(1);
453             },
454             },
455             # Flag messages with matching recipients
456             t => {
457             arg => 1,
458             code => sub {
459 0     0   0 $self->add_actions( [ \&flag_remote, @_ ] );
460             },
461             },
462             # Delete all messages in queues
463             D => {
464             arg => 0,
465             code => sub {
466 0     0   0 $self->add_action( [ \&del_all ] );
467 0         0 $self->deletions(1);
468             },
469             },
470             # Display program version
471             V => {
472             arg => 0,
473             code => sub {
474 0     0   0 $self->add_action( [ \&version ] );
475             },
476             },
477             # Display help
478             '?' => {
479             arg => 0,
480             code => sub {
481 0     0   0 $self->usage;
482             },
483             },
484 1         87 );
485              
486 1 100       8 my $optstring = join '', map { $_ . ( $option{$_}{arg} ? ':' : '' ) }
  20         39  
487             keys %option;
488              
489 1         9 getopts( $optstring, \%opt );
490              
491 1         37 foreach my $opt ( keys %opt ) {
492 0 0       0 if (! exists $option{$opt}) {
493 0         0 warn "$opt is not a valid option\n";
494 0         0 next;
495             }
496 0 0 0     0 if ( $option{$opt}{arg} and not $opt{$opt} ) {
497 0         0 die "Option $opt must have an argument\n";
498             }
499              
500 0 0       0 if ($option{$opt}{arg}) {
501 0         0 $option{$opt}{code}->($opt{$opt});
502             } else {
503 0         0 $option{$opt}{code}->();
504             }
505             }
506              
507 1         78 return;
508             }
509              
510             =head2 stop_qmail()
511              
512             Optionally stop the qmail daemon.
513              
514             =cut
515              
516             sub stop_qmail {
517 1     1 1 3529 my $self = shift;
518              
519             # Don't need to stop qmail if we're not planning to delete stuff
520 1 50       34 return unless $self->deletions;
521              
522             # If qmail is running, we stop it
523 0 0       0 if ( my $qmpid = $self->qmail_pid ) {
524              
525             # If there is a system script available, we use it
526 0 0       0 if ( $self->commands->{stop} ne '' ) {
527              
528 0         0 warn "Calling system script to terminate qmail...\n";
529 0 0       0 if ( system( $self->commands->{stop} ) > 0 ) {
530 0         0 die 'Could not stop qmail';
531             }
532 0         0 sleep 1 while $self->qmail_pid;
533              
534             # Otherwise, we're killers!
535             }
536             else {
537 0         0 warn "Terminating qmail (pid $qmpid)... ",
538             "this might take a while if qmail is working.\n";
539 0         0 kill 'TERM', $qmpid;
540              
541 0         0 sleep 1 while $self->qmail_pid;
542             }
543              
544             # If it isn't, we don't. We also return a false value so our caller
545             # knows they might not want to restart it later.
546             }
547             else {
548 0         0 warn "Qmail isn't running... no need to stop it.\n";
549 0         0 return;
550             }
551              
552 0         0 $self->restart(1);
553              
554 0         0 return 1;
555             }
556              
557              
558             =head2 start_qmail()
559              
560             Restart the qmail daemon if it was previously stopped.
561              
562             =cut
563              
564             sub start_qmail {
565 0     0 1 0 my $self = shift;
566              
567 0 0       0 return unless $self->restart;
568              
569             # If qmail is running, why restart it?
570 0 0       0 if ( my $qmpid = $self->qmail_pid ) {
571 0         0 warn "Qmail is already running again, so it won't be restarted.\n";
572 0         0 return 1;
573             }
574              
575             # In any other case, we restart it
576 0         0 warn "Restarting qmail... \n";
577 0         0 system( $self->commands->{start} );
578 0         0 warn "Done (hopefully).\n";
579              
580 0         0 return 1;
581             }
582              
583             =head2 get_subject($msg_id)
584              
585             Given the id of a message, return the subject of that message.
586              
587             =cut
588              
589             sub get_subject {
590 0     0 1 0 my $self = shift;
591 0         0 my ($msg_id) = @_;
592              
593 0         0 my $msgsub;
594 0         0 my $queue = $self->queue;
595 0 0       0 open( my $msg_fh, '<', "${queue}mess/$msg_id" )
596             or die("cannot open message $msg_id! Is qmail-send running?\n");
597 0         0 while (<$msg_fh>) {
598 0         0 chomp;
599 0 0       0 last if !/\S/; # End of headers
600 0 0       0 if (/^Subject: (.*)/) {
601 0         0 $msgsub = $1;
602 0         0 last;
603             }
604             }
605 0         0 close($msg_fh);
606 0         0 return $msgsub;
607             }
608              
609             =head2 get_sender($msg_id)
610              
611             Given the id of a message, return the sender of the message.
612              
613             =cut
614              
615             sub get_sender {
616 0     0 1 0 my $self = shift;
617 0         0 my ($msg_id) = @_;
618              
619 0         0 my $queue = $self->queue;
620              
621 0 0       0 open( my $msg_fh, '<', "${queue}/info/$msg_id" )
622             or die( "cannot open info file ${queue}/info/$msg_id! ",
623             "Is qmail-send running?\n" );
624 0         0 my $sender = <$msg_fh>;
625 0         0 substr( $sender, 0, 1 ) = '';
626 0         0 chomp $sender;
627 0         0 close($msg_fh);
628 0         0 return $sender;
629             }
630              
631             =head2 send_msgs()
632              
633             Attempt to send all currently queued messages.
634              
635             It does this by sending SIGALRM to the qmail daemon.
636              
637             =cut
638              
639             sub send_msgs {
640 0     0 1 0 my $self = shift;
641              
642             # If qmail is running, we force sending of messages
643 0 0       0 if ( my $qmpid = $self->qmail_pid ) {
644              
645 0         0 kill 'ALRM', $qmpid;
646              
647             }
648             else {
649              
650 0         0 warn "Qmail isn't running, can't send messages!\n";
651              
652             }
653 0         0 return;
654             }
655              
656             =head2 show_msg_info($msg_id)
657              
658             Given a message id, display the information about that message.
659              
660             =cut
661              
662             sub show_msg_info {
663 0     0 1 0 my $self = shift;
664 0         0 my ($msg_id) = @_;
665              
666 0         0 my %msg;
667 0         0 my $queue = $self->queue;
668              
669 0         0 open( my $info_fh, '<', "${queue}info/$msg_id" );
670 0         0 $msg{ret} = <$info_fh>;
671 0         0 substr( $msg{ret}, 0, 1 ) = '';
672 0         0 chomp $msg{ret};
673 0         0 close($info_fh);
674 0         0 my ( $dirno, $rmsg ) = split( /\//, $msg_id );
675 0         0 print "$rmsg ($dirno, $msg_id)\n";
676              
677             # Get message (file) size
678 0         0 $msg{fsize} = ( stat("${queue}mess/$msg_id") )[7];
679              
680 0         0 my %header = (
681             Date => 'date',
682             From => 'from',
683             Subject => 'subject',
684             To => 'to',
685             Cc => 'cc',
686             );
687              
688             # Read something from message header (sender, receiver, subject, date)
689 0         0 open( my $msg_fh, '<', "${queue}mess/$msg_id" );
690 0         0 while (<$msg_fh>) {
691 0         0 chomp;
692             # Stop processing at the end of the headers
693 0 0       0 last unless /\S/;
694 0         0 foreach my $h ( keys %header ) {
695 0 0       0 if (/^$h: (.*)/) {
696 0         0 $msg{ $header{$h} } = $1;
697 0         0 last;
698             }
699             }
700             }
701 0         0 close($msg_fh);
702              
703             # Add "pseudo-headers" for output
704 0         0 $header{'Return-path'} = 'ret';
705 0         0 $header{Size} = 'fsize';
706              
707 0         0 my $colours = $self->colours;
708 0         0 my ( $cmsg, $cend ) = @{$colours}{qw[msg end]};
  0         0  
709              
710 0         0 for (qw[Return-path From To Cc Subject Date Size]) {
711 0 0       0 next unless exists $msg{ $header{$_} };
712              
713 0         0 print " ${cmsg}$_${cend}: $msg{$header{$_}}\n";
714             }
715              
716 0         0 return;
717             }
718              
719             =head2 list_msg($queue)
720              
721             Display information for all messages in a given queue.
722              
723             The $queue parameter should be 'L' to display only local messages, 'R'
724             to display only remote messages or anything else to display all messages.
725              
726             =cut
727              
728             sub list_msg {
729 0     0 1 0 my $self = shift;
730 0         0 my ($q) = @_;
731              
732 0         0 $q = uc $q;
733              
734 0         0 my $local = $q ne 'R';
735 0         0 my $remote = $q ne 'L';
736              
737 0         0 my $msglist = $self->msglist;
738 0 0       0 if ( !$self->summary ) {
739 0         0 for my $msg ( keys %$msglist ) {
740 0 0 0     0 next if $local and not $msglist->{$msg}{local};
741 0 0 0     0 next if $remote and not $msglist->{$msg}{remote};
742              
743 0         0 $self->show_msg_info($msg);
744             }
745             }
746              
747 0         0 $self->stats;
748 0         0 return;
749             }
750              
751             =head2 view_msg($msg_id)
752              
753             View a message in the queue
754              
755             =cut
756              
757             sub view_msg {
758 0     0 1 0 my $self = shift;
759 0         0 my ($msg_id) = @_;
760              
761 0 0       0 if ( $msg_id =~ /\D/ ) {
762 0         0 warn "$msg_id is not a valid message number!\n";
763 0         0 return;
764             }
765              
766             # Search message
767 0         0 my $ok = 0;
768 0         0 my $queue = $self->queue;
769 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
770 0 0       0 if ( $msg =~ /\/$msg_id$/ ) {
771 0         0 $ok = 1;
772 0         0 print "\n --------------\nMESSAGE NUMBER $msg_id \n --------------\n";
773 0         0 open( my $msg_fh, '<', "${queue}mess/$msg" );
774 0         0 print while <$msg_fh>;
775 0         0 close($msg_fh);
776 0         0 last;
777             }
778             }
779              
780             # If the message isn't found, print a notice
781 0 0       0 if ( !$ok ) {
782 0         0 warn "Message $msg_id not found in the queue!\n";
783             }
784              
785 0         0 return;
786             }
787              
788             =head2 trash_msgs()
789              
790             Delete all of the messages whose ids are in the C<all_to_delete>
791             array.
792              
793             =cut
794              
795             sub trash_msgs {
796 0     0 1 0 my $self = shift;
797              
798 0         0 my $queue = $self->queue;
799 0         0 my $msglist = $self->msglist;
800 0         0 my @todelete = ();
801 0         0 my $grouped = 0;
802 0         0 my $deleted = 0;
803 0         0 foreach my $msg ( $self->all_to_delete ) {
804 0         0 $grouped++;
805 0         0 $deleted++;
806 0         0 my $msgno = ( split( /\//, $msg ) )[1];
807 0 0       0 if ( $msglist->{$msg}{bounce} ) {
808 0         0 push @todelete, "${queue}bounce/$msgno";
809             }
810 0         0 push @todelete, "${queue}mess/$msg", "${queue}info/$msg";
811 0 0       0 if ( $msglist->{$msg}{remote} ) {
812 0         0 push @todelete, "${queue}remote/$msg";
813             }
814 0 0       0 if ( $msglist->{$msg}{local} ) {
815 0         0 push @todelete, "${queue}local/$msg";
816             }
817 0 0       0 if ( $msglist->{$msg}{todo} ) {
818 0         0 push @todelete, "${queue}todo/$msglist->{$msg}{'todo'}",
819             "${queue}intd/$msglist->{$msg}{'todo'}";
820             }
821 0 0       0 if ( $grouped == 11 ) {
822 0         0 unlink @todelete;
823 0         0 @todelete = ();
824 0         0 $grouped = 0;
825             }
826             }
827 0 0       0 if ($grouped) {
828 0         0 unlink @todelete;
829             }
830 0 0       0 my $msg_str = $deleted == 1 ? 'message' : 'messages';
831 0         0 warn "Deleted $deleted $msg_str from queue\n";
832 0         0 return;
833             }
834              
835             =head2 flag_msgs()
836              
837             Flag all messages whose ids are in the C<all_to_flag> array.
838              
839             =cut
840              
841             sub flag_msgs {
842 0     0 1 0 my $self = shift;
843              
844 0         0 my $queue = $self->queue;
845 0         0 my $now = time;
846 0         0 my @flagqueue = ();
847 0         0 my $flagged = 0;
848 0         0 foreach my $msg ( $self->all_to_flag ) {
849 0         0 push @flagqueue, "${queue}info/$msg";
850 0         0 $flagged++;
851 0 0       0 if ( $flagged == 30 ) {
852 0         0 utime $now, $now, @flagqueue;
853 0         0 $flagged = 0;
854 0         0 @flagqueue = ();
855             }
856             }
857 0 0       0 if ($flagged) {
858 0         0 utime $now, $now, @flagqueue;
859             }
860 0         0 return;
861             }
862              
863             =head2 del_msg($msg_id)
864              
865             Given a message id, add that message to the list of messages to delete.
866              
867             The actual deletion is carried out by C<trash_msgs>.
868              
869             =cut
870              
871             sub del_msg {
872 0     0 1 0 my $self = shift;
873 0         0 my ($msg_id) = @_;
874              
875 0 0       0 if ( $msg_id =~ /\D/ ) {
876 0         0 warn "$msg_id is not a valid message number!\n";
877 0         0 return;
878             }
879              
880             # Search message
881 0         0 my $ok = 0;
882 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
883 0 0       0 if ( $msg =~ /\/$msg_id$/ ) {
884 0         0 $ok = 1;
885 0         0 $self->add_to_delete($msg);
886 0         0 last;
887             }
888             }
889              
890             # If the message isn't found, print a notice
891 0 0       0 if ( !$ok ) {
892 0         0 warn "Message $msg_id not found in the queue!\n";
893             }
894              
895 0         0 return;
896             }
897              
898             =head2 del_msg_from_sender($sender)
899              
900             Given a sender's email address, add all messages from that sender to the
901             list of messages to delete.
902              
903             The actual deletion is carried out by C<trash_msgs>.
904              
905             =cut
906              
907             sub del_msg_from_sender {
908 0     0 1 0 my $self = shift;
909 0         0 my ($sender) = @_;
910              
911 0         0 warn "Looking for messages from $sender\n";
912              
913 0         0 my $ok = 0;
914 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
915 0 0       0 if ( $self->msglist->{$msg}{sender} ) {
916 0         0 my $msg_sender = $self->get_sender($msg);
917 0 0       0 if ( $msg_sender eq $sender ) {
918 0         0 $ok = 1;
919 0         0 $self->add_to_delete($msg);
920             }
921             }
922             }
923              
924             # If no messages are found, print a notice
925 0 0       0 if ( !$ok ) {
926 0         0 warn "No messages from $sender found in the queue!\n";
927             }
928              
929 0         0 return;
930             }
931              
932             =head2 del_msg_from_sender_r($sender)
933              
934             Given a sender's email address, add all messages from that sender to the
935             list of messages to delete.
936              
937             This method treats $sender as a regex.
938              
939             The actual deletion is carried out by C<trash_msgs>.
940              
941             =cut
942              
943             sub del_msg_from_sender_r {
944 0     0 1 0 my $self = shift;
945 0         0 my ($sender_re) = @_;
946              
947 0         0 warn "Looking for messages from senders matching $sender_re\n";
948              
949 0         0 my $ok = 0;
950 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
951 0 0       0 if ( $self->msglist->{$msg}{sender} ) {
952 0         0 my $msg_sender = $self->get_sender($msg);
953 0 0       0 if ( $msg_sender =~ /$sender_re/ ) {
954 0         0 $ok = 1;
955 0         0 $self->add_to_delete($msg);
956             }
957             }
958             }
959              
960             # If no messages are found, print a notice
961 0 0       0 if ( !$ok ) {
962 0         0 warn "No messages from senders matching ",
963             "$sender_re found in the queue!\n";
964             }
965              
966 0         0 return;
967             }
968              
969             =head2 del_msg_header($header_re, $is_case_sensitive)
970              
971             Given a regex, add all messages with headers that match the regex to the
972             list of messages to delete.
973              
974             The actual deletion is carried out by C<trash_msgs>.
975              
976             =cut
977              
978             sub del_msg_header_r {
979 0     0 0 0 my $self = shift;
980 0         0 my ( $header_re, $is_case_sensitive ) = @_;
981              
982 0         0 warn "Looking for messages with headers matching $header_re\n";
983              
984 0 0       0 $header_re = "(?i)$header_re" if $is_case_sensitive;
985              
986 0         0 my $queue = $self->queue;
987 0         0 my $ok = 0;
988 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
989 0 0       0 open( my $msg_fh, '<', "${queue}mess/$msg" )
990             or die("cannot open message $msg! Is qmail-send running?\n");
991 0         0 while (<$msg_fh>) {
992 0         0 chomp;
993 0 0       0 last if ! /\S/; # End of headers
994 0 0       0 if (/$header_re/) {
995 0         0 $ok = 1;
996 0         0 $self->add_to_delete($msg);
997 0         0 last;
998             }
999             }
1000 0         0 close($msg_fh);
1001              
1002             }
1003              
1004             # If no messages are found, print a notice
1005 0 0       0 if ( !$ok ) {
1006 0         0 warn "No messages with headers matching $header_re ",
1007             "found in the queue!\n";
1008             }
1009              
1010 0         0 return;
1011             }
1012              
1013             =head2 del_msg_body_r($body_re, $is_case_sensitive)
1014              
1015             Given a regex, add all messages with a body that matches the regex to the
1016             list of messages to delete.
1017              
1018             The actual deletion is carried out by C<trash_msgs>.
1019              
1020             =cut
1021              
1022             sub del_msg_body_r {
1023 0     0 1 0 my $self = shift;
1024 0         0 my ( $body_re, $is_case_sensitive ) = @_;
1025              
1026 0         0 my $queue = $self->queue;
1027              
1028 0         0 warn "Looking for messages with body matching $body_re\n";
1029              
1030 0 0       0 $body_re = "(?i)$body_re" if $is_case_sensitive;
1031              
1032 0         0 my $ok = 0;
1033 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
1034 0 0       0 open( my $msg_fh, '<', "${queue}mess/$msg" )
1035             or die("cannot open message $msg! Is qmail-send running?\n");
1036             # Skip headers
1037 0         0 while (<$msg_fh>) {
1038 0         0 chomp;
1039 0 0       0 last if !/\S/;
1040             }
1041 0         0 while (<$msg_fh>) {
1042 0 0       0 if (/$body_re/) {
1043 0         0 $ok = 1;
1044 0         0 $self->add_to_delete($msg);
1045 0         0 last;
1046             }
1047             }
1048 0         0 close($msg_fh);
1049             }
1050              
1051             # If no messages are found, print a notice
1052 0 0       0 if ( !$ok ) {
1053 0         0 warn "No messages with body matching $body_re found in the queue!\n";
1054             }
1055              
1056 0         0 return;
1057             }
1058              
1059             =head2 del_msg_subj($subject, $is_case_sensitive)
1060              
1061             Given a subject, add all messages with that subject to the list of messages
1062             to delete.
1063              
1064             The actual deletion is carried out by C<trash_msgs>.
1065              
1066             =cut
1067              
1068             sub del_msg_subj {
1069 0     0 1 0 my $self = shift;
1070 0         0 my ($subject) = @_;
1071              
1072 0         0 warn "Looking for messages with Subject: $subject\n";
1073              
1074             # Search messages
1075 0         0 my $ok = 0;
1076 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
1077 0         0 my $msgsub = $self->get_subject($msg);
1078              
1079 0 0 0     0 if ( $msgsub and $msgsub =~ /$subject/ ) {
1080 0         0 $ok = 1;
1081 0         0 $self->add_to_delete($msg);
1082             }
1083              
1084             }
1085              
1086             # If no messages are found, print a notice
1087 0 0       0 if ( !$ok ) {
1088 0         0 warn "No messages matching Subject \"$subject\" found in the queue!\n";
1089             }
1090              
1091 0         0 return;
1092             }
1093              
1094             =head2 del_all()
1095              
1096             Delete all messages in the queue.
1097              
1098             The actual deletion is carried out by C<trash_msgs>.
1099              
1100             =cut
1101              
1102             sub del_all {
1103 0     0 1 0 my $self = shift;
1104              
1105             # Search messages
1106 0         0 my $ok = 0;
1107 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
1108 0         0 $ok = 1;
1109 0         0 $self->add_to_delete($msg);
1110             }
1111              
1112             # If no messages are found, print a notice
1113 0 0       0 if ( !$ok ) {
1114 0         0 warn "No messages found in the queue!\n";
1115             }
1116              
1117 0         0 return;
1118             }
1119              
1120             =head2 flag_remote($recipient_re)
1121              
1122             Flag all remote messages whose recipient matches the given regex.
1123              
1124             =cut
1125              
1126             sub flag_remote {
1127 0     0 1 0 my $self = shift;
1128 0         0 my ($recipient_re) = @_;
1129              
1130 0         0 my $queue = $self->queue;
1131              
1132 0         0 warn "Looking for messages with recipients in $recipient_re\n";
1133              
1134 0         0 my $ok = 0;
1135 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
1136 0 0       0 if ( $self->msglist->{$msg}{remote} ) {
1137 0 0       0 open( my $msg_fh, '<', "${queue}remote/$msg" )
1138             or die( "cannot open remote file for message $msg! ",
1139             "Is qmail-send running?\n" );
1140 0         0 my $recipients = <$msg_fh>;
1141 0         0 chomp($recipients);
1142 0         0 close($msg_fh);
1143 0 0       0 if ( $recipients =~ /$recipient_re/ ) {
1144 0         0 $ok = 1;
1145 0         0 $self->add_to_flag($msg);
1146 0         0 warn "Message $msg being tagged for earlier retry ",
1147             "(and lengthened stay in queue)!\n";
1148             }
1149             }
1150             }
1151              
1152             # If no messages are found, print a notice
1153 0 0       0 if ( !$ok ) {
1154 0         0 warn "No messages with recipients in $recipient_re ",
1155             "found in the queue!\n";
1156 0         0 return;
1157             }
1158              
1159 0         0 $self->flag_msgs;
1160              
1161 0         0 return;
1162             }
1163              
1164             =head2 stats()
1165              
1166             Display statistics about the queue.
1167              
1168             =cut
1169              
1170             sub stats {
1171 0     0 1 0 my $self = shift;
1172              
1173 0         0 my $total = 0;
1174 0         0 my $l = 0;
1175 0         0 my $r = 0;
1176 0         0 my $b = 0;
1177 0         0 my $t = 0;
1178              
1179 0         0 foreach my $msg ( keys %{ $self->msglist } ) {
  0         0  
1180 0         0 $total++;
1181 0 0       0 $self->msglist->{$msg}{local} && $l++;
1182 0 0       0 $self->msglist->{$msg}{remote} && $r++;
1183 0 0       0 $self->msglist->{$msg}{bounce} && $b++;
1184 0 0       0 $self->msglist->{$msg}{todo} && $t++;
1185             }
1186              
1187 0         0 my $colours = $self->colours;
1188 0         0 my ( $cstat, $cend ) = @{$colours}{qw[stat end]};
  0         0  
1189              
1190 0         0 print <<"END_OF_STATS";
1191             ${cstat}Total messages${cend}: $total
1192             ${cstat}Messages with local recipients${cend}: $l
1193             ${cstat}Messages with remote recipients${cend}: $r
1194             ${cstat}Messages with bounces${cend}: $b
1195             ${cstat}Messages in preprocess${cend}: $t
1196             END_OF_STATS
1197 0         0 return;
1198             }
1199              
1200             =head2 qmail_pid()
1201              
1202             Get the pid of the qmail daemon
1203              
1204             =cut
1205              
1206             sub qmail_pid {
1207 0     0 1 0 my $self = shift;
1208 0         0 my $pidcmd = $self->commands->{pid};
1209 0         0 my $qmpid = `$pidcmd`;
1210 0 0       0 return 0 unless $qmpid;
1211 0         0 chomp($qmpid);
1212 0         0 $qmpid =~ s/\s+//g;
1213 0 0       0 return 0 if $qmpid =~ /\D/;
1214 0         0 return $qmpid;
1215             }
1216              
1217             =head2 usage()
1218              
1219             Display usage information.
1220              
1221             =cut
1222              
1223             sub usage {
1224 1     1 1 16 print <<"END_OF_HELP";
1225             $me v$VERSION
1226             Copyright (c) 2016 Dave Cross <dave\@perlhacks.com>
1227             Based on original version by Michele Beltrame <mb\@italpro.net>
1228              
1229             Available parameters:
1230             -a : try to send queued messages now (qmail must be running)
1231             -l : list message queues
1232             -L : list local message queue
1233             -R : list remote message queue
1234             -s : show some statistics
1235             -mN : display message number N
1236             -dN : delete message number N
1237             -fsender : delete message from sender
1238             -F're' : delete message from senders matching regular expression re
1239             -Stext : delete all messages that have/contain text as Subject
1240             -h're' : delete all messages with headers matching regular expression
1241             re (case insensitive)
1242             -b're' : delete all messages with body matching regular expression
1243             re (case insensitive)
1244             -H're' : delete all messages with headers matching regular expression
1245             re (case sensitive)
1246             -B're' : delete all messages with body matching regular expression
1247             re (case sensitive)
1248             -t're' : flag messages with recipients in regular expression 're' for
1249             earlier retry (note: this lengthens the time message can
1250             stay in queue)
1251             -D : delete all messages in the queue (local and remote)
1252             -V : print program version
1253             -? : Display this help
1254              
1255             Additional (optional) parameters:
1256             -c : display colored output
1257             -N : list message numbers only
1258             (to be used either with -l, -L or -R)
1259              
1260             You can view/delete multiple message i.e. -d123 -m456 -d567
1261              
1262             END_OF_HELP
1263             }
1264              
1265 1     1   9 no Moose;
  1         2  
  1         7  
1266             __PACKAGE__->meta->make_immutable;
1267              
1268             =head2 version()
1269              
1270             Display the version.
1271              
1272             =cut
1273              
1274             sub version {
1275 0     0 1   print "$me v$VERSION\n";
1276 0           return;
1277             }
1278              
1279             =head2 AUTHOR
1280              
1281             Copyright (c) 2016 Dave Cross E<lt>dave@perlhacks.comE<gt>
1282              
1283             Based on original version by Michele Beltrame E<lt>mb@italpro.netE<gt>
1284              
1285             =head2 LICENCE
1286              
1287             This program is distributed under the GNU GPL.
1288             For more information have a look at http://www.gnu.org
1289              
1290             =cut
1291              
1292             1;