File Coverage

lib/Mojolicious/Plugin/MailException.pm
Criterion Covered Total %
statement 86 87 98.8
branch 18 26 69.2
condition 9 19 47.3
subroutine 15 16 93.7
pod 1 1 100.0
total 129 149 86.5


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Mojolicious::Plugin::MailException - Mojolicious plugin to send crash information by email
4              
5             =head1 SYNOPSIS
6              
7             package MyServer;
8             use Mojo::Base 'Mojolicious';
9              
10             sub startup {
11             my ($self) = @_;
12              
13             $self->plugin(MailException => {
14             from => 'robot@my.site.com',
15             to => 'mail1@my.domain.com, mail2@his.domain.com',
16             subject => 'My site crashed!',
17             headers => {
18             'X-MySite' => 'crashed'
19             },
20              
21             stack => 10
22             });
23             }
24              
25             =head1 DESCRIPTION
26              
27             The plugin catches all exceptions, packs them into email and sends
28             them to email.
29              
30             There are some plugin options:
31              
32             =over
33              
34             =item from
35              
36             From-address for email (default B)
37              
38             =item to
39              
40             To-address(es) for email (default B)
41              
42             =item subject
43              
44             Subject for crash email
45              
46             =item headers
47              
48             Hash with headers that have to be added to mail
49              
50             =item stack
51              
52             Stack size for crash mail. Default is C<20>.
53              
54             =item maildir
55              
56             This option saves (stores) messages in the maildir instead of
57             sending them. If you catch too many crashes, then their sending
58             probably uses too much of the CPU, so by using this option you
59             may save your messages instead of sending them.
60              
61             The option is ignored if C option is defined.
62              
63             =item send
64              
65             Subroutine that can be used to send the mail, example:
66              
67             sub startup {
68             my ($self) = @_;
69              
70             $self->plugin(MailException => {
71             send => sub {
72             my ($mail, $exception) = @_;
73              
74             $mail->send; # prepared MIME::Lite object
75             }
76             });
77             }
78              
79             In the function You can send email by yourself and (or) prepare and
80             send Your own mail (sms, etc) message using B<$exception> object.
81             See L.
82              
83             =back
84              
85             The plugin provides additional method (helper) B.
86              
87             $cx->mail_exception('my_error', { 'X-Add-Header' => 'value' });
88              
89             You can use the helper to raise exception with additional mail headers.
90              
91             =head1 VCS
92              
93             The plugin is placed on
94             L.
95              
96             =head1 COPYRIGHT AND LICENCE
97              
98             Copyright (C) 2012 by Dmitry E. Oboukhov
99             Copyright (C) 2012 by Roman V. Nikolaev
100              
101             This library is free software; you can redistribute it and/or modify
102             it under the same terms as Perl itself, either Perl version 5.8.8 or,
103             at your option, any later version of Perl 5 you may have available.
104              
105             =cut
106              
107             package Mojolicious::Plugin::MailException;
108              
109             our $VERSION = '0.24';
110 2     2   890901 use 5.008008;
  2         15  
111 2     2   8 use strict;
  2         3  
  2         29  
112 2     2   7 use warnings;
  2         4  
  2         48  
113              
114 2     2   8 use Mojo::Base 'Mojolicious::Plugin';
  2         5  
  2         14  
115 2     2   1397 use Data::Dumper;
  2         4  
  2         76  
116 2     2   9 use Mojo::Exception;
  2         4  
  2         12  
117 2     2   36 use Carp;
  2         2  
  2         77  
118 2     2   9 use MIME::Lite;
  2         4  
  2         43  
119 2     2   7 use MIME::Words ':all';
  2         3  
  2         238  
120 2     2   15 use File::Spec::Functions 'rel2abs', 'catfile';
  2         4  
  2         1202  
121              
122              
123             my $mail_prepare = sub {
124             my ($e, $conf, $self, $from, $to, $headers, $stack_depth) = @_;
125             my $subject = $conf->{subject} || 'Caught exception';
126             $subject .= ' (' . $self->req->method . ': ' .
127             $self->req->url->to_abs->to_string . ')';
128             utf8::encode($subject) if utf8::is_utf8 $subject;
129             $subject = encode_mimeword $subject, 'B', 'utf-8';
130              
131              
132             my $text = '';
133             $text .= "Exception\n";
134             $text .= "~~~~~~~~~\n";
135              
136              
137             $text .= $e->message;
138             $text .= "\n";
139              
140             my $maxl = eval { length $e->lines_after->[-1][0]; };
141             $maxl ||= 5;
142             $text .= sprintf " %*d %s\n", $maxl, @{$_}[0,1] for @{ $e->lines_before };
143             $text .= sprintf " * %*d %s\n", $maxl, @{ $e->line }[0,1] if $e->line->[0];
144             $text .= sprintf " %*d %s\n", $maxl, @{$_}[0,1] for @{ $e->lines_after };
145              
146             if (@{ $e->frames }) {
147             my $no = 0;
148             $text .= "\n";
149             $text .= "Stack\n";
150             $text .= "~~~~~\n";
151             for (@{ $e->frames }) {
152             $no++;
153             if ($no > $stack_depth) {
154             $text .= " ...\n";
155             last;
156             }
157             $text .= sprintf " %s: %d\n", @{$_}[1,2];
158             }
159             }
160              
161              
162             if (eval { $self->session; scalar keys %{ $self->session } }) {
163             local $Data::Dumper::Indent = 1;
164             local $Data::Dumper::Terse = 1;
165             local $Data::Dumper::Useqq = 1;
166             local $Data::Dumper::Deepcopy = 1;
167             local $Data::Dumper::Maxdepth = 0;
168              
169             $text .= "\n";
170             $text .= "Session\n";
171             $text .= "~~~~~~~\n";
172             $text .= Dumper($self->session);
173             }
174              
175             eval { utf8::encode($text) if utf8::is_utf8 $text };
176              
177              
178             my $mail = MIME::Lite->new(
179             From => $from,
180             To => $to,
181             Subject => $subject,
182             Type => 'multipart/mixed',
183             );
184              
185              
186             $mail->attach(
187             Type => 'text/plain; charset=utf-8',
188             Data => $text
189             );
190              
191             $text = "Request\n";
192             $text .= "~~~~~~~\n";
193             my $req = $self->req->to_string;
194             $req =~ s/^/ /gm;
195             $text .= $req;
196              
197             $mail->attach(
198             Type => 'text/plain; charset=utf-8',
199             Filename => 'request.txt',
200             Disposition => 'inline',
201             Data => $text
202             );
203              
204             $mail->add($_ => $headers->{$_}) for keys %$headers;
205             return $mail;
206             };
207              
208 2     2   12 use Fcntl;
  2         4  
  2         1861  
209              
210             my $store_maildir = sub {
211             my ($dir, $mail) = @_;
212            
213             unless (-x $dir and -d $dir and -w $dir) {
214             warn "Directory `$dir' does not exists or accessible\n";
215             return;
216             }
217              
218             my $now = time;
219             for (my $i = 0; $i < 1000; $i++) {
220             my $fname = catfile $dir, sprintf '%d.%05d', $now, $i;
221              
222             my $fh;
223              
224             if (sysopen $fh, $fname, O_CREAT | O_WRONLY) {
225             binmode $fh => ':raw';
226              
227             my $str = $mail->as_string;
228             if (utf8::is_utf8 $str) {
229             utf8::encode $str;
230             }
231             print $fh $str;
232             close $fh;
233             last;
234             }
235             }
236             };
237              
238              
239             sub register {
240 2     2 1 22107 my ($self, $app, $conf) = @_;
241              
242 2   50     15 my $stack_depth = $conf->{stack} || 20;
243              
244 2         5 my $cb = $conf->{send};
245            
246 2 100       8 unless ('CODE' eq ref $cb) {
247 1     0   5 $cb = sub { $_[0]->send };
  0         0  
248 1 50       3 if (my $dir = $conf->{maildir}) {
249 1 50 33     36 warn "Directory `$dir' does not exists or accessible"
      33        
250             unless -x $dir and -d $dir and -w $dir;
251 1         7 $dir = rel2abs $dir;
252 1     1   21 $cb = sub { $store_maildir->($dir, shift) };
  1         5  
253             }
254             }
255 2 50       9 croak "Usage: app->plugin('ExceptionMail'[, send => sub { ... })'"
256             unless 'CODE' eq ref $cb;
257              
258 2   50     7 my $headers = $conf->{headers} || {};
259 2   50     10 my $from = $conf->{from} || 'root@localhost';
260 2   50     10 my $to = $conf->{to} || 'webmaster@localhost';
261              
262 2 50       7 croak "headers must be a HASHREF" unless 'HASH' eq ref $headers;
263              
264             $app->hook(around_dispatch => sub {
265 6     6   59305 my ($next, $c) = @_;
266              
267 6         19 my $e;
268             {
269 6         11 local $SIG{__DIE__} = sub {
270              
271 5         2915 ($e) = @_;
272              
273 5 100 66     28 unless (ref $e and $e->isa('Mojo::Exception')) {
274 4         12 my @caller = caller;
275              
276 4         31 $e =~ s/at\s+(.+?)\s+line\s+(\d+).*//s;
277              
278 4         41 $e = Mojo::Exception->new(
279             sprintf "%s at %s line %d\n", "$e", @caller[1,2]
280             );
281 4         60 $e->trace(1);
282 4 50       1220 $e->inspect if $e->can('inspect');
283             }
284              
285              
286 5         2073 CORE::die $e;
287 6         56 };
288              
289 6         12 eval { $next->() };
  6         15  
290             }
291              
292 6 100       11557 return unless $@;
293              
294 4 100       25 unless ($e) {
295 1         11 $e = Mojo::Exception->new($@);
296 1         16 $e->trace(1);
297 1 50       262 $e->inspect if $e->can('inspect');
298             }
299              
300 4         627 my $hdrs = $headers;
301              
302 1         5 $hdrs = { %$hdrs, %{ $e->{local_headers} } }
303 4 100       19 if ref $e->{local_headers};
304              
305 4         19 my $mail = $mail_prepare->( $e, $conf, $c, $from, $to, $hdrs, $stack_depth );
306              
307 4 50       8 eval {
308 4         86 local $SIG{CHLD} = 'IGNORE';
309 4         22 local $SIG{__DIE__};
310 4         23 $cb->($mail, $e);
311 4         76 1;
312             } or warn $@;
313              
314             # propagate Mojo::Exception
315 4         47 die $e;
316 2         29 });
317              
318             $app->helper(mail_exception => sub {
319 1     1   1725 my ($self, $et, $hdrs) = @_;
320 1         15 my @caller = caller 1;
321 1   50     5 $et ||= 'exception';
322 1         58 my $e = Mojo::Exception->new(
323             sprintf '%s at %s line %d', $et, @caller[1,2]
324             );
325 1         15 $e->trace(2);
326 1 50       345 $e->inspect if $e->can('inspect');
327              
328 1         679 $e->{local_headers} = $hdrs;
329 1         11 CORE::die $e;
330 2         70 });
331             }
332              
333             1;