chiark / gitweb /
1d64c5bd0708da373bddcde8640e69d9f4ff92da
[d.git] / service
1 #!/usr/bin/perl -w
2 our $usage1 = <<'END';
3 usage: ../fyvzl [OPTIONS DATABASE-FILE ACTION ARG
4 options
5   -lLENGTH   (for create)
6   -mMAXPERUSER
7   -dDOM
8   -qQUALDOM
9   -C     (show comments in output)
10   -h     (display help)
11 END
12 our $usage2 = <<'END';
13 actions
14   create [REDIRECT] [#COMMENT]    (default for REDIRECT is your username)
15   choose [REDIRECT] [#COMMENT]    (interactively see and allocate suggestions)
16   update ADDR [REDIRECT] [#COMMENT]
17   show ADDR
18   list
19   list-actions
20 empty string for REDIRECT means reject
21 END
22 our $usage3 = <<'END';
23 privileged actions
24   list-user USER
25   insert-exact ADDR USER REDIRECT COMMENT
26   donate ADDR USER
27   enable-user|disable-user USER
28 END
29 #/
30
31 use strict;
32
33 use DBI;
34 use POSIX;
35
36 our $randlength = 6;
37 our $maxperuser = 10000;
38 our $qualdom;
39 our $dbh;
40 our $dom;
41 our $user;
42 our $priv;
43 our $showcomment;
44
45 sub nextarg () {
46     die "too few arguments\n" unless @ARGV;
47     my $v = shift @ARGV;
48     die "option too late on command line\n" if $v =~ m/^-/;
49     return $v;
50 }
51
52 sub addr2localpart ($) {
53     my ($addr) = @_;
54     return $addr if $addr !~ m/\@/;
55     die "address not in correct domain (\@$dom)\n" unless $' eq $dom; #';
56     return $`; #`;
57 }
58
59 sub nextarg_addr () {
60     return addr2localpart nextarg;
61 }
62
63 sub nomoreargs () {
64     die "too many arguments\n" if @ARGV;
65 }
66
67 sub isdisabled ($) {
68     my ($u) = @_;
69     our $dis_q;
70     $dis_q ||= $dbh->prepare("SELECT * FROM disabled_users WHERE user=?");
71     $dis_q->execute($u);
72     my $row = $dis_q->fetchrow_arrayref();
73     return !!$row;
74 }
75
76 sub prow ($) {
77     my ($row) = @_;
78     my $u = $row->{'user'};
79     our $last_u;
80     if (!defined $last_u or $last_u ne $u) {
81         print "# user $u ".(isdisabled($u) ? 'disabled' : 'enabled')."\n";
82         $last_u = $u;
83     }
84     my $pa = $row->{'localpart'};
85     $pa .= '@'.$dom if defined $dom;
86     if (length $row->{'redirect'}) {
87         print "$pa: $row->{'redirect'}" or die $!;
88     } else {
89         print "# reject $pa" or die $!;
90     }
91     if ($showcomment || !$priv) {
92         print " #$row->{'comment'}" or die $!;
93     }
94     print "\n" or die $!;
95 }
96
97 sub goodrand ($) {
98     my ($lim) = @_;
99     for (;;) {
100         my $ch;
101         read(R, $ch, 1) == 1 or die $!;
102         my $o = ord $ch;
103         my $v = $o % $lim;
104         next unless $o-$v+$lim < 256;
105 #       print STDERR "goodrand($lim)=$v\n";
106         return $v;
107     }
108 }
109
110 sub qualify (\$) {
111     my ($ref) = @_;
112     if (defined $$ref && $$ref =~ m/[^\041-\177]/) {
113         die "bad characters in redirection target\n";
114     }
115     if (defined $$ref && length $$ref && $$ref !~ m/\@/) {
116         die "unqualified redirection target\n" unless defined $qualdom;
117         $$ref .= '@'.$qualdom;
118     }
119 }
120
121 sub insertrow ($) {
122     my ($row) = @_;
123     my $stmt =
124         "INSERT INTO addrs (".
125         (join ",", sort keys %$row).
126         ") VALUES (".
127         (join ",", map { "?" } sort keys %$row).
128         ") ";
129     $dbh->do($stmt, {}, map { $row->{$_} } sort keys %$row);
130 }
131
132 sub rhsargs ($) {
133     my ($defrow) = @_;
134     my $row = { };
135     while (@ARGV) {
136         $_ = shift @ARGV;
137         my $f = (s/^\#// ? 'comment' : 'redirect');
138         die "$f supplied twice\n" if exists $row->{$f};
139         $row->{$f} = $_;
140     }
141     foreach my $f (keys %$defrow) {
142         next if defined $row->{$f};
143         $row->{$f} = $defrow->{$f};
144     }
145     qualify $row->{'redirect'};
146     return $row;
147 }
148
149 sub local_part_inuse ($) {
150     my ($s) = @_;
151     our $checkexist_q ||=
152         $dbh->prepare("SELECT localpart FROM addrs WHERE localpart=?");
153     $checkexist_q->execute($s);
154     my $row = $checkexist_q->fetchrow_arrayref();
155     return !!$row;
156 }
157
158 sub generate_local_part () {
159     my $s;
160     for (;;) {
161         $s = chr(ord('a')+goodrand(26));
162         while (length $s < $randlength) {
163             my $v = goodrand(36);
164             $s .= chr($v < 26
165                       ? ord('a')+($v)
166                       : ord('0')+($v-26));
167         }
168 #       print STDERR "$s\n";
169         last if !local_part_inuse($s);
170     }
171     return $s;
172 }
173
174 sub prepare_create () {
175     my $countq = $dbh->prepare("SELECT count(*) FROM addrs WHERE user=?");
176     $countq->execute($user);
177     my ($count) = $countq->fetchrow_array();
178     die unless defined $count;
179     die "too many aliases for this user\n" if $count >= $maxperuser;
180     open R, "/dev/urandom" or die $!;
181     binmode R;
182 }
183
184 sub action_create {
185     my $newrow = rhsargs({'redirect'=>$user, 'comment'=>''});
186     prepare_create();
187     $newrow->{'user'} = $user;
188     $newrow->{'localpart'} = generate_local_part();
189     insertrow($newrow);
190     $dbh->commit();
191     prow($newrow);
192 }
193
194 sub action_choose {
195     my $template = rhsargs({'redirect'=>$user, 'comment'=>''});
196     $template->{'user'} = $user;
197     prepare_create();
198     my %s;
199     while (keys %s < 10) {
200         my $s = generate_local_part();
201         next if $s{$s};
202         print $s or die $!;
203         print "\@$dom" or die $! if $dom;
204         print "\n" or die $!;
205         $s{$s} = 1;
206     }
207     print "# enter addresses or local parts to create,".
208         " then \`.' on a line by itself\n"
209         or die $!;
210
211     while (<STDIN>) {
212         chomp;
213         last if m/^\.$/;
214         my $s;
215         if (eval {
216             $s = addr2localpart $_;
217             $s{$s} or die "not an (as-yet-unused) suggestion\n";
218             delete $s{$s};
219             die "just taken in the meantime (bad luck!)\n"
220                 if local_part_inuse $s;
221             1;
222         }) {
223             my $newrow = { %$template, 'localpart' => $s };
224             $dbh->commit();
225             prow($newrow);
226         } else {
227             $dbh->rollback();
228             print "!error: $@" or die $!;
229         }
230     }
231 }
232
233 sub selectrow ($) {
234     my ($localpart) = @_;
235     our $row_q ||= $dbh->prepare("SELECT * FROM addrs WHERE localpart=?");
236     $row_q->execute($localpart);
237     return $row_q->fetchrow_hashref();
238 }
239
240 sub begin_row ($) {
241     my ($localpart) = @_;
242     my $q = $dbh->prepare("SELECT * FROM addrs WHERE localpart=?");
243     my $row = selectrow $localpart;
244     die "unknown localpart\n" unless defined $row;
245     die "not owned by you\n" unless $priv || $row->{user} eq $user;
246     return $row;
247 }
248
249 sub action_update {
250     my $localpart = nextarg_addr;
251     my $updrow = rhsargs({});
252     nomoreargs;
253     begin_row($localpart);
254     foreach my $f (qw(redirect comment)) {
255         my $v = $updrow->{$f};
256         next unless defined $v;
257         $dbh->do("UPDATE addrs SET $f=? WHERE localpart=?",
258                  {}, $v, $localpart);
259     }
260     my $row = selectrow $localpart;
261     $dbh->commit;
262     prow($row);
263 }
264
265 sub action_show {
266     my $localpart = nextarg_addr;
267     nomoreargs;
268     my $row = begin_row($localpart);
269     prow($row);
270 }
271
272 sub listq ($) {
273     my ($q) = @_;
274     while (my $row = $q->fetchrow_hashref()) {
275         prow($row);
276     }
277 }
278
279 sub action_list {
280     nomoreargs;
281     my $q = $dbh->prepare("SELECT * FROM addrs WHERE user=?".
282                           " ORDER BY localpart");
283     $q->execute($user);
284     listq($q);
285 }
286
287 sub action_list_user {
288     die unless $priv;
289     $user = nextarg;
290     nomoreargs;
291     action_list;
292 }
293
294 sub action_list_all {
295     die unless $priv;
296     nomoreargs;
297     my $q = $dbh->prepare("SELECT * FROM addrs".
298                           " ORDER BY user, localpart");
299     $q->execute();
300     listq($q)
301 }
302
303 sub action_insert_exact {
304     die unless $priv;
305     my $row = { };
306     $row->{'localpart'} = nextarg_addr;
307     $row->{'user'} = $user = nextarg;
308     $row->{'redirect'} = nextarg;
309     $row->{'comment'} = nextarg;
310     nomoreargs;
311     insertrow($row);
312     $dbh->commit;
313 }
314
315 sub action_donate {
316     die unless $priv;
317     my $localpart = nextarg_addr;
318     my $newuser = nextarg;
319     nomoreargs;
320     begin_row($localpart);
321     $dbh->do('UPDATE addrs SET user=? WHERE localpart=?',
322              {}, $newuser, $localpart);
323     $dbh->commit;
324 }
325
326 sub action_enable_user {
327     die unless $priv;
328     $user = nextarg;
329     nomoreargs;
330     $dbh->do('DELETE FROM disabled_users WHERE user=?',{},$user);
331     $dbh->commit;
332 }
333
334 sub action_disable_user {
335     die unless $priv;
336     $user = nextarg;
337     nomoreargs;
338     $dbh->do('INSERT INTO disabled_users VALUES user (?)',{},$user);
339     $dbh->commit;
340 }
341
342 sub action_list_actions {
343     print $usage2 or die $!;
344 }
345
346 while (@ARGV) {
347     last unless $ARGV[0] =~ m/^-/;
348     $_ = shift @ARGV;
349     last if m/^--?$/;
350     for (;;) {
351         last unless m/^-./;
352         if (s/^-l(\d+)$//) {
353             $randlength = $1;
354         } elsif (s/^-m(\d+)$//) {
355             $maxperuser = $1;
356         } elsif (s/^-d(\S+)$//) {
357             $dom = $1;
358         } elsif (s/^-q(\S+)$//) {
359             $qualdom = $1;
360         } elsif (s/^-C/-/) {
361             $showcomment = 1;
362         } elsif (s/^-h/-/) {
363             print $usage1.$usage2.$usage3 or die $!;
364             exit 0;
365         } else {
366             die "unknown option \`$_'\n";
367         }
368     }
369 }
370
371 my $dbfile = nextarg();
372
373 if (defined $ENV{'USERV_USER'}) {
374     $priv=0;
375     $user = $ENV{'USERV_USER'};
376 } else {
377     $priv=1;
378     $user = ((getpwuid $<)[0]) or die;
379 }
380
381 $usage2 .= defined $dom
382     ? "ADDR may be a local part, implicitly qualified with \@$dom\n"
383     : "ADDR must be a local part (only)\n";
384 $usage2 .= "REDIRECT is implicitly qualified with \@$qualdom if it has no \@\n"
385     if defined $qualdom;
386
387 $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","","",
388                     { PrintError => 0, AutoCommit => 0, RaiseError => 1 }) 
389     or die "$dbfile $!";
390
391 my $action = nextarg();
392 $action =~ y/-/_/;
393 { no strict qw(refs); &{"action_$action"}(); }