root/trunk/plagger/lib/Plagger/Plugin/Publish/Gmail.pm

Revision 908 (checked in by miyagawa, 14 years ago)

don't send email when there's no entry. Fixes #297

  • Property svn:keywords set to Id Revision
Line 
1 package Plagger::Plugin::Publish::Gmail;
2 use strict;
3 use base qw( Plagger::Plugin );
4
5 our $VERSION = '0.10';
6
7 use DateTime;
8 use DateTime::Format::Mail;
9 use Encode;
10 use Encode::MIME::Header;
11 use HTML::Entities;
12 use HTML::Parser;
13 use MIME::Lite;
14
15 our %TLSConn;
16
17 sub rule_hook { 'publish.feed' }
18
19 sub register {
20     my($self, $context) = @_;
21     $context->register_hook(
22         $self,
23         'publish.init' => \&initialize,
24         'publish.feed' => \&notify,
25     );
26 }
27
28 sub init {
29     my $self = shift;
30     $self->SUPER::init(@_);
31
32     $self->conf->{mailto} or Plagger->context->error("mailto is required");
33     $self->conf->{mailfrom} ||= 'plagger@localhost';
34 }
35
36 sub initialize {
37     my($self,$context) = @_;
38
39     # authenticate POP before SMTP
40     if (my $conf = $self->conf->{pop3}) {
41         require Net::POP3;
42         my $pop = Net::POP3->new($conf->{host});
43         if ($pop->login($conf->{username}, $conf->{password})) {
44             $context->log(info => 'POP3 login succeed');
45         } else {
46             $context->log(error => 'POP3 login error');
47         }
48         $pop->quit;
49     }
50 }
51
52 sub notify {
53     my($self, $context, $args) = @_;
54
55     return if $args->{feed}->count == 0;
56
57     my $feed = $args->{feed};
58     my $subject = $feed->title || '(no-title)';
59
60     my @enclosure_cb;
61     if ($self->conf->{attach_enclosures}) {
62         for my $entry ($args->{feed}->entries) {
63             push @enclosure_cb, $self->prepare_enclosures($entry);
64         }
65     }
66
67     my $body = $self->templatize($context, $feed);
68
69     my $cfg = $self->conf;
70     $context->log(info => "Sending $subject to $cfg->{mailto}");
71
72     my $feed_title = $feed->title;
73        $feed_title =~ tr/,//d;
74
75     my $now = Plagger::Date->now(timezone => $context->conf->{timezone});
76
77     my $msg = MIME::Lite->new(
78         Date => $now->format('Mail'),
79         From => encode('MIME-Header', qq("$feed_title" <$cfg->{mailfrom}>)),
80         To   => $cfg->{mailto},
81         Subject => encode('MIME-Header', $subject),
82         Type => 'multipart/related',
83     );
84     $msg->replace("X-Mailer" => "Plagger/$Plagger::VERSION");
85
86     $msg->attach(
87         Type => 'text/html; charset=utf-8',
88         Data => encode("utf-8", $body),
89         Encoding => 'quoted-printable',
90     );
91
92     for my $cb (@enclosure_cb) {
93         $cb->($msg);
94     }
95
96     my $route = $cfg->{mailroute} || { via => 'smtp', host => 'localhost' };
97     $route->{via} ||= 'smtp';
98
99     eval {
100         if ($route->{via} eq 'smtp_tls') {
101             $self->{tls_args} = [
102                 $route->{host},
103                 User     => $route->{username},
104                 Password => $route->{password},
105                 Port     => $route->{port} || 587,
106             ];
107             $msg->send_by_smtp_tls(@{ $self->{tls_args} });
108         } elsif ($route->{via} eq 'sendmail') {
109             my %param = (FromSender => "<$cfg->{mailfrom}>");
110             $param{Sendmail} = $route->{command} if defined $route->{command};
111             $msg->send('sendmail', %param);
112         } else {
113             my @args  = $route->{host} ? ($route->{host}) : ();
114             $msg->send($route->{via}, @args);
115         }
116     };
117
118     if ($@) {
119         $context->log(error => "Error while sending emails: $@");
120     }
121 }
122
123 sub prepare_enclosures {
124     my($self, $entry) = @_;
125
126     if (grep $_->is_inline, $entry->enclosures) {
127         # replace inline enclosures to cid: entities
128         my %url2enclosure = map { $_->url => $_ } $entry->enclosures;
129
130         my $output;
131         my $p = HTML::Parser->new(api_version => 3);
132         $p->handler( default => sub { $output .= $_[0] }, "text" );
133         $p->handler( start => sub {
134                          my($tag, $attr, $attrseq, $text) = @_;
135                          # TODO: use HTML::Tagset?
136                          if (my $url = $attr->{src}) {
137                              if (my $enclosure = $url2enclosure{$url}) {
138                                  $attr->{src} = "cid:" . $self->enclosure_id($enclosure);
139                              }
140                              $output .= $self->generate_tag($tag, $attr, $attrseq);
141                          } else {
142                              $output .= $text;
143                          }
144                      }, "tag, attr, attrseq, text");
145         $p->parse($entry->body);
146         $p->eof;
147
148         $entry->body($output);
149     }
150
151     return sub {
152         my $msg = shift;
153
154         for my $enclosure (grep $_->local_path, $entry->enclosures) {
155             my %param = (
156                 Type => $enclosure->type,
157                 Path => $enclosure->local_path,
158                 Filename => $enclosure->filename,
159             );
160
161             if ($enclosure->is_inline) {
162                 $param{Id} = '<' . $self->enclosure_id($enclosure) . '>';
163                 $param{Disposition} = 'inline';
164             } else {
165                 $param{Disposition} = 'attachment';
166             }
167
168             $msg->attach(%param);
169         }
170     }
171 }
172
173 sub generate_tag {
174     my($self, $tag, $attr, $attrseq) = @_;
175
176     return "<$tag " .
177         join(' ', map { $_ eq '/' ? '/' : sprintf qq(%s="%s"), $_, encode_entities($attr->{$_}, q(<>"')) } @$attrseq) .
178         '>';
179 }
180
181 sub enclosure_id {
182     my($self, $enclosure) = @_;
183     return Digest::MD5::md5_hex($enclosure->url->as_string) . '@Plagger';
184 }
185
186 sub templatize {
187     my($self, $context, $feed) = @_;
188     my $tt = $context->template();
189     $tt->process('gmail_notify.tt', {
190         feed => $feed,
191     }, \my $out) or $context->error($tt->error);
192     $out;
193 }
194
195 sub DESTORY {
196     my $self = shift;
197     return unless $self->{tls_args};
198
199     my $conn_key = join "|", @{ $self->{tls_args} };
200     eval {
201         local $SIG{__WARN__} = sub { };
202         $TLSConn{$conn_key} && $TLSConn{$conn_key}->quit;
203     };
204
205     # known error from Gmail SMTP
206     if ($@ && $@ !~ /An error occurred disconnecting from the mail server/) {
207         warn $@;
208     }
209 }
210
211 # hack MIME::Lite to support TLS Authentication
212 *MIME::Lite::send_by_smtp_tls = sub {
213     my($self, @args) = @_;
214
215     ### We need the "From:" and "To:" headers to pass to the SMTP mailer:
216     my $hdr   = $self->fields();
217     my($from) = MIME::Lite::extract_addrs( $self->get('From') );
218     my $to    = $self->get('To');
219
220     ### Sanity check:
221     defined($to) or Carp::croak "send_by_smtp_tls: missing 'To:' address\n";
222
223     ### Get the destinations as a simple array of addresses:
224     my @to_all = MIME::Lite::extract_addrs($to);
225     if ($MIME::Lite::AUTO_CC) {
226         foreach my $field (qw(Cc Bcc)) {
227             my $value = $self->get($field);
228             push @to_all, MIME::Lite::extract_addrs($value) if defined($value);
229         }
230     }
231
232     ### Create SMTP TLS client:
233     require Net::SMTP::TLS;
234
235     my $conn_key = join "|", @args;
236     my $smtp;
237     unless ($smtp = $TLSConn{$conn_key}) {
238         $smtp = $TLSConn{$conn_key} = MIME::Lite::SMTP::TLS->new(@args)
239             or Carp::croak("Failed to connect to mail server: $!\n");
240     }
241     $smtp->mail($from);
242     $smtp->to(@to_all);
243     $smtp->data();
244
245     ### MIME::Lite can print() to anything with a print() method:
246     $self->print_for_smtp($smtp);
247     $smtp->dataend();
248
249     1;
250 };
251
252 @MIME::Lite::SMTP::TLS::ISA = qw( Net::SMTP::TLS );
253 sub MIME::Lite::SMTP::TLS::print { shift->datasend(@_) }
254
255 1;
256
257 __END__
258
259 =head1 NAME
260
261 Plagger::Plugin::Publish::Gmail - Notify updates to your email account
262
263 =head1 SYNOPSIS
264
265   - module: Publish::Gmail
266     config:
267       mailto: example@gmail.com
268       mailfrom: you@example.net
269
270 =head1 DESCRIPTION
271
272 This plugin creates HTML emails and sends them to your Gmail mailbox.
273
274 =head1 CONFIG
275
276 =over 4
277
278 =item mailto
279
280 Your email address to send updatess to. Required.
281
282 =item mailfrom
283
284 Email address to send email from. Defaults to I<plagger@localhost>.
285
286 =item mailroute
287
288 Hash to specify how to send emails. Defaults to:
289
290   mailroute:
291     via: smtp
292     host: localhost
293
294 the value of I<via> would be either I<smtp>, I<smtp_tls> or I<sendmail>.
295
296   mailroute:
297     via: sendmail
298     command: /usr/sbin/sendmail
299
300 =item attach_enclosures
301
302 Flag to attach enclosures as Email attachments. Defaults to 0.
303
304 =back
305
306 =head1 AUTHOR
307
308 Tatsuhiko Miyagawa
309
310 =head1 SEE ALSO
311
312 L<Plagger>, L<MIME::Lite>
313
314 =cut
315
Note: See TracBrowser for help on using the browser.