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