File Coverage

blib/lib/Mail/DWIM.pm
Criterion Covered Total %
statement 125 211 59.2
branch 32 76 42.1
condition 7 12 58.3
subroutine 24 30 80.0
pod 0 20 0.0
total 188 349 53.8


line stmt bran cond sub pod time code
1             ###########################################
2             package Mail::DWIM;
3             ###########################################
4              
5 2     2   32016 use strict;
  2         5  
  2         71  
6 2     2   7 use warnings;
  2         1  
  2         149  
7             our @ISA = qw(Exporter);
8             our @EXPORT_OK = qw(mail);
9             our $VERSION = "0.08";
10             our @HTML_MODULES = qw(HTML::FormatText HTML::TreeBuilder MIME::Lite);
11             our @ATTACH_MODULES = qw(File::MMagic MIME::Lite);
12              
13 2     2   894 use YAML qw(LoadFile);
  2         14788  
  2         142  
14 2     2   1606 use Log::Log4perl qw(:easy);
  2         79503  
  2         13  
15 2     2   800 use Config;
  2         2  
  2         90  
16 2     2   965 use Mail::Mailer;
  2         28531  
  2         9  
17 2     2   64 use Sys::Hostname;
  2         3  
  2         93  
18 2     2   9 use File::Basename;
  2         3  
  2         96  
19 2     2   8 use POSIX qw(strftime);
  2         2  
  2         7  
20 2     2   92 use File::Spec;
  2         3  
  2         4204  
21              
22             my $error;
23              
24             ###########################################
25             sub mail {
26             ###########################################
27 2     2 0 689 my(@params) = @_;
28              
29 2         10 my $mailer = Mail::DWIM->new(@params);
30 2         6 $mailer->send();
31             }
32              
33             ###########################################
34             sub new {
35             ###########################################
36 7     7 0 887 my($class, %options) = @_;
37              
38 7         159 my($homedir) = glob "~";
39              
40 7         11 my %defaults;
41              
42 7         46 my $self = {
43             global_cfg_file => "/etc/maildwim",
44             user_cfg_file => "$homedir/.maildwim",
45             transport => "sendmail",
46             raise_error => 1,
47             %options,
48             };
49              
50             # Guess the 'from' address
51 7 100       23 if(! exists $self->{from}) {
52 4         692 my $user = scalar getpwuid($<);
53 4         14 my $domain = domain();
54 4         12 $self->{from} = "$user\@$domain";
55             }
56              
57             # Guess the 'date'
58 7 50       20 if (!exists $self->{date}) {
59 7         423 $self->{date} = strftime("%a, %e %b %Y %H:%M:%S %Z", localtime(time));
60             }
61              
62 7         17 for my $cfg (qw(global_cfg_file user_cfg_file)) {
63 14 100       175 if(-f $self->{$cfg}) {
64 8         25 my $yml = LoadFile( $self->{$cfg} );
65 8 50 66     15864 if(defined $yml and ref $yml ne 'HASH') {
66             # Needs to be a hash, but YAML file can be empty (undef)
67 0         0 LOGDIE "YAML file $self->{$cfg} format not a hash";
68             }
69             # merge with existing hash
70 8 100       45 %defaults = (%defaults, %$yml) if defined $yml;
71             }
72             }
73              
74 7         67 %$self = (%$self, %defaults, %options);
75              
76 7         40 bless $self, $class;
77             }
78              
79             ###########################################
80             sub cmd_line_mail {
81             ###########################################
82 0     0 0 0 my($self) = @_;
83              
84 0 0       0 $self->{subject} = 'no subject' unless defined $self->{subject};
85              
86 0         0 my $mailer;
87 0 0       0 $mailer = $self->{program} if defined $self->{program};
88 0 0       0 $mailer = bin_find("mail") unless defined $mailer;
89              
90 0 0       0 open(PIPE, "|-", $mailer,
91             "-s", $self->{subject}, $self->{to},
92             ) or LOGDIE "Opening $mailer failed: $!";
93              
94 0         0 print PIPE $self->{text};
95 0         0 return close PIPE;
96             }
97              
98             ###########################################
99             sub send {
100             ###########################################
101 4     4 0 17 my($self, $evaled) = @_;
102              
103 4 100 66     19 if(!$self->{raise_error} && ! $evaled) {
104 1         3 return $self->send_evaled();
105             }
106              
107 3         17 my $msg =
108             "Sending from=$self->{from} to=$self->{to} " .
109             "subj=" . snip($self->{subject}, 20) . " " .
110             "text=" . snip($self->{text}, 20) .
111             "";
112              
113 3         6 my @options = ();
114              
115 3 100       11 if(0) {
    50          
    0          
116 0         0 } elsif($self->{transport} eq "sendmail") {
117 2         3 @options = ();
118             } elsif($self->{transport} eq "smtp") {
119             # Mail::SMTP likes it that way
120 1         6 $ENV{ MAILADDRESS } = $self->{ from };
121              
122 1 50       8 LOGDIE "No smtp_server set" unless defined $self->{smtp_server};
123 0         0 @options = ("smtp", Server => $self->{smtp_server});
124 0 0       0 push @options, (Port => $self->{smtp_port})
125             if exists $self->{smtp_port};
126 0         0 $self->{to} = [split /\s*,\s*/, $self->{to}];
127              
128             # some smtp servers want SASL auth
129 0 0       0 if( defined $self->{user} ) {
130 0         0 require Authen::SASL;
131 0         0 push @options, (Auth => [ $self->{user}, $self->{password} ]);
132             }
133             } elsif($self->{transport} eq "mail") {
134 0         0 return $self->cmd_line_mail();
135             } else {
136 0         0 LOGDIE "Unknown transport '$self->{transport}'";
137             }
138              
139 2         16 my $mailer = Mail::Mailer->new(@options);
140 2         7130 my %headers;
141 2         7 for (qw(from to cc bcc subject date)) {
142 12 100       37 $headers{ucfirst($_)} = $self->{$_} if exists $self->{$_};
143             }
144              
145 2         6 my $text = $self->{text};
146 2 50       5 if($self->{html_compat}) {
147 0         0 my $h;
148 0         0 ($h, $text) = html_msg($text);
149 0         0 %headers = (%headers, %$h);
150             }
151              
152 2 50       8 if($self->{attach}) {
153 0         0 my $h;
154 0         0 ($h, $text) = attach_msg($text, @{$self->{attach}});
  0         0  
155 0         0 %headers = (%headers, %$h);
156             }
157              
158 2 50       9 if($ENV{MAIL_DWIM_TEST}) {
159 2         12 DEBUG "Appending to test file $ENV{MAIL_DWIM_TEST}";
160 2         20 my $txt;
161 2         7 for (keys %headers) {
162 8 50       22 $txt .= "$_: $headers{$_}\n" if defined $headers{$_};
163             }
164 2         4 $txt .= "\n";
165              
166 2         8 test_file_append($txt . $text);
167 2         23 return 1;
168             } else {
169 0         0 DEBUG $msg;
170             }
171              
172 0         0 $mailer->open(\%headers);
173 0         0 print $mailer $text;
174 0         0 $mailer->close();
175             }
176              
177             ###########################################
178             sub send_evaled {
179             ###########################################
180 1     1 0 2 my($self) = @_;
181              
182 1         2 eval {
183 1         6 return $self->send(1);
184             };
185              
186 1 50       56 if($@) {
187 1         4 error($@);
188 1         11 return undef;
189             }
190             }
191              
192             ###########################################
193             sub error {
194             ###########################################
195 2     2 0 292 my($text) = @_;
196              
197 2 100       5 if(defined $text) {
198 1         2 $error = $text;
199             }
200              
201 2         9 return $error;
202             }
203              
204             ###########################################
205             sub test_file_append {
206             ###########################################
207 2     2 0 2 my($msg) = @_;
208              
209 2 50       66 open FILE, ">>$ENV{MAIL_DWIM_TEST}" or
210             LOGDIE "Cannot open $ENV{MAIL_DWIM_TEST} ($!)";
211 2         11 print FILE $msg, "\n\n";
212 2         91 close FILE;
213             }
214              
215             ###########################################
216             sub html_requirements {
217             ###########################################
218              
219 1     1 0 6 for (@HTML_MODULES) {
220 1         56 eval "require $_";
221 1 50       5 if($@) {
222 1         3 return undef;
223             }
224             }
225              
226 0         0 1;
227             }
228              
229             ###########################################
230             sub attach_requirements {
231             ###########################################
232              
233 1     1 0 7 for (@ATTACH_MODULES) {
234 1         52 eval "require $_";
235 1 50       8 if($@) {
236 1         3 return undef;
237             }
238             }
239              
240 0         0 1;
241             }
242              
243             ###########################################
244             sub html_msg {
245             ###########################################
246 0     0 0 0 my($htmltext) = @_;
247              
248 0 0       0 if(! html_requirements()) {
249 0         0 LOGDIE "Please install ",
250             join(" ", @HTML_MODULES), " from CPAN";
251             }
252              
253 0         0 my $tree = HTML::TreeBuilder->new();
254 0         0 $tree->parse($htmltext);
255 0         0 $tree->eof();
256 0         0 my $formatter = HTML::FormatText->new();
257 0         0 my $plaintext = $formatter->format($tree);
258              
259 0         0 my $msg = MIME::Lite->new(
260             Type => 'multipart/alternative',
261             );
262              
263 0         0 $msg->attach(
264             Type => 'text/plain',
265             Data => $plaintext
266             );
267              
268 0         0 $msg->attach(
269             Type => 'text/html',
270             Data => $htmltext,
271             );
272              
273 0         0 my %headers;
274              
275 0         0 for (qw(Content-Transfer-Encoding Content-Type
276             MIME-version)) {
277 0         0 $headers{$_} = $msg->attr($_);
278             }
279              
280             # evil hack to compensate for MIME::Lite's shortcomings
281 0 0       0 if( $headers{ "Content-Type" } !~ /boundary/ ) {
282             $headers{ "Content-Type" } =
283             $headers{ "Content-Type" } .
284             qq{; boundary="} .
285             $msg->{ SubAttrs }->{ "content-type" }->{ boundary } .
286 0         0 qq{"};
287             }
288              
289 0         0 return \%headers, $msg->body_as_string;
290             }
291              
292             ###########################################
293             sub attach_msg {
294             ###########################################
295 0     0 0 0 my($text, @files) = @_;
296              
297 0 0       0 if(! attach_requirements()) {
298 0         0 LOGDIE "Please install ",
299             join(" ", @ATTACH_MODULES), " from CPAN";
300             }
301              
302 0         0 my $msg = MIME::Lite->new(
303             Type => "multipart/mixed"
304             );
305              
306 0         0 $msg->attach(Type => "TEXT",
307             Data => $text,
308             );
309              
310 0         0 for my $file (@files) {
311 0         0 my $mm = File::MMagic->new();
312 0         0 my $type = $mm->checktype_filename($file);
313 0 0       0 LOGDIE "Cannot determine mime type of $file" unless defined $type;
314              
315 0         0 $msg->attach(Type => $type,
316             Path => $file,
317             Filename => basename($file),
318             Disposition => "attachment",
319             );
320             }
321              
322 0         0 my $headers = mime_lite_headers($msg);
323              
324 0         0 return $headers, $msg->body_as_string;
325             }
326              
327             ###########################################
328             sub mime_lite_headers {
329             ###########################################
330 0     0 0 0 my($mlite) = @_;
331              
332 0         0 my %wanted = map { lc($_) => $_ }
  0         0  
333             qw(Content-Transfer-Encoding Content-Type
334             MIME-version);
335 0         0 my %headers = ();
336              
337 0         0 for my $field (@{$mlite->fields}) {
  0         0  
338 0 0       0 if(exists $wanted{$field->[0]}) {
339 0         0 my($name, $value) = split /:\s*/,
340             $mlite->fields_as_string([$field]), 2;
341 0         0 $headers{$name} = $value;
342             }
343             }
344              
345 0         0 return(\%headers);
346             }
347              
348             ###########################################
349             sub header_ucfirst {
350             ###########################################
351 0     0 0 0 my($name) = @_;
352              
353 0         0 $name =~ s/^(\w)/uc($1)/g;
354 0         0 $name =~ s/-(\w)/uc($1)/g;
355              
356 0         0 return $name;
357             }
358              
359             ###########################################
360             sub snip {
361             ###########################################
362 6     6 0 27 my($data, $maxlen) = @_;
363              
364 6 100       14 if(length $data <= $maxlen) {
365 4         7 return lenformat($data);
366             }
367              
368 2 50       5 $maxlen = 12 if $maxlen < 12;
369 2         4 my $sniplen = int(($maxlen - 8) / 2);
370              
371 2         3 my $start = substr($data, 0, $sniplen);
372 2         20 my $end = substr($data, -$sniplen);
373 2         5 my $snipped = length($data) - 2*$sniplen;
374              
375 2         6 return lenformat("$start\[...]$end", length $data);
376             }
377              
378             ###########################################
379             sub lenformat {
380             ###########################################
381 6     6 0 8 my($data, $orglen) = @_;
382              
383 6   66     25 return "(" . ($orglen || length($data)) . ")[" .
384             printable($data) . "]";
385             }
386              
387             ###########################################
388             sub printable {
389             ###########################################
390 6     6 0 8 my($data) = @_;
391              
392 6         109 $data =~ s/[^ \w.;!?@#$%^&*()+\\|~`'-,><[\]{}="]/./g;
393 6         26 return $data;
394             }
395              
396             ###########################################
397             sub blurt {
398             ###########################################
399 8     8 0 3820 my($data, $file) = @_;
400              
401 8 50       386 open FILE, ">$file" or die "Cannot open $file";
402 8         36 print FILE $data;
403 8         235 close FILE;
404             }
405              
406             ###########################################
407             sub slurp {
408             ###########################################
409 2     2 0 50 my($file) = @_;
410              
411 2         7 local($/);
412 2         4 $/ = undef;
413              
414 2 50       52 open FILE, "<$file" or die "Cannot open $file";
415 2         20 my $data = ;
416 2         10 close FILE;
417 2         10 return $data;
418             }
419              
420             ###########################################
421             sub domain {
422             ###########################################
423              
424 4     4 0 594 my $domain = $Config{mydomain};
425              
426 4 50 33     2084 if(defined $domain and length($domain)) {
427 4         15 $domain =~ s/^\.//;
428 4         12 return $domain;
429             }
430              
431 0           eval { require Sys::Hostname; };
  0            
432 0 0         if(! $@) {
433 0           $domain = hostname();
434 0           return $domain;
435             }
436              
437 0           $domain = "localhost";
438              
439 0           return $domain;
440             }
441              
442             ######################################
443             sub bin_find {
444             ######################################
445 0     0 0   my($exe) = @_;
446              
447 0           for my $path (split /:/, $ENV{PATH}) {
448 0           my $full = File::Spec->catfile($path, $exe);
449              
450 0 0         return $full if -x $full;
451             }
452              
453 0           return undef;
454             }
455              
456             1;
457              
458             __END__