File Coverage

blib/lib/Sendmail/Queue.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Sendmail::Queue;
2 3     3   323933 use strict;
  3         9  
  3         107  
3 3     3   17 use warnings;
  3         8  
  3         210  
4 3     3   19 use Carp;
  3         10  
  3         293  
5 3     3   42 use 5.8.0;
  3         11  
  3         229  
6              
7             our $VERSION = '0.800';
8              
9 3     3   1924 use Sendmail::Queue::Qf;
  0            
  0            
10             use Sendmail::Queue::Df;
11             use File::Spec;
12             use IO::Handle;
13             use Fcntl qw( :flock );
14             use File::Temp qw(tempfile);
15             my $fcntl_struct = 's H60';
16             my $fcntl_structlockp = pack($fcntl_struct, Fcntl::F_WRLCK,
17             "000000000000000000000000000000000000000000000000000000000000");
18             my $fcntl_structunlockp = pack($fcntl_struct, Fcntl::F_UNLCK,
19             "000000000000000000000000000000000000000000000000000000000000");
20              
21             use Sendmail::Queue::Base;
22             our @ISA = qw( Sendmail::Queue::Base );
23             __PACKAGE__->make_accessors(qw(
24             queue_directory
25             qf_directory
26             df_directory
27             ));
28              
29             =head1 NAME
30              
31             Sendmail::Queue - Manipulate Sendmail queues directly
32              
33             =head1 SYNOPSIS
34              
35             use Sendmail::Queue;
36              
37             # The high-level interface:
38             #
39             # Create a new queue object. Throws exception on error.
40             my $q = Sendmail::Queue->new({
41             queue_directory => '/var/spool/mqueue'
42             });
43              
44             # Queue one copy of a message (one qf, one df)
45             my $id = $q->queue_message({
46             sender => 'user@example.com',
47             recipients => [
48             'first@example.net',
49             'second@example.org',
50             ],
51             data => $string_or_object,
52             });
53              
54             # Queue multiple copies of a message using multiple envelopes, but
55             # the same body. Results contain the envelope name as key,
56             # and the queue ID as the value.
57             my %results = $q->queue_multiple({
58             sender => 'user@example.com',
59             envelopes => {
60             'envelope one' => {
61             sender => 'differentuser@example.com',
62             recipients => [
63             'first@example.net',
64             'second@example.org',
65             ],
66             },
67             'envelope two' => {
68             recipients => [
69             'third@example.net',
70             'fourth@example.org',
71             ],
72             }
73             },
74             data => $string_or_object,
75             });
76              
77             # The low-level interface:
78              
79             # Create a new qf file object
80             my $qf = Sendmail::Queue::Qf->new();
81              
82             # Generate a Sendmail 8.12-compatible queue ID
83             $qf->create_and_lock();
84              
85             my $df = Sendmail::Queue::Df->new();
86              
87             # Need to give it the same queue ID as your $qf
88             $df->set_queue_id( $qf->get_queue_id );
89             $df->set_data( $some_body );
90              
91             # Or....
92             $df->set_data_from( $some_fh );
93              
94             # Or, if you already have a file...
95             my $second_df = Sendmail::Queue::Df->new();
96             $second_df->set_queue_id( $qf->get_queue_id );
97             $second_df->hardlink_to( $df ); # Need better name
98              
99             $qf->set_sender('me@example.com');
100             $qf->add_recipient('you@example.org');
101              
102             $q->enqueue( $qf, $df );
103              
104             =head1 DESCRIPTION
105              
106             Sendmail::Queue provides a mechanism for directly manipulating Sendmail queue files.
107              
108             =head1 METHODS
109              
110             =head2 new ( \%args )
111              
112             Create a new Sendmail::Queue object.
113              
114             Required arguments are:
115              
116             =over 4
117              
118             =item queue_directory
119              
120             The queue directory to use. Should (usually) be the same as your
121             Sendmail QueueDirectory variable for the client submission queue.
122              
123             =back
124              
125             =cut
126              
127             sub new
128             {
129             my ($class, $args) = @_;
130              
131             $args ||= {};
132              
133             if(!exists $args->{queue_directory}) {
134             die q{queue_directory argument must be provided};
135             }
136              
137             my $self = { queue_directory => $args->{queue_directory}, };
138              
139             $self->{lock_both} = 0;
140              
141             bless $self, $class;
142              
143             if(!-d $self->{queue_directory}) {
144             die q{ Queue directory doesn't exist};
145             }
146              
147             if(-d File::Spec->catfile($self->{queue_directory}, 'qf')) {
148             $self->set_qf_directory(File::Spec->catfile($self->{queue_directory}, 'qf'));
149             } else {
150             $self->set_qf_directory(File::Spec->catfile($self->{queue_directory}));
151             }
152              
153             if(-d File::Spec->catfile($self->{queue_directory}, 'df')) {
154             $self->set_df_directory(File::Spec->catfile($self->{queue_directory}, 'df'));
155             } else {
156             $self->set_df_directory(File::Spec->catfile($self->{queue_directory}));
157             }
158              
159             # Check if both fcntl-style and flock-style locking is available
160             my ($fh, $filename) = tempfile();
161             if ($fh) {
162             my $flock_status = flock($fh, LOCK_EX | LOCK_NB);
163             my $fcntl_status = fcntl($fh, Fcntl::F_SETLK, $fcntl_structlockp);
164             if ($flock_status && $fcntl_status) {
165             $self->{lock_both} = 1;
166             }
167             $fh->close();
168             }
169             unlink($filename) if $filename;
170             return $self;
171             }
172              
173             =head2 queue_message ( $args )
174              
175             High-level interface for queueing a message. Creates qf and df files
176             in the object's queue directory using the arguments provided.
177              
178             Returns the queue ID for the queued message.
179              
180             Required arguments:
181              
182             =over 4
183              
184             =item sender
185              
186             Envelope sender for message.
187              
188             =item recipients
189              
190             Array ref containing one or more recipients for this message.
191              
192             =item data
193              
194             Scalar containing message headers and body, in RFC-2822 format (ASCII
195             text, headers separated from body by \n\n).
196              
197             Data should use local line-ending conventions (as used by Sendmail) and
198             not the \r\n used on the wire for SMTP.
199              
200             =back
201              
202             Optional arguments may be specified as well. These will be handed off
203             directly to the underlying Sendmail::Queue::Qf object:
204              
205             =over 4
206              
207             =item product_name
208              
209             Name to use for this product in the generated Recieved: header. May be
210             set to blank or undef to disable. Defaults to 'Sendmail::Queue'.
211              
212             =item helo
213              
214             The HELO or EHLO name provided by the host that sent us this message,
215             or undef if none. Defaults to undef.
216              
217             =item relay_address
218              
219             The IP address of the host that sent us this message, or undef if none.
220             Defaults to undef.
221              
222             =item relay_hostname
223              
224             The name of the host that sent us this message, or undef if none.
225             Defaults to undef.
226              
227             =item local_hostname
228              
229             The name of the host that received this message. Defaults to 'localhost'
230              
231             =item protocol
232              
233             Protocol over which this message was received. Valid values are blank,
234             SMTP, and ESMTP. Default is blank.
235              
236             =item timestamp
237              
238             A UNIX seconds-since-epoch timestamp. If omitted, defaults to current time.
239              
240             =item macros
241              
242             A hash reference containing Sendmail macros that should be set in the resulting
243             queue file.
244              
245             The names of macros should be the bare name, as the module will add the leading
246             $ and any surrounding {} necessary for multi-character macro names.
247              
248             If omitted, the '$r' macro will be set to the 'protocol' value. Other macros will
249             not be set by default.
250              
251             =back
252              
253             On error, this method may die() with a number of different runtime errors.
254              
255             =cut
256              
257             # FUTURE: use an exception class?
258              
259             sub queue_message
260             {
261             my ($self, $args) = @_;
262              
263             foreach my $argname qw( sender recipients data ) {
264             die qq{$argname argument must be specified} unless exists $args->{$argname}
265              
266             }
267              
268             if( ref $args->{data} ) {
269             die q{data as an object not yet supported};
270             }
271              
272             $args->{envelopes} = {
273             single_envelope => {
274             recipients => delete $args->{recipients}
275             }
276             };
277              
278             my $result = $self->queue_multiple( $args );
279              
280             return $result->{single_envelope};
281             }
282              
283             =head2 enqueue ( $qf, $df )
284              
285             Enqueue a message, given a L object and a
286             L object.
287              
288             This method is mostly for internal use. You should probably use
289             C or C instead.
290              
291             Returns true if queuing was successful. Otherwise, cleans up any qf
292             and df data that may have been written to disk, and rethrows any
293             exception that may have occurred.
294              
295             =cut
296              
297             =for internal doc
298              
299             Here are the file ops (from inotify) on a /usr/sbin/sendmail enqueuing:
300              
301             /var/spool/mqueue-client/ CREATE dfo2JEQb7J002161
302             /var/spool/mqueue-client/ OPEN dfo2JEQb7J002161
303             /var/spool/mqueue-client/ MODIFY dfo2JEQb7J002161
304             /var/spool/mqueue-client/ CLOSE_WRITE,CLOSE dfo2JEQb7J002161
305             /var/spool/mqueue-client/ OPEN dfo2JEQb7J002161
306             /var/spool/mqueue-client/ CREATE qfo2JEQb7J002161
307             /var/spool/mqueue-client/ OPEN qfo2JEQb7J002161
308             /var/spool/mqueue-client/ MODIFY qfo2JEQb7J002161
309             /var/spool/mqueue-client/ CREATE tfo2JEQb7J002161
310             /var/spool/mqueue-client/ OPEN tfo2JEQb7J002161
311             /var/spool/mqueue-client/ MODIFY tfo2JEQb7J002161
312             /var/spool/mqueue-client/ MOVED_FROM tfo2JEQb7J002161
313             /var/spool/mqueue-client/ MOVED_TO qfo2JEQb7J002161
314             /var/spool/mqueue-client/ OPEN,ISDIR
315             /var/spool/mqueue-client/ CLOSE_NOWRITE,CLOSE,ISDIR
316             /var/spool/mqueue-client/ CLOSE_WRITE,CLOSE qfo2JEQb7J002161
317             /var/spool/mqueue-client/ CLOSE_NOWRITE,CLOSE dfo2JEQb7J002161
318              
319              
320             =cut
321              
322             sub enqueue
323             {
324             my ($self, $qf, $df) = @_;
325              
326             eval {
327             $df->write();
328             $qf->write();
329             $qf->sync();
330             $qf->close();
331             };
332             if( $@ ) { ## no critic
333             $df->unlink();
334             $qf->unlink();
335              
336             # Rethrow the exception after cleanup
337             die $@;
338             }
339              
340             return 1;
341             }
342              
343              
344             =head2 queue_multiple ( $args )
345              
346             Queue multiple copies of a message using multiple envelopes, but the
347             same body.
348              
349             Returns a results hash containing the recipient set name as key, and the
350             queue ID as the value.
351              
352              
353             my %results = $q->queue_multiple({
354             envelopes => {
355             'envelope one' => {
356             sender => 'user@example.com',
357             recipients => [
358             'first@example.net',
359             'second@example.org',
360             ],
361             }
362             'envelope two' => {
363             sender => 'user@example.com',
364             recipients => [
365             'third@example.net',
366             'fourth@example.org',
367             ],
368             }
369             },
370             data => $string_or_object,
371             });
372              
373             In the event that we cannot create a queue file for ANY of the envelopes, we
374             die() with an appropriate error after unlinking all created queue files --
375             either all succeed, or none succeed.
376              
377             =cut
378              
379             sub queue_multiple
380             {
381             my ($self, $args) = @_;
382              
383             foreach my $argname qw( envelopes data ) {
384             die qq{$argname argument must be specified} unless exists $args->{$argname}
385             }
386              
387             if( ref $args->{data} ) {
388             die q{data as an object not yet supported};
389             }
390              
391             my ($headers, $data) = split(/\n\n/, $args->{data}, 2);
392              
393             my $qf = Sendmail::Queue::Qf->new({
394             queue_directory => $self->get_qf_directory(),
395             });
396              
397             # m// match is faster than tr/// for any case where there's an 8-bit
398             # character before the end of the file, and is not significantly
399             # slower in the case of no 8-bit characters.
400             if( $data =~ m/[\200-\377]/o ) {
401             # EF_HAS8BIT flag bit gets set if we have 8 bit characters in body.
402             $qf->set_data_is_8bit(1);
403             }
404              
405             # Allow passing of optional info down to Qf object
406             foreach my $optarg qw( product_name helo relay_address relay_hostname local_hostname protocol timestamp macros ) {
407             if( exists $args->{$optarg} ) {
408             my $method = 'set_' . $optarg;
409             $qf->$method($args->{$optarg} );
410             }
411             }
412              
413             # Prepare a generic queue file
414             $qf->set_headers( $headers );
415              
416             my $first_df;
417             my @queued_qfs = ();
418              
419             my %results;
420              
421             # Now, loop over all of the rest
422             # FUTURE: validate data in the envelopes sections?
423             eval {
424             while( my($env_name, $env_data) = each %{ $args->{envelopes} } ) {
425             my $cur_qf = $qf->clone();
426              
427             my $sender = exists $env_data->{sender}
428             ? $env_data->{sender}
429             : exists $args->{sender}
430             ? $args->{sender}
431             : die q{no 'sender' available};
432              
433             $cur_qf->set_sender( $sender );
434             $cur_qf->add_recipient( @{ $env_data->{recipients} } );
435             $cur_qf->create_and_lock($self->{lock_both});
436              
437             # As soon as it's created, put it on the list so it can
438             # be cleaned up later if necessary.
439             push @queued_qfs, $cur_qf;
440              
441             $cur_qf->synthesize_received_header();
442             $cur_qf->write();
443             $cur_qf->sync();
444              
445             my $cur_df = Sendmail::Queue::Df->new({
446             queue_directory => $self->get_df_directory(),
447             queue_id => $cur_qf->get_queue_id(),
448             });
449             if( ! $first_df ) {
450             # If this is the first one, create and write
451             # the df file
452             $first_df = $cur_df;
453             $first_df->set_data( $data );
454             $first_df->write();
455             } else {
456             # Otherwise, link to the first df
457             eval { $cur_df->hardlink_to( $first_df->get_data_filename() ); };
458             if ($@) {
459             if ($@ =~ /Path .* does not exist/) {
460             # This should NEVER happen...
461             # but it was observed to happen!
462             # Sorry to spew to STDERR, but there's no
463             # feasible way to log this
464             print STDERR 'Sendmail::Queue warning: ' . $first_df->get_data_filename() . ' has disappeared! Writing new file as ' . $cur_df->get_data_filename() . "\n";
465             $first_df = $cur_df;
466             $first_df->set_data($data);
467             $first_df->write();
468             } else {
469             die($@);
470             }
471             }
472             }
473              
474             $results{ $env_name } = $cur_qf->get_queue_id;
475             }
476              
477             $self->sync();
478              
479             # Close the queue files to release the locks
480             $_->close() for (@queued_qfs);
481             };
482             if( $@ ) {
483             # Something bad happened... wrap it all up and re-throw
484             for my $qf (@queued_qfs) {
485             my $df = Sendmail::Queue::Df->new({
486             queue_directory => $self->get_df_directory(),
487             queue_id => $qf->get_queue_id(),
488             });
489             $df->unlink;
490             $qf->unlink;
491             }
492             die $@;
493             }
494              
495             return \%results;
496             }
497              
498             =head2 sync ( )
499              
500             Ensure that the queue directories have been synced.
501              
502             =cut
503              
504             sub sync
505             {
506             my ($self) = @_;
507              
508             # Evil hack. Why? Well:
509             # - you can't fsync() a filehandle directly, you must use
510             # IO::Handle->sync
511             # so, we have to sysopen to a filehandle glob, and then fdopen
512             # the fileno we get from that glob.
513             # FUTURE: File::Sync::fsync() can sync directories directly, but isn't core perl.
514             # TODO: this needs testing on solaris and bsd
515             my $directory = $self->get_df_directory();
516              
517             sysopen(DIR_FH, $directory, Fcntl::O_RDONLY) or die qq{Couldn't sysopen $directory: $!};
518              
519             my $handle = IO::Handle->new();
520             $handle->fdopen(fileno(DIR_FH), 'w') or die qq{Couldn't fdopen the directory handle: $!};
521             $handle->sync or die qq{Couldn't sync: $!};
522             $handle->close;
523              
524             close(DIR_FH);
525              
526             return 1;
527             }
528              
529             1;
530             __END__