File Coverage

blib/lib/Sendmail/Queue/Qf.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             package Sendmail::Queue::Qf;
2 4     4   143080 use strict;
  4         9  
  4         126  
3 4     4   24 use warnings;
  4         6  
  4         102  
4 4     4   19 use Carp;
  4         9  
  4         281  
5              
6 4     4   25 use Scalar::Util qw(blessed);
  4         4  
  4         288  
7 4     4   22 use File::Spec;
  4         9  
  4         114  
8 4     4   3819 use IO::File;
  4         15277  
  4         635  
9 4     4   22721 use Time::Local ();
  4         15271  
  4         196  
10 4     4   28 use Fcntl qw( :flock );
  4         7  
  4         777  
11 4     4   1030 use Errno qw( EEXIST );
  4         1779  
  4         636  
12 4     4   7410 use Mail::Header::Generator ();
  0            
  0            
13             use Storable ();
14              
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             ## no critic 'ProhibitMagicNumbers'
22              
23             # TODO: should we fail if total size of headers > 32768 bytes, or let sendmail die?
24              
25             use Sendmail::Queue::Base;
26             our @ISA = qw( Sendmail::Queue::Base );
27             __PACKAGE__->make_accessors(qw(
28             queue_id
29             queue_fh
30             queue_directory
31             sender
32             recipients
33             headers
34             timestamp
35             product_name
36             helo
37             relay_address
38             relay_hostname
39             local_hostname
40             protocol
41             received_header
42             priority
43             qf_version
44             data_is_8bit
45             user
46             macros
47             ));
48              
49             =head1 NAME
50              
51             Sendmail::Queue::Qf - Represent a Sendmail qfXXXXXXXX (control) file
52              
53             =head1 SYNOPSIS
54              
55             use Sendmail::Queue::Qf;
56              
57             # Create a new qf file object
58             my $qf = Sendmail::Queue::Qf->new({
59             queue_directory => $dir
60             });
61              
62             # Creates a new qf file, locked.
63             $qf->create_and_lock();
64              
65             $qf->set_sender('me@example.com');
66             $qf->add_recipient('you@example.org');
67              
68             $qf->set_headers( $some_header_data );
69              
70             # Add a received header using the information already provided
71             $qf->synthesize_received_header();
72              
73             $qf->write( );
74              
75             $qf->sync();
76              
77             $qf->close();
78              
79             =head1 DESCRIPTION
80              
81             Sendmail::Queue::Qf provides a representation of a Sendmail qf file.
82              
83             =head1 METHODS
84              
85             =head2 new ( \%args )
86              
87             Create a new Sendmail::Queue::Qf object.
88              
89             =cut
90              
91             sub new
92             {
93             my ($class, $args) = @_;
94              
95             my $self = {
96             headers => '',
97             recipients => [],
98             product_name => 'Sendmail::Queue',
99             local_hostname => 'localhost',
100             timestamp => time,
101             priority => 30000,
102             macros => {},
103              
104             # This code generates V6-compatible qf files to work
105             # with Sendmail 8.12.
106             qf_version => '6',
107             %{ $args || {} }, };
108              
109             bless $self, $class;
110              
111             return $self;
112             }
113              
114             {
115             my @base_60_chars = ( 0..9, 'A'..'Z', 'a'..'x' );
116             sub _generate_queue_id_template
117             {
118             my ($time) = @_;
119             $time = time unless defined $time;
120             my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime( $time );
121              
122             # First char is year minus 1900, mod 60
123             # (perl's localtime conveniently gives us the year-1900 already)
124             # 2nd and 3rd are month, day
125             # 4th through 6th are hour, minute, second
126             # 7th and 8th characters are a random sequence number
127             # (to be filled in later)
128             # 9th through 14th are the PID
129             my $tmpl = join('', @base_60_chars[
130             $year % 60,
131             $mon,
132             $mday,
133             $hour,
134             $min,
135             $sec],
136             '%2.2s',
137             sprintf('%06d', $$)
138             );
139              
140             return $tmpl;
141             }
142              
143             sub _fill_template
144             {
145             my ($template, $seq_number) = @_;
146              
147             return sprintf $template,
148             $base_60_chars[ int($seq_number / 60) ] . $base_60_chars[ $seq_number % 60 ];
149             }
150             }
151              
152             =head2 create_and_lock ( [$lock_both] )
153              
154             Generate a Sendmail 8.12-compatible queue ID, and create a locked qf
155             file with that name. If $lock_both is true, we lock the file using
156             both fcntl and flock-style locking.
157              
158             See Bat Book 3rd edition, section 11.2.1 for information on how the
159             queue file name is generated.
160              
161             Note that we create the qf file directly, rather than creating an
162             intermediate tf file and renaming aftewards. This is all good and well
163             for creating /new/ qf files -- sendmail does it that way as well -- but
164             if we ever want to rewrite one, it's not safe.
165              
166             For future reference, Sendmail queuefile creation in queueup() inside
167             sendmail/queue.c does things in the same way -- newly-created queue files
168             are created directly with the qf prefix, then locked, then written.
169              
170             =cut
171              
172             sub create_and_lock
173             {
174             my ($self, $lock_both) = @_;
175              
176             if( ! -d $self->get_queue_directory ) {
177             die q{Cannot create queue file without queue directory!};
178             }
179              
180             # 7th and 8th is random sequence number
181             my $seq = int(rand(3600));
182              
183             my $tmpl = _generate_queue_id_template( $self->get_timestamp );
184              
185             my $iterations = 0;
186             while( ++$iterations < 3600 ) {
187             my $qid = _fill_template($tmpl, $seq);
188             my $path = File::Spec->catfile( $self->{queue_directory}, "qf$qid" );
189              
190             my $old_umask = umask(002);
191             my $fh = IO::File->new( $path, O_RDWR|O_CREAT|O_EXCL );
192             umask($old_umask);
193             if( $fh ) {
194             if( ! flock $fh, LOCK_EX | LOCK_NB ) {
195             # Opened but couldn't lock. This means we probably had:
196             # A: open (us, create)
197             # B: open (them, for read)
198             # B: lock (them, for read)
199             # A: lock (us, failed)
200             # so, give up on this one and try again
201             close($fh);
202             unlink($path);
203             $seq = ($seq + 1) % 3600;
204             next;
205             }
206             if ($lock_both && !fcntl($fh, Fcntl::F_SETLK, $fcntl_structlockp)) {
207             # See above... couldn't lock with fcntl
208             close($fh);
209             unlink($path);
210             $seq = ($seq + 1) % 3600;
211             next;
212             }
213             $self->set_queue_id( $qid );
214             $self->set_queue_fh( $fh );
215             last;
216             } elsif( $! == EEXIST ) {
217             # Try the next one
218             $seq = ($seq + 1) % 3600;
219             } else {
220             die qq{Error creating qf file $path: $!};
221             }
222              
223             }
224              
225             if ($iterations >= 3600 ) {
226             die q{Could not create queue file; too many iterations};
227             }
228              
229             return 1;
230             }
231              
232             # _tz_diff and _format_rfc2822_date borrowed from Email::Date. Why?
233             # Because they depend on Date::Parse and Time::Piece, and I don't want
234             # to add them as dependencies.
235             # Similar functions exist in MIMEDefang as well
236             sub _tz_diff
237             {
238             my ($time) = @_;
239              
240             my $diff = Time::Local::timegm(localtime $time)
241             - Time::Local::timegm(gmtime $time);
242              
243             my $direc = $diff < 0 ? '-' : '+';
244             $diff = abs $diff;
245             my $tz_hr = int( $diff / 3600 );
246             my $tz_mi = int( $diff / 60 - $tz_hr * 60 );
247              
248             return ($direc, $tz_hr, $tz_mi);
249             }
250              
251             sub _format_rfc2822_date
252             {
253             my ($time) = @_;
254             $time = time unless defined $time;
255              
256             my ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime $time;
257             my $day = (qw[Sun Mon Tue Wed Thu Fri Sat])[$wday];
258             my $month = (qw[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec])[$mon];
259             $year += 1900;
260              
261             my ($direc, $tz_hr, $tz_mi) = _tz_diff($time);
262              
263             sprintf '%s, %d %s %d %02d:%02d:%02d %s%02d%02d',
264             $day, $mday, $month, $year, $hour, $min, $sec, $direc, $tz_hr, $tz_mi;
265             }
266              
267             =head2 synthesize_received_header ( )
268              
269             Create a properly-formatted Received: header for this message, using
270             any data available from the object.
271              
272             The generated header is saved internally as 'received_header'.
273              
274             =cut
275              
276             sub synthesize_received_header
277             {
278             my ($self) = @_;
279              
280             my $g = Mail::Header::Generator->new();
281              
282             my $user = $self->get_user();
283             if(!$user) {
284             $user = getpwuid($>);
285             }
286              
287             $self->{received_header} = $g->received({
288             helo => $self->get_helo(),
289             hostname => $self->get_local_hostname(),
290             product_name => $self->get_product_name(),
291             protocol => ($self->get_protocol || ''),
292             queue_id => $self->get_queue_id(),
293             recipients => $self->get_recipients(),
294             relay_address => $self->get_relay_address(),
295             relay_hostname => $self->get_relay_hostname(),
296             sender => $self->get_sender(),
297             timestamp => $self->get_timestamp(),
298             user => $user
299             });
300              
301             return $self->{received_header};
302             }
303              
304             =head2 get_queue_filename
305              
306             Return the full path name of this queue file.
307              
308             Will return undef if no queue ID exists, and die if queue directory is
309             unset.
310              
311             =cut
312              
313             sub get_queue_filename
314             {
315             my ($self) = @_;
316              
317             if( ! $self->get_queue_directory ) {
318             die q{queue directory not set};
319             }
320              
321             if( ! $self->get_queue_id ) {
322             return undef;
323             }
324              
325             return File::Spec->catfile( $self->get_queue_directory(), 'qf' . $self->get_queue_id() );
326             }
327              
328             =head2 add_recipient ( $recipient [, $recipient, $recipient ] )
329              
330             Add one or more recipients to this object.
331              
332             =cut
333              
334             sub add_recipient
335             {
336             my ($self, @recips) = @_;
337              
338             push @{$self->{recipients}}, @recips;
339             }
340              
341             =head2 write ( )
342              
343             Writes a qfXXXXXXX file using the object's data.
344              
345             A path to create this queue file under must be provided, by first
346             calling ->set_queue_directory()
347              
348             =cut
349              
350             sub write
351             {
352             my ($self) = @_;
353              
354             my $fh = $self->get_queue_fh;
355              
356             if ( ! $fh || ! $fh->opened ) {
357             die q{write() cannot write without an open filehandle};
358             }
359              
360             foreach my $chunk (
361             $self->_format_qf_version(),
362             $self->_format_create_time(),
363             $self->_format_last_processed(),
364             $self->_format_times_processed(),
365             $self->_format_priority(),
366             $self->_format_flag_bits(),
367             $self->_format_macros(),
368             $self->_format_sender_address(),
369             $self->_format_recipient_addresses(),
370             $self->_format_headers(),
371             $self->_format_end_of_qf(),
372             ) {
373             if( ! $fh->print( $chunk, "\n" ) ) {
374             die q{Couldn't print to } . $self->get_queue_filename . ": $!";
375             }
376             }
377              
378             return 1;
379             }
380              
381             =head2 sync ( )
382              
383             Force any data written to the current filehandle to be flushed to disk.
384             Returns 1 on success, undef if no queue file is open, and will die on error.
385              
386             =cut
387              
388             sub sync
389             {
390             my ($self) = @_;
391              
392             my $fh = $self->get_queue_fh;
393              
394             if( ! $fh->opened ) {
395             return undef;
396             }
397              
398             if( ! $fh->flush ) {
399             croak q{Couldn't flush filehandle!};
400             }
401              
402             if( ! $fh->sync ) {
403             croak q{Couldn't sync filehandle!};
404             }
405              
406             return 1;
407             }
408              
409             =head2 close ( )
410              
411             Returns true on success, false (as undef) if filehandle wasn't open, or if
412             closing the filehandle fails, and dies if the internal filehandle is missing or
413             isn't a filehandle.
414              
415             =cut
416              
417             sub close
418             {
419             my ($self) = @_;
420              
421             my $fh = $self->get_queue_fh;
422              
423             if( ! ($fh && blessed $fh && $fh->isa('IO::Handle')) ) {
424             croak "get_queue_fh() returned something that isn't a filehandle";
425             }
426              
427             if( ! $fh->opened ) {
428             return undef;
429             }
430              
431             if( ! $fh->close ) {
432             return undef;
433             }
434              
435             return 1;
436             }
437              
438             =head2 clone ( )
439              
440             Return a clone of this Sendmail::Queue::Qf object, containing everything EXCEPT:
441              
442             =over 4
443              
444             =item * recipients
445              
446             =item * queue ID
447              
448             =item * open queue filehandle
449              
450             =item * synthesized Received: header
451              
452             =back
453              
454             =cut
455              
456             sub clone
457             {
458             my ($self) = @_;
459              
460             # Localize queue_fh first, as dclone() chokes on GLOB values, and we
461             # don't want it cloned anyway.
462             local $self->{queue_fh};
463              
464             my $clone = Storable::dclone( $self );
465              
466             # Now clobber the values that shouldn't persist across a clone. We
467             # set_recipients to [] as that's what the constructor does, and delete
468             # the rest.
469             $clone->set_recipients([]);
470             delete $clone->{$_} for qw( sender queue_id received_header queue_fh );
471              
472             return $clone;
473             }
474              
475             =head2 unlink ( )
476              
477             Unlink the queue file. Returns true (1) on success, false (undef) on
478             failure.
479              
480             Unlinking the queue file will only succeed if:
481              
482             =over 4
483              
484             =item *
485              
486             we have a queue directory and queue ID configured for this object
487              
488             =item *
489              
490             the queue file is open and locked
491              
492             =back
493              
494             Otherwise, we fail to delete.
495              
496             =cut
497              
498             sub unlink
499             {
500             my ($self) = @_;
501              
502             if( ! $self->get_queue_filename ) {
503             # No filename, can't unlink
504             return undef;
505             }
506              
507             if( ! $self->get_queue_fh ) {
508             return undef;
509             }
510              
511             # Only delete the queue file if we have it locked. Thus, we
512             # must call unlink() before close(), or we're no longer holding
513             # the lock.
514             if( 1 != unlink($self->get_queue_filename) ) {
515             return undef;
516             }
517             $self->get_queue_fh->close;
518             $self->set_queue_fh(undef);
519              
520             return 1;
521             }
522              
523              
524             # Internal methods
525              
526             sub _clean_email_address
527             {
528             my ($self, $addr) = @_;
529              
530             # Sanitize $addr a little. We want to remove any leading/trailing
531             # whitespace, and any < > that might be present
532             # FUTURE: do we want to do any other validation or cleaning of address
533             # here?
534             $addr =~ s/^[<\s]+//;
535             $addr =~ s/[>\s]+$//;
536              
537             return $addr;
538             }
539              
540             sub _format_qf_version
541             {
542             my ($self) = @_;
543             return 'V' . $self->get_qf_version();
544             }
545              
546             sub _format_create_time
547             {
548             my ($self) = @_;
549             return 'T' . $self->get_timestamp();
550             }
551              
552             sub _format_last_processed
553             {
554             # Never processed, so zero.
555             return 'K0';
556             }
557              
558             sub _format_times_processed
559             {
560             return 'N0';
561             }
562              
563             sub _format_priority
564             {
565             my ($self) = @_;
566              
567             return 'P' . $self->get_priority();
568             }
569              
570             sub _format_flag_bits
571             {
572             my ($self) = @_;
573              
574             my $flags = '';
575             # Possible flag bits for V6 queue file:
576             # 8 = Body has 8-bit data (EF_HAS8BIT)
577             # - This should be set if the body contains any
578             # octets with the high bit set. This can be detected
579             # by running
580             # $data =~ tr/\200-\377//
581             # (Sendmail does the C equivalent, char|0x80 in a loop)
582             # but... we don't have the data here in the qf object,
583             # so it must be set in Sendmail::Queue by calling set_data_is_8bit(1).
584             $flags .= '8' if $self->get_data_is_8bit();
585             # b = delete Bcc: header (EF_DELETE_BCC)
586             # - for our purposes, we want to reproduce the
587             # Bcc: header in the queued mail. Future uses
588             # of this module may wish to set this to have
589             # it removed.
590             # d = envelope has DSN RET= (EF_RET_PARAM)
591             # n = don't return body (EF_NO_BODY_RETN)
592             # - these two work together to set the value of
593             # the ${dsn_ret} macro. If we have both d and
594             # n flags, it's equivalent to RET=HDRS, and if
595             # we have d and no n flag, it's RET=FULL. No d
596             # and no n means a standard DSN, and no d with
597             # n means to suppress the body.
598             # - We will avoid setting this one for now, as
599             # whether or not to return headers should be a
600             # site policy decision.
601             # r = response (EF_RESPONSE)
602             # - this is set if this mail is a bounce,
603             # autogenerated return receipt message, or some
604             # other return-to-sender type thing.
605             # - we will avoid setting this, since we're not
606             # generating DSNs with this code yet.
607             # s = split (EF_SPLIT)
608             # - envelope with multiple recipients has been
609             # split into several envelopes
610             # (dmo) At this point, I think that this flag
611             # means that the envelope has /already/ been
612             # split according to number of recipients, or
613             # queue groups, or what have you by Sendmail,
614             # so we probably want to leave it off.
615             # w = warning sent (EF_WARNING)
616             # - message is a warning DSN. We probably don't
617             # want this flag set, but see 'r' flag above.
618             # Some details available in $$11.11.7 of the bat book. Other
619             # details require looking at Sendmail sources.
620             'F' . $flags;
621             }
622              
623             sub __format_single_macro
624             {
625             my ($name, $value) = @_;
626              
627             $value = '' unless defined $value; # //= would be nice, but we have to support 5.8.x
628              
629             if( length($name) > 1 ) {
630             return "\${$name}$value";
631             }
632             return "\$$name$value";
633             }
634              
635             sub _format_macros
636             {
637             my ($self) = @_;
638              
639             my $macro_text = '';
640              
641             my %macro_hash = %{ $self->get_macros() || {} };
642              
643             if( ! exists $macro_hash{r} ) {
644             $macro_hash{r} = $self->get_protocol();
645             }
646              
647             # ${daemon_flags} macro - shouldn't need any of these, so set a
648             # blank one.
649             $macro_hash{daemon_flags} = '';
650              
651             return join("\n",
652             map { __format_single_macro($_, $macro_hash{$_}) }
653             sort keys %macro_hash);
654             }
655              
656             sub _format_sender_address
657             {
658             my ($self) = @_;
659              
660             if( ! defined $self->get_sender() ) {
661             die q{Cannot queue a message with no sender address};
662             }
663             return 'S<' . $self->_clean_email_address( $self->get_sender() ). '>';
664             }
665              
666             sub _format_headers
667             {
668             my ($self) = @_;
669              
670             my @headers;
671              
672             # Ensure we prepend our generated received header, if it
673             # exists.
674             foreach my $line ( split(/\n/, $self->get_received_header || ''), split(/\n/, $self->get_headers) ) {
675             # Sendmail will happily deal with over-length lines in
676             # a queue file when transmitting, by breaking each line
677             # after 998 characters (to allow for \r\n under the
678             # 1000 character RFC limit) and splitting into a new
679             # line. This is ugly and breaks headers, so we do it nicely by
680             # adding a continuation \n\t at the first whitespace before 998
681             # characters.
682             # FUTURE: Note that this fails miserably if there is _no_ whitespace in the header.
683             if( length($line) > 998 ) {
684             my @tokens = split(/ /, $line);
685             my $new_line = shift @tokens;
686             foreach my $token (@tokens) {
687             if( length($new_line) + length($token) + 1 < 998 ) {
688             $new_line .= " $token";
689             } else {
690             push @headers, $new_line;
691             $new_line = "\t$token";
692             }
693             }
694             push @headers, $new_line;
695             } else {
696             push @headers, $line;
697             }
698             }
699              
700             # It doesn't appear that we need to escape any possible
701             # ${whatever} macro expansion in H?? lines, based on
702             # tests using 8.13.8 queue files.
703             #
704             # We do not want any delivery-agent flags between ??.
705             # Even Return-Path, which ordinarily has ?P?, we shall
706             # ignore flags for, as we want to pass on every header
707             # that we originally received.
708             return join("\n",
709             # Handle already-wrapped lines properly, by appending them
710             # as-is (no H?? prepend). Wrapped lines can begin with any
711             # whitespace, but it's most commonly a tab.
712             map { /^\s/ ? $_ : "H??$_" } @headers);
713             }
714              
715             sub _format_end_of_qf
716             {
717             my ($self) = @_;
718              
719             # Dot signifies end of queue file.
720             return '.';
721             }
722              
723             sub _format_recipient_addresses
724             {
725             my ($self) = @_;
726              
727             my $recips = $self->get_recipients();
728             if( scalar @$recips < 1 ) {
729             die q{Cannot queue a message with no recipient addresses};
730             }
731              
732             my @out;
733             foreach my $recip ( map { $self->_clean_email_address( $_ ) } @{$recips} ) {
734              
735             push @out, "rRFC822; $recip";
736              
737              
738             # R line: R:
739             # Possible flags:
740             # P - Primary address. Addresses via SMTP or
741             # commandline are always considered primary, so
742             # we need this flag.
743             # S,F,D - DSN Notify on success, failure or delay.
744             # We may not want this notification for the
745             # client queue, but current injection with
746             # sendmail binary does add FD, so we will do so
747             # here.
748             # N - Flag says whether or not notification was
749             # enabled at SMTP time with the NOTIFY extension.
750             # If not enabled, S, F and D have no effect.
751             # A - address is result of alias expansion. No,
752             # we don't want this
753             push @out, "RPFD:$recip";
754             }
755              
756             return join("\n", @out);
757             }
758              
759              
760             1;
761             __END__