rc2mail.pl 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #! /usr/bin/perl
  2. # Copyright (C) 2010–2019 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. use Net::SMTP;
  23. use Authen::SASL qw(Perl);
  24. # This script can be invoked as follows:
  25. # perl rc2mail.pl -r http://localhost/cgi-bin/wiki \
  26. # -p test \
  27. # -m "alex:*secret*@mail.epfarms.org" \
  28. # -f "kensanata@gmail.com" \
  29. # -t ~/.rc2mail
  30. # -n Don't send email; useful if debugging the script
  31. # -p Oddmuse administrator password
  32. # -r Oddmuse full URL, eg. http://localhost/cgi-bin/wiki
  33. # gets http://localhost/cgi-bin/wiki?action=rss;days=1;full=1;short=0
  34. # And http://localhost/cgi-bin/wiki?action=subscriptionlist;raw=1;pwd=foo
  35. # -m user:password@mailhost for sending email using SMTP Auth. Without this
  36. # information, the script will send mail to localhost. The host can end
  37. # in a port number, e.g. "kensanata:*secret*@smtp.migadu.com:587"
  38. # -f email address to use as the sender.
  39. # -t timestamp file; it's last modified date is used to determine when the
  40. # the last run was and an appropriate URL is used. Instead of days=1 it
  41. # will use from=n where n is the last modified date of the timestamp file.
  42. # -q quiet (default: number of messages sent)
  43. # -v verbose output (recipients)
  44. # -x debug output
  45. my %opts;
  46. getopts('np:r:m:f:t:qvx', \%opts);
  47. my $nomail = exists $opts{n};
  48. my $verbose = exists $opts{v};
  49. my $quiet = exists $opts{q};
  50. my $debug = exists $opts{x};
  51. my $admin_password = $opts{p};
  52. my $root = $opts{r};
  53. die "Must provide an url with the -r option\n" unless $root;
  54. $opts{m} =~ /(.*?):(.*)\@(.*)/;
  55. my ($user, $password, $host) = ($1, $2, $3);
  56. die "Cannot parse -m " . $opts{m} . "\n" if $opts{m} && !$host;
  57. my $from = $opts{f};
  58. die "Must provide sender using -f\n" if !$nomail && $host && !$from;
  59. my $ts = $opts{t};
  60. my $ua = new LWP::UserAgent;
  61. # Fetch subscribers first because we need to verify the password
  62. sub get_subscribers {
  63. my $url = "$root?action=subscriptionlist;raw=1;pwd=$admin_password";
  64. print "Getting $url\n" if $debug;
  65. my $response = $ua->get($url);
  66. die "Must provide an admin password with the -p option\n"
  67. if $response->code == 403 and not $admin_password;
  68. die "Must provide the correct admin password with the -p option\n"
  69. if $response->code == 403;
  70. die $url, "\n", $response->status_line unless $response->is_success;
  71. my %data;
  72. foreach my $line (split(/\n/, $response->content)) {
  73. my ($key, @entries) = split(/ +/, $line);
  74. # print "Subscription for $key: ", join(', ', @entries), "\n";
  75. $data{$key} = \@entries;
  76. }
  77. print "Found " . scalar(keys(%data)) . " subscribers\n" if $debug;
  78. return \%data;
  79. }
  80. # Fetch RSS feed
  81. sub get_timestamp {
  82. if ($ts and -f $ts) {
  83. return "from=" . (stat($ts))[9];
  84. } else {
  85. return "days=1";
  86. }
  87. }
  88. sub update_timestamp {
  89. # Only update timestamps if $ts is provided.
  90. return unless $ts;
  91. if (-f $ts) {
  92. # File exists: update timestamp.
  93. utime undef, undef, $ts;
  94. } else {
  95. # File does not exist: create it. File content is ignored on the
  96. # next run!
  97. my $dir = dirname($ts);
  98. mkpath($dir) unless -d $dir;
  99. open(F, ">$ts") or warn "Unable to create $ts: $!";
  100. close(F);
  101. }
  102. }
  103. sub get_rss {
  104. my $url = "$root?action=rss;full=1;short=0;" . get_timestamp();
  105. print "Getting $url\n" if $debug;
  106. my $response = $ua->get($url);
  107. die $url, $response->status_line unless $response->is_success;
  108. my $rss = new XML::RSS;
  109. $rss->parse($response->content);
  110. print "Found " . @{$rss->{items}} . " items.\n" if $debug;
  111. return $rss;
  112. }
  113. sub send_files {
  114. my ($rss, $subscribers) = @_;
  115. my @items = @{$rss->{items}};
  116. die "No items to send\n" unless @items;
  117. my $sent = 0;
  118. foreach my $item (@items) {
  119. my $title = $item->{title};
  120. print "Looking at $title\n" if $debug;
  121. my $id = $title;
  122. $id =~ s/ /_/g;
  123. my @subscribers = @{$subscribers->{$id}};
  124. print "Subscribers: ", join(', ', @subscribers), "\n" if $debug;
  125. $sent += @subscribers;
  126. send_file($id, $title, $item, @subscribers);
  127. }
  128. print "$sent messages sent\n" if $sent and not $quiet;
  129. }
  130. sub send_file {
  131. my ($id, $title, $item, @subscribers) = @_;
  132. return unless @subscribers;
  133. my $fh = File::Temp->new(SUFFIX => '.html');
  134. binmode($fh, ":utf8");
  135. warn "No content for $title\n" unless $item->{description};
  136. my $link = $item->{link};
  137. my $sub = "$root?action=subscriptions";
  138. my $text = qq(<p>Visit <a href="$link">$title</a>)
  139. . qq( or <a href="$sub">manage your subscriptions</a>.</p><hr />)
  140. . $item->{description};
  141. # prevent 501 Syntax error - line too long
  142. $text =~ s/<(p|h[1-6]|[duo]l|pre|li|form|div|blockquote|hr|table|tr)>/\r\n<$1>/gi;
  143. print $fh $text;
  144. $fh->close;
  145. foreach my $subscriber (@subscribers) {
  146. send_mail($subscriber, $title, $fh);
  147. }
  148. }
  149. sub send_mail {
  150. my ($subscriber, $title, $fh) = @_;
  151. print "Skipping mail to $subscriber...\n" if $debug && $nomail;
  152. return if $nomail;
  153. my $mail = new MIME::Entity->build(To => $subscriber,
  154. From => $from,
  155. Subject => $title,
  156. Path => $fh,
  157. Type=> "text/html");
  158. if ($host) {
  159. print "$root\nSending $title to $subscriber using ${user}\@${host}\n" if $verbose;
  160. my $smtp = Net::SMTP->new($host, Debug => $debug);
  161. $smtp->starttls();
  162. # the following requires Authen::SASL!
  163. $smtp->auth($user, $password);
  164. $smtp->mail($from);
  165. if ($smtp->to($subscriber)) {
  166. $smtp->data;
  167. $smtp->datasend($mail->stringify);
  168. $smtp->dataend;
  169. } else {
  170. warn "Error: ", $smtp->message();
  171. }
  172. $smtp->quit;
  173. }
  174. }
  175. sub main {
  176. my $rss = get_rss();
  177. if (@{$rss->{items}}) {
  178. my $subscribers = get_subscribers();
  179. if (%{$subscribers}) {
  180. send_files($rss, $subscribers);
  181. }
  182. }
  183. update_timestamp();
  184. }
  185. main ();