1 | package folder; |
1 | package folder; |
2 | |
2 | |
3 | BEGIN { *slog = \&::slog }; |
3 | BEGIN { *slog = \&::slog }; |
4 | |
4 | |
5 | use Digest::SHA1; |
5 | use Fcntl; |
|
|
6 | use File::Sync (); |
|
|
7 | |
|
|
8 | use Inline Config => NAME => "syncmail::folder"; |
|
|
9 | use Inline C; |
6 | |
10 | |
7 | use constant MDIFVERSION => 1; |
11 | use constant MDIFVERSION => 1; |
|
|
12 | |
|
|
13 | BEGIN { |
|
|
14 | if (1) { |
|
|
15 | use OpenSSL (); |
|
|
16 | *hash = \&OpenSSL::Digest::sha1_hex; |
|
|
17 | } elsif (0) { |
|
|
18 | # use Digest::SHA1 (); |
|
|
19 | my $digest = new Digest::SHA1; |
|
|
20 | *hash = sub { |
|
|
21 | $digest->reset; |
|
|
22 | $digest->add(@_); |
|
|
23 | $mid = $digest->hexdigest; |
|
|
24 | }; |
|
|
25 | } |
|
|
26 | } |
|
|
27 | |
|
|
28 | sub fflush { |
|
|
29 | my $oldfh = select $_[0]; |
|
|
30 | $| = 1; $| = 0; |
|
|
31 | select $oldfh; |
|
|
32 | } |
|
|
33 | |
|
|
34 | # rename a file and fsync the directory |
|
|
35 | sub replace { |
|
|
36 | my ($src, $dst) = @_; |
|
|
37 | my $self = shift; |
|
|
38 | |
|
|
39 | rename $src, $dst; |
|
|
40 | |
|
|
41 | $dst =~ s/[^\/]*$/./; |
|
|
42 | |
|
|
43 | # now sync the directory |
|
|
44 | |
|
|
45 | open my $dir, "<", $dst |
|
|
46 | or die "$dst: $!"; |
|
|
47 | |
|
|
48 | File::Sync::fsync($dir); |
|
|
49 | } |
8 | |
50 | |
9 | sub new { |
51 | sub new { |
10 | my $class = shift; |
52 | my $class = shift; |
11 | my %arg = @_; |
53 | my %arg = @_; |
12 | bless { |
54 | my $self = bless { |
13 | path => "$::PREFIX/$arg{name}", |
55 | path => "$::PREFIX/$arg{name}", |
14 | %arg, |
56 | %arg, |
15 | }, $class; |
57 | }, $class; |
|
|
58 | $self->open(0); |
|
|
59 | $self; |
16 | } |
60 | } |
17 | |
61 | |
18 | sub dirty { |
62 | sub dirty { |
19 | $_[0]{dirty} = 1; |
63 | $_[0]{dirty} = 1; |
20 | } |
64 | } |
21 | |
65 | |
22 | sub DESTROY { |
66 | sub DESTROY { |
23 | $_[0]->write_mdif; |
67 | #$_[0]->write_mdif; # do NOT! |
24 | } |
68 | } |
25 | |
69 | |
26 | # parse_mbox(mbox-file-path, callback) |
70 | # parse_mbox(mbox-file-path, callback) |
27 | # callback gets called with \$header and \$body, |
71 | # callback gets called with \$header and \$body, |
28 | # $header includes the mbox From_ line without |
72 | # $header includes the mbox From_ line without |
29 | # the leading From_ itself. |
73 | # the leading From_ itself. |
30 | sub parse_mbox { |
74 | sub parse_mbox { |
31 | my ($path, $cb) = @_; |
75 | my ($fh, $cb) = @_; |
32 | |
|
|
33 | open my $fh, "<", $path |
|
|
34 | or die "$path: $!"; |
|
|
35 | |
76 | |
36 | local $/ = "\n\n"; |
77 | local $/ = "\n\n"; |
37 | |
78 | |
38 | my ($head, $body, $offs); |
79 | my ($head, $body, $offs); |
39 | |
80 | |
40 | 5 == read $fh, $head, 5 |
81 | read $fh, $head, 5; |
41 | or return; |
|
|
42 | |
82 | |
43 | $head eq "From " |
83 | $head eq "From " or $head eq "" |
44 | or return; |
84 | or return; |
45 | |
85 | |
46 | $offs = 0; |
86 | $offs = 0; |
47 | while (defined ($head = <$fh>)) { |
87 | while (defined ($head = <$fh>)) { |
48 | $head =~ /^.*? [A-Z][a-z][a-z] [A-Z][a-z][a-z] [ 0-9][0-9] \d\d:\d\d:\d\d(?: [+-]\d\d\d\d)? \d\d(?:\d\d)\n/ |
88 | $head =~ /^.*? [A-Z][a-z][a-z] [A-Z][a-z][a-z] [ 0-9][0-9] \d\d:\d\d:\d\d(?: [+-]\d\d\d\d)? \d\d(?:\d\d)\n/ |
… | |
… | |
57 | $body = <$fh>; |
97 | $body = <$fh>; |
58 | #} |
98 | #} |
59 | chomp $body; |
99 | chomp $body; |
60 | $cb->($offs, \$head, \$body); |
100 | $cb->($offs, \$head, \$body); |
61 | $offs = (tell $fh) - 5; |
101 | $offs = (tell $fh) - 5; |
62 | ::give unless ++$ecnt & 1023; |
102 | &::give unless ++$ecnt & 255; |
63 | } |
103 | } |
64 | |
104 | |
65 | 1; |
105 | 1; |
66 | } |
106 | } |
67 | |
107 | |
… | |
… | |
134 | or die "$path~: $!"; |
174 | or die "$path~: $!"; |
135 | |
175 | |
136 | print $fh "# automatically generated, do NOT edit\n"; |
176 | print $fh "# automatically generated, do NOT edit\n"; |
137 | |
177 | |
138 | print $fh "[SYNCMAIL]\n"; |
178 | print $fh "[SYNCMAIL]\n"; |
139 | print $fh "$_=$self->{$_}\n" for (qw(fsize mtime version)); |
179 | print $fh "$_=$self->{$_}\n" for (qw(fsize mtime version ctime)); |
140 | |
180 | |
141 | print $fh "[HOSTS]\n"; |
181 | print $fh "[HOSTS]\n"; |
142 | while (my ($k,$v) = each %{$self->{host}}) { |
182 | while (my ($k,$v) = each %{$self->{host}}) { |
143 | print $fh "$k=$v\n"; |
183 | print $fh "$k=$v\n"; |
144 | } |
184 | } |
… | |
… | |
150 | print $fh "[DIFF $_->[0]]\n"; |
190 | print $fh "[DIFF $_->[0]]\n"; |
151 | print $fh "+$_\n" for @{$_->[1]}; |
191 | print $fh "+$_\n" for @{$_->[1]}; |
152 | print $fh "-$_\n" for @{$_->[2]}; |
192 | print $fh "-$_\n" for @{$_->[2]}; |
153 | } |
193 | } |
154 | |
194 | |
|
|
195 | fflush($fh); |
|
|
196 | File::Sync::fsync($fh); |
155 | close $fh |
197 | close $fh |
156 | or die "$path~: unable to create updated .mdif: $!"; |
198 | or die "$path~: unable to create updated .mdif: $!"; |
157 | |
199 | |
158 | rename "$path~", $path; |
200 | replace("$path~", $path); |
159 | |
201 | |
160 | delete $self->{dirty}; |
202 | delete $self->{dirty}; |
161 | } |
203 | } |
162 | |
204 | |
163 | if (1) { |
|
|
164 | use OpenSSL (); |
|
|
165 | *hash = \&OpenSSL::Digest::sha1_hex; |
|
|
166 | } elsif (0) { |
|
|
167 | # use Digest::SHA1; |
|
|
168 | my $digest = new Digest::SHA1; |
|
|
169 | *hash = sub { |
|
|
170 | $digest->reset; |
|
|
171 | $digest->add(@_); |
|
|
172 | $mid = $digest->hexdigest; |
|
|
173 | }; |
|
|
174 | } |
|
|
175 | |
|
|
176 | sub gendiff { |
205 | sub gendiff { |
177 | my ($d1, $d2) = @_; |
206 | my ($self, $d1, $d2) = @_; |
178 | |
207 | |
179 | my (@add, @del); |
208 | my (@add, @del); |
180 | my (%d1, %d2); |
209 | my (%d1, %d2); |
181 | |
210 | |
182 | for (@$d2) { |
211 | for (@$d2) { |
… | |
… | |
193 | # add msgs in d2 but not in d1 |
222 | # add msgs in d2 but not in d1 |
194 | for (@$d2) { |
223 | for (@$d2) { |
195 | push @add, $_->[1] unless exists $d1{$_->[1]}; |
224 | push @add, $_->[1] unless exists $d1{$_->[1]}; |
196 | } |
225 | } |
197 | |
226 | |
|
|
227 | push @{$self->{diff}}, [ |
|
|
228 | $self->{ctime}, |
198 | (\@add, \@del); |
229 | \@add, \@del, |
|
|
230 | ] if @add || @del; |
|
|
231 | } |
|
|
232 | |
|
|
233 | sub open { |
|
|
234 | my ($self, $rw) = @_; |
|
|
235 | |
|
|
236 | if (!$self->{fh} || $self->{rw} != $rw) { |
|
|
237 | $self->close; |
|
|
238 | $self->{rw} = $rw; |
|
|
239 | sysopen $self->{fh}, $self->{path}, |
|
|
240 | O_CREAT | ($rw ? O_RDWR : O_RDONLY), |
|
|
241 | 0666 |
|
|
242 | or die "$self->{path}: $!"; |
|
|
243 | 0 == setlkw(fileno $self->{fh}, $rw ? 2 : 1) |
|
|
244 | or die "$self->{path}: $!"; |
|
|
245 | |
|
|
246 | $self->check; |
|
|
247 | } |
|
|
248 | } |
|
|
249 | |
|
|
250 | sub close { |
|
|
251 | my $self = shift; |
|
|
252 | |
|
|
253 | if ($self->{rw} && $self->{fh}) { |
|
|
254 | fflush($self->{fh}); |
|
|
255 | File::Sync::fsync($self->{fh}); |
|
|
256 | } |
|
|
257 | |
|
|
258 | $self->write_mdif; |
|
|
259 | |
|
|
260 | delete $self->{fh}; |
199 | } |
261 | } |
200 | |
262 | |
201 | sub check { |
263 | sub check { |
202 | my $self = shift; |
264 | my $self = shift; |
203 | my $path = $self->{path}; |
|
|
204 | my $conf = $self->conf_path; |
265 | my $conf = $self->conf_path; |
205 | my $guard = $::lockdisk->guard; |
266 | my $guard = $::lockdisk->guard; |
206 | |
267 | |
207 | slog 3, "checking $path\n"; |
268 | slog 3, "checking $self->{path}\n"; |
208 | |
269 | |
209 | if (stat $path) { |
|
|
210 | my ($fsize, $mtime) = (stat _)[7, 9]; |
270 | my ($fsize, $mtime) = (stat $self->{fh})[7, 9]; |
211 | |
271 | |
|
|
272 | if ($self->{idx}) { |
|
|
273 | return 1 if $fsize == $self->{fsize} |
|
|
274 | && $mtime == $self->{mtime}; |
|
|
275 | } else { |
212 | if (open my $fh, "<", $conf) { |
276 | if (open my $fh, "<", $conf) { |
213 | my %conf; |
277 | my %conf; |
214 | <$fh>; # skip initial comment |
278 | <$fh>; # skip initial comment |
215 | <$fh> eq "[SYNCMAIL]\n" |
279 | <$fh> eq "[SYNCMAIL]\n" |
216 | or die "$conf: format error"; |
280 | or die "$conf: format error"; |
… | |
… | |
219 | } |
283 | } |
220 | return 1 if $fsize == $conf{fsize} |
284 | return 1 if $fsize == $conf{fsize} |
221 | && $mtime == $conf{mtime}; |
285 | && $mtime == $conf{mtime}; |
222 | |
286 | |
223 | $conf{mtime} <= $mtime |
287 | $conf{mtime} <= $mtime |
224 | or die "$path: folder older than mdif"; |
288 | or die "$self->{path}: folder older than mdif"; |
225 | } |
289 | } |
|
|
290 | } |
226 | |
291 | |
227 | slog 2, "updating $path\n"; |
292 | slog 2, "updating $self->{path}\n"; |
228 | |
293 | |
229 | my @idx; |
294 | my @idx; |
230 | |
295 | |
|
|
296 | seek $self->{fh}, 0, SEEK_SET; |
|
|
297 | |
231 | parse_mbox $path, sub { |
298 | parse_mbox $self->{fh}, sub { |
232 | my ($offs, $head, $body) = @_; |
299 | my ($offs, $head, $body) = @_; |
233 | push @idx, [$offs, hash($$head, "\0", $$body)]; |
300 | push @idx, [$offs, hash($$head, "\n\n", $$body)]; |
234 | } or return (); |
301 | } or die "$self->{path}: no valid mbox file"; |
235 | |
302 | |
236 | $self->read_mdif; |
303 | $self->read_mdif; |
237 | |
304 | |
238 | $self->{version} ||= MDIFVERSION; |
305 | $self->{version} ||= MDIFVERSION; |
239 | my ($add, $del) = gendiff $self->{idx}, \@idx; |
306 | $self->{ctime} = time; |
240 | push @{$self->{diff}}, [ |
|
|
241 | $mtime, |
|
|
242 | $add, $del, |
|
|
243 | ] if @$add || @$del; |
|
|
244 | |
307 | |
|
|
308 | $self->gendiff($self->{idx}, \@idx); |
|
|
309 | |
245 | $self->{fsize} = $fsize; |
310 | $self->{fsize} = $fsize; |
246 | $self->{mtime} = $mtime; |
311 | $self->{mtime} = $mtime; |
247 | $self->{idx} = \@idx; |
312 | $self->{idx} = \@idx; |
248 | |
313 | |
249 | $self->dirty; |
314 | $self->dirty; |
|
|
315 | $self->write_mdif;#d# |
|
|
316 | } |
250 | |
317 | |
251 | return 2; |
318 | sub inventory { |
252 | } else { |
319 | hash sort map { $_->[1] } @{$_[0]{idx}}; |
253 | slog 2, "$path: no longer exists\n"; |
320 | } |
254 | unlink $conf; |
|
|
255 | |
321 | |
256 | return (); |
322 | sub iidx { |
|
|
323 | my $self = shift; |
|
|
324 | |
|
|
325 | $self->{iidx} ||= do { |
|
|
326 | my %iidx; |
|
|
327 | my $idx = $self->{idx}; |
|
|
328 | |
|
|
329 | push @$idx, [$self->{fsize}]; |
|
|
330 | my $ofs = 0; |
|
|
331 | for (0 .. @$idx - 2) { |
|
|
332 | $iidx{$idx->[$_][1]} = [$idx->[$_][0], $idx->[$_+1][0] - $idx->[$_][0]]; |
|
|
333 | } |
|
|
334 | pop @$idx, [$self->{fsize}]; |
|
|
335 | |
|
|
336 | \%iidx; |
257 | } |
337 | }; |
|
|
338 | } |
|
|
339 | |
|
|
340 | sub exists { |
|
|
341 | $_[0]->iidx unless $_[0]{iidx}; |
|
|
342 | return $_[0]{iidx}{$_[1]}; |
|
|
343 | } |
|
|
344 | |
|
|
345 | sub fetch { |
|
|
346 | my ($self, $hash) = @_; |
|
|
347 | |
|
|
348 | $self->iidx unless $self->{iidx}; |
|
|
349 | |
|
|
350 | my $mail; |
|
|
351 | |
|
|
352 | my $msg = $self->{iidx}{$hash} |
|
|
353 | or die "$hash: no such message in $self->{path}"; |
|
|
354 | |
|
|
355 | print STDERR "$self->{fh}, $msg->[0], SEEK_SET\n";#d# |
|
|
356 | |
|
|
357 | seek $self->{fh}, $msg->[0], SEEK_SET |
|
|
358 | or die "$self->{path}: $!"; |
|
|
359 | |
|
|
360 | print STDERR "$msg->[1] == read $self->{fh}, $msg->[1]\n";#d# |
|
|
361 | |
|
|
362 | $msg->[1] == read $self->{fh}, $mail, $msg->[1] |
|
|
363 | or die "$self->{path}: $!"; |
|
|
364 | |
|
|
365 | $mail =~ /^From \S/ |
|
|
366 | or die "$self->{path}: mail folder corrupted"; |
|
|
367 | |
|
|
368 | $mail; |
|
|
369 | } |
|
|
370 | |
|
|
371 | # begin updating folder |
|
|
372 | sub begin_update { |
|
|
373 | my $self = shift; |
|
|
374 | |
|
|
375 | $self->{oidx} = $self->{idx}; |
|
|
376 | } |
|
|
377 | |
|
|
378 | sub delete { |
|
|
379 | my $self = shift; |
|
|
380 | my $temp = "$self->{path}~"; |
|
|
381 | |
|
|
382 | $self->iidx unless $self->{iidx}; |
|
|
383 | |
|
|
384 | for (@_) { |
|
|
385 | if (exists $self->{iidx}{$_}) { # at least one message exists |
|
|
386 | my $guard = $::lockdisk->guard; |
|
|
387 | my %del; @del{@_} = (); |
|
|
388 | my @nidx; |
|
|
389 | my $dofs = 0; |
|
|
390 | |
|
|
391 | open my $fh, "+>", $temp |
|
|
392 | or die "$temp: $!"; |
|
|
393 | |
|
|
394 | eval { |
|
|
395 | 0 == setlkw(fileno $fh, 2) |
|
|
396 | or die "$self->{path}~: $!"; |
|
|
397 | |
|
|
398 | $self->{fsize} = 0; # we virtually truncated the file |
|
|
399 | |
|
|
400 | for (@{delete $self->{idx}}) { |
|
|
401 | my $hash = $_->[1]; |
|
|
402 | my $buf; |
|
|
403 | |
|
|
404 | unless (exists $del{$hash}) { |
|
|
405 | my ($ofs, $len) = @{$self->{iidx}{$hash}}; |
|
|
406 | |
|
|
407 | $len or die; |
|
|
408 | |
|
|
409 | seek $self->{fh}, $ofs, SEEK_SET |
|
|
410 | or die "$self->{path}: $!"; |
|
|
411 | |
|
|
412 | $len == read $self->{fh}, $buf, $len |
|
|
413 | or die "$self->{path}: $!"; |
|
|
414 | |
|
|
415 | $buf =~ /^From \S/ |
|
|
416 | or die "$self->{path}: corrupted mail folder"; |
|
|
417 | |
|
|
418 | print $fh $buf |
|
|
419 | or die "$self->{path}: $!"; |
|
|
420 | |
|
|
421 | push @nidx, [$dofs, $hash]; |
|
|
422 | $self->{iidx}{$hash}[0] = $dofs; |
|
|
423 | $dofs += $len; |
|
|
424 | |
|
|
425 | &::give unless ++$ecnt & 255; |
|
|
426 | } else { |
|
|
427 | delete $self->{iidx}{$hash}; |
|
|
428 | slog 0, "skipping/deleting $hash\n"; |
|
|
429 | } |
|
|
430 | } |
|
|
431 | }; |
|
|
432 | |
|
|
433 | if ($@) { |
|
|
434 | close $fh; |
|
|
435 | unlink $temp; |
|
|
436 | die; |
|
|
437 | } |
|
|
438 | |
|
|
439 | fflush($fh); |
|
|
440 | File::Sync::fsync($fh); |
|
|
441 | replace $temp, $self->{path}; |
|
|
442 | |
|
|
443 | $self->{fh} = $fh; |
|
|
444 | $self->{rw} = 1; |
|
|
445 | |
|
|
446 | delete $self->{iidx}; |
|
|
447 | |
|
|
448 | $self->{idx} = \@nidx; |
|
|
449 | $self->{fsize} = $dofs; |
|
|
450 | |
|
|
451 | return; |
|
|
452 | } |
|
|
453 | } |
|
|
454 | } |
|
|
455 | |
|
|
456 | sub append { |
|
|
457 | my ($self, $hash, $mail) = @_; |
|
|
458 | |
|
|
459 | if (length $mail) { |
|
|
460 | $self->open(1); |
|
|
461 | |
|
|
462 | seek $self->{fh}, $self->{fsize}, SEEK_SET |
|
|
463 | or die "$self->{path}: $!"; |
|
|
464 | |
|
|
465 | print {$self->{fh}} $mail |
|
|
466 | or die "$self->{path}: $!"; |
|
|
467 | |
|
|
468 | push @{$self->{idx}}, [$self->{fsize}, $hash]; |
|
|
469 | $self->{fsize} += length $mail; |
|
|
470 | } |
|
|
471 | } |
|
|
472 | |
|
|
473 | sub end_update { |
|
|
474 | my $self = shift; |
|
|
475 | |
|
|
476 | $self->gendiff((delete $self->{oidx}), $self->{idx}); |
|
|
477 | |
|
|
478 | fflush($self->{fh}); |
|
|
479 | File::Sync::fsync($self->{fh}); |
|
|
480 | |
|
|
481 | stat $self->{fh} |
|
|
482 | or die "$self->{path}: $!"; |
|
|
483 | |
|
|
484 | $self->{fsize} = (stat _)[7]; |
|
|
485 | $self->{mtime} = (stat _)[9]; |
|
|
486 | |
|
|
487 | $self->dirty; |
258 | } |
488 | } |
259 | |
489 | |
260 | 1; |
490 | 1; |
261 | |
491 | |
|
|
492 | __DATA__ |
|
|
493 | __C__ |
|
|
494 | #include <unistd.h> |
|
|
495 | #include <fcntl.h> |
|
|
496 | |
|
|
497 | /* mode0 unlock, mode1 rlock, mode2 rwlock */ |
|
|
498 | int setlkw(int fd, int mode) |
|
|
499 | { |
|
|
500 | struct flock l; |
|
|
501 | |
|
|
502 | l.l_type = mode == 0 ? F_UNLCK |
|
|
503 | : mode == 1 ? F_RDLCK |
|
|
504 | : F_WRLCK; |
|
|
505 | l.l_whence = SEEK_SET; |
|
|
506 | l.l_start = 0; |
|
|
507 | l.l_len = 0; |
|
|
508 | |
|
|
509 | return fcntl (fd, F_SETLKW, &l); |
|
|
510 | } |
|
|
511 | |