--- /dev/null
+++ b/extra/cch/control/checkgroups.pl
@@ -0,0 +1,82 @@
+##  $Id: checkgroups.pl,v 1.2 2001/07/19 00:32:56 rra Exp $
+##
+##  checkgroups control message handler.
+##
+##  Copyright 2001 by Marco d'Itri <md@linux.it>
+##
+##  Redistribution and use in source and binary forms, with or without
+##  modification, are permitted provided that the following conditions
+##  are met:
+##
+##   1. Redistributions of source code must retain the above copyright
+##      notice, this list of conditions and the following disclaimer.
+##
+##   2. Redistributions in binary form must reproduce the above copyright
+##      notice, this list of conditions and the following disclaimer in the
+##      documentation and/or other materials provided with the distribution.
+
+use strict;
+
+sub control_checkgroups {
+    my ($par, $sender, $replyto, $site, $action, $log, $approved,
+        $headers, $body) = @_;
+    my ($newsgrouppats) = @$par;
+
+    if ($action eq 'mail') {
+        my $mail = sendmail("checkgroups by $sender");
+        print $mail "$sender posted the following checkgroups message:\n";
+        print $mail map { s/^~/~~/; "$_\n" } @$headers;
+        print $mail <<END;
+
+If you want to process it, feed the body
+of the message to docheckgroups while logged
+in as user ID "$inn::newsuser":
+
+$inn::controlprogs/docheckgroups '$newsgrouppats' <<zRbJ
+END
+        print $mail map { s/^~/~~/; "$_\n" } @$body;
+        print $mail "zRbJ\n";
+        close $mail or logdie("Cannot send mail: $!");
+    } elsif ($action eq 'log') {
+        if ($log) {
+            logger($log, "checkgroups by $sender", $headers, $body);
+        } else {
+            logmsg("checkgroups by $sender");
+        }
+    } elsif ($action eq 'doit') {
+        if (defined &local_docheckgroups) {
+            local_docheckgroups($body, $newsgrouppats, $log, $sender);
+        } else {
+            docheckgroups($body, $newsgrouppats, $log, $sender);
+        }
+    }
+}
+
+sub docheckgroups {
+    my ($body, $newsgrouppats, $log, $sender) = @_;
+
+    my $tempfile = "$inn::tmpdir/checkgroups.$$";
+    open(TEMPART, ">$tempfile.art")
+        or logdie("Cannot open $tempfile.art: $!");
+    print TEMPART map { s/^~/~~/; "$_\n" } @$body;
+    close TEMPART;
+
+    open(OLDIN, '<&STDIN') or die $!;
+    open(OLDOUT, '>&STDOUT') or die $!;
+    open(STDIN, "$tempfile.art") or die $!;
+    open(STDOUT, ">$tempfile") or die $!;
+    my $st = system("$inn::controlprogs/docheckgroups", $newsgrouppats);
+    logdie('Cannot run docheckgroups: ' . $!) if $st == -1;
+    logdie('docheckgroups returned status ' . ($st & 255)) if $st > 0;
+    open(STDIN, '<&OLDIN') or die $!;
+    open(STDOUT, '>&OLDOUT') or die $!;
+
+    open(TEMPFILE, $tempfile) or logdie("Cannot open $tempfile: $!");
+    my @output = <TEMPFILE>;
+    chop @output;
+    logger($log || 'mail', "checkgroups by $sender", \@output);
+    close TEMPFILE;
+    unlink($tempfile, "$tempfile.art");
+}
+
+1;
--- /dev/null
+++ b/extra/cch/control/ihave.pl
@@ -0,0 +1,58 @@
+##  $Id: ihave.pl,v 1.3 2001/07/19 00:32:56 rra Exp $
+##
+##  ihave control message handler.
+##
+##  Copyright 2001 by Marco d'Itri <md@linux.it>
+##
+##  Redistribution and use in source and binary forms, with or without
+##  modification, are permitted provided that the following conditions
+##  are met:
+##
+##   1. Redistributions of source code must retain the above copyright
+##      notice, this list of conditions and the following disclaimer.
+##
+##   2. Redistributions in binary form must reproduce the above copyright
+##      notice, this list of conditions and the following disclaimer in the
+##      documentation and/or other materials provided with the distribution.
+
+use strict;
+
+sub control_ihave {
+    my ($par, $sender, $replyto, $site, $action, $log, $approved,
+        $headers, $body) = @_;
+
+    if ($action eq 'mail') {
+        my $mail = sendmail("ihave by $sender");
+        print $mail map { s/^~/~~/; "$_\n" } @$body;
+        close $mail or logdie('Cannot send mail: ' . $!);
+    } elsif ($action eq 'log') {
+        if ($log) {
+            logger($log, "ihave $sender", $headers, $body);
+        } else {
+            logmsg("ihave $sender");
+        }
+    } elsif ($action eq 'doit') {
+        my $tempfile = "$inn::tmpdir/ihave.$$";
+        open(GREPHIST, "|grephistory -i > $tempfile")
+            or logdie('Cannot run grephistory: ' . $!);
+	foreach (@$body) {
+            print GREPHIST "$_\n";
+        }
+        close GREPHIST;
+
+        if (-s $tempfile) {
+            my $inews = open("$inn::inews -h")
+                or logdie('Cannot run inews: ' . $!);
+            print $inews "Newsgroups: to.$site\n"
+               . "Subject: cmsg sendme $inn::pathhost\n"
+               . "Control: sendme $inn::pathhost\n\n";
+            open(TEMPFILE, $tempfile) or logdie("Cannot open $tempfile: $!");
+            print $inews $_ while <TEMPFILE>;
+            close $inews or die $!;
+            close TEMPFILE;
+        }
+        unlink $tempfile;
+    }
+}
+
+1;
--- /dev/null
+++ b/extra/cch/control/newgroup.pl
@@ -0,0 +1,186 @@
+##  $Id: newgroup.pl,v 1.3 2001/07/19 00:32:56 rra Exp $
+##
+##  newgroup control message handler.
+##
+##  Copyright 2001 by Marco d'Itri <md@linux.it>
+##
+##  Redistribution and use in source and binary forms, with or without
+##  modification, are permitted provided that the following conditions
+##  are met:
+##
+##   1. Redistributions of source code must retain the above copyright
+##      notice, this list of conditions and the following disclaimer.
+##
+##   2. Redistributions in binary form must reproduce the above copyright
+##      notice, this list of conditions and the following disclaimer in the
+##      documentation and/or other materials provided with the distribution.
+
+use strict;
+
+sub control_newgroup {
+    my ($par, $sender, $replyto, $site, $action, $log, $approved,
+        $headers, $body) = @_;
+    my ($groupname, $modflag) = @$par;
+
+    $modflag ||= '';
+    my $modcmd = $modflag eq 'moderated' ? 'm' : 'y';
+
+    my $errmsg;
+    $errmsg= local_checkgroupname($groupname) if defined &local_checkgroupname;
+    if ($errmsg) {
+        $errmsg = checkgroupname($groupname) if $errmsg eq 'DONE';
+    }
+
+    if ($errmsg) {
+        if ($log) {
+            logger($log, "skipping newgroup ($errmsg)", $headers, $body);
+        } else {
+            logmsg("skipping newgroup ($errmsg)");
+        }
+        return;
+    }
+
+    # Scan active to see what sort of change we are making.
+    open(ACTIVE, $inn::active) or logdie("Cannot open $inn::active: $!");
+    my @oldgroup;
+    while (<ACTIVE>) {
+        next unless /^(\Q$groupname\E)\s\d+\s\d+\s(\w)/;
+        @oldgroup = split /\s+/;
+        last;
+    }
+    close ACTIVE;
+    my $status;
+    if (@oldgroup) {
+        if ($oldgroup[3] eq 'm' and $modflag ne 'moderated') {
+            $status = 'made unmoderated';
+        } elsif ($oldgroup[3] ne 'm' and $modflag eq 'moderated') {
+            $status = 'made moderated';
+        } else {
+            $status = 'no change';
+        }
+    } elsif (not $approved) {
+        $status = 'unapproved';
+    } else {
+        $status = 'created';
+    }
+
+    if ($action eq 'mail' and $status !~ /no change|unapproved/) {
+        my $mail = sendmail("newgroup $groupname $modcmd $sender");
+        print $mail <<END;
+$sender asks for $groupname
+to be $status.
+
+If this is acceptable, type:
+  $inn::newsbin/ctlinnd newgroup $groupname $modcmd $sender
+
+The control message follows:
+
+END
+        print $mail map { s/^~/~~/; "$_\n" } @$headers;
+        print $mail "\n";
+        print $mail map { s/^~/~~/; "$_\n" } @$body;
+        close $mail or logdie("Cannot send mail: $!");
+    } elsif ($action eq 'log') {
+        if ($log) {
+            logger($log, "skipping newgroup $groupname $modcmd"
+                . " $sender (would be $status)", $headers, $body);
+        } else {
+            logmsg("skipping newgroup $groupname $modcmd $sender"
+                . " (would be $status)");
+        }
+    } elsif ($action eq 'doit' and $status ne 'unapproved') {
+        ctlinnd('newgroup', $groupname, $modcmd, $sender)
+            if $status ne 'no change';
+
+        # If there is a tag line, update newsgroups too, even if the group
+        # did already exist.
+        my $found = 0;
+        my $ngline = '';
+        foreach (@$body) {
+            if ($found) {
+                $ngline = $_;
+                last;
+            }
+            $found = 1 if $_ eq 'For your newsgroups file:';
+        }
+        my ($ngname, $ngdesc) = split(/\s+/, $ngline, 2);
+        if ($ngdesc) {
+            $ngdesc =~ s/\s+$//;
+            $ngdesc =~ s/\s+\(moderated\)\s*$//i;
+            $ngdesc .= ' (Moderated)' if $modflag eq 'moderated';
+        }
+        update_desc($ngname, $ngdesc) if $ngdesc and $ngname eq $groupname;
+
+        logger($log, "newgroup $groupname $modcmd $status $sender",
+            $headers, $body) if $log;
+    }
+    return;
+}
+
+sub update_desc {
+    my ($name, $desc) = @_;
+    shlock("$inn::locks/LOCK.newsgroups");
+    my $tempfile = "$inn::newsgroups.$$";
+    open(NEWSGROUPS, $inn::newsgroups)
+        or logdie("Cannot open $inn::newsgroups: $!");
+    open(TEMPFILE, ">$tempfile") or logdie("Cannot open $tempfile: $!");
+    my $olddesc = '';
+    while (<NEWSGROUPS>) {
+        if (/^\Q$name\E\s+(.*)/) {
+            $olddesc = $1;
+            next;
+        }
+        print TEMPFILE $_;
+    }
+    print TEMPFILE "$name\t$desc\n";
+    close TEMPFILE;
+    close NEWSGROUPS;
+    # install the modified file only if the description has changed
+    if ($desc ne $olddesc) {
+        rename($tempfile, $inn::newsgroups)
+            or logdie("Cannot rename $tempfile: $!");
+    } else {
+        unlink($tempfile);
+    }
+    unlink("$inn::locks/LOCK.newsgroups", $tempfile);
+}
+
+# Check the group name.  This is partially derived from C News.
+# Some checks are commented out if I think they're too strict or
+# language-dependent.  Your mileage may vary.
+sub checkgroupname {
+    local $_ = shift;
+
+    # whole-name checking
+    return 'Empty group name' if /^$/;
+    return 'Whitespace in group name' if /\s/;
+    return 'unsafe group name' if /[\`\/:;]/;
+    return 'Bad dots in group name' if /^\./ or /\.$/ or /\.\./;
+#    return 'Group name does not begin/end with alphanumeric'
+#        if (/^[a-zA-Z0-9].+[a-zA-Z0-9]$/;
+    return 'Group name begins in control. or junk.' if /^(?:junk|control)\./;
+#    return 'Group name too long' if length $_ > 128;
+
+    my @components = split(/\./);
+    # prevent alt.a.b.c.d.e.f.g.w.x.y.z...
+    return 'Too many components' if $#components > 9;
+
+    # per-component checking
+    for (my $i = 0; $i <= $#components; $i++) {
+        local $_ = $components[$i];
+        return 'all-numeric name component' if /^[0-9]+$/;
+#        return 'name component starts with non-alphanumeric' if /^[a-zA-Z0-9]/;
+#        return 'name component does not contain letter' if not /[a-zA-Z]/;
+        return "`all' or `ctl' used as name component" if /^(?:all|ctl)$/;
+#        return 'name component longer than 30 characters' if length $_ > 30;
+#        return 'uppercase letter(s) in name' if /[A-Z]/;
+        return 'illegal character(s) in name' if /[^a-z0-9+_\-.]/;
+        # sigh, c++ etc must be allowed
+        return 'repeated punctuation in name' if /--|__|\+\+./;
+#        return 'repeated component(s) in name' if ($i + 2 <= $#components
+#            and $_ eq $components[$i + 1] and $_ eq $components[$i + 2]);
+    }
+    return '';
+}
+
+1;
--- /dev/null
+++ b/extra/cch/control/rmgroup.pl
@@ -0,0 +1,89 @@
+##  $Id: rmgroup.pl,v 1.3 2001/07/19 00:32:56 rra Exp $
+##
+##  rmgroup control message handler.
+##
+##  Copyright 2001 by Marco d'Itri <md@linux.it>
+##
+##  Redistribution and use in source and binary forms, with or without
+##  modification, are permitted provided that the following conditions
+##  are met:
+##
+##   1. Redistributions of source code must retain the above copyright
+##      notice, this list of conditions and the following disclaimer.
+##
+##   2. Redistributions in binary form must reproduce the above copyright
+##      notice, this list of conditions and the following disclaimer in the
+##      documentation and/or other materials provided with the distribution.
+
+use strict;
+
+sub control_rmgroup {
+    my ($par, $sender, $replyto, $site, $action, $log, $approved,
+        $headers, $body) = @_;
+    my ($groupname) = @$par;
+
+    # Scan active to see what sort of change we are making.
+    open(ACTIVE, $inn::active) or logdie("Cannot open $inn::active: $!");
+    my @oldgroup;
+    while (<ACTIVE>) {
+        next unless /^(\Q$groupname\E)\s\d+\s\d+\s(\w)/;
+        @oldgroup = split /\s+/;
+        last;
+    }
+    close ACTIVE;
+    my $status;
+    if (not @oldgroup) {
+        $status = 'no change';
+    } elsif (not $approved) {
+        $status = 'unapproved';
+    } else {
+        $status = 'removed';
+    }
+
+    if ($action eq 'mail' and $status !~ /(no change|unapproved)/) {
+        my $mail = sendmail("rmgroup $groupname $sender");
+        print $mail <<END;
+$sender asks for $groupname
+to be $status.
+
+If this is acceptable, type:
+  $inn::newsbin/ctlinnd rmgroup $groupname
+
+The control message follows:
+
+END
+        print $mail map { s/^~/~~/; "$_\n" } @$headers;
+        print $mail "\n";
+        print $mail map { s/^~/~~/; "$_\n" } @$body;
+        close $mail or logdie("Cannot send mail: $!");
+    } elsif ($action eq 'log') {
+        if ($log) {
+            logger($log, "skipping rmgroup $groupname"
+                . " $sender (would be $status)", $headers, $body);
+        } else {
+            logmsg("skipping rmgroup $groupname $sender (would be $status)");
+        }
+    } elsif ($action eq 'doit' and $status !~ /(no change|unapproved)/) {
+        ctlinnd('rmgroup', $groupname);
+        # Update newsgroups too.
+        shlock("$inn::locks/LOCK.newsgroups");
+        open(NEWSGROUPS, $inn::newsgroups)
+            or logdie("Cannot open $inn::newsgroups: $!");
+        my $tempfile = "$inn::newsgroups.$$";
+        open(TEMPFILE, ">$tempfile") or logdie("Cannot open $tempfile: $!");
+        while (<NEWSGROUPS>) {
+            print TEMPFILE $_ if not /^\Q$groupname\E\s/;
+        }
+        close TEMPFILE;
+        close NEWSGROUPS;
+        rename($tempfile, $inn::newsgroups)
+            or logdie("Cannot rename $tempfile: $!");
+        unlink "$inn::locks/LOCK.newsgroups";
+        unlink $tempfile;
+
+        logger($log, "rmgroup $groupname $status $sender", $headers, $body)
+            if $log;
+    }
+}
+
+1;
--- /dev/null
+++ b/extra/cch/control/sendme.pl
@@ -0,0 +1,55 @@
+##  $Id: sendme.pl,v 1.3 2001/07/19 00:32:56 rra Exp $
+##
+##  sendme control message handler.
+##
+##  Copyright 2001 by Marco d'Itri <md@linux.it>
+##
+##  Redistribution and use in source and binary forms, with or without
+##  modification, are permitted provided that the following conditions
+##  are met:
+##
+##   1. Redistributions of source code must retain the above copyright
+##      notice, this list of conditions and the following disclaimer.
+##
+##   2. Redistributions in binary form must reproduce the above copyright
+##      notice, this list of conditions and the following disclaimer in the
+##      documentation and/or other materials provided with the distribution.
+
+use strict;
+
+sub control_sendme {
+    my ($par, $sender, $replyto, $site, $action, $log, $approved,
+        $headers, $body) = @_;
+
+    if ($action eq 'mail') {
+        my $mail = sendmail("sendme by $sender");
+        print $mail map { s/^~/~~/; "$_\n" } @$body;
+        close $mail or logdie('Cannot send mail: ' . $!);
+    } elsif ($action eq 'log') {
+        if ($log) {
+            logger($log, "sendme $sender", $headers, $body);
+        } else {
+            logmsg("sendme from $sender");
+        }
+    } elsif ($action eq 'doit') {
+        my $tempfile = "$inn::tmpdir/sendme.$$";
+        open(GREPHIST, "|grephistory -s > $tempfile")
+            or logdie("Cannot run grephistory: $!");
+	foreach (@$body) {
+            print GREPHIST "$_\n";
+	}
+        close GREPHIST or logdie("Cannot run grephistory: $!");
+
+        if (-s $tempfile and $site =~ /^[a-zA-Z0-9.-_]+$/) {
+            open(TEMPFILE, $tempfile) or logdie("Cannot open $tempfile: $!");
+            open(BATCH, ">>$inn::batch/$site.work")
+                or logdie("Cannot open $inn::batch/$site.work: $!");
+            print BATCH $_ while <TEMPFILE>;
+            close BATCH;
+            close TEMPFILE;
+        }
+        unlink $tempfile;
+    }
+}
+
+1;
--- /dev/null
+++ b/extra/cch/control/sendsys.pl
@@ -0,0 +1,64 @@
+##  $Id: sendsys.pl,v 1.2 2001/07/19 00:32:56 rra Exp $
+##
+##  sendsys control message handler.
+##
+##  Copyright 2001 by Marco d'Itri <md@linux.it>
+##
+##  Redistribution and use in source and binary forms, with or without
+##  modification, are permitted provided that the following conditions
+##  are met:
+##
+##   1. Redistributions of source code must retain the above copyright
+##      notice, this list of conditions and the following disclaimer.
+##
+##   2. Redistributions in binary form must reproduce the above copyright
+##      notice, this list of conditions and the following disclaimer in the
+##      documentation and/or other materials provided with the distribution.
+
+use strict;
+
+sub control_sendsys {
+    my ($par, $sender, $replyto, $site, $action, $log, $approved,
+        $headers, $body) = @_;
+    my ($where) = @$par;
+
+    if ($action eq 'mail') {
+        my $mail = sendmail("sendsys $sender");
+        print $mail <<END;
+$sender has requested that you send a copy
+of your newsgroups file.
+
+If this is acceptable, type:
+  $inn::mailcmd -s "sendsys reply from $inn::pathhost" $replyto < $inn::newsfeeds
+
+The control message follows:
+
+END
+        print $mail map { s/^~/~~/; "$_\n" } @$headers;
+        print $mail "\n";
+        print $mail map { s/^~/~~/; "$_\n" } @$body;
+        close $mail or logdie("Cannot send mail: $!");
+    } elsif ($action eq 'log') {
+        if ($log) {
+            logger($log, "sendsys $sender", $headers, $body);
+        } else {
+            logmsg("sendsys $sender");
+        }
+    } elsif ($action =~ /^(doit|doifarg)$/) {
+        if ($action eq 'doifarg' and $where ne $inn::pathhost) {
+            logmsg("skipped sendsys $sender");
+            return;
+        }
+        my $mail = sendmail("sendsys reply from $inn::pathhost", $replyto);
+        open(NEWSFEEDS, $inn::newsfeeds)
+            or logdie("Cannot open $inn::newsfeeds: $!");
+        print $mail $_ while <NEWSFEEDS>;
+        print $mail "\n";
+        close NEWSFEEDS;
+        close $mail or logdie("Cannot send mail: $!");
+
+        logger($log, "sendsys $sender to $replyto", $headers, $body) if $log;
+    }
+}
+
+1;
--- /dev/null
+++ b/extra/cch/control/senduuname.pl
@@ -0,0 +1,61 @@
+##  $Id: senduuname.pl,v 1.2 2001/07/19 00:32:56 rra Exp $
+##
+##  senduuname control message handler.
+##
+##  Copyright 2001 by Marco d'Itri <md@linux.it>
+##
+##  Redistribution and use in source and binary forms, with or without
+##  modification, are permitted provided that the following conditions
+##  are met:
+##
+##   1. Redistributions of source code must retain the above copyright
+##      notice, this list of conditions and the following disclaimer.
+##
+##   2. Redistributions in binary form must reproduce the above copyright
+##      notice, this list of conditions and the following disclaimer in the
+##      documentation and/or other materials provided with the distribution.
+
+use strict;
+
+sub control_senduuname {
+    my ($par, $sender, $replyto, $site, $action, $log, $approved,
+        $headers, $body) = @_;
+    my ($where) = @$par;
+
+    if ($action eq 'mail') {
+        my $mail = sendmail("senduuname $sender");
+        print $mail <<END;
+$sender has requested information about your UUCP name.
+
+If this is acceptable, type:
+  uuname | $inn::mailcmd -s "senduuname reply from $inn::pathhost" $replyto
+
+The control message follows:
+
+END
+        print $mail map { s/^~/~~/; "$_\n" } @$headers;
+        print $mail "\n";
+        print $mail map { s/^~/~~/; "$_\n" } @$body;
+        close $mail or logdie("Cannot send mail: $!");
+    } elsif ($action eq 'log') {
+        if ($log) {
+            logger($log, "senduuname $sender", $headers, $body);
+        } else {
+            logmsg("senduuname $sender");
+        }
+    } elsif ($action =~ /^(doit|doifarg)$/) {
+        if ($action eq 'doifarg' and $where ne $inn::pathhost) {
+            logmsg("skipped senduuname $sender");
+            return;
+        }
+        my $mail = sendmail("senduuname reply from $inn::pathhost", $replyto);
+        open(UUNAME, 'uuname|') or logdie("Cannot run uuname: $!");
+        print $mail $_ while <UUNAME>;
+        close UUNAME or logdie("Cannot run uuname: $!");
+        close $mail or logdie("Cannot send mail: $!");
+
+        logger($log, "senduuname $sender to $replyto", $headers, $body) if $log;
+    }
+}
+
+1;
--- /dev/null
+++ b/extra/cch/control/version.pl
@@ -0,0 +1,61 @@
+##  $Id: version.pl,v 1.2 2001/07/19 00:32:56 rra Exp $
+##
+##  version control message handler.
+##
+##  Copyright 2001 by Marco d'Itri <md@linux.it>
+##
+##  Redistribution and use in source and binary forms, with or without
+##  modification, are permitted provided that the following conditions
+##  are met:
+##
+##   1. Redistributions of source code must retain the above copyright
+##      notice, this list of conditions and the following disclaimer.
+##
+##   2. Redistributions in binary form must reproduce the above copyright
+##      notice, this list of conditions and the following disclaimer in the
+##      documentation and/or other materials provided with the distribution.
+
+use strict;
+
+sub control_version {
+    my ($par, $sender, $replyto, $site, $action, $log, $approved,
+        $headers, $body) = @_;
+    my ($where) = @$par;
+
+    my $version = $inn::version || '(unknown version)';
+
+    if ($action eq 'mail') {
+        my $mail = sendmail("version $sender");
+        print $mail <<END;
+$sender has requested information about your
+news software version.
+
+If this is acceptable, type:
+  echo "InterNetNews $version" | $inn::mailcmd -s "version reply from $inn::pathhost" $replyto
+
+The control message follows:
+
+END
+        print $mail map { s/^~/~~/; "$_\n" } @$headers;
+        print $mail "\n";
+        print $mail map { s/^~/~~/; "$_\n" } @$body;
+        close $mail or logdie("Cannot send mail: $!");
+    } elsif ($action eq 'log') {
+        if ($log) {
+            logger($log, "version $sender", $headers, $body);
+        } else {
+            logmsg("version $sender");
+        }
+    } elsif ($action =~ /^(doit|doifarg)$/) {
+        if ($action eq 'doifarg' and $where ne $inn::pathhost) {
+            logmsg("skipped version $sender");
+            return;
+        }
+        sendmail("version reply from $inn::pathhost", $replyto,
+            [ "InterNetNews $version\n" ]);
+
+        logger($log, "version $sender to $replyto", $headers, $body) if $log;
+    }
+}
+
+1;
--- /dev/null
+++ b/extra/cch/controlchan
@@ -0,0 +1,463 @@
+#! /usr/bin/perl -w
+require '/usr/lib/news/innshellvars.pl';
+
+# convert INN 1.x variables to 2.x style
+$inn::syslog_facility = 'news';
+$inn::pgpverify = 'true';
+$inn::mta = $inn::sendmail;
+$inn::pathbin = $inn::newsbin;
+$inn::tmpdir = $inn::spooltemp;
+$inn::pathhost = `$inn::innconfval pathhost`;
+$inn::version = '1.7.2+insync-1.1d+debian';
+
+##  $Id: controlchan.in,v 1.7 2002/11/21 00:03:15 vinocur Exp $
+##
+##  Channel feed program to route control messages to an appropriate handler.
+##
+##  Copyright 2001 by Marco d'Itri <md@linux.it>
+##
+##  Redistribution and use in source and binary forms, with or without
+##  modification, are permitted provided that the following conditions
+##  are met:
+##
+##   1. Redistributions of source code must retain the above copyright
+##      notice, this list of conditions and the following disclaimer.
+##
+##   2. Redistributions in binary form must reproduce the above copyright
+##      notice, this list of conditions and the following disclaimer in the
+##      documentation and/or other materials provided with the distribution.
+##
+##  Give this program its own newsfeed.  Make sure that you've created
+##  the newsgroup control.cancel so that you don't have to scan through
+##  cancels, which this program won't process anyway.
+##
+##  Make a newsfeeds entry like this:
+##
+##  controlchan!\
+##     :!*,control,control.*,!control.cancel\
+##     :Tc,Wnsm\
+##     :@prefix@/bin/controlchan
+
+require 5.004_03;
+use strict;
+
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+
+# globals
+my ($cachedctl, $curmsgid);
+my $lastctl = 0;
+my $use_syslog = 0;
+my $debug = 0;
+
+# setup logging ###########################################################
+# do not log to syslog if stderr is connected to a console
+if (not -t 2) {
+    eval { require INN::Syslog; import INN::Syslog; $use_syslog = 1; };
+    eval { require Sys::Syslog; import Sys::Syslog; $use_syslog = 1; }
+        unless $use_syslog;
+}
+
+if ($use_syslog) {
+    eval "sub Sys::Syslog::_PATH_LOG { '/dev/log' }" if $^O eq 'dec_osf';
+    Sys::Syslog::setlogsock('unix') if $^O =~ /linux|dec_osf/;
+    openlog('controlchan', 'pid', $inn::syslog_facility);
+}
+logmsg('starting');
+
+# load modules from the control directory #################################
+opendir(CTL, $inn::controlprogs)
+    or logdie("Cannot open $inn::controlprogs", 'crit');
+foreach (readdir CTL) {
+    next if not /^([a-z\.]+\.pl)$/ or not -f "$inn::controlprogs/$_";
+    eval { require "$inn::controlprogs/$1" };
+    if ($@) {
+        $@ =~ s/\n/  /g;
+        logdie($@, 'crit');
+    }
+    logmsg("loaded $inn::controlprogs/$1", 'debug');
+}
+closedir CTL;
+
+# main loop ###############################################################
+while (<STDIN>) {
+    chop;
+    my ($token, $sitepath, $msgid) = split(/\s+/, $_);
+    next if not defined $token;
+    $sitepath ||= '';
+    $curmsgid = $msgid || '';
+
+    my $artfh = open_article($token);
+    next if not defined $artfh;
+
+    # suck in headers and body, normalize the strange ones
+    my (@headers, @body, %hdr);
+    if (not parse_article($artfh, \@headers, \@body, \%hdr)) {
+        close $artfh;
+        next;
+    }
+    close $artfh or logdie('sm died with status ' . ($? >> 8));
+
+    next if not exists $hdr{control};
+
+    $curmsgid = $hdr{'message-id'};
+    my $sender = cleanaddr($hdr{sender} || $hdr{from});
+    my $replyto = cleanaddr($hdr{'reply-to'} || $hdr{from});
+
+    my (@progparams, $progname);
+    if ($hdr{control} =~ /\s/) {
+        $hdr{control} =~ /^(\S+)\s+(.+)?/;
+        $progname = lc $1;
+        @progparams = split(/\s+/, lc $2) if $2;
+    } else {
+        $progname = lc $hdr{control};
+    }
+
+    next if $progname eq 'cancel';
+
+    if ($progname !~ /^([a-z]+)$/) {
+        logmsg("Naughty control in article $curmsgid ($progname)");
+        next;
+    }
+    $progname = $1;
+
+    # Do we want to process the message?  Let's check the permissions.
+    my ($action, $logname, $newsgrouppats) =
+        ctlperm($progname, $sender, $progparams[0],
+                $token, \@headers, \@body);
+
+    next if $action eq 'drop';
+
+    if ($action eq '_pgpfail') {
+        my $type = '';
+        if ($progname and $progname eq 'newgroup') {
+            if ($progparams[1] and $progparams[1] eq 'moderated') {
+                $type = 'm ';
+            } else {
+                $type = 'y ';
+            }
+        }
+        logmsg("skipping $progname $type$sender"
+            . "(pgpverify failed) in $curmsgid");
+        next;
+    }
+
+    # used by checkgroups. Convert from perl regexp to grep regexp.
+    if (local $_ = $newsgrouppats) {
+        s/\$\|/|/g;
+        s/[^\\]\.[^*]/?/g;
+        s/\$//;
+        s/\.\*/*/g;
+        s/\\([\$\+\.])/$1/g;
+        $progparams[0] = $_;
+    }
+
+    # find the appropriate module and call it
+    my $subname = "control_$progname";
+    my $subfind = \&$subname;
+    if (not defined &$subfind) {
+        if ($logname) {
+            logger($logname, "Unknown control message by $sender",
+                \@headers, \@body);
+        } else {
+            logmsg("Unknown \"$progname\" control by $sender");
+        }
+        next;
+    }
+
+    my $approved = $hdr{approved} ? 1 : 0;
+    logmsg("$subname, " . join(' ', @progparams)
+        . " $sender $replyto $token, $sitepath, $action"
+        . ($logname ? "=$logname" : '') .", $approved");
+
+    &$subfind(\@progparams, $sender, $replyto, $sitepath,
+        $action, $logname, $approved, \@headers, \@body);
+}
+
+closelog() if $use_syslog;
+exit 0;
+
+print $inn::most_logs.$inn::syslog_facility.$inn::mta.
+    $inn::pathhost.$inn::tmpdir.$inn::sendmail.$inn::version.
+    $inn::pathbin.$inn::innconfval.$inn::spooltemp.$
+    $inn::newsmaster.$inn::locks; # lint food
+
+# misc functions ##########################################################
+sub parse_article {
+    my ($artfh, $headers, $body, $hdr) = @_;
+    my $h;
+    my %uniquehdr = map { $_ => 1 }    qw(date followup-to from message-id
+        newsgroups path reply-to subject sender);
+
+    while (<$artfh>) {
+        chop;
+        last if /^$/;
+        push @$headers, $_;
+        if (/^(\S+):\s+(.+)/) {
+            $h = lc $1;
+            if (exists $hdr->{$h}) {
+                if (exists $uniquehdr{$h}) {
+                    logmsg("Multiple $1 headers in article $curmsgid");
+                    return 0;
+                }
+                $hdr->{$h} .= ' ' . $2;
+            } else {
+                $hdr->{$h} = $2;
+            }
+            next;
+        } elsif (/^\s+(.+)/) {
+            if (defined $h) {
+                $hdr->{$h} .= ' ' . $1;
+                next;
+            }
+        }
+        logmsg("Broken headers in article $curmsgid");
+        return 0;
+    }
+
+    # article is empty or does not exist
+    return 0 if not @$headers;
+
+    chop (@$body = <$artfh>);
+    return 1;
+}
+
+# Strip a mail address, innd-style.
+sub cleanaddr {
+    local $_ = shift;
+    s/(\s+)?\(.*\)(\s+)?//g;
+    s/.*<(.*)>.*/$1/;
+    s/[^-a-zA-Z0-9+_.@%]/_/g;    # protect MTA
+    s/^-/_/;                    # protect MTA
+    return $_;
+}
+
+# Read and cache control.ctl.
+sub readctlfile {
+    my $mtime = (stat($inn::ctlfile))[9];
+    return $cachedctl if $lastctl == $mtime;    # mtime has not changed.
+    $lastctl = $mtime;
+
+    my @ctllist;
+    open(CTLFILE, $inn::ctlfile)
+        or logdie("Cannot open $inn::ctlfile", 'crit');
+    while (<CTLFILE>) {
+        chop;
+        # Not a comment or blank? Convert wildmat to regex
+        next if not /^(\s+)?[^\#]/ or /^$/;
+        if (not /:(?:doit|doifarg|drop|log|mail|verify-.*)(?:=.*)?$/) {
+            s/.*://;
+            logmsg("$_ is not a valid action for control.ctl", 'err');
+            next;
+        }
+        # Convert to a : separated list of regexps
+        s/^all:/*:/i;
+        s/([\$\+\.])/\\$1/g;
+        s/\*/.*/g;
+        s/\?/./g;
+        s/(.*)/^$1\$/;
+        s/:/\$:^/g;
+        s/\|/\$|^/g;
+        push @ctllist, $_;
+    }
+    close CTLFILE;
+
+    logmsg('warning: control.ctl is empty!', 'err') if not @ctllist;
+    return $cachedctl = [ reverse @ctllist ];
+}
+
+# Parse a control message's permissions.
+sub ctlperm {
+    my ($type, $sender, $newsgroup, $token, $headers, $body) = @_;
+
+    my $action = 'drop';    # default
+    my ($logname, $hier);
+
+    # newgroup and rmgroup require newsgroup names; check explicitly for that
+    # here and return drop if the newsgroup is missing (to avoid a bunch of
+    # warnings from undefined values later on in permission checking).
+    if ($type eq 'newgroup' or $type eq 'rmgroup') {
+        unless ($newsgroup) {
+            return ('drop', undef, undef);
+        }
+    }
+
+    my $ctllist = readctlfile();
+    foreach (@$ctllist) {
+        my @ctlline = split /:/;
+        # 0: type  1: from@addr  2: group.*  3: action
+        if ($type =~ /$ctlline[0]/ and $sender =~ /$ctlline[1]/i and
+            ($type !~ /(?:new|rm)group/ or $newsgroup =~ /$ctlline[2]/)) {
+            $action = $ctlline[3];
+            $action =~ s/\^(.+)\$/$1/;
+            $action =~ s/\\//g;
+            $hier = $ctlline[2] if $type eq 'checkgroups';
+            last;
+        }
+    }
+
+    ($action, $logname) = split(/=/, $action);
+
+    if ($action =~ /^verify-(.+)/) {
+        my $keyowner = $1;
+        if ($inn::pgpverify and $inn::pgpverify =~ /^(?:true|on|yes)$/i) {
+            my $pgpresult = defined &local_pgpverify ?
+                local_pgpverify($token, $headers, $body) : pgpverify($token);
+            if ($keyowner eq $pgpresult) {
+                $action = 'doit';
+            } else {
+                $action = '_pgpfail';
+            }
+        } else {
+            $action = 'mail';
+        }
+    }
+
+    return ($action, $logname, $hier);
+}
+
+# Write stuff to a log or send mail to the news admin.
+sub logger {
+    my ($logfile, $message, $headers, $body) = @_;
+
+    if ($logfile eq 'mail') {
+        my $mail = sendmail($message);
+        print $mail map { s/^~/~~/; "$_\n" } @$headers;
+        print $mail "$_\n" . join ('', map { s/^~/~~/; "$_\n" } @$body)
+            if $body;
+        close $mail or logdie("Cannot send mail: $!");
+        return;
+    }
+
+    if ($logfile =~ /^([^.\/].*)/) {
+        $logfile = $1;
+    } else {
+        logmsg("Invalid log file: $logfile", 'err');
+        $logfile = 'control';
+    }
+
+    $logfile = "$inn::most_logs/$logfile.log" unless $logfile =~ /^\//;
+    my $lockfile = $logfile;
+    $lockfile =~ s#.*/##;
+    $lockfile = "$inn::locks/LOCK.$lockfile";
+    shlock($lockfile);
+
+    open(LOGFILE, ">>$logfile") or logdie("Cannot open $logfile: $!");
+    print LOGFILE "$message\n";
+    foreach (@$headers, '', @$body, '') {
+        print LOGFILE "    $_\n";
+    }
+    close LOGFILE;
+    unlink $lockfile;
+}
+
+# write to syslog or errlog
+sub logmsg {
+    my ($msg, $lvl) = @_;
+
+    return if $lvl and $lvl eq 'debug' and not $debug;
+    if ($use_syslog) {
+        syslog($lvl || 'notice', '%s', $msg);
+    } else {
+        print STDERR (scalar localtime) . ": $msg\n";
+    }
+}
+
+# log a message and then die
+sub logdie {
+    my ($msg, $lvl) = @_;
+
+    $msg .= " ($curmsgid)" if $curmsgid;
+    logmsg($msg, $lvl || 'err');
+    exit 1;
+}
+
+# wrappers executing external programs ####################################
+
+# Open an article appropriately to our storage method (or lack thereof).
+sub open_article {
+    my $token = shift;
+
+    return *ART if open(ART, $token);
+    logmsg("Cannot open article $token: $!");
+    return undef;
+}
+
+sub pgpverify {
+    my $token = shift;
+
+    open(PGPCHECK, "$inn::newsbin/pgpverify < $token |") or goto ERROR;
+    my $pgpresult = <PGPCHECK>;
+    close PGPCHECK or goto ERROR;
+    $pgpresult ||= '';
+    chop $pgpresult;
+    return $pgpresult;
+ERROR:
+    logmsg("pgpverify failed: $!", 'debug');
+    return '';
+}
+
+sub ctlinnd {
+    my ($cmd, @args) = @_;
+
+    my $st = system("$inn::newsbin/ctlinnd", '-s', $cmd, @args);
+    logdie('Cannot run ctlinnd: ' . $!) if $st == -1;
+    logdie('ctlinnd returned status ' . ($st & 255)) if $st > 0;
+}
+
+sub shlock {
+    my $lockfile = shift;
+
+    my $locktry = 0;
+    while ($locktry < 60) {
+        if (system("$inn::newsbin/shlock", '-p', $$, '-f', $lockfile) == 0) {
+            return 1;
+        }
+        $locktry++;
+        sleep 2;
+    }
+
+    my $lockreason;
+    if (open(LOCKFILE, $lockfile)) {
+        $lockreason = 'held by ' . (<LOCKFILE> || '?');
+        close LOCKFILE;
+    } else {
+        $lockreason = $!;
+    }
+    logdie("Cannot get lock $lockfile: $lockreason");
+    return undef;
+}
+
+# If $body is not defined, returns a file handle which must be closed.
+# Don't forget checking the return value of close().
+# $addresses may be a scalar or a reference to a list of addresses.
+# If not defined, $inn::newsmaster is the default.
+# parts of this code stolen from innmail.pl
+sub sendmail {
+    my ($subject, $addresses, $body) = @_;
+    $addresses = [ $addresses || $inn::newsmaster ] if not ref $addresses;
+    $subject ||= '(no subject)';
+
+    # fix up all addresses
+    my @addrs = map { s#[^-a-zA-Z0-9+_.@%]##g; $_ } @$addresses;
+
+    my $sm = $inn::mta;
+    if ($sm =~ /%s/) {
+        $sm = sprintf($sm, join(' ', @addrs));
+    } else {
+        $sm .= ' ' . join(' ', @addrs);
+    }
+
+    # fork and spawn the MTA whitout using the shell
+    my $pid = open(MTA, '|-');
+    logdie('Cannot fork: ' . $!) if $pid < 0;
+    if ($pid == 0) {
+        exec(split(/\s+/, $sm)) or logdie("Cannot exec $sm: $!");
+    }
+
+    print MTA 'To: ' . join(",\n\t", @addrs) . "\nSubject: $subject\n\n";
+    return *MTA if not defined $body;
+    $body = join("\n", @$body) if ref $body eq 'ARRAY';
+    print MTA $body . "\n";
+    close MTA or logdie("Execution of $sm failed: $!");
+    return 1;
+}
