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