chiark / gitweb /
wip
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 30 Aug 2013 13:32:31 +0000 (14:32 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 30 Aug 2013 13:32:31 +0000 (14:32 +0100)
.gitignore [new file with mode: 0644]
fyvzl [new file with mode: 0755]
fyvzl.service [new file with mode: 0644]
schema [new file with mode: 0644]
slimy-rot13-mail [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..b25c15b
--- /dev/null
@@ -0,0 +1 @@
+*~
diff --git a/fyvzl b/fyvzl
new file mode 100755 (executable)
index 0000000..b8ab577
--- /dev/null
+++ b/fyvzl
@@ -0,0 +1,242 @@
+#!/usr/bin/perl -w
+#
+# usage: ../fyvzl [-lLENGTH] [-mMAXPERUSER] [-dDEFDOM] DATABASE-FILE ACTION ARG
+# actions
+#   create [REDIRECT-TO]
+#   update LOCAL-PART REDIRECT-TO
+#   reject LOCAL-PART
+#   show LOCAL-PART
+#   list
+# privileged actions
+#   list-user USER
+#   insert-exact LOCAL-PART USER REDIRECT
+#   donate LOCAL-PART USER
+#   enable-user|disable-user USER
+
+use strict;
+
+use DBI;
+use POSIX;
+
+our $randlength = 6;
+our $maxperuser = 10000;
+our $defdom;
+our $dbh;
+our $user;
+our $priv;
+
+sub nextarg () {
+    die "too few arguments\n" unless @ARGV;
+    my $v = shift @ARGV;
+    die "option too late on command line\n" if $v =~ m/^-/;
+    return $v;
+}
+
+sub nomoreargs () {
+    die "too many arguments\n" if @ARGV;
+}
+
+sub isdisabled ($) {
+    my ($u) = @_;
+    our $dis_q;
+    $dis_q ||= $dbh->prepare("SELECT * FROM disabled_users WHERE user=?");
+    $dis_q->execute($u);
+    my $row = $dis_q->fetchrow_arrayref();
+    return !!$row;
+}
+
+sub prow ($) {
+    my ($row) = @_;
+    my $u = $row->{'user'};
+    our $last_u;
+    if (!defined $last_u or $last_u ne $u) {
+       print "# user $u ".(isdisabled($u) ? 'disabled' : 'enabled')."\n";
+       $last_u = $u;
+    }
+    if (defined $row->{'redirect'}) {
+       print "$row->{'localpart'}: $row->{'redirect'}\n";
+    } else {
+       print "# reject $row->{'localpart'}\n";
+    }
+}
+
+sub goodrand ($) {
+    my ($lim) = @_;
+    for (;;) {
+       my $ch;
+       read(R, $ch, 1) == 1 or die $!;
+       my $o = ord $ch;
+       my $v = $o % $lim;
+       next unless $o-$v+$lim < 256;
+#      print STDERR "goodrand($lim)=$v\n";
+       return $v;
+    }
+}
+
+sub qualify ($) {
+    my ($t) = @_;
+    return $t if $t =~ m/\@/;
+    die "unqualified redirection target\n" unless defined $defdom;
+    return $t.'@'.$defdom;
+}
+
+sub insertrow ($) {
+    my ($row) = @_;
+    my $stmt =
+       "INSERT INTO addrs (".
+       (join ",", sort keys %$row).
+       ") VALUES (?, ?, ?) ";
+    $dbh->do($stmt, {}, map { $row->{$_} } sort keys %$row);
+}
+
+sub action_create {
+    my $redirect;
+    if (@ARGV) { $redirect = nextarg; nomoreargs; } else { $redirect = $user; }
+    $redirect = qualify $redirect;
+    open R, "/dev/urandom" or die $!;
+    binmode R;
+    my $q = $dbh->prepare("SELECT localpart FROM addrs WHERE localpart=?");
+    my $s;
+    for (;;) {
+       $s = chr(ord('a')+goodrand(26));
+       while (length $s < $randlength) {
+           my $v = goodrand(36);
+           $s .= chr($v < 26
+                     ? ord('a')+($v)
+                     : ord('0')+($v-26));
+       }
+#      print STDERR "$s\n";
+       $q->execute($s);
+       my $row = $q->fetchrow_arrayref();
+       last if !$row;
+       $dbh->abort();
+    }
+    my $row = {'localpart'=>$s, 'user'=>$user, 'redirect'=>$redirect};
+    insertrow($row);
+    $dbh->commit();
+    prow($row);
+}
+
+sub begin_row ($) {
+    my ($localpart) = @_;
+    my $q = $dbh->prepare("SELECT * FROM addrs WHERE localpart=?");
+    $q->execute($localpart);
+    my $row = $q->fetchrow_hashref();
+    die "unknown localpart\n" unless defined $row;
+    die "not owned by you\n" unless $priv || $row->{user} eq $user;
+    return $row;
+                }
+
+sub action_update {
+    my $localpart = nextarg;
+    my $redirect = qualify nextarg;
+    nomoreargs;
+    begin_row($localpart);
+    $dbh->do("UPDATE addrs SET redirect=? WHERE localpart=?",
+            {}, $redirect, $localpart);
+    $dbh->commit;
+}
+
+sub action_reject {
+    my $localpart = nextarg;
+    nomoreargs;
+    begin_row($localpart);
+    $dbh->do("UPDATE addrs SET redirect=NULL WHERE localpart=?",
+            {}, $localpart);
+    $dbh->commit;
+}
+
+sub action_show {
+    my $localpart = nextarg;
+    nomoreargs;
+    my $row = begin_row($localpart);
+    prow($row);
+}
+
+sub action_list {
+    nomoreargs;
+    my $q = $dbh->prepare("SELECT * FROM addrs WHERE user=?".
+                         " ORDER BY localpart");
+    $q->execute($user);
+    while (my $row = $q->fetchrow_hashref()) {
+       prow($row);
+    }
+}
+
+sub action_list_user {
+    die unless $priv;
+    $user = nextarg;
+    nomoreargs;
+    action_list;
+}
+
+sub action_insert_exact {
+    die unless $priv;
+    my $localpart = nextarg;
+    $user = nextarg;
+    my $redirect = nextarg;
+    nomoreargs;
+    my $row = {'localpart'=>$localpart, 'user'=>$user, 'redirect'=>$redirect};
+    insertrow($row);
+    $dbh->commit;
+}
+
+sub action_donate {
+    die unless $priv;
+    my $localpart = nextarg;
+    my $newuser = nextarg;
+    nomoreargs;
+    begin_row($localpart);
+    $dbh->do('UPDATE addrs SET user=? WHERE localpart=?',
+            {}, $newuser, $localpart);
+    $dbh->commit;
+}
+
+sub action_enable_user {
+    die unless $priv;
+    $user = nextarg;
+    nomoreargs;
+    $dbh->do('DELETE FROM disabled_users WHERE user=?',{},$user);
+    $dbh->commit;
+}
+
+sub action_disable_user {
+    die unless $priv;
+    $user = nextarg;
+    nomoreargs;
+    $dbh->do('INSERT INTO disabled_users VALUES user (?)',{},$user);
+    $dbh->commit;
+}
+
+while (@ARGV) {
+    last unless $ARGV[0] =~ m/^-/;
+    $_ = shift @ARGV;
+    last if m/^--?$/;
+    if (m/^-l(\d+)$/) {
+       $randlength = $1;
+    } elsif (m/^-m(\d+)$/) {
+       $maxperuser = $1;
+    } elsif (m/^-d(\S+)$/) {
+       $defdom = $1;
+    } else {
+       die "unknown option \`$_'\n";
+    }
+}
+
+my $dbfile = nextarg();
+
+if (defined $ENV{'USERV_USER'}) {
+    $priv=0;
+    $user = $ENV{'USERV_USER'};
+} else {
+    $priv=1;
+    $user = ((getpwuid $<)[0]) or die;
+}
+
+$dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","","",
+                   { PrintError => 0, AutoCommit => 0, RaiseError => 1 }) 
+    or die "$dbfile $!";
+
+my $action = nextarg();
+$action =~ y/-/_/;
+{ no strict qw(refs); &{"action_$action"}(); }
diff --git a/fyvzl.service b/fyvzl.service
new file mode 100644 (file)
index 0000000..5f43d5d
--- /dev/null
@@ -0,0 +1,9 @@
+if ( grep calling-user-shell /etc/shells
+   & glob service-user mail
+   )
+       reset
+       no-set-environment
+       no-disconnect-hup
+       execute /usr/local/lib/fyvzl/fyvzl \
+               -dchiark.greenend.org.uk /var/lib/mail/fyvzl.db
+fi
diff --git a/schema b/schema
new file mode 100644 (file)
index 0000000..c14c315
--- /dev/null
+++ b/schema
@@ -0,0 +1,9 @@
+CREATE TABLE addrs (
+       localpart       STRING PRIMARY KEY,
+       user            STRING NOT NULL,
+       redirect        STRING
+);
+CREATE TABLE disabled_users (
+       user            STRING PRIMARY KEY
+);
+CREATE INDEX addr_users ON addrs (user, localpart);
diff --git a/slimy-rot13-mail b/slimy-rot13-mail
new file mode 100755 (executable)
index 0000000..b1c06f0
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+set -e
+exec userv mail fyvzl "$@"