File Coverage

lib/Petal/Mail.pm
Criterion Covered Total %
statement 142 143 99.3
branch 36 40 90.0
condition 20 24 83.3
subroutine 21 21 100.0
pod 2 2 100.0
total 221 230 96.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Petal::Mail - Format text e-mail using Petal
4              
5              
6             =head1 SYNOPSIS
7              
8             use Petal::Mail;
9             use Petal::Mail;
10             use Petal::Mail;
11             use Petal::Mail;
12             my $petal_mail = new Petal::Mail ('email.xml');
13             my $text_mail = $petal_mail->process (%args);
14              
15              
16             =head1 SUMMARY
17              
18             L processes a Petal XML template, and then turns the resulting XML
19             into a text email which can be sent through sendmail or other. The XML has to
20             follow a certain syntax which is defined in this documentation.
21              
22             Since L's is a subclass of Petal, its API is the same. Which
23             means you need to read about L before you can use L.
24              
25             =cut
26             package Petal::Mail;
27 3     3   118676 use strict;
  3         10  
  3         183  
28 3     3   17 use warnings;
  3         5  
  3         85  
29 3     3   8103 use MKDoc::XML::TreeBuilder;
  3         26220  
  3         99  
30 3     3   6552 use MKDoc::XML::Decode;
  3         46503  
  3         111  
31 3     3   3738 use Encode;
  3         56888  
  3         341  
32 3     3   32 use base qw /Petal/;
  3         7  
  3         3174  
33              
34              
35             =head1 GLOBAL / LOCAL VARIABLES
36              
37             =head2 local $Petal::Mail::Line_Wrap = 68;
38              
39             Amounts of characters allowed before text-flowed wrapping.
40              
41             =head2 local $Petal::Mail::Indent = 4;
42              
43             Amounts of whitespace when indenting
tags
44              
45             =head2 local $Petal::Mail::Sendmail = '/usr/sbin/sendmail -t';
46              
47             If you set this variable to your sendmail executable, and make sure the
48             $ENV{SERVER_ADMIN} is set to a proper email address for processing bounces,
49             then you can use the send() method instead of the process() method and
50             Petal::Mail will send the email once it's been created.
51              
52             =cut
53             our $Line_Wrap = 68;
54             our $DD_Indent = 4;
55             our $Sendmail = '/usr/sbin/sendmail -t';
56              
57              
58             # can't touch those
59             our $Decode = new MKDoc::XML::Decode qw/xml xhtml numeric/;
60              
61             =head1 FUNCTIONS
62              
63             =head2 process
64              
65             This function processes a template. It takes a hash or hashref which is used
66             to fill out any elements in the template. It returns the processed template as
67             a string. See L for further details.
68              
69             =cut
70             our $VERSION = 0.31;
71              
72             =head1 FUNCTIONS
73              
74             =head2 process
75              
76             This function processes a template. It takes a hash or hashref which is used
77             to fill out any elements in the template. It returns the processed template as
78             a string. See L for further details.
79             =head2 send
80              
81             This function processes a template and sends an email message according to the
82             headers in the template. It takes the same parameters as process in addition
83             to the email address of the authorized sender. The authorized sender may also
84             be set with the environment parameter 'SERVER_ADMIN'. Returns null on success
85             or dies on failure.
86              
87             $petal_mail->send(AUTH_SENDER => 'lewis@carroll.net', %args);
88              
89             =cut
90              
91             =cut
92              
93             =head1 FUNCTIONS
94              
95             =head2 process
96              
97             This function processes a template. It takes a hash or hashref which is used
98             to fill out any elements in the template. It returns the processed template as
99             a string. See L for further details.
100             =head2 send
101              
102             This function processes and sends an email message according to a template.
103             It takes the same parameters as process. Returns null(?) on success or dies on
104             failure.
105              
106             =cut
107              
108             =head1 FUNCTIONS
109              
110             =head2 process
111              
112             This function processes a template. It takes a hash or hashref which is used
113             to fill out any elements in the template. It returns the processed template as
114             a string. See L for further details.
115             =head2 send
116              
117             This function processes and sends an email message according to a template.
118             It takes the same parameters as process. Returns null(?) on success or dies on
119             failure.
120              
121             =cut
122              
123             sub process
124             {
125 8     8 1 23962 my $self = shift;
126 8         75 my $xml = $self->SUPER::process (@_);
127 8         11759 return $self->_xml_to_text ($xml);
128             }
129              
130              
131             =head2 send
132              
133             This function processes and sends an email message according to a template.
134             It takes the same parameters as process. Returns null(?) on success or dies on
135             failure.
136              
137             =cut
138             sub send
139             {
140 3     3 1 1148 my $self = shift;
141 3         12 my %args = @_;
142 3   100     36 my $authorized_sender = $args{'AUTH_SENDER'} || $ENV{SERVER_ADMIN} || '';
143 3         8 delete $args{'AUTH_SENDER'};
144 3 100       19 $authorized_sender || die 'No authorized sender defined and $ENV{SERVER_ADMIN} not set';
145            
146 2   50     17 my $mail = $self->process (%args) || die '$self->process (\@_) returned undef';
147 2 50       17679 open (SENDMAIL, "| $Sendmail -f $authorized_sender") || die "error opening sendmail [$Sendmail]: $!";
148 2         90 binmode (SENDMAIL, ":utf8");
149 2   50     191 print SENDMAIL $mail || die "error writing to sendmail [$Sendmail]: $!";
150 2         9760 close SENDMAIL;
151             }
152              
153              
154             sub _xml_to_text
155             {
156 8     8   19 my $self = shift;
157 8         16 my $xml = shift;
158            
159 8         120 my @nodes = MKDoc::XML::TreeBuilder->process_data ($xml);
160 8 50 33     164 my @result = map {
161 8         131638 ((ref $_) and ($_->{_tag} eq 'Message')) ?
162             $self->__Message ($_) : ()
163             } @nodes;
164            
165 8         350 return join "\n", @result;
166             }
167              
168              
169             sub __Message
170             {
171 8     8   22 my $self = shift;
172 8         14 my $node = shift;
173              
174 8         42 my $headers = $self->__Headers ($node);
175 8         45 my $body = $self->__Body ($node);
176 8         70 return join "\n\n", ($headers, $body);
177             }
178              
179              
180             sub __Headers
181             {
182 8     8   21 my $self = shift;
183 8         13 my $node = shift;
184             my @res = map {
185 196 100 100     849 (ref $_ and $_->{_tag} !~ /^body$/i) ? do {
  8         28  
186 86         436 my $text = $self->__Content_To_Text ($_);
187 86         251 $text = $Decode->process ($text);
188 86         779 $text =~ s/\n/ /gsm;
189 86         225 $text = encode ('MIME-Header', $text);
190            
191 86         38086 my $tag = $_->{_tag};
192 86 100       430 ($tag =~ /^from$/i) ?
193             ("$tag: $text", $self->__Headers_message_id ($text)) :
194             ("$tag: $text");
195             } : ()
196 8         19 } @{$node->{_content}};
197            
198 8         51 my $res = join "\n", @res;
199 8         39 return $res;
200             }
201              
202              
203             sub __Headers_message_id
204             {
205 8     8   51 my $self = shift;
206 8         17 my $id = shift;
207 8         43 my $time = time();
208 8         25 my $rand = join '', map { chr (ord ('A') + int rand (26)) } 1..5;
  40         247  
209 8         77 $id =~ s/^.*
210 8         70 $id =~ s/>.*$//;
211 8         29 $id =~ s/^\s+//;
212 8         25 $id =~ s/\s+$//;
213 8         39 $id =~ s/^.*\@//;
214 8         32 $id = "$time.$rand\@$id";
215 8         41 return qq |Message-ID: <$id>|;
216             }
217              
218              
219             sub __Content_To_Text
220             {
221 202     202   240 my $self = shift;
222 202         224 my $node = shift;
223 204 100       1061 my @res = map {
224 202         414 (ref $_) ? ( $self->__Content_To_Text ($_) ) : ( $Decode->process ($_) )
225 202         208 } @{$node->{_content}};
226 202         3109 return join '', @res;
227             }
228              
229              
230             sub __Body
231             {
232 8     8   14 my $self = shift;
233 8         14 my $node = shift;
234 196 100 100     687 my @res = map {
235 8         45 (ref $_ and $_->{_tag} =~ /^body$/i) ?
236             $self->__Body_Process ($_) : ()
237 8         16 } @{$node->{_content}};
238              
239 8         39 my $res = join "\n", @res;
240 8         47 $res =~ s/^\n+//gs;
241 8         25 return $res;
242             }
243              
244              
245             sub __Body_Process
246             {
247 15     15   20 my $self = shift;
248 15         20 my $node = shift;
249            
250 259         338 my @res = map {
251 15         39 my $node = $_;
252             $self->__Body_P ($node) ||
253             $self->__Body_DT ($node) ||
254             $self->__Body_DD ($node) ||
255             $self->__Body_PRE ($node) ||
256 259 100 100     476 do { ref ($_) ? ( $self->__Body_Process ($node) ) : () }
  144 100 100     317  
      100        
257 15         25 } @{$node->{_content}};
258            
259 15         103 my $res = join '', @res;
260 15         51 return $res;
261             }
262              
263              
264             sub __Body_PRE
265             {
266 152     152   170 my $self = shift;
267 152         167 my $node = shift;
268            
269 152 100       656 ref $node || return;
270 15 100       73 return unless ($node->{_tag} =~ /^pre$/i);
271            
272 8         24 my $text = $self->__Content_To_Text ($node);
273 8         43 return "\n\n$text";
274             }
275              
276              
277             sub __Body_P
278             {
279 287     287   332 my $self = shift;
280 287         303 my $node = shift;
281              
282 287 100       930 ref $node || return;
283 150 100       683 return unless ($node->{_tag} =~ /^p$/i);
284            
285 79         151 my $text = $self->__Content_To_Text ($node);
286 79         253 $text =~ s/\n/ /gsm;
287 79         150 $text =~ s/^\s+//;
288 79         369 $text =~ s/\s+$//;
289            
290 79         548 my @tokens = split /\s+/, $text;
291 79         229 return "\n\n" . $self->_soft_wrap (@tokens);
292             }
293              
294              
295             sub __Body_DT
296             {
297 208     208   225 my $self = shift;
298 208         236 my $node = shift;
299            
300 208 100       897 ref $node || return;
301 71 100       347 return unless ($node->{_tag} =~ /^dt$/i);
302            
303             # treat a DT as a paragraph
304 28         43 $node->{_tag} = 'p';
305 28         53 return $self->__Body_P ($node);
306             }
307              
308              
309             sub __Body_DD
310             {
311 180     180   199 my $self = shift;
312 180         192 my $node = shift;
313            
314 180 100       1047 ref $node || return;
315 43 100       182 return unless ($node->{_tag} =~ /^dd$/i);
316            
317 28         54 my $text = $self->__Content_To_Text ($node);
318 28         62 $text =~ s/\n/ /gsm;
319 28         62 $text =~ s/^\s+//;
320 28         94 $text =~ s/\s+$//;
321            
322 28         133 my @tokens = split /\s+/, $text;
323            
324 28         75 local ($Line_Wrap) = $Line_Wrap - length ($DD_Indent);
325 28         62 my $res = $self->_soft_wrap (@tokens);
326              
327 28         77 my $indent = $self->_text_indent();
328 28         95 $res =~ s/\n/\n$indent/g;
329 28         198 return "\n$indent$res";
330             }
331              
332              
333             sub _text_indent
334             {
335 28     28   74 return " " x $DD_Indent;
336             }
337              
338              
339             sub _soft_wrap
340             {
341 107     107   133 my $self = shift;
342 107         415 my @tokens = @_;
343            
344 107         133 my @res = ();
345 107         119 my @next_line = ();
346 107         233 while (@tokens)
347             {
348 1610         1966 my $next_token = shift (@tokens);
349 1610         3078 my $potential_line = join " ", (@next_line, $next_token);
350            
351 1610 100       2552 if (length ($potential_line) > $Line_Wrap)
352             {
353 92 50       221 if (@next_line == 0)
354             {
355 0         0 push @res, $next_token;
356             }
357             else
358             {
359 92         146 unshift (@tokens, $next_token);
360 92         312 push @res, join " ", @next_line;
361 92         336 @next_line = ();
362             }
363             }
364             else
365             {
366 1518         3400 push @next_line, $next_token;
367             }
368             }
369            
370 107 50       324 push @res, join " ", @next_line unless (@next_line == 0);
371 107         1205 return join " \n", @res;
372             }
373              
374              
375             1;
376              
377              
378             __END__