File Coverage

blib/lib/Mail/Queue/DB.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             #
3             # Mail::Queue::DB - cache outgoing mail locally to a Berkley DB
4             #
5             # Copyright (C) 2004 S. Zachariah Sprackett
6             #
7             # This program is free software; you can redistribute it and/or modify
8             # it under the terms of the GNU General Public License as published by
9             # the Free Software Foundation; either version 2 of the License, or
10             # (at your option) any later version.
11             #
12             # This program is distributed in the hope that it will be useful,
13             # but WITHOUT ANY WARRANTY; without even the implied warranty of
14             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15             # GNU General Public License for more details.
16             #
17             # You should have received a copy of the GNU General Public License
18             # along with this program; if not, write to the Free Software
19             # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20             #
21              
22             package Mail::Queue::DB;
23              
24             require 5.004;
25              
26 1     1   1212 use vars qw($VERSION $DBVERSION @EXPORT_OK);
  1         3  
  1         95  
27              
28             $VERSION = '0.03';
29             $DBVERSION = '0.01';
30             @EXPORT_OK = qw();
31 1     1   7 use constant DEBUG => 1;
  1         2  
  1         61  
32              
33 1     1   24 use strict;
  1         2  
  1         34  
34 1     1   476 use DB_File::Lock;
  0            
  0            
35             use Fcntl qw(:flock O_RDWR O_CREAT O_RDONLY);
36             use Carp qw(croak carp);
37              
38             =head1 NAME
39              
40             Mail::Queue::DB - store outgoing email locally in a Berkely DB
41              
42             =head1 SYNOPSIS
43              
44             use Mail::Queue::DB;
45             my $d = new Mail::Queue:DB( db_file => '.database');
46              
47             print $d->count_queue() . " messages in the queue.\n";
48             my $msg_id = $d->queue_mail($args, $msg);
49              
50             $z->dequeue_mail($msg_id);
51             print $d->count_queue() . " messages in the queue.\n";
52              
53             =head1 DESCRIPTION
54              
55             Mail::Queue::DB allows one to create a local outgoing email store in
56             Berkely DB format. This mail can then be flushed over SSH or some other
57             connection to an appropriate mailhost. This module and the associated
58             tools work well on systems like laptops that need to be able to send
59             mail while offline. It was designed to be complementary to OfflineIMAP.
60              
61             =head1 METHODS
62            
63             =head2 new(db_file => $file)
64              
65             Creates a new Mail::Queue::DB object. The argument db_file must be defined.
66              
67             =cut
68             sub new {
69             my $class = shift;
70              
71             my $self = bless {
72             @_,
73             }, $class;
74              
75             croak "db_file is not defined" unless $self->{db_file};
76              
77             return $self;
78             }
79              
80             =head2 queue_mail($args, $msg)
81              
82             Adds a new message to the queue. Args must contain the arguments required
83             to pass to sendmail to actually send the email. Typically, these arguments
84             will be something like: -oem -oi -- user@example.com
85              
86             Msg contains the actual email message to be transmitted.
87              
88             On success, the message id of the newly queued email will be returned. On
89             failure, queue_mail() returns undef
90              
91             =cut
92             sub queue_mail {
93             my ($self, $mailargs, $msg) = @_;
94              
95             croak "Unable to lock database for writing" if (_db_write_lock($self, 1));
96              
97             my $id;
98             do {
99             $id = _gen_msg_id(8);
100             } while (exists $self->{dbh}{'message-' . $id});
101              
102             $self->{dbh}{'args-' . $id} = $mailargs;
103             $self->{dbh}{'message-' . $id} = $msg;
104              
105             # Add this new message to the index
106             $self->{dbh}{message_ids} = '' unless(exists $self->{dbh}{message_ids});
107             $self->{dbh}{message_ids} .= ',' if (length($self->{dbh}{message_ids}));
108             $self->{dbh}{message_ids} .= $id;
109              
110             _db_unlock($self);
111             }
112              
113             =head2 queue_mail($id, $have_lock)
114              
115             Deletes a message from the queue. Id must contain a valid message id.
116             dequeue_mail() will attempt to attain a write lock on the database unless
117             the boolean value have_lock is set.
118              
119             On success, queue_mail() returns 0. On failure, it returns a negative
120             value.
121              
122             =cut
123             sub dequeue_mail {
124             my ($self, $id, $have_lock) = (@_);
125              
126             if (!$have_lock) {
127             croak "Unable to lock database for writing" if (_db_write_lock($self));
128             } elsif ($self->{lock} !~ /^write$/) {
129             croak "dequeue_mail() called with \$have_lock set but no write lock.";
130             }
131              
132             my %msgs;
133             foreach my $t (split /,/, $self->{dbh}{message_ids}) {
134             $msgs{$t} = 1;
135             }
136             # if it doesn't exist, return fail
137             if(!$msgs{$id}) {
138             _db_unlock($self);
139             return -1;
140             }
141              
142             # if it exists purge it
143             delete $self->{dbh}{'message-' . $id};
144             delete $self->{dbh}{'args-' . $id};
145             delete $msgs{$id};
146              
147             # rewrite the message index
148             $self->{dbh}{message_ids} = join(',', sort keys %msgs);
149             if (!$have_lock) {
150             _db_unlock($self);
151             }
152             return 0;
153             }
154              
155             =head2 get_mail($id)
156              
157             Fetches the message identified by Id from the queue. On success, it returns
158             an array of Args, Msg. On failure it returns undef.
159              
160             =cut
161             sub get_mail {
162             my ($self, $id) = @_;
163              
164             croak "get_mail() requires a message id." unless $id;
165             croak "Unable to lock database for reading" if (_db_read_lock($self));
166              
167             my %msgs;
168             foreach my $t (split /,/, $self->{dbh}{message_ids}) {
169             $msgs{$t} = 1;
170             }
171              
172             return undef unless $msgs{$id};
173             return($self->{dbh}{'args-' . $id}, $self->{dbh}{'message-' . $id})
174             }
175              
176             =head2 iterate_queue($callback, $locking)
177              
178             For each message in the queue, run the passed callback function.
179             Lock state specifies the lock to hold for the entire iteration run. It can
180             be one of either read or write. If not specified, a read lock is assumed.
181              
182             The passed in callback will receive arguments in the form
183             callback( $id, $args, $msg )
184              
185             =cut
186             sub iterate_queue {
187             my ($self, $callback, $locking) = (@_);
188              
189             if (!$locking) {
190             $locking = 'read';
191             }
192              
193             if ($locking =~ /^read$/) {
194             croak "Unable to lock database for reading" if (_db_read_lock($self));
195             } elsif ($locking =~ /^write$/) {
196             croak "Unable to lock database for writing" if (_db_write_lock($self));
197             } else {
198             croak "Lock state must be either read or write. Invalid state [$locking]";
199             }
200              
201             foreach my $id (split /,/, $self->{dbh}{message_ids}) {
202             &$callback($id, $self->{dbh}{'args-' . $id},
203             $self->{dbh}{'message-' . $id});
204             }
205              
206             _db_unlock($self);
207             }
208              
209             =head2 count_queue( )
210              
211             Returns an integer representing the number of emails currently in the
212             queue.
213              
214             =cut
215             sub count_queue {
216             my ($self) = (@_);
217              
218             croak "Unable to lock database for reading" if (_db_read_lock($self));
219              
220             my $count = 0;
221             foreach my $id (split /,/, $self->{dbh}{message_ids}) {
222             $count++;
223             }
224              
225             _db_unlock($self);
226             return $count;
227             }
228             #
229             # tie and lock $self->{dbh} for writing
230             #
231             # args: $create - boolean. specifies whether or not to use O_CREAT
232             sub _db_write_lock {
233             my ($self, $create) = (@_);
234              
235             my $flags;
236             if ($create) {
237             $flags = O_CREAT|O_RDWR;
238             } else {
239             $flags = O_RDWR;
240             }
241              
242             if ($self->{lock} && length($self->{lock})) {
243             croak "Attempt to write_lock database, but it is already locked for "
244             . $self->{lock};
245             }
246              
247             tie (%{$self->{dbh}}, "DB_File::Lock", $self->{db_file},
248             $flags, 0600, $DB_HASH, 'write') || return -1;
249              
250             if (
251             exists $self->{dbh}{'database_version'} &&
252             length($self->{dbh}{'database_version'})
253             ) {
254             croak "Database version mismatch want $DBVERSION got "
255             . $self->{dbh}{'database_version'}
256             if ($DBVERSION ne $self->{dbh}{'database_version'});
257             } else {
258             $self->{dbh}{'database_version'} = $DBVERSION;
259             }
260              
261             $self->{lock} = 'write';
262             return 0;
263             }
264              
265             #
266             # tie and lock $self->{dbh} for reading
267             #
268             sub _db_read_lock {
269             my ($self) = (@_);
270              
271             if ($self->{lock} && length($self->{lock})) {
272             croak "Attempt to read_lock database, but it is already locked for "
273             . $self->{lock};
274             }
275              
276             tie (%{$self->{dbh}}, "DB_File::Lock", $self->{db_file},
277             O_RDONLY, 0600, $DB_HASH, 'read') || return -1;
278              
279             if (!$self->{dbh}{'database_version'} ||
280             $self->{dbh}{'database_version'} ne $DBVERSION) {
281             croak "Database version mismatch want $DBVERSION got "
282             . ($self->{dbh}{'database_version'} ?
283             $self->{dbh}{'database_version'} : "undefined");
284             }
285              
286             $self->{lock} = 'read';
287             return 0;
288             }
289              
290             sub _db_unlock {
291             my ($self) = (@_);
292              
293             if ($self->{dbh}) {
294             untie $self->{dbh};
295             delete $self->{dbh};
296             }
297             if ($self->{lock}) {
298             delete $self->{lock};
299             }
300              
301             return 0;
302             }
303              
304             sub _gen_msg_id {
305             my $len = shift;
306             my $v = "1234567890abcdefghijklmnopqrstuvwxyz";
307             my $str;
308              
309             $len = 8 unless $len;
310             while($len--) {
311             $str .= substr($v, rand(length($v)), 1);
312             }
313             return $str;
314             }
315             1;
316             __END__