rc2mail.pl 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. #! /usr/bin/perl
  2. # Copyright (C) 2010 Alex Schroeder <alex@gnu.org>
  3. # This program is free software: you can redistribute it and/or modify it under
  4. # the terms of the GNU General Public License as published by the Free Software
  5. # Foundation, either version 3 of the License, or (at your option) any later
  6. # version.
  7. #
  8. # This program is distributed in the hope that it will be useful, but WITHOUT
  9. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  10. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  11. #
  12. # You should have received a copy of the GNU General Public License along with
  13. # this program. If not, see <http://www.gnu.org/licenses/>.
  14. package OddMuse;
  15. use Getopt::Std;
  16. use XML::RSS;
  17. use LWP::UserAgent;
  18. use MIME::Entity;
  19. use File::Temp;
  20. use File::Basename;
  21. use File::Path;
  22. # This script can be invoked as follows:
  23. # perl rc2mail.pl -r http://localhost/cgi-bin/wiki \
  24. # -p test \
  25. # -m "alex:*secret*@mail.epfarms.org" \
  26. # -f "kensanata@gmail.com" \
  27. # -t ~/.rc2mail
  28. # -n Don't send email; useful if debugging the script
  29. # -p Oddmuse administrator password
  30. # -r Oddmuse full URL, eg. http://localhost/cgi-bin/wiki
  31. # gets http://localhost/cgi-bin/wiki?action=rss;days=1;full=1;short=0
  32. # And http://localhost/cgi-bin/wiki?action=subscriptionlist;raw=1;pwd=foo
  33. # -m user:password@mailhost for sending email using SMTP Auth. Without this
  34. # information, the script will send mail to localhost.
  35. # -f email address to use as the sender.
  36. # -t timestamp file; it's last modified date is used to determine when the
  37. # the last run was and an appropriate URL is used. Instead of days=1 it
  38. # will use from=n where n is the last modified date of the timestamp file.
  39. # -q quiet (default: number of messages sent)
  40. # -v verbose output (recipients)
  41. # -x debug output
  42. my %opts;
  43. getopts('np:r:m:f:t:qvx', \%opts);
  44. my $nomail = exists $opts{n};
  45. my $verbose = exists $opts{v};
  46. my $quiet = exists $opts{q};
  47. my $debug = exists $opts{x};
  48. my $admin_password = $opts{p};
  49. my $root = $opts{r};
  50. die "Must provide an url with the -r option\n" unless $root;
  51. $opts{m} =~ /(.*?):(.*)\@(.*)/;
  52. my ($user, $password, $host) = ($1, $2, $3);
  53. die "Cannot parse -m " . $opts{m} . "\n" if $opts{m} && !$host;
  54. my $from = $opts{f};
  55. die "Must provide sender using -f\n" if !$nomail && $host && !$from;
  56. my $ts = $opts{t};
  57. my $ua = new LWP::UserAgent;
  58. # Fetch subscribers first because we need to verify the password
  59. sub get_subscribers {
  60. my $url = "$root?action=subscriptionlist;raw=1;pwd=$admin_password";
  61. print "Getting $url\n" if $debug;
  62. my $response = $ua->get($url);
  63. die "Must provide an admin password with the -p option\n"
  64. if $response->code == 403 and not $admin_password;
  65. die "Must provide the correct admin password with the -p option\n"
  66. if $response->code == 403;
  67. die $url, "\n", $response->status_line unless $response->is_success;
  68. my %data;
  69. foreach my $line (split(/\n/, $response->content)) {
  70. my ($key, @entries) = split(/ +/, $line);
  71. # print "Subscription for $key: ", join(', ', @entries), "\n";
  72. $data{$key} = \@entries;
  73. }
  74. print "Found " . scalar(keys(%data)) . " subscribers\n" if $debug;
  75. return \%data;
  76. }
  77. # Fetch RSS feed
  78. sub get_timestamp {
  79. if ($ts and -f $ts) {
  80. return "from=" . (stat($ts))[9];
  81. } else {
  82. return "days=1";
  83. }
  84. }
  85. sub update_timestamp {
  86. # Only update timestamps if $ts is provided.
  87. return unless $ts;
  88. if (-f $ts) {
  89. # File exists: update timestamp.
  90. utime undef, undef, $ts;
  91. } else {
  92. # File does not exist: create it. File content is ignored on the
  93. # next run!
  94. my $dir = dirname($ts);
  95. mkpath($dir) unless -d $dir;
  96. open(F, ">$ts") or warn "Unable to create $ts: $!";
  97. close(F);
  98. }
  99. }
  100. sub get_rss {
  101. my $url = "$root?action=rss;full=1;short=0;" . get_timestamp();
  102. print "Getting $url\n" if $debug;
  103. my $response = $ua->get($url);
  104. die $url, $response->status_line unless $response->is_success;
  105. my $rss = new XML::RSS;
  106. $rss->parse($response->content);
  107. print "Found " . @{$rss->{items}} . " items.\n" if $debug;
  108. return $rss;
  109. }
  110. sub send_files {
  111. my ($rss, $subscribers) = @_;
  112. my @items = @{$rss->{items}};
  113. die "No items to send\n" unless @items;
  114. my $sent = 0;
  115. foreach my $item (@items) {
  116. my $title = $item->{title};
  117. print "Looking at $title\n" if $debug;
  118. my $id = $title;
  119. $id =~ s/ /_/g;
  120. my @subscribers = @{$subscribers->{$id}};
  121. print "Subscribers: ", join(', ', @subscribers), "\n" if $debug;
  122. $sent += @subscribers;
  123. send_file($id, $title, $item, @subscribers);
  124. }
  125. print "$sent messages sent\n" if $sent;
  126. }
  127. sub send_file {
  128. my ($id, $title, $item, @subscribers) = @_;
  129. return unless @subscribers;
  130. my $fh = File::Temp->new(SUFFIX => '.html');
  131. binmode($fh, ":utf8");
  132. warn "No content for $title\n" unless $item->{description};
  133. my $link = $item->{link};
  134. my $sub = "$root?action=subscriptions";
  135. my $text = qq(<p>Visit <a href="$link">$title</a>)
  136. . qq( or <a href="$sub">manage your subscriptions</a>.</p><hr />)
  137. . $item->{description};
  138. # prevent 501 Syntax error - line too long
  139. $text =~ s/<(p|h[1-6]|[duo]l|pre|li|form|div|blockquote|hr|table|tr)>/\r\n<$1>/gi;
  140. print $fh $text;
  141. $fh->close;
  142. foreach my $subscriber (@subscribers) {
  143. send_mail($subscriber, $title, $fh);
  144. }
  145. }
  146. sub send_mail {
  147. my ($subscriber, $title, $fh) = @_;
  148. print "Skipping mail to $subscriber...\n" if $debug && $nomail;
  149. return if $nomail;
  150. my $mail = new MIME::Entity->build(To => $subscriber,
  151. From => $from,
  152. Subject => $title,
  153. Path => $fh,
  154. Type=> "text/html");
  155. if ($host) {
  156. print "Sending $title to $subscriber using ${user}\@${host}\n" if $verbose;
  157. eval {
  158. require Net::SMTP::TLS;
  159. my $smtp = Net::SMTP::TLS->new($host,
  160. User => $user,
  161. Password => $password);
  162. $smtp->mail($from);
  163. $smtp->to($subscriber);
  164. $smtp->data;
  165. $smtp->datasend($mail->stringify);
  166. $smtp->dataend;
  167. $smtp->quit;
  168. };
  169. if ($@) {
  170. require Net::SMTP::SSL;
  171. my $smtp = Net::SMTP::SSL->new($host, Port => 465);
  172. $smtp->auth($user, $password);
  173. $smtp->mail($from);
  174. $smtp->to($subscriber);
  175. $smtp->data;
  176. $smtp->datasend($mail->stringify);
  177. $smtp->dataend;
  178. $smtp->quit;
  179. }
  180. } else {
  181. my @recipients = $mail->smtpsend();
  182. if (@recipients) {
  183. print "Sent $title to ", join(', ', @recipients), "\n" unless $quiet;
  184. } else {
  185. print "Failed to send $title to $subscriber\n" unless $quiet;
  186. }
  187. }
  188. }
  189. sub main {
  190. my $rss = get_rss();
  191. return unless @{$rss->{items}};
  192. my $subscribers = get_subscribers();
  193. return unless %{$subscribers};
  194. send_files($rss, $subscribers);
  195. update_timestamp();
  196. }
  197. main ();