chiark / gitweb /
break out generate_local_part and prepare_create
[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 generate_local_part () {
145     our $checkexist_q ||=
146         $dbh->prepare("SELECT localpart FROM addrs WHERE localpart=?");
147     my $s;
148     for (;;) {
149         $s = chr(ord('a')+goodrand(26));
150         while (length $s < $randlength) {
151             my $v = goodrand(36);
152             $s .= chr($v < 26
153                       ? ord('a')+($v)
154                       : ord('0')+($v-26));
155         }
156 #       print STDERR "$s\n";
157         $checkexist_q->execute($s);
158         my $row = $checkexist_q->fetchrow_arrayref();
159         last if !$row;
160     }
161     return $s;
162 }
163
164 sub prepare_create () {
165     my $countq = $dbh->prepare("SELECT count(*) FROM addrs WHERE user=?");
166     $countq->execute($user);
167     my ($count) = $countq->fetchrow_array();
168     die unless defined $count;
169     die "too many aliases for this user\n" if $count >= $maxperuser;
170     open R, "/dev/urandom" or die $!;
171     binmode R;
172 }
173
174 sub action_create {
175     my $newrow = rhsargs({'redirect'=>$user, 'comment'=>''});
176     prepare_create();
177     $newrow->{'user'} = $user;
178     $newrow->{'localpart'} = generate_local_part();
179     insertrow($newrow);
180     $dbh->commit();
181     prow($newrow);
182 }
183
184 sub selectrow ($) {
185     my ($localpart) = @_;
186     our $row_q ||= $dbh->prepare("SELECT * FROM addrs WHERE localpart=?");
187     $row_q->execute($localpart);
188     return $row_q->fetchrow_hashref();
189 }
190
191 sub begin_row ($) {
192     my ($localpart) = @_;
193     my $q = $dbh->prepare("SELECT * FROM addrs WHERE localpart=?");
194     my $row = selectrow $localpart;
195     die "unknown localpart\n" unless defined $row;
196     die "not owned by you\n" unless $priv || $row->{user} eq $user;
197     return $row;
198 }
199
200 sub action_update {
201     my $localpart = nextarg_addr;
202     my $updrow = rhsargs({});
203     nomoreargs;
204     begin_row($localpart);
205     foreach my $f (qw(redirect comment)) {
206         my $v = $updrow->{$f};
207         next unless defined $v;
208         $dbh->do("UPDATE addrs SET $f=? WHERE localpart=?",
209                  {}, $v, $localpart);
210     }
211     my $row = selectrow $localpart;
212     $dbh->commit;
213     prow($row);
214 }
215
216 sub action_show {
217     my $localpart = nextarg_addr;
218     nomoreargs;
219     my $row = begin_row($localpart);
220     prow($row);
221 }
222
223 sub listq ($) {
224     my ($q) = @_;
225     while (my $row = $q->fetchrow_hashref()) {
226         prow($row);
227     }
228 }
229
230 sub action_list {
231     nomoreargs;
232     my $q = $dbh->prepare("SELECT * FROM addrs WHERE user=?".
233                           " ORDER BY localpart");
234     $q->execute($user);
235     listq($q);
236 }
237
238 sub action_list_user {
239     die unless $priv;
240     $user = nextarg;
241     nomoreargs;
242     action_list;
243 }
244
245 sub action_list_all {
246     die unless $priv;
247     nomoreargs;
248     my $q = $dbh->prepare("SELECT * FROM addrs".
249                           " ORDER BY user, localpart");
250     $q->execute();
251     listq($q)
252 }
253
254 sub action_insert_exact {
255     die unless $priv;
256     my $row = { };
257     $row->{'localpart'} = nextarg_addr;
258     $row->{'user'} = $user = nextarg;
259     $row->{'redirect'} = nextarg;
260     $row->{'comment'} = nextarg;
261     nomoreargs;
262     insertrow($row);
263     $dbh->commit;
264 }
265
266 sub action_donate {
267     die unless $priv;
268     my $localpart = nextarg_addr;
269     my $newuser = nextarg;
270     nomoreargs;
271     begin_row($localpart);
272     $dbh->do('UPDATE addrs SET user=? WHERE localpart=?',
273              {}, $newuser, $localpart);
274     $dbh->commit;
275 }
276
277 sub action_enable_user {
278     die unless $priv;
279     $user = nextarg;
280     nomoreargs;
281     $dbh->do('DELETE FROM disabled_users WHERE user=?',{},$user);
282     $dbh->commit;
283 }
284
285 sub action_disable_user {
286     die unless $priv;
287     $user = nextarg;
288     nomoreargs;
289     $dbh->do('INSERT INTO disabled_users VALUES user (?)',{},$user);
290     $dbh->commit;
291 }
292
293 sub action_list_actions {
294     print $usage2 or die $!;
295 }
296
297 while (@ARGV) {
298     last unless $ARGV[0] =~ m/^-/;
299     $_ = shift @ARGV;
300     last if m/^--?$/;
301     for (;;) {
302         last unless m/^-./;
303         if (s/^-l(\d+)$//) {
304             $randlength = $1;
305         } elsif (s/^-m(\d+)$//) {
306             $maxperuser = $1;
307         } elsif (s/^-d(\S+)$//) {
308             $dom = $1;
309         } elsif (s/^-q(\S+)$//) {
310             $qualdom = $1;
311         } elsif (s/^-C/-/) {
312             $showcomment = 1;
313         } elsif (s/^-h/-/) {
314             print $usage1.$usage2.$usage3 or die $!;
315             exit 0;
316         } else {
317             die "unknown option \`$_'\n";
318         }
319     }
320 }
321
322 my $dbfile = nextarg();
323
324 if (defined $ENV{'USERV_USER'}) {
325     $priv=0;
326     $user = $ENV{'USERV_USER'};
327 } else {
328     $priv=1;
329     $user = ((getpwuid $<)[0]) or die;
330 }
331
332 $usage2 .= "ADDR may be a local part, implicitly qualified with \@$dom\n"
333     if defined $qualdom;
334 $usage2 .= "REDIRECT is implicitly qualified with \@$qualdom if it has no \@\n"
335     if defined $qualdom;
336
337 $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","","",
338                     { PrintError => 0, AutoCommit => 0, RaiseError => 1 }) 
339     or die "$dbfile $!";
340
341 my $action = nextarg();
342 $action =~ y/-/_/;
343 { no strict qw(refs); &{"action_$action"}(); }