chiark / gitweb /
break out puser; call it early during choose
[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 puser ($) {
77     my ($u) = @_;
78     our $last_u;
79     if (!defined $last_u or $last_u ne $u) {
80         print "# user $u ".(isdisabled($u) ? 'disabled' : 'enabled')."\n";
81         $last_u = $u;
82     }
83 }
84
85 sub prow ($) {
86     my ($row) = @_;
87     my $u = $row->{'user'};
88     puser($u);
89     my $pa = $row->{'localpart'};
90     $pa .= '@'.$dom if defined $dom;
91     if (length $row->{'redirect'}) {
92         print "$pa: $row->{'redirect'}" or die $!;
93     } else {
94         print "# reject $pa" or die $!;
95     }
96     if ($showcomment || !$priv) {
97         print " #$row->{'comment'}" or die $!;
98     }
99     print "\n" or die $!;
100 }
101
102 sub goodrand ($) {
103     my ($lim) = @_;
104     for (;;) {
105         my $ch;
106         read(R, $ch, 1) == 1 or die $!;
107         my $o = ord $ch;
108         my $v = $o % $lim;
109         next unless $o-$v+$lim < 256;
110 #       print STDERR "goodrand($lim)=$v\n";
111         return $v;
112     }
113 }
114
115 sub qualify (\$) {
116     my ($ref) = @_;
117     if (defined $$ref && $$ref =~ m/[^\041-\177]/) {
118         die "bad characters in redirection target\n";
119     }
120     if (defined $$ref && length $$ref && $$ref !~ m/\@/) {
121         die "unqualified redirection target\n" unless defined $qualdom;
122         $$ref .= '@'.$qualdom;
123     }
124 }
125
126 sub insertrow ($) {
127     my ($row) = @_;
128     my $stmt =
129         "INSERT INTO addrs (".
130         (join ",", sort keys %$row).
131         ") VALUES (".
132         (join ",", map { "?" } sort keys %$row).
133         ") ";
134     $dbh->do($stmt, {}, map { $row->{$_} } sort keys %$row);
135 }
136
137 sub rhsargs ($) {
138     my ($defrow) = @_;
139     my $row = { };
140     while (@ARGV) {
141         $_ = shift @ARGV;
142         my $f = (s/^\#// ? 'comment' : 'redirect');
143         die "$f supplied twice\n" if exists $row->{$f};
144         $row->{$f} = $_;
145     }
146     foreach my $f (keys %$defrow) {
147         next if defined $row->{$f};
148         $row->{$f} = $defrow->{$f};
149     }
150     qualify $row->{'redirect'};
151     return $row;
152 }
153
154 sub local_part_inuse ($) {
155     my ($s) = @_;
156     our $checkexist_q ||=
157         $dbh->prepare("SELECT localpart FROM addrs WHERE localpart=?");
158     $checkexist_q->execute($s);
159     my $row = $checkexist_q->fetchrow_arrayref();
160     return !!$row;
161 }
162
163 sub generate_local_part () {
164     my $s;
165     for (;;) {
166         $s = chr(ord('a')+goodrand(26));
167         while (length $s < $randlength) {
168             my $v = goodrand(36);
169             $s .= chr($v < 26
170                       ? ord('a')+($v)
171                       : ord('0')+($v-26));
172         }
173 #       print STDERR "$s\n";
174         last if !local_part_inuse($s);
175     }
176     return $s;
177 }
178
179 sub prepare_create () {
180     my $countq = $dbh->prepare("SELECT count(*) FROM addrs WHERE user=?");
181     $countq->execute($user);
182     my ($count) = $countq->fetchrow_array();
183     die unless defined $count;
184     die "too many aliases for this user\n" if $count >= $maxperuser;
185     open R, "/dev/urandom" or die $!;
186     binmode R;
187 }
188
189 sub action_create {
190     my $newrow = rhsargs({'redirect'=>$user, 'comment'=>''});
191     prepare_create();
192     $newrow->{'user'} = $user;
193     $newrow->{'localpart'} = generate_local_part();
194     insertrow($newrow);
195     $dbh->commit();
196     prow($newrow);
197 }
198
199 sub action_choose {
200     my $template = rhsargs({'redirect'=>$user, 'comment'=>''});
201     $template->{'user'} = $user;
202     prepare_create();
203     puser($user);
204     my %s;
205     while (keys %s < 10) {
206         my $s = generate_local_part();
207         next if $s{$s};
208         print $s or die $!;
209         print "\@$dom" or die $! if $dom;
210         print "\n" or die $!;
211         $s{$s} = 1;
212     }
213     print "# enter addresses or local parts to create,".
214         " then \`.' on a line by itself\n"
215         or die $!;
216
217     while (<STDIN>) {
218         chomp;
219         last if m/^\.$/;
220         my $s;
221         if (eval {
222             $s = addr2localpart $_;
223             $s{$s} or die "not an (as-yet-unused) suggestion\n";
224             delete $s{$s};
225             die "just taken in the meantime (bad luck!)\n"
226                 if local_part_inuse $s;
227             1;
228         }) {
229             my $newrow = { %$template, 'localpart' => $s };
230             $dbh->commit();
231             prow($newrow);
232         } else {
233             $dbh->rollback();
234             print "!error: $@" or die $!;
235         }
236     }
237 }
238
239 sub selectrow ($) {
240     my ($localpart) = @_;
241     our $row_q ||= $dbh->prepare("SELECT * FROM addrs WHERE localpart=?");
242     $row_q->execute($localpart);
243     return $row_q->fetchrow_hashref();
244 }
245
246 sub begin_row ($) {
247     my ($localpart) = @_;
248     my $q = $dbh->prepare("SELECT * FROM addrs WHERE localpart=?");
249     my $row = selectrow $localpart;
250     die "unknown localpart\n" unless defined $row;
251     die "not owned by you\n" unless $priv || $row->{user} eq $user;
252     return $row;
253 }
254
255 sub action_update {
256     my $localpart = nextarg_addr;
257     my $updrow = rhsargs({});
258     nomoreargs;
259     begin_row($localpart);
260     foreach my $f (qw(redirect comment)) {
261         my $v = $updrow->{$f};
262         next unless defined $v;
263         $dbh->do("UPDATE addrs SET $f=? WHERE localpart=?",
264                  {}, $v, $localpart);
265     }
266     my $row = selectrow $localpart;
267     $dbh->commit;
268     prow($row);
269 }
270
271 sub action_show {
272     my $localpart = nextarg_addr;
273     nomoreargs;
274     my $row = begin_row($localpart);
275     prow($row);
276 }
277
278 sub listq ($) {
279     my ($q) = @_;
280     while (my $row = $q->fetchrow_hashref()) {
281         prow($row);
282     }
283 }
284
285 sub action_list {
286     nomoreargs;
287     my $q = $dbh->prepare("SELECT * FROM addrs WHERE user=?".
288                           " ORDER BY localpart");
289     $q->execute($user);
290     listq($q);
291 }
292
293 sub action_list_user {
294     die unless $priv;
295     $user = nextarg;
296     nomoreargs;
297     action_list;
298 }
299
300 sub action_list_all {
301     die unless $priv;
302     nomoreargs;
303     my $q = $dbh->prepare("SELECT * FROM addrs".
304                           " ORDER BY user, localpart");
305     $q->execute();
306     listq($q)
307 }
308
309 sub action_insert_exact {
310     die unless $priv;
311     my $row = { };
312     $row->{'localpart'} = nextarg_addr;
313     $row->{'user'} = $user = nextarg;
314     $row->{'redirect'} = nextarg;
315     $row->{'comment'} = nextarg;
316     nomoreargs;
317     insertrow($row);
318     $dbh->commit;
319 }
320
321 sub action_donate {
322     die unless $priv;
323     my $localpart = nextarg_addr;
324     my $newuser = nextarg;
325     nomoreargs;
326     begin_row($localpart);
327     $dbh->do('UPDATE addrs SET user=? WHERE localpart=?',
328              {}, $newuser, $localpart);
329     $dbh->commit;
330 }
331
332 sub action_enable_user {
333     die unless $priv;
334     $user = nextarg;
335     nomoreargs;
336     $dbh->do('DELETE FROM disabled_users WHERE user=?',{},$user);
337     $dbh->commit;
338 }
339
340 sub action_disable_user {
341     die unless $priv;
342     $user = nextarg;
343     nomoreargs;
344     $dbh->do('INSERT INTO disabled_users VALUES user (?)',{},$user);
345     $dbh->commit;
346 }
347
348 sub action_list_actions {
349     print $usage2 or die $!;
350 }
351
352 while (@ARGV) {
353     last unless $ARGV[0] =~ m/^-/;
354     $_ = shift @ARGV;
355     last if m/^--?$/;
356     for (;;) {
357         last unless m/^-./;
358         if (s/^-l(\d+)$//) {
359             $randlength = $1;
360         } elsif (s/^-m(\d+)$//) {
361             $maxperuser = $1;
362         } elsif (s/^-d(\S+)$//) {
363             $dom = $1;
364         } elsif (s/^-q(\S+)$//) {
365             $qualdom = $1;
366         } elsif (s/^-C/-/) {
367             $showcomment = 1;
368         } elsif (s/^-h/-/) {
369             print $usage1.$usage2.$usage3 or die $!;
370             exit 0;
371         } else {
372             die "unknown option \`$_'\n";
373         }
374     }
375 }
376
377 my $dbfile = nextarg();
378
379 if (defined $ENV{'USERV_USER'}) {
380     $priv=0;
381     $user = $ENV{'USERV_USER'};
382 } else {
383     $priv=1;
384     $user = ((getpwuid $<)[0]) or die;
385 }
386
387 $usage2 .= defined $dom
388     ? "ADDR may be a local part, implicitly qualified with \@$dom\n"
389     : "ADDR must be a local part (only)\n";
390 $usage2 .= "REDIRECT is implicitly qualified with \@$qualdom if it has no \@\n"
391     if defined $qualdom;
392
393 $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","","",
394                     { PrintError => 0, AutoCommit => 0, RaiseError => 1 }) 
395     or die "$dbfile $!";
396
397 my $action = nextarg();
398 $action =~ y/-/_/;
399 { no strict qw(refs); &{"action_$action"}(); }