File Coverage

blib/lib/Mail/Postfix/Postdrop.pm
Criterion Covered Total %
statement 24 111 21.6
branch 0 32 0.0
condition 0 7 0.0
subroutine 8 20 40.0
pod 6 7 85.7
total 38 177 21.4


line stmt bran cond sub pod time code
1             # $Id: Postdrop.pm 2376 2008-10-27 09:36:02Z makholm $
2             package Mail::Postfix::Postdrop;
3              
4 1     1   24554 use strict;
  1         2  
  1         46  
5 1     1   7 use warnings;
  1         3  
  1         61  
6              
7             our $VERSION = '0.3';
8              
9 1     1   6 use Carp;
  1         6  
  1         76  
10 1     1   7 use Exporter qw(import);
  1         1  
  1         51  
11             our @EXPORT_OK = qw(inject);
12              
13 1     1   886 use Email::Abstract;
  1         37833  
  1         40  
14 1     1   1146 use Email::Address;
  1         54362  
  1         120  
15              
16 1     1   1584 use File::Temp qw(tempfile);
  1         25005  
  1         80  
17              
18 1     1   976 use IO::Socket::UNIX;
  1         16594  
  1         8  
19              
20             # Can we deduce these?
21             our $MAILDROP_QUEUE_DIR = '/var/spool/postfix/maildrop/';
22             our $PICKUP_SERVICE_SOCKET = '/var/spool/postfix/public/pickup';
23              
24             my %rec_types;
25             %rec_types = (
26             REC_TYPE_SIZE => 'C', # first record, created by cleanup
27             REC_TYPE_TIME => 'T', # time stamp, required
28             REC_TYPE_FULL => 'F', # full name, optional
29             REC_TYPE_INSP => 'I', # inspector transport
30             REC_TYPE_FILT => 'L', # loop filter transport
31             REC_TYPE_FROM => 'S', # sender, required
32             REC_TYPE_DONE => 'D', # delivered recipient, optional
33             REC_TYPE_RCPT => 'R', # todo recipient, optional
34             REC_TYPE_ORCP => 'O', # original recipient, optional
35             REC_TYPE_WARN => 'W', # warning message time
36             REC_TYPE_ATTR => 'A', # named attribute for extensions
37              
38             REC_TYPE_MESG => 'M', # start message records
39              
40             REC_TYPE_CONT => 'L', # long data record
41             REC_TYPE_NORM => 'N', # normal data record
42              
43             REC_TYPE_XTRA => 'X', # start extracted records
44              
45             REC_TYPE_RRTO => 'r', # return-receipt, from headers
46             REC_TYPE_ERTO => 'e', # errors-to, from headers
47             REC_TYPE_PRIO => 'P', # priority
48             REC_TYPE_VERP => 'V', # VERP delimiters
49              
50             REC_TYPE_END => 'E', # terminator, required
51              
52             );
53              
54             sub inject {
55 0     0 1   my $postdrop = __PACKAGE__->new(@_);
56              
57 0 0         $postdrop->build or return;
58 0 0         $postdrop->write or return;
59 0 0         $postdrop->release or return;
60              
61 0           $postdrop->notify;
62              
63 0           return 1;
64             }
65              
66             sub new {
67 0     0 1   my ( $class, $message, %overrides ) = @_;
68              
69             # Email::Abstract->new() is just a no-op when called with
70             # an object taht allready is an Email::Abstract. So this is ok:
71 0           $message = Email::Abstract->new($message);
72              
73 0   0       $overrides{Attr} ||= { rewrite_context => 'local' };
74 0 0         $overrides{Timestamp} = time() unless exists $overrides{Timestamp};
75              
76 0 0         unless ( exists $overrides{Sender} ) {
77 0           my $sender = $message->get_header("Sender");
78 0   0       $sender ||= $message->get_header("From");
79              
80 0           $overrides{Sender} = ( Email::Address->parse($sender) )[0]->address;
81             }
82              
83 0 0         unless ( exists $overrides{Recipients} ) {
84 0           $overrides{Recipients} = [
85 0           map { $_->address }
86 0           map { Email::Address->parse($_) }
87 0           map { $message->get_header($_) } qw(To Cc Bcc)
88             ];
89             }
90              
91 0           return bless {
92             message => $message,
93             args => \%overrides,
94             }, $class;
95             }
96              
97             sub build {
98 0     0 1   my ($self) = @_;
99              
100 0           $self->_build_rec_time();
101 0           $self->_build_attr( %{ $self->{args}->{Attr} } );
  0            
102 0   0       $self->_build_rec( 'REC_TYPE_FROM', $self->{args}->{Sender} || "" );
103 0           for ( @{ $self->{args}->{Recipients} } ) {
  0            
104 0           $self->_build_rec( 'REC_TYPE_RCPT', $_ );
105             }
106              
107             # add an empty message length record.
108             # cleanup is supposed to understand that.
109             # see src/pickup/pickup.c
110 0           $self->_build_rec( 'REC_TYPE_MESG', "" );
111              
112             # a received header has already been added in SMTP.pm
113             # so we can just copy the message:
114              
115 0           for ( split( /\r?\n/, $self->{message}->as_string() ) ) {
116 0           $self->_build_msg_line($_);
117             }
118              
119             # finish it.
120 0           $self->_build_rec( 'REC_TYPE_XTRA', "" );
121 0           $self->_build_rec( 'REC_TYPE_END', "" );
122              
123 0           return $self->{content};
124             }
125              
126             # We're a method so it ok
127             sub write { ## no critic (ProhibitBuiltinHomonyms)
128 0     0 1   my $self = shift;
129              
130 0 0         my $oldumask = umask oct(177)
131             or return;
132              
133 0 0         my ( $fh, $filename ) = tempfile( DIR => $MAILDROP_QUEUE_DIR )
134             or return;
135              
136 0           $self->{filename} = $filename;
137              
138 0 0         print $fh $self->{content}
139             or return;
140              
141 0 0         close $fh
142             or return;
143              
144 0           umask $oldumask;
145              
146 0           return 1;
147             }
148              
149             sub release {
150 0     0 1   my $self = shift;
151              
152 0 0         chmod oct(744), $self->{filename}
153             or return;
154              
155 0           return 1;
156             }
157              
158             sub drop {
159 0     0 0   my $self = shift;
160              
161 0           unlink $self->{filename};
162              
163 0           return 1;
164             }
165              
166             sub notify {
167 0     0 1   my $fh;
168              
169 0 0         open $fh, ">", $PICKUP_SERVICE_SOCKET
170             or return;
171              
172 0 0         print $fh "W"
173             or return;
174              
175 0           close $fh;
176              
177 0           return 1;
178             }
179              
180             ############################################################
181             #
182             # Auxillary functions for building the queue file
183             #
184             ############################################################
185              
186             sub _build_rec {
187 0     0     my ( $self, $type, @list ) = @_;
188              
189 0 0         croak "unknown record type" unless ( $rec_types{$type} );
190 0           $self->{content} .= ( $rec_types{$type} );
191              
192             # the length is a little endian base-128 number where each
193             # byte except the last has the high bit set:
194 0           my $s = "@list";
195 0           my $ln = length($s);
196 0           while ( $ln >= 0x80 ) {
197 0           my $lnl = $ln & 0x7F;
198 0           $ln >>= 7;
199 0           $self->{content} .= ( chr( $lnl | 0x80 ) );
200             }
201 0           $self->{content} .= ( chr($ln) );
202              
203 0           $self->{content} .= ($s);
204              
205 0           return;
206             }
207              
208             sub _build_rec_size {
209 0     0     my ( $self, $content_size, $data_offset, $rcpt_count ) = @_;
210              
211 0           my $s = sprintf( "%15ld %15ld %15ld", $content_size, $data_offset, $rcpt_count );
212 0           $self->_build_rec( 'REC_TYPE_SIZE', $s );
213 0           return;
214             }
215              
216             sub _build_rec_time {
217 0     0     my ( $self, $time ) = @_;
218              
219 0 0         $time = $self->{args}->{Timestamp} unless ( defined($time) );
220              
221 0 0         return unless defined $time;
222              
223 0           my $s = sprintf( "%d", $time );
224 0           $self->_build_rec( 'REC_TYPE_TIME', $s );
225 0           return;
226             }
227              
228             sub _build_attr {
229 0     0     my ( $self, %kv ) = @_;
230 0           for ( keys %kv ) {
231 0           $self->_build_rec( 'REC_TYPE_ATTR', "$_=$kv{$_}" );
232             }
233 0           return;
234             }
235              
236             sub _build_msg_line {
237 0     0     my ( $self, $line ) = @_;
238              
239 0           $line =~ s/\r?\n$//s;
240              
241             # split into 1k chunks.
242 0           while ( length($line) > 1024 ) {
243 0           my $s = substr( $line, 0, 1024 );
244 0           $line = substr( $line, 1024 );
245 0           $self->_build_rec( 'REC_TYPE_CONT', $s );
246             }
247 0           $self->_build_rec( 'REC_TYPE_NORM', $line );
248 0           return;
249             }
250              
251             1;
252              
253             __END__