summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config.sample15
-rwxr-xr-xdo-email25
-rwxr-xr-xnsa388
-rwxr-xr-xparse-mail132
-rwxr-xr-xparse-nagios88
-rwxr-xr-xparse-wiki111
-rwxr-xr-xrun-both4
7 files changed, 763 insertions, 0 deletions
diff --git a/config.sample b/config.sample
new file mode 100644
index 0000000..5fb761e
--- /dev/null
+++ b/config.sample
@@ -0,0 +1,15 @@
+---
+ irc:
+ nick: kgb
+ username: kgb
+ server: irc.oftc.net
+ realname: CVS Commit 2 IRC
+ nickservpassword: xxxxxxxxxxxxxxxx
+ nickserv_is_smart: true
+ port: 6667
+ mailin: /home/commit/Maildir
+ projects:
+ 'oftc': [ '#oftc' ]
+ 'weasel': [ 'weasel' ]
+ '*': [ '#commits' ]
+# vim:syn=yaml
diff --git a/do-email b/do-email
new file mode 100755
index 0000000..ff1574e
--- /dev/null
+++ b/do-email
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+set -e
+cd $HOME
+
+while [ 1 ] ; do
+ #for project in mixminion or freehaven debian.de oftc; do
+ for project in mixminion or debian.de oftc; do
+ for i in `( cd Maildir.$project/new ; ls -1)` ; do
+ bin/parse-mail $project Maildir.$project/new/$i && rm Maildir.$project/new/$i
+ done
+ done
+ for project in or oftc; do
+ for i in `( cd Maildir.$project-wiki/new ; ls -1)` ; do
+ bin/parse-wiki $project Maildir.$project-wiki/new/$i && rm Maildir.$project-wiki/new/$i
+ done
+ done
+ for project in weasel oftc; do
+ for i in `( cd Maildir.$project-nagios/new ; ls -1)` ; do
+ bin/parse-nagios $project Maildir.$project-nagios/new/$i && rm Maildir.$project-nagios/new/$i
+ done
+ done
+ sleep 10
+done
+
diff --git a/nsa b/nsa
new file mode 100755
index 0000000..47c484e
--- /dev/null
+++ b/nsa
@@ -0,0 +1,388 @@
+#!/usr/bin/ruby
+
+# (c) 2005 Peter Palfrader <weasel@debian.org>
+
+require 'socket'
+require 'yaml'
+require 'monitor'
+
+default_irc = {
+ 'server' => 'irc.oftc.net',
+ 'port' => 6667,
+ 'username' => 'unknown_nsa',
+ 'nick' => 'unknown_nsa',
+ 'realname' => 'Unknown NSA instance'
+}
+
+CONFIG = YAML::load( File.open( 'config' ) )
+default_irc.each_pair do |k,v|
+ CONFIG['irc'][k] = v unless CONFIG['irc'][k]
+end
+CONFIG['mailin'] = '/home/commit/Maildir' unless CONFIG['mailin'];
+
+Log = Object.new
+class << Log
+ def init
+ @ignore_list = []
+ @ignore_list << "dispatch thread"
+ @ignore_list << "connection"
+ @ignore_list << "waituntilonline - waiter"
+ @ignore_list << "waituntilonline - runthread"
+ end
+
+ def log section, message
+ puts message unless @ignore_list.include?(section)
+ end
+end
+
+
+class Connection
+ def initialize
+ @inQueue = []
+ @inQueue.extend(MonitorMixin)
+ @inEmpty = @inQueue.new_cond
+
+ @outQueue = []
+ @outQueue.extend(MonitorMixin)
+ @outEmpty = @outQueue.new_cond
+
+ @sock = TCPSocket.new(CONFIG['irc']['server'], CONFIG['irc']['port']);
+ puts "Connected!"
+
+ createInThread
+ createOutThread
+ end
+
+ def print line
+ @outQueue.synchronize do
+ @outQueue << line
+ @outEmpty.signal
+ end
+ end
+
+ def getline
+ line = ""
+ @inQueue.synchronize do
+ @inEmpty.wait_while { @inQueue.empty? }
+ line = @inQueue.shift
+ end
+ return line
+ end
+
+ private
+
+ def createInThread
+ @inThread = Thread.new {
+ begin
+ while true
+ line = @sock.readline
+ Log.log "connection", "[connection] <<< " + line
+ line.chop!
+
+ @inQueue.synchronize do
+ @inQueue << line
+ @inEmpty.signal
+ end
+ end
+ rescue => e
+ puts e.class.to_s+": "+e.message
+ puts e.backtrace
+ end
+ Thread.main.exit
+ }
+ end
+
+ def createOutThread
+ @outThread = Thread.new {
+ begin
+ while true
+ @outQueue.synchronize do
+ @outEmpty.wait_while { @outQueue.empty? }
+ line = @outQueue.shift
+ @sock.puts line
+ Log.log "connection", "[connection] >>> " + line
+ end
+ end
+ rescue => e
+ puts e.class.to_s+": "+e.message
+ puts e.backtrace
+ end
+ Thread.main.exit
+ }
+ end
+end
+
+IrcHandleDevNull = Object.new
+class << IrcHandleDevNull
+ def handle(irc, parsed)
+ end
+end
+
+IrcHandlePing = Object.new
+class << IrcHandlePing
+ def handle(irc, parsed)
+ irc.print "PONG "+irc.getNick
+ end
+end
+
+IrcHandle001 = Object.new
+class << IrcHandle001
+ def handle(irc, parsed)
+ irc.weAreOnline
+ end
+end
+
+IrcHandlePickAnotherNick = Object.new
+class << IrcHandlePickAnotherNick
+ def handle(irc, parsed)
+ irc.useADifferentNick
+ end
+end
+
+IrcHandleMode = Object.new
+class << IrcHandleMode
+ def handle(irc, parsed)
+ irc.modeUpdate(parsed)
+ end
+end
+
+class Irc
+ IRC_NOT_CONNECTED = 0
+ IRC_CONNECTED = 1
+ IRC_SENT_NICK = 2
+ IRC_PICK_NEW_NICK = 3
+ IRC_ONLINE = 4
+
+ def initialize
+ @handler = {}
+ @nick = "#{CONFIG['irc']['nick']}"
+ @onlineMonitor = Monitor.new;
+ @onlineCond = @onlineMonitor.new_cond
+
+
+ @handler['PING'] = IrcHandlePing # PING
+ @handler['NOTICE'] = IrcHandleDevNull # NOTICE
+ @handler['MODE'] = IrcHandleMode # NOTICE
+ @handler['001'] = IrcHandle001 # w :Welcome to the OFTC Internet
+ @handler['002'] = IrcHandleDevNull # w :Your host is neutron.oftc.net..
+ @handler['003'] = IrcHandleDevNull # w :This server was created Fri ....
+ @handler['004'] = IrcHandleDevNull # w neutron.oftc.net hybrid-7.1+oftc1....
+ @handler['005'] = IrcHandleDevNull # w WALLCHOPS KNOCK EXCEPTS INVEX....
+ @handler['375'] = IrcHandleDevNull # RPL_MOTDSTART
+ @handler['372'] = IrcHandleDevNull # RPL_MOTD
+ @handler['376'] = IrcHandleDevNull # RPL_ENDOFMOTD
+ @handler['250'] = IrcHandleDevNull # Highest connection count: 2 (2 clients) (7 connections received)
+ @handler['251'] = IrcHandleDevNull # RPL_LUSERCLIENT
+ @handler['252'] = IrcHandleDevNull # RPL_LUSEROP
+ @handler['253'] = IrcHandleDevNull # 1 :unknown connection(s)
+ @handler['254'] = IrcHandleDevNull # 575 :channels formed
+ @handler['255'] = IrcHandleDevNull # RPL_LUSERME
+ @handler['265'] = IrcHandleDevNull # Current local users: 2 Max: 2
+ @handler['266'] = IrcHandleDevNull # Current global users: 2 Max: 2
+ @handler['353'] = IrcHandleDevNull # RAB = #rab :RAB @weasel·
+ @handler['366'] = IrcHandleDevNull # RAB #rab :End of /NAMES list.
+ @handler['433'] = IrcHandlePickAnotherNick # * oftc-bot :Nickname is already in use.
+
+ dispatchToHandlers
+ run
+ end
+
+ def print line
+ @connection.print line
+ end
+
+ def getNick
+ @nick
+ end
+
+ def weAreOnline
+ throw "Current run_state is @{run_state}. that's unexpected" unless @run_state == IRC_SENT_NICK
+ @run_state = IRC_ONLINE
+ @run_thread.wakeup
+ end
+
+ def useADifferentNick
+ throw "Current run_state is @{run_state}. that's unexpected" unless @run_state == IRC_SENT_NICK
+ @run_state = IRC_PICK_NEW_NICK
+ @run_thread.wakeup
+ end
+
+ def modeUpdate(message)
+ puts "[irc] Received mode update: " + message.to_yaml.gsub("\n","\n ")
+ end
+
+
+ def waitUntilOnline
+ Log.log "waituntilonline - waiter", "[waituntilonline] entering monitor"
+ @onlineMonitor.synchronize do
+ Log.log "waituntilonline - waiter", "[waituntilonline] entered monitor"
+ @onlineCond.wait_while { @run_state != IRC_ONLINE }
+ Log.log "waituntilonline - waiter", "[waituntilonline] waiting done"
+ @onlineCond.signal
+ Log.log "waituntilonline - waiter", "[waituntilonline] woke up the rest"
+ end
+ Log.log "waituntilonline - waiter", "[waituntilonline] left monitor"
+ end
+
+ private
+
+ def run
+ @run_thread = Thread.new {
+ @run_state = IRC_NOT_CONNECTED
+ while true
+ puts "[run thread] state '#{@run_state}'"
+ case @run_state
+ when IRC_NOT_CONNECTED
+ @connection = Connection.new
+ @dispatch_thread.wakeup
+ @run_state = IRC_CONNECTED
+ when IRC_CONNECTED
+ @connection.print "USER #{CONFIG['irc']['username']} . . :#{CONFIG['irc']['realname']}"
+ issueNick
+ @run_state = IRC_SENT_NICK
+ when IRC_SENT_NICK
+ sleep
+ when IRC_PICK_NEW_NICK
+ @nick.succ!
+ issueNick
+ @run_state = IRC_SENT_NICK
+ when IRC_ONLINE
+ @connection.print "MODE #{@nick} +w"
+
+ Log.log "waituntilonline - runthread", "[run thread] entering monitor onlineMonitor"
+ @onlineMonitor.synchronize do
+ Log.log "waituntilonline - runthread", "[run thread] entered monitor onlineMonitor"
+ @onlineCond.signal
+ Log.log "waituntilonline - runthread", "[run thread] sent signal on onlineCond"
+ end
+ Log.log "waituntilonline - runthread", "[run thread] left monitor onlineMonitor"
+ sleep
+ end
+ end
+ }
+ end
+
+ def dispatchToHandlers
+ @dispatch_thread = Thread.new {
+ while true
+ while not @connection
+ Log.log "dispatch thread", "[dispatch thread] waiting for connection"
+ sleep
+ Log.log "dispatch thread", "[dispatch thread] waiting for connection done"
+ end
+
+ Log.log "dispatch thread", "[dispatch thread] waiting for line"
+ line = @connection.getline
+ Log.log "dispatch thread", "[dispatch thread] waiting for line done"
+
+ parsed = parseLine line
+
+ if @handler.has_key?(parsed['command'])
+ Log.log "dispatch thread", "[dispatch thread] dispatching #{parsed['command']}: " + parsed['params'].join(' ')
+ @handler[parsed['command']].handle( self, parsed )
+ else
+ Log.log "dispatch thread - unhandled", "[dispatch thread] Unhandled: #{line}"
+ end
+ end
+ }
+ end
+
+ def issueNick
+ @connection.print "NICK #{@nick}"
+ end
+
+ def parseLine line
+ source = nil
+ (source, line) = line.split(' ', 2) if line[0,1] == ':'
+ source = source[1,source.length-1] if source
+ (command, line) = line.split(' ', 2)
+ params = []
+ while line and line[0,1] != ':'
+ (middle, line) = line.split(' ', 2)
+ params << middle
+ end
+ params << line[1,line.length-1] if line and line[0,1] == ':'
+ throw "hmmmm. line is '#{line}'." if line and line[0,1] != ':'
+
+ return {
+ 'source' => source,
+ 'command' => command,
+ 'params' => params
+ }
+ end
+end
+
+Thread.abort_on_exception = true
+
+Dir.chdir( CONFIG['mailin'] + "/new" )
+
+Log.init
+bot = Irc.new
+bot.waitUntilOnline
+sleep 1
+if CONFIG['irc']['nickserv_is_smart']
+ bot.print "NICKSERV :identify #{CONFIG['irc']['nickservpassword']} #{CONFIG['irc']['nick']}"
+else
+ bot.print "NICKSERV :identify #{CONFIG['irc']['nickservpassword']}"
+end
+sleep 5
+
+channels = {}
+CONFIG['projects'].each_value do |cl|
+ cl.each do |c|
+ channels[c] = true
+ end
+end
+channels.each_key do |c|
+ bot.print "JOIN #{c}"
+end
+
+while (1) do
+ oldcommitmsg = nil
+ Dir.foreach('.') { |filename|
+ next if filename == "."
+ next if filename == ".."
+
+ in_headers = true
+ fh = File.open(filename, "r")
+ project = nil
+ lines = []
+ commitmsg = ""
+ fh.readlines.each { |line|
+ line.chomp!
+ in_headers = false if line == ""
+ if (in_headers and not line =~ /^\s/)
+ (header, content) = line.split(':', 2);
+ content.strip!
+ if header.upcase == "SUBJECT"
+ project = /Announce\s+([A-Za-z0-9_-]+)/.match(content)[1];
+ end
+ elsif (not in_headers)
+ lines.push line
+ commitmsg = commitmsg + line
+ end
+ }
+ fh.close
+ File.unlink(filename)
+
+ puts "Project "+project
+ puts "commitmsg "+commitmsg
+
+ if project and commitmsg != oldcommitmsg
+ pr = "%c"%(002) + project + "%c: "%(002)
+ lines.each{ |line|
+ if (line != '')
+ line = pr + line
+ channellist = CONFIG['projects'].has_key?(project) ? CONFIG['projects'][project] : CONFIG['*']
+ channellist.each do |c|
+ bot.print "PRIVMSG #{c} :#{line}"
+ end
+ end
+ }
+ end
+ oldcommitmsg = commitmsg
+ }
+ sleep 5
+end
+
+Thread.stop
diff --git a/parse-mail b/parse-mail
new file mode 100755
index 0000000..4c64f57
--- /dev/null
+++ b/parse-mail
@@ -0,0 +1,132 @@
+#!/usr/bin/perl -wT
+
+use strict;
+use English;
+use File::Basename;
+
+my $HOME = '/home/commit/';
+my $MAX_LINES = 4;
+my $ENVELOPE_FROM = 'nobody@commit.noreply.org';
+my $HEADER_FROM = 'nobody@commit.noreply.org';
+my $BOT_ADDRESS = 'commit@commit.noreply.org';
+my $SENDMAIL = '/usr/sbin/sendmail';
+my $UNPARSEABLE = $HOME.'/Maildir.unparseable';
+
+$ENV{'PATH'} = '/bin:/usr/bin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+
+die ("Usage: $PROGRAM_NAME <project> <mailfile>\n") unless (scalar @ARGV == 2);
+my $project = $ARGV[0];
+
+umask 077;
+
+my $TMPDIR = "$HOME/cvs-tmp/";
+-d $TMPDIR or
+ mkdir $TMPDIR or
+ die ("Cannot mkdir $TMPDIR: $0\n");
+
+$project =~ m/^([a-zA-Z-]+(\.[a-zA-Z-]+)*)+$/;
+$project = $1;
+die ("Project is not a nice name.\n") unless defined $project;
+
+sub whine_and_store_away($) {
+ my ($complaint) = @_;
+
+ $ARGV[1] =~ m#(Maildir.[a-z]+/new/[0-9]+\.[0-9_]+\.[a-z]+)#;
+ my $clean_filename = $1;
+ die ("Unable to clean filename '$ARGV[1]' for taint mode.") unless defined $clean_filename;
+
+ my $newname = $UNPARSEABLE.'/new/'.basename($clean_filename);
+ link ($clean_filename, $newname) or die ("Cannot link $clean_filename to $newname: $!\n");
+ unlink ($clean_filename) or die ("Cannot unlink $clean_filename: $!\n");
+ print $complaint;
+ exit (1);
+};
+
+my $from;
+my $message;
+my $in_msg;
+my $found_update_of;
+my $tag;
+my $cvs_mailcommit_directory;
+
+open (MAIL, $ARGV[1]) || die ("Cannot open $ARGV[1]: $!\n");
+my @mail = <MAIL>;
+close(MAIL);
+
+my $line;
+while (defined($line = shift @mail)) {
+ if ($line =~ m/^From:.*CVS User ([a-zA-Z0-9-.]*)/ && !defined $from) {
+ $from = $1;
+ } elsif ($line =~ m/^From:.*<(\S*?)@/ && !defined $from) {
+ $from = $1;
+ } elsif ($line =~ m/^From:\s*(\S*?)@/ && !defined $from) {
+ $from = $1;
+ # Subject: [oftc-cvs-commits] CVS oftc-hybrid
+ } elsif ($line =~ m/^Subject: (?:\[\S*\] )?CVS\s*(.*)/) {
+ $cvs_mailcommit_directory = $1;
+ } elsif ($line =~ m/^Update of /) {
+ $found_update_of = 1;
+ } elsif ($line =~ m/^\s*Tag:\s*(.*)$/) {
+ $tag = $1;
+ } elsif ($line =~ m/^Log Message:/) {
+ $in_msg = 1;
+ last;
+ }
+};
+die ("$PROGRAM_NAME - $project: No author found.\n") unless defined $from;
+whine_and_store_away("$PROGRAM_NAME - $project: 'Update of ' not found.\n") unless defined $found_update_of;
+
+my $cur_line = 0;
+my $last_line = '';
+if ($in_msg) {
+ while (defined($line = shift @mail)) {
+ if ($line =~ m/^Index: / ||
+ $line =~ m/^--- NEW FILE/ ||
+ $line =~ m/^--/ ||
+ $line =~ m/^\*\*\*\*\*\*\*\*/
+ ) {
+ last;
+ }
+ next if ($line =~ m/^\s*$/);
+ if ($cur_line == $MAX_LINES) {
+ $last_line = $_;
+ } elsif ($cur_line > $MAX_LINES) {
+ $last_line = "...\n";
+ last;
+ } else {
+ $message .= $_;
+ };
+ $cur_line++;
+ }
+}
+$message .= $last_line;
+
+die ("No message found.\n") unless defined $message;
+my $notice = "commit by $from: $message";
+
+my $old_notice = '';
+my $filename = $TMPDIR.'/'.$project;
+if (-e $filename) {
+ open(F, $filename) or die ("cannot open $filename: $!\n");
+ local $/ = undef;
+ $old_notice = <F>;
+ close(F);
+}
+if ($notice eq $old_notice) {
+ exit(0);
+}
+open(F, ">".$filename) or die ("cannot open $filename for writing: $!\n");
+print F $notice;
+close(F);
+
+#open(MAIL, "|cat") or
+open(MAIL, "|$SENDMAIL -t -oi -f $ENVELOPE_FROM") or
+ die ("Cannot exec sendmail: $!\n");
+print MAIL "From: $HEADER_FROM\n";
+print MAIL "To: $BOT_ADDRESS\n";
+print MAIL "Subject: Announce $project\n";
+print MAIL "Precedence: junk\n";
+print MAIL "\n";
+print MAIL (defined $tag ? "[$tag] " : '') . (defined $cvs_mailcommit_directory && length ($cvs_mailcommit_directory) ? "[$cvs_mailcommit_directory] " : ''). $notice;
+close(MAIL);
diff --git a/parse-nagios b/parse-nagios
new file mode 100755
index 0000000..3edb127
--- /dev/null
+++ b/parse-nagios
@@ -0,0 +1,88 @@
+#!/usr/bin/perl -wT
+
+use strict;
+use English;
+use File::Basename;
+
+my $MAX_LINES = 4;
+my $ENVELOPE_FROM = 'nobody@commit.noreply.org';
+my $HEADER_FROM = 'nobody@commit.noreply.org';
+my $BOT_ADDRESS = 'commit@commit.noreply.org';
+my $SENDMAIL = '/usr/sbin/sendmail';
+
+$ENV{'PATH'} = '/bin:/usr/bin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+
+die ("Usage: $PROGRAM_NAME <project> <mailfile>\n") unless (scalar @ARGV == 2);
+my $project = $ARGV[0];
+
+umask 077;
+
+
+$project =~ m/^([a-zA-Z-]+(\.[a-zA-Z-]+)*)+$/;
+$project = $1;
+die ("Project is not a nice name.\n") unless defined $project;
+
+
+my $type;
+my $service;
+my $host;
+my $state;
+my $info;
+my $running_number;
+my $ack_by;
+my $comment;
+
+open (MAIL, $ARGV[1]) || die ("Cannot open $ARGV[1]: $!\n");
+my @mail = <MAIL>;
+close(MAIL);
+
+my $line;
+while (defined($line = shift @mail)) {
+ if ($line =~ m/^Subject:.*?alert ([0-9]+) - /) {
+ $running_number = $1;
+ } elsif ($line =~ m/^Notification Type:\s*(.*)/) {
+ $type = $1;
+ } elsif ($line =~ m/^Service:\s*(.*)/) {
+ $service = $1;
+ } elsif ($line =~ m/^Host:\s*(.*)/) {
+ $host = $1;
+ } elsif ($line =~ m/^State:\s*(.*)/) {
+ $state = $1;
+ } elsif ($line =~ m/^ACK by:\s*(.*)/) {
+ $ack_by = $1;
+ } elsif ($line =~ m/^Comment:\s*(.*)/) {
+ $comment = $1;
+ } elsif ($line =~ m/^(Additional )?Info:/) {
+ while (defined($line = shift @mail) && !($line =~ /\S/)) {
+ # null
+ }
+ if (defined $line) {
+ chomp $line;
+ $info = $line;
+ };
+ }
+};
+die ("$PROGRAM_NAME - $project: No type found.\n") unless defined $type;
+# die ("$PROGRAM_NAME - $project: No service found.\n") unless defined $service;
+die ("$PROGRAM_NAME - $project: No host found.\n") unless defined $host;
+die ("$PROGRAM_NAME - $project: No state found.\n") unless defined $state;
+die ("$PROGRAM_NAME - $project: No info found.\n") unless defined $info;
+
+$type =~ tr/A-Z/a-z/;
+$service = $host unless defined $service;
+
+my $extra = (defined $running_number) ? " (#$running_number)" : "";
+
+#open(MAIL, "|cat") or
+open(MAIL, "|$SENDMAIL -t -oi -f $ENVELOPE_FROM") or
+ die ("Cannot exec sendmail: $!\n");
+print MAIL "From: $HEADER_FROM\n";
+print MAIL "To: $BOT_ADDRESS\n";
+print MAIL "Subject: Announce $project\n";
+print MAIL "Precedence: junk\n";
+print MAIL "\n";
+print MAIL "[$type\@$host] $service is $state$extra: $info\n";
+print MAIL "ACK by: $ack_by\n" if ($ack_by);
+print MAIL "Comment: $comment\n" if ($comment);
+close(MAIL);
diff --git a/parse-wiki b/parse-wiki
new file mode 100755
index 0000000..822857b
--- /dev/null
+++ b/parse-wiki
@@ -0,0 +1,111 @@
+#!/usr/bin/perl -wT
+
+use strict;
+use English;
+use MIME::Base64;
+
+
+my $HOME = '/home/commit/';
+my $MAX_LINES = 4;
+my $ENVELOPE_FROM = 'nobody@commit.noreply.org';
+my $HEADER_FROM = 'nobody@commit.noreply.org';
+my $BOT_ADDRESS = 'commit@commit.noreply.org';
+my $SENDMAIL = '/usr/sbin/sendmail';
+
+$ENV{'PATH'} = '/bin:/usr/bin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+
+die ("Usage: $PROGRAM_NAME <project> <mailfile>\n") unless (scalar @ARGV == 2);
+my $project = $ARGV[0];
+
+umask 077;
+
+my $TMPDIR = "$HOME/cvs-tmp/";
+-d $TMPDIR or
+ mkdir $TMPDIR or
+ die ("Cannot mkdir $TMPDIR: $0\n");
+
+$project =~ m/^([a-zA-Z-]+(\.[a-zA-Z-]+)*)+$/;
+$project = $1;
+die ("Project is not a nice name.\n") unless defined $project;
+
+
+my $from;
+my $what;
+my $is_base64 = 0;
+
+
+open (MAIL, $ARGV[1]) || die ("Cannot open $ARGV[1]: $!\n");
+my @mail = <MAIL>;
+close(MAIL);
+my $line;
+
+my @headers;
+my $lastline = undef;
+while (defined($line = shift @mail)) {
+ my $chomped = $line;
+ chomp $chomped;
+ if ($chomped =~ m/^\s+/) {
+ $lastline .= ' '.$line;
+ } elsif ($chomped =~ m/^$/) {
+ push @headers, $lastline if defined $lastline;
+ push @headers, $line;
+ push @headers, @mail;
+ last
+ } else {
+ push @headers, $lastline if defined $lastline;
+ $lastline = $line;
+ }
+}
+
+while (defined($line = shift @headers)) {
+ if ($line =~ m/^Subject:\s+(?:\[.*?\]\s+)?(?:Trivial )?Update\s+of\s+"(.*)"\s+by\s+(.*?)\s*$/s) {
+ $what = $1;
+ $from = $2;
+ # =?utf-8?q?=5BOFTC=5D_Update_of_=22OFTC=22_by_PeterPalfrader?=
+ # =?utf-8?q?=5BOFTC=5D_Update_of_=22Staff=22_by_SethArnold?=
+ } elsif ($line =~ m/^Subject: (?:\[.*?\] )?=\?utf-8\?q\?=5BOFTC=5D_(?:Trivial_)?Update_of_=22(.*)=22_by_(.*?)\?=$/s) {
+ $what = $1;
+ $from = $2;
+ } elsif ($line =~ m/^Content-Transfer-Encoding: base64$/s) {
+ $is_base64 = 1;
+ } elsif ($line =~ m/^$/) {
+ last;
+ }
+};
+
+$from =~ s/\?=\s*=\?utf-8\?q\?//sg;
+
+die ("$PROGRAM_NAME - $project: No author found.\n") unless defined $from;
+die ("$PROGRAM_NAME - $project: No what found.\n") unless defined $what;
+
+my $body = join '', @mail;
+$body = decode_base64($body) if ($is_base64);
+
+my $message;
+my $url;
+my @lines = split(/\n/, $body);
+while (defined($line = shift @lines)) {
+ if ($line =~ m/^The following page has been changed by /) {
+ $url = shift @lines
+ } elsif ($line =~ m/^The comment on the change is:/) {
+ $message = shift @lines;
+ last;
+ };
+};
+
+my $notice = (defined $message) ?
+ "$from updated $what: $message" :
+ "$from updated $what";
+$notice .= " - $url" if defined $url;
+
+#open(MAIL, "|cat") or
+open(MAIL, "|$SENDMAIL -t -oi -f $ENVELOPE_FROM") or
+ die ("Cannot exec sendmail: $!\n");
+print MAIL "From: $HEADER_FROM\n";
+print MAIL "To: $BOT_ADDRESS\n";
+print MAIL "Subject: Announce $project\n";
+print MAIL "Precedence: junk\n";
+print MAIL "\n";
+print MAIL "[wiki] $notice\n";
+close(MAIL);
diff --git a/run-both b/run-both
new file mode 100755
index 0000000..d6d6a8d
--- /dev/null
+++ b/run-both
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+./bin/do-email &
+while [ 1 ] ; do ./bin/nsa; sleep 10; done