File Coverage

blib/lib/Exim/SpoolMessage.pm
Criterion Covered Total %
statement 18 92 19.5
branch 0 28 0.0
condition 0 6 0.0
subroutine 6 9 66.6
pod 1 1 100.0
total 25 136 18.3


line stmt bran cond sub pod time code
1             package Exim::SpoolMessage;
2              
3 1     1   20759 use warnings;
  1         2  
  1         30  
4 1     1   5 use strict;
  1         1  
  1         25  
5 1     1   5 use vars qw($AUTOLOAD);
  1         5  
  1         46  
6              
7 1     1   4 use Carp qw(carp croak);
  1         2  
  1         63  
8              
9 1     1   5 use Fcntl qw(:DEFAULT SEEK_SET);
  1         1  
  1         480  
10 1     1   814 use Mail::Header;
  1         5074  
  1         1115  
11              
12              
13             =head1 NAME
14              
15             Exim::SpoolMessage - Read and parse Exim spool files.
16              
17             =cut
18              
19             our $VERSION = '0.05';
20              
21              
22             =head1 SYNOPSIS
23              
24             Exim::SpoolMessage provides access to the messages stored in Exim's
25             spool directory.
26              
27             The format of the Exim spool files is described in section 53 of the
28             Exim specification document.
29              
30             use Exim::SpoolMessage;
31              
32             my $msg = Exim::SpoolMessage->load('/var/spool/exim/input',
33             '1N4toN-000G2Z-6M');
34             print "Return-path: <", $msg->return_path, ">\n";
35             print $msg->head->as_string(), "\n", @{$msg->body};
36              
37             The module was written in order to be able to provide external commands
38             access to the contents of the message during message filtering process.
39              
40             =head1 CONSTRUCTOR
41              
42             =head2 Exim::SpoolMessage->load($input_dir, $message_id)
43              
44             Takes to parameters - location of the input directory where spool files
45             are located and the message id of the desired message.
46              
47             NOTE: $input_dir has to be location of the directory where spool files
48             are located. If split_spool_directory it will not equal to the spool
49             directory.
50              
51             Returns an object with a lot of methods, please see below for details.
52              
53             The constructor croaks on errors.
54              
55             =cut
56              
57             sub load {
58 0     0 1   my $proto = shift;
59 0   0       my $class = ref($proto) || $proto;
60 0 0         my $dir = shift or croak 'Missing input directory';
61 0 0         my $id = shift or croak 'Missing message id';
62 0           my $self = {input_dir => $dir, message_id => $id};
63 0           bless $self, $class;
64              
65 0           my %keys = keys %{$self};
  0            
66 0           delete @keys{qw(input_dir message_id)};
67 0           delete @{$self}{keys %keys};
  0            
68              
69 0 0         open my $hh, "$self->{'input_dir'}/$self->{'message_id'}-H" or
70             croak "Cannot open '$self->{'input_dir'}/$self->{'message_id'}-H': $!";
71 0 0         open my $dh, "$self->{'input_dir'}/$self->{'message_id'}-D" or
72             croak "Cannot open '$self->{'input_dir'}/$self->{'message_id'}-D': $!";
73              
74             # The format of the Exim spool files is described in section 53 of the
75             # Exim specification.
76              
77             # The first line contains the final component of the file name.
78 0           chomp(my $tmp = <$hh>);
79 0 0         croak "Corrupted spool file '$self->{'message_id'}-H'"
80             if $tmp ne "$self->{'message_id'}-H";
81              
82              
83             # The second line contains the login name for the uid of the process
84             # that called Exim to read the message, followed by the numerical uid
85             # and gid.
86 0           chomp($tmp = <$hh>);
87 0           @{$self}{qw(caller_name caller_uid caller_gid)} = split / /, $tmp, 3;
  0            
88              
89             # The third line of the file contains the address of the message’s
90             # sender as transmitted in the envelope, contained in angle brackets.
91             # The sender address is empty for bounce messages.
92 0           chomp($tmp = <$hh>);
93 0           ($self->{'return_path'} = $tmp) =~ s/^<(.+)>$/$1/;
94              
95             # The fourth line contains two numbers. The first is the time that the
96             # message was received, in the conventional Unix form – the number of
97             # seconds since the start of the epoch. The second number is a count of
98             # the number of messages warning of delayed delivery that have been
99             # sent to the sender.
100 0           chomp($tmp = <$hh>);
101 0           @{$self}{qw(time_received warning_count)} = split / /, $tmp, 2;
  0            
102              
103             # There follow a number of lines starting with a hyphen.
104             # These can appear in any order, and are omitted when not relevant.
105 0           while (1) {
106             # -acl
107             # -aclc
108             # -aclm
109             # -active_hostname
110             # -allow_unqualified_recipient
111             # -allow_unqualified_sender
112             # -auth_id
113             # -auth_sender
114             # -body_linecount
115             # -body_zerocount
116             # -deliver_firsttime
117             # -frozen
118             # -helo_name
119             # -host_address
.
120             # -host_auth
121             # -host_lookup_failed
122             # -host_name
123             # -ident
124             # -interface_address
.
125             # -local
126             # -localerror
127             # -local_scan
128             # -manual_thaw
129             # -N
130             # -received_protocol
131             # -sender_set_untrusted
132             # -spam_score_int
133             # -tls_certificate_verified
134             # -tls_cipher
135             # -tls_peerdn
136 0           chomp($tmp = <$hh>);
137 0 0         if ($tmp =~ /^-(acl[mc]) (\d+) (\d+)/) {
    0          
138 0           my ($key, $num, $len) = ($1, $2 + 1);
139 0           read $hh, $tmp, $len;
140 0           chomp($self->{$key} = $tmp);
141 0           next;
142             } elsif ($tmp =~ /^-acl (\d+) (\d+)/) {
143 0           my ($num, $len) = ($1, $2 + 1);
144 0 0         my $key = ($num < 10) ? 'acl_c' . $num : 'acl_m' . ($num - 10);
145 0           read $hh, $tmp, $len;
146 0           chomp($self->{$key} = $tmp);
147 0           next;
148             }
149 0 0         last if $tmp !~ s/^-//;
150 0           my ($key, $val) = split / /, $tmp, 2;
151 0           $self->{"opt_$key"} = $val;
152             }
153              
154             # Following the options there is a list of those addresses to which the
155             # message is not to be delivered. This set of addresses is initialized from
156             # the command line when the -t option is used and
157             # extract_addresses_remove_arguments is set; otherwise it starts out empty.
158             # Whenever a successful delivery is made, the address is added to this set.
159             # The addresses are kept internally as a balanced binary tree, and it is a
160             # representation of that tree which is written to the spool file. If an
161             # address is expanded via an alias or forward file, the original address is
162             # added to the tree when deliveries to all its child addresses are
163             # complete.
164             #
165             # If the tree is empty, there is a single line in the spool file containing
166             # just the text "XX”. Otherwise, each line consists of two letters, which
167             # are either Y or N, followed by an address.
168 0           while (1) {
169 0           chomp($tmp = <$hh>);
170 0 0         last if $tmp !~ /^[XYN][XYN]/;
171             }
172              
173             # After the non-recipients tree, there is a list of the message’s
174             # recipients. This is a simple list, preceded by a count. It includes all
175             # the original recipients of the message, including those to whom the
176             # message has already been delivered.
177             #
178             # In the simplest case, the list contains one address per line.
179             #
180             # However, when a child address has been added to the top-level addresses
181             # as a result of the use of the one_time option on a redirect router, each
182             # line is of the following form:
183             #
184             # ,#
185             #
186             # The 01 flag bit indicates the presence of the three other fields that
187             # follow the top-level address. Other bits may be used in future to support
188             # additional fields. The is the offset in the recipients
189             # list of the original parent of the "one time" address. The first two
190             # fields are the envelope sender that is associated with this address and
191             # its length. If the length is zero, there is no special envelope sender
192             # (there are then two space characters in the line). A non-empty field can
193             # arise from a redirect router that has an errors_to setting.
194 0           $self->{'recipients'} = [];
195 0           for my $i (1 .. $tmp) {
196 0           chomp($tmp = <$hh>);
197 0           push @{$self->{'recipients'}}, $tmp;
  0            
198             }
199              
200             # A blank line separates the envelope and status information from the
201             # headers which follow.
202 0           $tmp = <$hh>;
203              
204             # A header may occupy several lines of the file, and
205             # to save effort when reading it in, each header is preceded by a number
206             # and an identifying character. The number is the number of characters in
207             # the header, including any embedded newlines and the terminating newline.
208             # The character is one of the following:
209             #
210             # header in which Exim has no special interest
211             # B Bcc: header
212             # C Cc: header
213             # F From: header
214             # I Message-id: header
215             # P Received: header – P for "postmark"
216             # R Reply-To: header
217             # S Sender: header
218             # T To: header
219             # * replaced or deleted header
220 0           my $pos = tell $hh;
221 0           my @headers;
222 0           while (defined(my $line = <$hh>)) {
223 0 0         $line =~ /^((\d+)([ BCFIPRST*]) )/ or next;
224 0           $pos += length($1);
225 0           my ($len, $char) = ($2, $3);
226 0           seek $hh, $pos, SEEK_SET;
227 0           read $hh, $tmp, $len;
228 0 0         push @headers, $tmp if $char ne '*';
229 0           $pos = tell $hh;
230             }
231              
232             # The first line contains the final component of the file name.
233 0           chomp($tmp = <$dh>);
234 0 0         croak "Corrupted spool file '$self->{'message_id'}-D'"
235             if $tmp ne "$self->{'message_id'}-D";
236              
237             # The data portion of the message is kept in the -D file on its own.
238 0           my @body;
239 0           while (defined(my $line = <$dh>)) {
240 0           push @body, $line;
241             }
242              
243 0           close $hh;
244 0           close $dh;
245              
246 0           $self->{'head'} = Mail::Header->new(\@headers, Modify => 0);
247 0           $self->{'body'} = \@body;
248              
249 0           return $self;
250             }
251              
252             =head1 METHODS
253              
254             =head2 $msg->head
255              
256             Mail::Header object with the message headers.
257              
258             =head2 $msg->body
259              
260             Reference to an array with the contents of the message body. Each
261             item represents a line of the body.
262              
263             =head2 Other methods
264              
265             In addition, there are many other mehotds that will provide access to
266             various details related to the message. Please consult the Exim
267             documentation.
268              
269             Available methods (not all may be present and there may be
270             other as well):
271              
272             =over
273              
274             =item * $msg->message_id
275              
276             =item * $msg->caller_name
277              
278             =item * $msg->caller_uid
279              
280             =item * $msg->caller_gid
281              
282             =item * $msg->return_path
283              
284             =item * $msg->time_received
285              
286             =item * $msg->warning_count
287              
288             =item * $msg->aclc*
289              
290             =item * $msg->aclm*
291              
292             =item * $msg->opt_active_hostname
293              
294             =item * $msg->opt_allow_unqualified_recipient
295              
296             =item * $msg->opt_allow_unqualified_sender
297              
298             =item * $msg->opt_auth_id
299              
300             =item * $msg->opt_auth_sender
301              
302             =item * $msg->opt_body_linecount
303              
304             =item * $msg->opt_body_zerocount
305              
306             =item * $msg->opt_deliver_firsttime
307              
308             =item * $msg->opt_frozen
309              
310             =item * $msg->opt_helo_name
311              
312             =item * $msg->opt_host_address
313              
314             =item * $msg->opt_host_auth
315              
316             =item * $msg->opt_host_lookup_failed
317              
318             =item * $msg->opt_host_name
319              
320             =item * $msg->opt_ident
321              
322             =item * $msg->opt_interface_address
323              
324             =item * $msg->opt_local
325              
326             =item * $msg->opt_localerror
327              
328             =item * $msg->opt_local_scan
329              
330             =item * $msg->opt_manual_thaw
331              
332             =item * $msg->opt_received_protocol
333              
334             =item * $msg->opt_sender_set_untrusted
335              
336             =item * $msg->opt_spam_score_int
337              
338             =item * $msg->opt_tls_certificate_verified
339              
340             =item * $msg->opt_tls_cipher
341              
342             =item * $msg->opt_tls_peerdn
343              
344             =item * $msg->recipients
345              
346             =back
347              
348             =cut
349              
350              
351             =head1 AUTHOR
352              
353             Kirill Miazine
354              
355              
356             =head1 COPYRIGHT & LICENSE
357              
358             Copyright 2009 Kirill Miazine.
359              
360             This software is distributed under an ISC style license, please see
361             for details.
362              
363             =cut
364              
365 0     0     sub DESTROY { 1; }
366              
367             sub AUTOLOAD {
368 0     0     my $self = shift;
369 0           my ($attr) = ($AUTOLOAD =~ /::([^:]+)$/);
370              
371 0 0 0       if ($attr =~ /^([a-zA-Z0-9_]+)$/ and exists $self->{$attr}) {
372 0           eval "sub $attr { shift->{'$attr'} }";
373 0           return $self->{$attr};
374             } else {
375 0           croak "Unknown attribute '$attr'";
376             }
377             }
378              
379             1;