#-*- perl -*- # # Copyright (C) 2001,2002,2003,2004,2005,2006,2008 Ken'ichi Fukamachi # All rights reserved. This program is free software; you can # redistribute it and/or modify it under the same terms as Perl itself. # # $FML: Queue.pm,v 1.71 2008/08/02 21:22:02 fukachan Exp $ # package Mail::Delivery::Queue; use strict; use Carp; use vars qw(@ISA $Counter $RandCounter @class_list @local_class_list); use File::Spec; # Mail::Delivery::Queue IS-A Mail::Delivery::Base. use Mail::Delivery::Base; @ISA = qw(Mail::Delivery::Base); =head1 NAME Mail::Delivery::Queue - handle mail queue system. =head1 SYNOPSIS use Mail::Message; $msg = new Mail::Message; use Mail::Delivery::Queue; my $queue = new Mail::Delivery::Queue { directory => "/some/where" }; # queue in a new message # "/some/where/new/$queue_id" is created. $queue->in( $msg ) || croak("fail to queue in"); # ok to deliver this message ! $queue->setrunnable() || croak("fail to set queue deliverable"); =head1 DESCRIPTION C provides basic manipulation of mail queue. =head1 DIRECTORY STRUCTURE C method assigns a new queue id C<$qid> but not do actual works. C method creates a new queue file C<$qf>. So, C<$qf> follows: $qf = "$queue_dir/new/$qid" When C<$qid> queue is ready to be delivered, you must move the queue file from new/ to active/ by C. To make this queue deliverable, use C method. $queue_dir/new/$qid ---> $queue_dir/active/$qid The actual delivery is done by other modules such as C. C manipulates only queue around things. =head1 METHODS =head2 new($args) constructor. You must specify at least C as $args->{ dirctory } . If C is not specified, C assigns the queue id, queue files to be used. C assigns them but do no actual works. =cut my $default_policy = "oldest"; my $default_dir_mode = 0755; @class_list = qw(lock new deferred active incoming info sender recipients transport strategy isolated ); # Descriptions: constructor. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: initialize object # Return Value: OBJ sub new { my ($self, $args) = @_; my ($type) = ref($self) || $self; my $dir = $args->{ directory } ||croak("specify directory"); my $id = $args->{ id } || _new_queue_id(); my $local_class = $args->{ local_class } || []; my $dir_mode = $args->{ directory_mode } || $default_dir_mode; my $expire_limit = $args->{ expire_limit } || 14 * 24 * 3600; my $me = {}; bless $me, $type; $me->set_queue_directory($dir); $me->set_queue_id($id); $me->set_directory_mode($dir_mode); $me->set_expire_limit($expire_limit); # update optional local class list. for my $c (@$local_class) { push(@local_class_list, $c);} # initialize directories. -d $dir || $me->_mkdirhier($dir); for my $class (@class_list, @local_class_list) { my $fp = sprintf("%s_dir_path", $class); my $_dir = $me->can($fp) ? $me->$fp() : $me->local_dir_path($class); -d $_dir || $me->_mkdirhier($_dir); } # prepare garbage collection. my $qf_new = $me->new_file_path($id); $me->set_garbage_list($qf_new); return bless $me, $type; } # Descriptions: mkdir recursively. # Arguments: OBJ($self) STR($dir) # Side Effects: create directory. # Return Value: none sub _mkdirhier { my ($self, $dir) = @_; my $dir_mode = $self->get_directory_mode(); eval q{ use File::Path; mkpath( [ $dir ], 0, $dir_mode); }; $self->_logerror($@) if $@; } # Descriptions: return new queue identifier. # Arguments: none # Side Effects: increment counter $Counter # Return Value: STR sub _new_queue_id { my ($seconds, $microseconds) = (0, 0); my $id; $Counter++; eval q{ use Time::HiRes qw(gettimeofday); ($seconds, $microseconds) = gettimeofday; }; if ($@) { my ($seconds, $microseconds) = (time, 0); $id = sprintf("%d.%06d.%d.%d", $seconds, $microseconds, $$, $Counter); } else { $id = sprintf("%d.%06d.%d.%d", $seconds, $microseconds, $$, $Counter); } return $id; } # Descriptions: clear this queue file. # Arguments: OBJ($self) # Side Effects: unlink this queue # Return Value: NUM sub DESTROY { my ($self) = @_; my $files = $self->get_garbage_list() || []; for my $file (@$files) { unlink $file if -f $file; } } =head1 INFORMATION =head2 id() same as get_queue_id(). return the queue id assigned to this object C<$self>. =head2 set_queue_id($id) save the queue id. =head2 get_queue_id() return the queue id assigned to this object C<$self>. =cut # Descriptions: return object identifier (queue id). # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub id { my ($self) = @_; $self->get_queue_id(); } # Descriptions: save object identifier (queue id). # Arguments: OBJ($self) STR($id) # Side Effects: update $self # Return Value: none sub set_queue_id { my ($self, $id) = @_; $self->{ _id } = $id || undef; } # Descriptions: return object identifier (queue id). # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub get_queue_id { my ($self) = @_; return( $self->{ _id } || undef ); } =head2 getidinfo($id) return information related with the queue id C<$id>. The returned information is id => $id, path => "$dir/active/$id", sender => $sender, recipients => \@recipients, =cut # XXX-TODO: remove getidinfo() (use each access method). # Descriptions: get information of queue for this object. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: HASH_REF sub getidinfo { my ($self, $id) = @_; $id ||= $self->id(); my $sender = $self->get_sender($id); my $rcpts = $self->get_recipient_as_array_ref($id); return { id => $id, path => $self->active_file_path($id), sender => $sender || '', recipients => $rcpts || [], }; } # Descriptions: when last modified. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: NUM(oldest unix time) sub last_modified_time { my ($self, $id) = @_; my $min_mtime = time; # queue id. $id ||= $self->id(); use File::stat; for my $class (@class_list, @local_class_list) { my $fp = sprintf("%s_file_path", $class); my $file = $self->can($fp) ? $self->$fp($id) : $self->local_file_path($class, $id); if (-f $file) { my $st = stat($file); if (defined $st) { my $mtime = $st->mtime(); # find oldest file info. $min_mtime = $min_mtime < $mtime ? $min_mtime : $mtime; } } } return $min_mtime; } =head2 list( [ $class ] ) return queue list as ARRAY REFERENCE. by default, return a list of queue filenames in C directory. $ra = $queue->list(); for $qid (@$ra) { something for $qid ... } where C<$qid> is like this: 990157187.20792.1 =head2 list_all() return all queue list in all classes. =cut # Descriptions: return queue file list as ARRAY_REF. # Arguments: OBJ($self) STR($class) STR($policy) # Side Effects: none # Return Value: ARRAY_REF sub list { my ($self, $class, $policy) = @_; $class ||= "active"; $policy ||= $default_policy; my $fp = $class ? "${class}_dir_path" : "active_dir_path"; my $dir = $self->can($fp) ? $self->$fp() : $self->local_dir_path($class); use DirHandle; my $dh = new DirHandle $dir; if (defined $dh) { my @r = (); my $file; ENTRY: while (defined ($file = $dh->read)) { next ENTRY unless $file =~ /^\d+/o; push(@r, $file); } return $self->_list_ordered_by_policy(\@r, $policy); } return []; } # Descriptions: return list of all queue irrespective of validity. # Arguments: OBJ($self) # Side Effects: none # Return Value: ARRAY_REF sub list_all { my ($self) = @_; my (@r) = (); for my $class (@class_list) { my $ra = $self->list($class, "default"); push(@r, @$ra); } # generate unique array by removing duplication. my (%r) = (); for my $q (@r) { $r{ $q } = 1; } @r = keys %r; return \@r; } =head2 set_policy($policy) set queue management policy. =head2 get_policy() return queue management policy. =cut # Descriptions: set queue management policy. # Arguments: OBJ($self) STR($policy) # Side Effects: update $self. # Return Value: none sub set_policy { my ($self, $policy) = @_; if (defined $policy) { $self->{ _policy } = $policy; } } # Descriptions: get queue management policy. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub get_policy { my ($self) = @_; return( $self->{ _policy } || $default_policy ); } # Descriptions: return re-ordering queue list. # Arguments: OBJ($self) ARRAY_REF($list) STR($_policy) # Side Effects: none # Return Value: ARRAY_REF sub _list_ordered_by_policy { my ($self, $list, $_policy) = @_; my $policy = $_policy || $self->get_policy() || $default_policy; if ($policy eq 'oldest') { my (@r) = sort _queue_streategy_oldest @$list; return \@r; } elsif ($policy eq 'newest') { my (@r) = sort _queue_streategy_newest @$list; return \@r; } elsif ($policy eq 'fair_queue' || $policy eq 'fair-queue') { my ($queue_hash) = {}; # create hash { TIME_SLICE => [ qid1, qid2, ... ] } for my $q (@$list) { if ($q =~ /^(\d+)/o) { my $hash_key = int( $1 / 87400 ); my $qlist = $queue_hash->{ $hash_key } || []; push(@$qlist, $q); $queue_hash->{ $hash_key } = $qlist; } } # randomized queue. my @p = (); srand(time | $$); $RandCounter = rand( time ); for my $i (sort _rand keys %$queue_hash) { my $qlist = $queue_hash->{ $i } || []; push(@p, sort _rand @$qlist); } return \@p; } else { ; } return $list; } # Descriptions: randomize (for sort routine). # Arguments: IMPLICIT # Side Effects: none # Return Value: NUM sub _rand { my $x = rand(time + $RandCounter++); my $y = rand(time + $RandCounter++); $x <=> $y; } # Descriptions: sort by normal date order. # Arguments: IMPLICIT # Side Effects: none # Return Value: NUM sub _queue_streategy_oldest { my $xa = $a; my $xb = $b; $xa =~ s/\.\d+.*$//; $xb =~ s/\.\d+.*$//; $xa <=> $xb; } # Descriptions: sort by reverse date order. # Arguments: IMPLICIT # Side Effects: none # Return Value: NUM sub _queue_streategy_newest { my $xa = $a; my $xb = $b; $xa =~ s/\.\d+.*$//; $xb =~ s/\.\d+.*$//; $xb <=> $xa; } =head1 SCHEDULE MANAGEMENT =head2 update_schedule($id) update scheduling for this queue (id = $id). =cut # Descriptions: update queue info for queue management policy. # Arguments: OBJ($self) STR($id) # Side Effects: update queue file. # Return Value: none sub update_schedule { my ($self, $id) = @_; $id ||= $self->id(); # get hints. my $hints = $self->_update_schedule_strategy($id); my $sleep = $hints->{ sleep } || 300; my $time = time + $sleep; # set expired time. my $qf_deferred = $self->deferred_file_path($id); utime $time, $time, $qf_deferred; } # Descriptions: get hints for this queue id. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: HASH_REF sub _update_schedule_strategy { my ($self, $id) = @_; my $info = {}; # update sleep time. my $cur_sleep = $self->get_sleep_count || 300; my $new_sleep = int( ($cur_sleep || 300 ) * (1 + rand(1)) ); $new_sleep = $new_sleep < 4000 ? $new_sleep : 4000; $self->set_sleep_count($new_sleep); $info->{ sleep } = $new_sleep; # increment retry count my $retry_count = $self->get_retry_count(); $retry_count++; $self->set_retry_count($retry_count); return $info; } # Descriptions: move deferred queue to active one. # Arguments: OBJ($self) STR($id) # Side Effects: update queue. # Return Value: none sub wakeup_queue { my ($self, $id) = @_; $self->_change_queue_mode($id, "active"); } # Descriptions: move active queue to deferred one. # Arguments: OBJ($self) STR($id) # Side Effects: update queue. # Return Value: none sub sleep_queue { my ($self, $id) = @_; $self->_change_queue_mode($id, "deferred"); } # Descriptions: change mode. # move active queue to deferred one, vice versa. # Arguments: OBJ($self) STR($id) STR($to_mode) # Side Effects: update queue. # Return Value: none sub _change_queue_mode { my ($self, $id, $to_mode) = @_; $id ||= $self->id(); if ($self->lock()) { my $qf_deferred = $self->deferred_file_path($id); my $qf_active = $self->active_file_path($id); if ($to_mode eq 'active') { if (-f $qf_deferred) { rename($qf_deferred, $qf_active); $self->touch($qf_active); if (-f $qf_active) { $self->_logdebug("qid=$id activated."); } else { $self->_logerror("qid=$id operation failed."); } } else { $self->_logerror("no such deferred queue qid=$id"); } } elsif ($to_mode eq 'deferred' || $to_mode eq 'defer') { if (-f $qf_active) { rename($qf_active, $qf_deferred); $self->touch($qf_deferred); $self->update_schedule($id); if (-f $qf_deferred) { $self->_logdebug("qid=$id deferred"); } else { $self->_logerror("qid=$id operation failed."); } } else { $self->_logerror("no such active queue qid=$id"); } } else { $self->_logerror("invalid queue mode"); } $self->unlock(); } else { $self->_logerror("qid=$id lock failed."); } } =head2 reschedule() reschedule queues. wake up queue if needed. =cut # Descriptions: reschedule queues. wake up queue if needed. # Arguments: OBJ($self) # Side Effects: wake up queue if needed. # Return Value: none sub reschedule { my ($self) = @_; my $count = 0; my $early = 0; my $total = 0; use File::stat; my $q_list = $self->list("deferred"); for my $qid (@$q_list) { my $qf = $self->deferred_file_path($qid); my $st = stat($qf); $total++; # wake up too old queue. if (defined $st) { if ($st->mtime < time) { $self->wakeup_queue($qid); $count++; } else { $early++; } } } if ($count) { $self->_logdebug("activate $count queue(s)"); $self->_logdebug("$early queue(s) sleeping") if $early; } else { $self->_logdebug("$early queue(s) sleeping") if $early; } } =head1 LOCK =head2 lock() =head2 unlock() =cut use FileHandle; use Fcntl qw(:DEFAULT :flock); # Descriptions: lock queue. # Arguments: OBJ($self) HASH_REF($args) # Side Effects: flock queue # Return Value: NUM(1 or 0) sub lock { my ($self, $args) = @_; my $wait = defined $args->{ wait } ? $args->{ wait } : 10; my $is_prep = defined $args->{ lock_before_runnable } ? 1 : 0; my $id = $self->id(); my $qf_new = $self->new_file_path($id); my $qf_lock = $self->lock_file_path($id); my $qf_act = $self->active_file_path($id); $self->touch($qf_lock); # my $lockfile = $is_prep ? $qf_new : (-f $qf_lock ? $qf_lock : $qf_act); my $lockfile = $qf_lock; use IO::Adapter; my $io = new IO::Adapter $lockfile; if (defined $io) { my $r = $io->lock( { wait => $wait } ); if ($r) { $self->_logdebug("got lock qid=$id"); $self->{ _lock }->{ $id } = $io; } else { if ($wait > 0) { $self->_logdebug("trylock timeout: qid=$id"); } else { $self->_logerror("cannot lock: qid=$id"); } } return $r; } else { $self->_logerror("cannot lock: qid=$id"); } return 0; } # Descriptions: unlock queue. # Arguments: OBJ($self) # Side Effects: unlock queue by flock(2) # Return Value: NUM(1 or 0) sub unlock { my ($self) = @_; my $id = $self->id(); my $io = $self->{ _lock }->{ $id } || undef; if (defined $io) { my $r = $io->unlock(); $self->_logdebug("got unlock qid=$id"); return $r; } else { $self->_logerror("not locked: qid=$id"); } return 0; } =head1 IO =head2 open($class, $args) open incoming queue of this queue id with mode $mode and return the file handle. =head2 close($class) close. =cut # Descriptions: open incoming queue of this object with mode $mode # and return the file handle. # Arguments: OBJ($self) STR($class) HASH_REF($op_args) # Side Effects: file handle opened. # Return Value: HANDLE sub open { my ($self, $class, $op_args) = @_; my $id = $self->id(); my $fp = sprintf("%s_file_path", $class); my $qf = $self->can($fp) ? $self->$fp($id) : $self->local_file_path($class, $id); if (defined $op_args->{ in_channel }) { my $channel = $op_args->{ in_channel }; my $mode = $op_args->{ mode } || "<"; open($channel, $mode, $qf); return $channel; } else { use FileHandle; my $mode = $op_args->{ mode } || "r"; my $fh = new FileHandle $qf, $mode; if (defined $fh) { $self->{ "_${class}_channel" } = $fh; return $fh; } else { return undef; } } } # Descriptions: close the incoming channel of this object. # Arguments: OBJ($self) STR($class) # Side Effects: file handle closed. # Return Value: none sub close { my ($self, $class) = @_; my $channel = $self->{ "_${class}_channel" } || undef; if (defined $channel) { close($channel); } } =head2 in($msg) C creates a queue file in C directory (C. C<$msg> is C object by default. If C<$msg> object has print() method, arbitrary C<$msg> is acceptable. REMEMBER YOU MUST DO C for the queue to be delivered. If you not C it, the queue file is removed by C. =cut # Descriptions: create a new queue file. # Arguments: OBJ($self) OBJ($msg) # Side Effects: none # Return Value: NUM(1 or 0) sub in { my ($self, $msg) = @_; my $id = $self->id(); my $qf_qstr = $self->strategy_file_path($id); my $qf_lock = $self->lock_file_path($id); my $qf_new = $self->new_file_path($id); # ASSERT unless (defined $msg) { $self->_logerror("in: undefined message"); return 0; } $self->touch($qf_lock) unless -f $qf_lock; $self->touch($qf_qstr) unless -f $qf_qstr; $self->touch($qf_new) unless -f $qf_new; use FileHandle; my $fh = new FileHandle "> $qf_new"; if (defined $fh) { $fh->autoflush(1); $fh->clearerr(); $msg->print($fh); if ($fh->error()) { $self->set_error("write error"); } $fh->close; if ($msg->can('write_count')) { my $write_count = $msg->write_count(); $self->set_write_count($write_count); use File::stat; my $try_count = 3; my $ok = 0; TRY: while ($try_count-- > 0) { my $st = stat($qf_new); if ($st->size == $write_count) { $ok = 1; last TRY; } sleep 1; } unless ($ok) { $self->set_error("write error: size mismatch"); } } } else { $self->_logerror("cannot open new queue file."); } # check the existence and the size > 0. return( (-e $qf_new && -s $qf_new) ? 1 : 0 ); } # Descriptions: create a new queue file. # Arguments: OBJ($self) OBJ($msg) # Side Effects: none # Return Value: NUM(1 or 0) sub add { my ($self, $msg) = @_; $self->in($msg); } =head2 add ($msg) same as in(). =head2 delete() remove all files assigned to this queue C<$self>. =head2 remove() same as delete(). =head2 valid() check if the queue file is broken or not. return 1 (valid) or 0 (broken). =cut # Descriptions: remove queue files for this object (queue). # Arguments: OBJ($self) STR($qid) # Side Effects: remove queue file(s) # Return Value: none sub delete { my ($self, $qid) = @_; $self->remove($qid); } # Descriptions: remove queue files for this object (queue). # Arguments: OBJ($self) STR($qid) # Side Effects: remove queue file(s) # Return Value: none sub remove { my ($self, $qid) = @_; my $id = $qid || $self->id(); my $count = 0; my $removed = 0; for my $class (@class_list, @local_class_list) { my $fp = sprintf("%s_file_path", $class); my $f = $self->can($fp) ? $self->$fp($id) : $self->local_file_path($class, $id); if (-f $f) { $count++; unlink $f; $removed++ unless -f $f; } } if ($count > 0) { if ($count == $removed) { $self->_logdebug("qid=$id removed"); } else { $self->_logerror("qid=$id remove failed"); } } } # Descriptions: return num of bytes written successfully. # Arguments: OBJ($self) # Side Effects: none # Return Value: NUM sub write_count { my ($self) = @_; $self->get_write_count(); } # Descriptions: return num of bytes written successfully. # Arguments: OBJ($self) # Side Effects: none # Return Value: NUM sub get_write_count { my ($self) = @_; return( $self->{ _write_count } || 0 ); } # Descriptions: save num of bytes written successfully. # Arguments: OBJ($self) NUM($count) # Side Effects: none # Return Value: NUM sub set_write_count { my ($self, $count) = @_; $self->{ _write_count } = $count || 0; } =head2 setrunnable() set the status of the queue assigned to this object C<$self> deliverable. This queue is scheduled to be delivered. In fact, setrunnable() Cs the queue id file from C directory to C directory like C queue strategy. =cut # Descriptions: enable this object queue to be deliverable. # Arguments: OBJ($self) # Side Effects: move $queue_id file from new/ to active/ # Return Value: NUM( 1 (success) or 0 (fail) ) sub setrunnable { my ($self) = @_; my $id = $self->id(); my $qf_new = $self->new_file_path($id); my $qf_active = $self->active_file_path($id); my $qf_sender = $self->sender_file_path($id); my $qf_recipients = $self->recipients_file_path($id); # something error. if ($self->get_error()) { $self->_logerror( $self->get_error() ); return 0; } # There must be a set of these three files. # 1. exisntence unless (-f $qf_new && -f $qf_sender && -f $qf_recipients) { $self->_logerror("setrunnable: some queue not exists"); return 0; } # 2. non-zero size. unless (-s $qf_new && -s $qf_sender && -s $qf_recipients) { $self->_logerror("setrunnable: some queue empty"); return 0; } # move new/$id to active/$id if (rename($qf_new, $qf_active)) { return 1; } else { $self->_logerror("setrunnable: cannot rename"); } return 0; } =head2 touch($file) =cut # Descriptions: touch file. # Arguments: OBJ($self) STR($file) # Side Effects: create $file. # Return Value: none sub touch { my ($self, $file) = @_; use FileHandle; my $fh = new FileHandle; if (defined $fh) { $fh->open($file, "a"); $fh->close(); my $now = time; utime $now, $now, $file; } } =head1 ACCESS METHOD =head2 set($key, $args) defined for compatibility. $queue->set('sender', $sender); $queue->set('recipients', [ $recipient0, $recipient1 ] ); It sets up delivery information in C and C directories. =cut # Descriptions: set value for key. # Arguments: OBJ($self) STR($key) STR($value) # Side Effects: none # Return Value: same as close() sub set { my ($self, $key, $value) = @_; my $id = $self->id(); if ($key eq 'sender') { $self->set_sender($id, $value); } elsif ($key eq 'recipients') { $self->set_recipient_as_array_ref($id, $value); } elsif ($key eq 'recipient_maps') { $self->set_recipient_maps($id, $value); } elsif ($key eq 'transport') { $self->set_transport($id, $value); } else { $self->_logerror("set: unknown type"); } } # Descriptions: set sender for queue $id. # Arguments: OBJ($self) STR($id) STR($value) # Side Effects: create queue file. # Return Value: none sub set_sender { my ($self, $id, $value) = @_; my $qf_sender = $self->sender_file_path($id); use FileHandle; my $fh = new FileHandle "> $qf_sender"; if (defined $fh) { $fh->clearerr(); print $fh $value, "\n"; if ($fh->error()) { $self->set_error("write error"); } $fh->close; } else { $self->set_error("cannot open $qf_sender"); } } # Descriptions: get sender for queue $id. # Arguments: OBJ($self) STR($id) # Side Effects: create queue file. # Return Value: STR sub get_sender { my ($self, $id) = @_; my $qf_sender = $self->sender_file_path($id); my $sender = ''; use FileHandle; my $fh = new FileHandle $qf_sender; if (defined $fh) { $sender = $fh->getline; $sender =~ s/[\n\s]*$//o; $fh->close; } else { $self->set_error("cannot open $qf_sender"); } return $sender; } # Descriptions: set recipient list for queue $id. # Arguments: OBJ($self) STR($id) ARRAY_REF($value) # Side Effects: create queue file. # Return Value: none sub set_recipient_as_array_ref { my ($self, $id, $value) = @_; my $qf_recipients = $self->recipients_file_path($id); use FileHandle; my $fh = new FileHandle ">> $qf_recipients"; if (defined $fh) { $fh->clearerr(); if (ref($value) eq 'ARRAY') { for my $rcpt (@$value) { print $fh $rcpt, "\n";} } else { $self->_logerror("set_recipient_as_array_ref: invalid input"); } if ($fh->error()) { $self->set_error("write error"); } $fh->close; } else { $self->set_error("cannot open $qf_recipients"); } } # Descriptions: get recipient list for queue $id. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: ARRAY_REF sub get_recipient_as_array_ref { my ($self, $id) = @_; my (@recipients) = (); my $qf_recipients = $self->recipients_file_path($id); use FileHandle; my $fh = new FileHandle $qf_recipients ; if (defined $fh) { my $buf; ENTRY: while (defined($buf = $fh->getline)) { $buf =~ s/[\n\s]*$//o; push(@recipients, $buf); } $fh->close; } else { $self->set_error("cannot open $qf_recipients "); } return \@recipients; } # Descriptions: set recipient list for queue $id. # Arguments: OBJ($self) STR($id) ARRAY_REF($value) # Side Effects: create queue file. # Return Value: none sub set_recipient_maps { my ($self, $id, $value) = @_; my $qf_recipients = $self->recipients_file_path($id); use FileHandle; my $fh = new FileHandle ">> $qf_recipients"; if (defined $fh) { $fh->clearerr(); if (ref($value) eq 'ARRAY') { for my $map (@$value) { use IO::Adapter; my $io = new IO::Adapter $map; if (defined $io) { $io->open(); my $buf; while ($buf = $io->get_next_key()) { print $fh $buf, "\n"; } $io->close(); } } } else { $self->_logerror("set_recipient_maps: invalid input"); } if ($fh->error()) { $self->set_error("write error"); } $fh->close; } } # Descriptions: set transport for queue $id. # Arguments: OBJ($self) STR($id) STR($value) # Side Effects: create queue file. # Return Value: none sub set_transport { my ($self, $id, $value) = @_; my $qf_transport = $self->transport_file_path($id); use FileHandle; my $fh = new FileHandle "> $qf_transport"; if (defined $fh) { $fh->clearerr(); print $fh $value, "\n"; if ($fh->error()) { $self->set_error("write error"); } $fh->close; } else { $self->set_error("cannot open $qf_transport"); } } # Descriptions: set queue directory. # Arguments: OBJ($self) STR($dir) # Side Effects: create queue file. # Return Value: none sub set_queue_directory { my ($self, $dir) = @_; $self->{ _directory } = $dir; } # Descriptions: get queue directory. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub get_queue_directory { my ($self) = @_; return $self->{ _directory }; } # Descriptions: set directory mode. # Arguments: OBJ($self) NUM($mode) # Side Effects: update $self. # Return Value: none sub set_directory_mode { my ($self, $mode) = @_; $self->{ _directory_mode } = $mode; } # Descriptions: get directory mode. # Arguments: OBJ($self) # Side Effects: none # Return Value: NUM sub get_directory_mode { my ($self) = @_; return( $self->{ _directory_mode } || $default_dir_mode ); } # Descriptions: set garbage collection list. # Arguments: OBJ($self) STR($file) # Side Effects: update $self # Return Value: none sub set_garbage_list { my ($self, $file) = @_; my $list = $self->{ _cleanup_files } || []; push(@$list, $file); $self->{ _cleanup_files } = $list; } # Descriptions: get garbage collection list. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub get_garbage_list { my ($self) = @_; return( $self->{ _cleanup_files } || [] ); } =head1 UTILITIES =head2 is_valid_active_queue() check if this object (queue) is sane as active queue? =cut # Descriptions: check if this object (queue) is sane as active queue? # Arguments: OBJ($self) # Side Effects: none # Return Value: 1 or 0 sub is_valid_active_queue { my ($self) = @_; my $ok = 0; my $id = $self->id(); # files to check. my $qf_active = $self->active_file_path($id); my $qf_sender = $self->sender_file_path($id); my $qf_recipients = $self->recipients_file_path($id); for my $f ($qf_active, $qf_sender, $qf_recipients) { $ok++ if -f $f && -s $f; } ($ok == 3) ? 1 : 0; } =head2 is_valid_queue() check if this object (queue) is sane as active queue? =cut # Descriptions: check if this object (queue) is sane as active queue? # Arguments: OBJ($self) # Side Effects: none # Return Value: 1 or 0 sub is_valid_queue { my ($self) = @_; my $ok = 0; my $id = $self->id(); # files to check. my $qf_active = $self->active_file_path($id); my $qf_deferred = $self->deferred_file_path($id); my $qf_sender = $self->sender_file_path($id); my $qf_recipients = $self->recipients_file_path($id); for my $f ($qf_sender, $qf_recipients) { $ok++ if -f $f && -s $f; } # XXX You need lock() before calling is_valid_*_queue() method. # # if "-s $qf_active ; rename() in some other process; -s $qf_deferred" # operation is done, this check fails. # make this check more robust, check again. # this logic is stupid and not perfect but effective ? if (-s $qf_active || -s $qf_deferred) { $ok++;} if (-s $qf_active || -s $qf_deferred) { $ok++;} ($ok == 4) ? 1 : 0; } =head2 dup_content($old_class, $new_class) duplicate $old_class content at a class $new_class. =cut # Descriptions: duplicate content at a class $class other than incoming. # return new queue id generated in dupilication. # Arguments: OBJ($self) STR($old_class) STR($new_class) # Side Effects: none # Return Value: STR sub dup_content { my ($self, $old_class, $new_class) = @_; my $id = $self->id(); my $new_id = _new_queue_id(); my $queue_file = $self->local_file_path($old_class, $id); my $new_qf = $self->local_file_path($new_class, $new_id); return( link($queue_file, $new_qf) ? $new_id : undef ); } =head1 DIR/FILE UTILITIES =cut # Descriptions: return "lock" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub lock_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "lock"); } # Descriptions: return "lock" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub lock_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "lock", $id); } # Descriptions: return "incoming" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub incoming_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "incoming"); } # Descriptions: return "incoming" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub incoming_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "incoming", $id); } # Descriptions: return "new" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub new_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "new"); } # Descriptions: return "new" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub new_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "new", $id); } # Descriptions: return "active" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub active_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "active"); } # Descriptions: return "active" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub active_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "active", $id); } # Descriptions: return "deferred" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub deferred_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "deferred"); } # Descriptions: return "deferred" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub deferred_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "deferred", $id); } # Descriptions: return "info" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub info_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info"); } # Descriptions: return "info" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub info_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info", $id); } # Descriptions: return "sender" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub sender_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info", "sender"); } # Descriptions: return "sender" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub sender_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info", "sender", $id); } # Descriptions: return "recipients" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub recipients_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info", "recipients"); } # Descriptions: return "recipients" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub recipients_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info", "recipients", $id); } # Descriptions: return "transport" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub transport_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info", "transport"); } # Descriptions: return "transport" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub transport_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info", "transport", $id); } # Descriptions: return "strategy" directory path. # Arguments: OBJ($self) # Side Effects: none # Return Value: STR sub strategy_dir_path { my ($self) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info", "strategy"); } # Descriptions: return "strategy" file path. # Arguments: OBJ($self) STR($id) # Side Effects: none # Return Value: STR sub strategy_file_path { my ($self, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); return File::Spec->catfile($dir, "info", "strategy", $id); } # Descriptions: return local class directory path. # Arguments: OBJ($self) STR($class) # Side Effects: none # Return Value: STR sub local_dir_path { my ($self, $class) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); if (defined $dir && defined $class) { return File::Spec->catfile($dir, $class); } else { return undef; } } # Descriptions: return local class file path. # Arguments: OBJ($self) STR($class) STR($id) # Side Effects: none # Return Value: STR sub local_file_path { my ($self, $class, $id) = @_; my $dir = $self->get_queue_directory() || croak("directory undefined"); if (defined $dir && defined $class && defined $id) { return File::Spec->catfile($dir, $class, $id); } else { return undef; } } # Descriptions: save sleep parameter in strategy file. # Arguments: OBJ($self) NUM($sleep_count) # Side Effects: update strategy file. # Return Value: none sub set_sleep_count { my ($self, $sleep_count) = @_; my $id = $self->id(); my $strategy_file = $self->strategy_file_path($id); use IO::Adapter; my $io = new IO::Adapter $strategy_file; $io->touch(); $io->open(); $io->delete("SLEEP"); $io->add("SLEEP", [ $sleep_count ]); $io->close(); } # Descriptions: get sleep parameter from strategy file. # Arguments: OBJ($self) # Side Effects: none # Return Value: NUM sub get_sleep_count { my ($self) = @_; my $id = $self->id(); my $strategy_file = $self->strategy_file_path($id); if (-f $strategy_file) { use IO::Adapter; my $io = new IO::Adapter $strategy_file; $io->open(); my $sleep_count = $io->find("SLEEP") || 0; $sleep_count =~ s/^.*SLEEP\s+//; $sleep_count =~ s/\s*$//; $io->close(); return $sleep_count; } else { return 0; } } # Descriptions: save retry parameter in strategy file. # Arguments: OBJ($self) NUM($retry_count) # Side Effects: update strategy file. # Return Value: none sub set_retry_count { my ($self, $retry_count) = @_; my $id = $self->id(); my $strategy_file = $self->strategy_file_path($id); use IO::Adapter; my $io = new IO::Adapter $strategy_file; $io->touch(); $io->open(); $io->delete("RETRY"); $io->add("RETRY", [ $retry_count ]); $io->close(); } # Descriptions: get retry parameter from strategy file. # Arguments: OBJ($self) # Side Effects: none # Return Value: NUM sub get_retry_count { my ($self) = @_; my $id = $self->id(); my $strategy_file = $self->strategy_file_path($id); if (-f $strategy_file) { use IO::Adapter; my $io = new IO::Adapter $strategy_file; $io->open(); my $retry_count = $io->find("RETRY") || 0; $retry_count =~ s/^.*RETRY\s+//; $retry_count =~ s/\s*$//; $io->close(); return $retry_count; } else { return 0; } } =head1 LOGGING INTERFACE =cut # Descriptions: log interface. # Arguments: OBJ($self) STR($buf) # Side Effects: none # Return Value: none sub _log { my ($self, $buf) = @_; $self->log("qmgr: $buf"); } # Descriptions: log interface. # Arguments: OBJ($self) STR($buf) # Side Effects: none # Return Value: none sub _logerror { my ($self, $buf) = @_; $self->logerror("qmgr: $buf"); } # Descriptions: log interface. # Arguments: OBJ($self) STR($buf) # Side Effects: none # Return Value: none sub _logdebug { my ($self, $buf) = @_; $self->logdebug("qmgr: $buf"); } =head1 CLEAN UP GARBAGES =head2 expire($class) remove too old queue files in the specified queue. expire all queue if $class unspecified. =head2 cleanup() remove too old incoming queue files. =cut # Descriptions: remove too old queue files in the $class queue. # Arguments: OBJ($self) STR($class) # Side Effects: remove too old incoming queue files. # Return Value: none sub expire { my ($self, $class) = @_; if ($class) { $self->_cleanup($class); } else { for my $class (@class_list, @local_class_list) { $self->_cleanup($class); } } } # Descriptions: remove too old incoming queue files. # Arguments: OBJ($self) # Side Effects: remove too old incoming queue files. # Return Value: none sub cleanup { my ($self) = @_; $self->_cleanup("incoming"); } # Descriptions: remove too old queue files in $class queue. # Arguments: OBJ($self) STR($class) # Side Effects: remove too old incoming queue files. # Return Value: none sub _cleanup { my ($self, $class) = @_; my $how_old = $self->get_expire_limit(); use DirHandle; use File::stat; my $fp = sprintf("%s_dir_path", $class); my $queue_dir = $self->can($fp) ? $self->$fp() : $self->local_dir_path($class); my $dh = new DirHandle $queue_dir; if (defined $dh) { my ($fp, $file, $entry, $stat); my $limit = time - $how_old; ENTRY: while ($entry = $dh->read()) { next ENTRY if $entry =~ /^\./o; $fp = sprintf("%s_file_path", $class); $file = $self->can($fp) ? $self->$fp($entry) : $self->local_file_path($class, $entry); next ENTRY unless -f $file; $stat = stat($file); if (defined $stat && $stat->mtime < $limit) { $self->_logdebug("remove too old queue: qid=$entry"); $self->remove($entry); # remove all files in all queue. unlink $file; unless (-f $file) { $self->_log("old $class queue removed qid=$entry"); } else { $self->_logerror("cannot remove $class queue qid=$entry"); } } } $dh->close(); } } # Descriptions: set expire_limit. # Arguments: OBJ($self) NUM($limit) # Side Effects: update $self. # Return Value: none sub set_expire_limit { my ($self, $limit) = @_; $self->{ _expire_limit } = $limit || 14*24*3600; } # Descriptions: get expire_limit. # Arguments: OBJ($self) # Side Effects: none # Return Value: NUM sub get_expire_limit { my ($self) = @_; return( $self->{ _expire_limit } || 14*24*3600 ); } =head1 DEBUG =cut if ($0 eq __FILE__) { my $queue_dir = shift @ARGV; my $queue = new Mail::Delivery::Queue { directory => $queue_dir }; $queue->set_policy("fair-queue"); my $fp = sub { print STDERR "LOG> ", @_, "\n"; }; $queue->set_log_function($fp); print "\n1. queue_id = ", $queue->id(), "\n"; use Mail::Message; my $msg = Mail::Message->parse({ file => "../testmails/text=plain" }); $queue->add($msg); my $ra = $queue->list_all() || []; for my $qid (@$ra) { $queue->log("wakeup_queue($qid)"); $queue->wakeup_queue($qid); } print "\n\n"; print "\n2. list up active queue in $queue_dir\n"; $ra = $queue->list() || []; for my $q (@$ra) { print "\t", $q, "\n"; } print "\n\n"; print "\n3. list up all queue in $queue_dir\n"; $ra = $queue->list_all() || []; for my $q (@$ra) { print "\t", $q, "\n"; } print "\n\n"; print "\n4. expire\n"; print "4.1 expire()\n"; $queue->expire(); print "4.2 expire(CLASS)\n"; CLASS: for my $class (@class_list, "submitted") { next CLASS if $class eq 'lock'; $queue->expire($class); } print "\n\n"; } =head1 CODING STYLE See C on fml coding style guide. =head1 AUTHOR Ken'ichi Fukamachi =head1 COPYRIGHT Copyright (C) 2001,2002,2003,2004,2005,2006,2008 Ken'ichi Fukamachi All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 HISTORY Mail::Delivery::Queue first appeared in fml8 mailing list driver package. See C for more details. =cut 1;