File Coverage

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


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   74483 use Moose;
  1         399065  
  1         7  
20              
21 1     1   7660 use Term::ANSIColor;
  1         8264  
  1         64  
22 1     1   1608 use Getopt::Std;
  1         42  
  1         47  
23 1     1   6 use File::Basename;
  1         1  
  1         5212  
24              
25             our $VERSION = '2.0.3';
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         4 $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 ( $dirno, $msgno ) = split( /\//, $file );
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 1 my $self = shift;
325              
326 1 50       6 @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         82 );
485              
486 1 100       7 my $optstring = join '', map { $_ . ( $option{$_}{arg} ? ':' : '' ) }
  20         45  
487             keys %option;
488              
489 1         8 getopts( $optstring, \%opt );
490              
491 1         32 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         56 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 2892 my $self = shift;
518              
519             # Don't need to stop qmail if we're not planning to delete stuff
520 1 50       29 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 ( $dirno, $msgno ) = split( /\//, $msg );
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 my ( $dirno, $msgno ) = split( /\//, $msg );
920 0         0 $self->add_to_delete($msg);
921             }
922             }
923             }
924              
925             # If no messages are found, print a notice
926 0 0       0 if ( !$ok ) {
927 0         0 warn "No messages from $sender found in the queue!\n";
928             }
929              
930 0         0 return;
931             }
932              
933             =head2 del_msg_from_sender_r($sender)
934              
935             Given a sender's email address, add all messages from that sender to the
936             list of messages to delete.
937              
938             This method treats $sender as a regex.
939              
940             The actual deletion is carried out by C<trash_msgs>.
941              
942             =cut
943              
944             sub del_msg_from_sender_r {
945 0     0 1 0 my $self = shift;
946 0         0 my ($sender_re) = @_;
947              
948 0         0 warn "Looking for messages from senders matching $sender_re\n";
949              
950 0         0 my $ok = 0;
951 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
952 0 0       0 if ( $self->msglist->{$msg}{sender} ) {
953 0         0 my $msg_sender = $self->get_sender($msg);
954 0 0       0 if ( $msg_sender =~ /$sender_re/ ) {
955 0         0 $ok = 1;
956 0         0 my ( $dirno, $msgno ) = split( /\//, $msg );
957 0         0 $self->add_to_delete($msg);
958             }
959             }
960             }
961              
962             # If no messages are found, print a notice
963 0 0       0 if ( !$ok ) {
964 0         0 warn "No messages from senders matching ",
965             "$sender_re found in the queue!\n";
966             }
967              
968 0         0 return;
969             }
970              
971             =head2 del_msg_header($header_re, $is_case_sensitive)
972              
973             Given a regex, add all messages with headers that match the regex to the
974             list of messages to delete.
975              
976             The actual deletion is carried out by C<trash_msgs>.
977              
978             =cut
979              
980             sub del_msg_header_r {
981 0     0 0 0 my $self = shift;
982 0         0 my ( $header_re, $is_case_sensitive ) = @_;
983              
984 0         0 warn "Looking for messages with headers matching $header_re\n";
985              
986 0 0       0 $header_re = "(?i)$header_re" if $is_case_sensitive;
987              
988 0         0 my $queue = $self->queue;
989 0         0 my $ok = 0;
990 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
991 0 0       0 open( my $msg_fh, '<', "${queue}mess/$msg" )
992             or die("cannot open message $msg! Is qmail-send running?\n");
993 0         0 while (<$msg_fh>) {
994 0         0 chomp;
995 0 0       0 last if ! /\S/; # End of headers
996 0 0       0 if (/$header_re/) {
997 0         0 $ok = 1;
998 0         0 my ( $dirno, $msgno ) = split( /\//, $msg );
999 0         0 $self->add_to_delete($msg);
1000 0         0 last;
1001             }
1002             }
1003 0         0 close($msg_fh);
1004              
1005             }
1006              
1007             # If no messages are found, print a notice
1008 0 0       0 if ( !$ok ) {
1009 0         0 warn "No messages with headers matching $header_re ",
1010             "found in the queue!\n";
1011             }
1012              
1013 0         0 return;
1014             }
1015              
1016             =head2 del_msg_body_r($body_re, $is_case_sensitive)
1017              
1018             Given a regex, add all messages with a body that matches the regex to the
1019             list of messages to delete.
1020              
1021             The actual deletion is carried out by C<trash_msgs>.
1022              
1023             =cut
1024              
1025             sub del_msg_body_r {
1026 0     0 1 0 my $self = shift;
1027 0         0 my ( $body_re, $is_case_sensitive ) = @_;
1028              
1029 0         0 my $queue = $self->queue;
1030              
1031 0         0 warn "Looking for messages with body matching $body_re\n";
1032              
1033 0 0       0 $body_re = "(?i)$body_re" if $is_case_sensitive;
1034              
1035 0         0 my $ok = 0;
1036 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
1037 0 0       0 open( my $msg_fh, '<', "${queue}mess/$msg" )
1038             or die("cannot open message $msg! Is qmail-send running?\n");
1039             # Skip headers
1040 0         0 while (<$msg_fh>) {
1041 0         0 chomp;
1042 0 0       0 last if !/\S/;
1043             }
1044 0         0 while (<$msg_fh>) {
1045 0 0       0 if (/$body_re/) {
1046 0         0 $ok = 1;
1047 0         0 my ( $dirno, $msgno ) = split( /\//, $msg );
1048 0         0 $self->add_to_delete($msg);
1049 0         0 last;
1050             }
1051             }
1052 0         0 close($msg_fh);
1053             }
1054              
1055             # If no messages are found, print a notice
1056 0 0       0 if ( !$ok ) {
1057 0         0 warn "No messages with body matching $body_re found in the queue!\n";
1058             }
1059              
1060 0         0 return;
1061             }
1062              
1063             =head2 del_msg_subj($subject, $is_case_sensitive)
1064              
1065             Given a subject, add all messages with that subject to the list of messages
1066             to delete.
1067              
1068             The actual deletion is carried out by C<trash_msgs>.
1069              
1070             =cut
1071              
1072             sub del_msg_subj {
1073 0     0 1 0 my $self = shift;
1074 0         0 my ($subject) = @_;
1075              
1076 0         0 warn "Looking for messages with Subject: $subject\n";
1077              
1078             # Search messages
1079 0         0 my $ok = 0;
1080 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
1081 0         0 my ( $dirno, $msgno ) = split( /\//, $msg );
1082 0         0 my $msgsub = $self->get_subject($msg);
1083              
1084 0 0 0     0 if ( $msgsub and $msgsub =~ /$subject/ ) {
1085 0         0 $ok = 1;
1086 0         0 $self->add_to_delete($msg);
1087             }
1088              
1089             }
1090              
1091             # If no messages are found, print a notice
1092 0 0       0 if ( !$ok ) {
1093 0         0 warn "No messages matching Subject \"$subject\" found in the queue!\n";
1094             }
1095              
1096 0         0 return;
1097             }
1098              
1099             =head2 del_all()
1100              
1101             Delete all messages in the queue.
1102              
1103             The actual deletion is carried out by C<trash_msgs>.
1104              
1105             =cut
1106              
1107             sub del_all {
1108 0     0 1 0 my $self = shift;
1109              
1110             # Search messages
1111 0         0 my $ok = 0;
1112 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
1113 0         0 $ok = 1;
1114 0         0 my ( $dirno, $msgno ) = split( /\//, $msg );
1115 0         0 $self->add_to_delete($msg);
1116             }
1117              
1118             # If no messages are found, print a notice
1119 0 0       0 if ( !$ok ) {
1120 0         0 warn "No messages found in the queue!\n";
1121             }
1122              
1123 0         0 return;
1124             }
1125              
1126             =head2 flag_remote($recipient_re)
1127              
1128             Flag all remote messages whose recipient matches the given regex.
1129              
1130             =cut
1131              
1132             sub flag_remote {
1133 0     0 1 0 my $self = shift;
1134 0         0 my ($recipient_re) = @_;
1135              
1136 0         0 my $queue = $self->queue;
1137              
1138 0         0 warn "Looking for messages with recipients in $recipient_re\n";
1139              
1140 0         0 my $ok = 0;
1141 0         0 for my $msg ( keys %{ $self->msglist } ) {
  0         0  
1142 0 0       0 if ( $self->msglist->{$msg}{remote} ) {
1143 0 0       0 open( my $msg_fh, '<', "${queue}remote/$msg" )
1144             or die( "cannot open remote file for message $msg! ",
1145             "Is qmail-send running?\n" );
1146 0         0 my $recipients = <$msg_fh>;
1147 0         0 chomp($recipients);
1148 0         0 close($msg_fh);
1149 0 0       0 if ( $recipients =~ /$recipient_re/ ) {
1150 0         0 $ok = 1;
1151 0         0 $self->add_to_flag($msg);
1152 0         0 warn "Message $msg being tagged for earlier retry ",
1153             "(and lengthened stay in queue)!\n";
1154             }
1155             }
1156             }
1157              
1158             # If no messages are found, print a notice
1159 0 0       0 if ( !$ok ) {
1160 0         0 warn "No messages with recipients in $recipient_re ",
1161             "found in the queue!\n";
1162 0         0 return;
1163             }
1164              
1165 0         0 $self->flag_msgs;
1166              
1167 0         0 return;
1168             }
1169              
1170             =head2 stats()
1171              
1172             Display statistics about the queue.
1173              
1174             =cut
1175              
1176             sub stats {
1177 0     0 1 0 my $self = shift;
1178              
1179 0         0 my $total = 0;
1180 0         0 my $l = 0;
1181 0         0 my $r = 0;
1182 0         0 my $b = 0;
1183 0         0 my $t = 0;
1184              
1185 0         0 foreach my $msg ( keys %{ $self->msglist } ) {
  0         0  
1186 0         0 $total++;
1187 0 0       0 $self->msglist->{$msg}{local} && $l++;
1188 0 0       0 $self->msglist->{$msg}{remote} && $r++;
1189 0 0       0 $self->msglist->{$msg}{bounce} && $b++;
1190 0 0       0 $self->msglist->{$msg}{todo} && $t++;
1191             }
1192              
1193 0         0 my $colours = $self->colours;
1194 0         0 my ( $cstat, $cend ) = @{$colours}{qw[stat end]};
  0         0  
1195              
1196 0         0 print <<"END_OF_STATS";
1197             ${cstat}Total messages${cend}: $total
1198             ${cstat}Messages with local recipients${cend}: $l
1199             ${cstat}Messages with remote recipients${cend}: $r
1200             ${cstat}Messages with bounces${cend}: $b
1201             ${cstat}Messages in preprocess${cend}: $t
1202             END_OF_STATS
1203 0         0 return;
1204             }
1205              
1206             =head2 qmail_pid()
1207              
1208             Get the pid of the qmail daemon
1209              
1210             =cut
1211              
1212             sub qmail_pid {
1213 0     0 1 0 my $self = shift;
1214 0         0 my $pidcmd = $self->commands->{pid};
1215 0         0 my $qmpid = `$pidcmd`;
1216 0 0       0 return 0 unless $qmpid;
1217 0         0 chomp($qmpid);
1218 0         0 $qmpid =~ s/\s+//g;
1219 0 0       0 return 0 if $qmpid =~ /\D/;
1220 0         0 return $qmpid;
1221             }
1222              
1223             =head2 usage()
1224              
1225             Display usage information.
1226              
1227             =cut
1228              
1229             sub usage {
1230 1     1 1 20 print <<"END_OF_HELP";
1231             $me v$VERSION
1232             Copyright (c) 2016 Dave Cross <dave\@perlhacks.com>
1233             Based on original version by Michele Beltrame <mb\@italpro.net>
1234              
1235             Available parameters:
1236             -a : try to send queued messages now (qmail must be running)
1237             -l : list message queues
1238             -L : list local message queue
1239             -R : list remote message queue
1240             -s : show some statistics
1241             -mN : display message number N
1242             -dN : delete message number N
1243             -fsender : delete message from sender
1244             -F're' : delete message from senders matching regular expression re
1245             -Stext : delete all messages that have/contain text as Subject
1246             -h're' : delete all messages with headers matching regular expression
1247             re (case insensitive)
1248             -b're' : delete all messages with body matching regular expression
1249             re (case insensitive)
1250             -H're' : delete all messages with headers matching regular expression
1251             re (case sensitive)
1252             -B're' : delete all messages with body matching regular expression
1253             re (case sensitive)
1254             -t're' : flag messages with recipients in regular expression 're' for
1255             earlier retry (note: this lengthens the time message can
1256             stay in queue)
1257             -D : delete all messages in the queue (local and remote)
1258             -V : print program version
1259             -? : Display this help
1260              
1261             Additional (optional) parameters:
1262             -c : display colored output
1263             -N : list message numbers only
1264             (to be used either with -l, -L or -R)
1265              
1266             You can view/delete multiple message i.e. -d123 -m456 -d567
1267              
1268             END_OF_HELP
1269             }
1270              
1271 1     1   7 no Moose;
  1         2  
  1         6  
1272             __PACKAGE__->meta->make_immutable;
1273              
1274             =head2 version()
1275              
1276             Display the version.
1277              
1278             =cut
1279              
1280             sub version {
1281 0     0 1   print "$me v$VERSION\n";
1282 0           return;
1283             }
1284              
1285             =head2 AUTHOR
1286              
1287             Copyright (c) 2016 Dave Cross E<lt>dave@perlhacks.comE<gt>
1288              
1289             Based on original version by Michele Beltrame E<lt>mb@italpro.netE</gt>
1290              
1291             =head2 LICENCE
1292              
1293             This program is distributed under the GNU GPL.
1294             For more information have a look at http://www.gnu.org
1295              
1296             =cut
1297              
1298             1;