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