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