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