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 flushfh { |
|
|
29 | my $oldfh = select $_[0]; |
|
|
30 | $| = 1; |
|
|
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 | } |
… | |
… | |
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 | |
… | |
… | |
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 | flushfh $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; |
199 | } |
231 | } |
200 | |
232 | |
201 | sub check { |
233 | sub check { |
202 | my $self = shift; |
234 | my $self = shift; |
203 | my $path = $self->{path}; |
235 | my $path = $self->{path}; |
204 | my $conf = $self->conf_path; |
236 | my $conf = $self->conf_path; |
205 | my $guard = $::lockdisk->guard; |
237 | my $guard = $::lockdisk->guard; |
206 | |
238 | |
207 | slog 3, "checking $path\n"; |
239 | slog 3, "checking $path\n"; |
208 | |
240 | |
209 | if (stat $path) { |
241 | stat $path |
|
|
242 | or die "$path: $!"; |
|
|
243 | |
210 | my ($fsize, $mtime) = (stat _)[7, 9]; |
244 | my ($fsize, $mtime) = (stat _)[7, 9]; |
211 | |
245 | |
212 | if (open my $fh, "<", $conf) { |
246 | if (open my $fh, "<", $conf) { |
213 | my %conf; |
247 | my %conf; |
214 | <$fh>; # skip initial comment |
248 | <$fh>; # skip initial comment |
215 | <$fh> eq "[SYNCMAIL]\n" |
249 | <$fh> eq "[SYNCMAIL]\n" |
216 | or die "$conf: format error"; |
250 | or die "$conf: format error"; |
217 | while (<$fh> =~ /^([a-z]+)\s*=\s*(.*)$/) { |
251 | while (<$fh> =~ /^([a-z]+)\s*=\s*(.*)$/) { |
218 | $conf{$1} = $2; |
252 | $conf{$1} = $2; |
|
|
253 | } |
|
|
254 | return 1 if $fsize == $conf{fsize} |
|
|
255 | && $mtime == $conf{mtime}; |
|
|
256 | |
|
|
257 | $conf{mtime} <= $mtime |
|
|
258 | or die "$path: folder older than mdif"; |
|
|
259 | } |
|
|
260 | |
|
|
261 | slog 2, "updating $path\n"; |
|
|
262 | |
|
|
263 | my @idx; |
|
|
264 | |
|
|
265 | parse_mbox $self->{fh}, sub { |
|
|
266 | my ($offs, $head, $body) = @_; |
|
|
267 | push @idx, [$offs, hash($$head, "\0", $$body)]; |
|
|
268 | } or die "$path: no valid mbox file"; |
|
|
269 | |
|
|
270 | $self->read_mdif; |
|
|
271 | |
|
|
272 | $self->{version} ||= MDIFVERSION; |
|
|
273 | $self->{ctime} = time; |
|
|
274 | |
|
|
275 | $self->gendiff($self->{idx}, \@idx); |
|
|
276 | |
|
|
277 | $self->{fsize} = $fsize; |
|
|
278 | $self->{mtime} = $mtime; |
|
|
279 | $self->{idx} = \@idx; |
|
|
280 | |
|
|
281 | $self->dirty; |
|
|
282 | $self->write_mdif; |
|
|
283 | } |
|
|
284 | |
|
|
285 | sub inventory { |
|
|
286 | hash sort map { $_->[1] } @{$_[0]{idx}}; |
|
|
287 | } |
|
|
288 | |
|
|
289 | sub open { |
|
|
290 | my ($self, $rw) = @_; |
|
|
291 | |
|
|
292 | if (!$self->{fh} || $self->{rw} != $rw) { |
|
|
293 | $self->close; |
|
|
294 | $self->{rw} = $rw; |
|
|
295 | sysopen $self->{fh}, $self->{path}, |
|
|
296 | O_CREAT | ($rw ? O_RDWR : O_RDONLY), |
|
|
297 | 0666 |
|
|
298 | or die "$self->{path}: $!"; |
|
|
299 | 0 == setlkw(fileno $self->{fh}, $rw ? 2 : 1) |
|
|
300 | or die "$self->{path}: $!"; |
|
|
301 | |
|
|
302 | } |
|
|
303 | } |
|
|
304 | |
|
|
305 | sub close { |
|
|
306 | my $self = shift; |
|
|
307 | |
|
|
308 | flushfh $self->{fh}; |
|
|
309 | File::Sync::fsync($self->{fh}); |
|
|
310 | delete $self->{fh}; |
|
|
311 | } |
|
|
312 | |
|
|
313 | # begin updating folder |
|
|
314 | sub begin_update { |
|
|
315 | my $self = shift; |
|
|
316 | |
|
|
317 | $self->{oidx} = $self->{idx}; |
|
|
318 | |
|
|
319 | } |
|
|
320 | |
|
|
321 | sub delete { |
|
|
322 | my $self = shift; |
|
|
323 | my $temp = "$self->{path}~"; |
|
|
324 | |
|
|
325 | if (@_) { |
|
|
326 | my $guard = $::lockdisk->guard; |
|
|
327 | my %del; @del{@_} = (); |
|
|
328 | |
|
|
329 | open my $fh, ">", $temp |
|
|
330 | or die "$temp: $!"; |
|
|
331 | |
|
|
332 | my $nidx; |
|
|
333 | my $idx = delete $self->{idx}; |
|
|
334 | push @$idx, [$self->{fsize}]; |
|
|
335 | $self->{fsize} = 0; # we virtually truncated the file |
|
|
336 | |
|
|
337 | slog 0, "XXXXXXXXXXXXXXX @_\n";#d# |
|
|
338 | |
|
|
339 | my $ofs = 0; |
|
|
340 | for (0 .. @$idx - 2) { |
|
|
341 | my $buf; |
|
|
342 | |
|
|
343 | unless (exists $del{$idx->[$_][1]}) { |
|
|
344 | my $len = $idx->[$_+1][0] - $idx->[$_][0]; |
|
|
345 | |
|
|
346 | slog 0, "$idx->[$_][1] $idx->[$_+1][0] - $idx->[$_][0]\n";#d# |
|
|
347 | |
|
|
348 | seek $self->{fh}, $idx->[$_][0],SEEK_SET |
|
|
349 | or die "$self->{path}: $!"; |
|
|
350 | |
|
|
351 | $len == read $self->{fh}, $buf, $len |
|
|
352 | or die "$self->{path}: $!"; |
|
|
353 | |
|
|
354 | $buf =~ /^From \S/ |
|
|
355 | or die "$self->{path}: corrupted mail folder"; |
|
|
356 | |
|
|
357 | &::give unless ++$ecnt & 255; |
|
|
358 | } else { |
|
|
359 | slog 0, "skipping $idx->[$_][1]\n"; |
219 | } |
360 | } |
220 | return 1 if $fsize == $conf{fsize} |
|
|
221 | && $mtime == $conf{mtime}; |
|
|
222 | |
|
|
223 | $conf{mtime} <= $mtime |
|
|
224 | or die "$path: folder older than mdif"; |
|
|
225 | } |
361 | } |
226 | |
362 | |
227 | slog 2, "updating $path\n"; |
363 | File::Sync::fsync($fh); |
|
|
364 | close $fh; |
228 | |
365 | |
229 | my @idx; |
366 | # replace $temp, $self->{path} |
230 | |
|
|
231 | parse_mbox $path, sub { |
|
|
232 | my ($offs, $head, $body) = @_; |
|
|
233 | push @idx, [$offs, hash($$head, "\0", $$body)]; |
|
|
234 | } or return (); |
|
|
235 | |
|
|
236 | $self->read_mdif; |
|
|
237 | |
|
|
238 | $self->{version} ||= MDIFVERSION; |
|
|
239 | $self->{fsize} = $fsize; |
|
|
240 | $self->{mtime} = $mtime; |
|
|
241 | $self->{ctime} = time; |
|
|
242 | $self->{idx} = \@idx; |
|
|
243 | |
|
|
244 | my ($add, $del) = gendiff $self->{idx}, \@idx; |
|
|
245 | push @{$self->{diff}}, [ |
|
|
246 | $self->{ctime}, |
|
|
247 | $add, $del, |
|
|
248 | ] if @$add || @$del; |
|
|
249 | |
|
|
250 | $self->dirty; |
|
|
251 | |
|
|
252 | return 2; |
|
|
253 | } else { |
|
|
254 | slog 2, "$path: no longer exists\n"; |
|
|
255 | unlink $conf; |
|
|
256 | |
|
|
257 | return (); |
|
|
258 | } |
367 | } |
|
|
368 | } |
|
|
369 | |
|
|
370 | sub end_update { |
|
|
371 | } |
|
|
372 | |
|
|
373 | sub append { |
|
|
374 | my $self = shift; |
|
|
375 | |
|
|
376 | &update; |
|
|
377 | #$self->open(1); |
259 | } |
378 | } |
260 | |
379 | |
261 | 1; |
380 | 1; |
262 | |
381 | |
|
|
382 | __DATA__ |
|
|
383 | __C__ |
|
|
384 | #include <unistd.h> |
|
|
385 | #include <fcntl.h> |
|
|
386 | |
|
|
387 | /* mode0 unlock, mode1 rlock, mode2 rwlock */ |
|
|
388 | int setlkw(int fd, int mode) |
|
|
389 | { |
|
|
390 | struct flock l; |
|
|
391 | |
|
|
392 | l.l_type = mode == 0 ? F_UNLCK |
|
|
393 | : mode == 1 ? F_RDLCK |
|
|
394 | : F_WRLCK; |
|
|
395 | l.l_whence = SEEK_SET; |
|
|
396 | l.l_start = 0; |
|
|
397 | l.l_len = 0; |
|
|
398 | |
|
|
399 | return fcntl (fd, F_SETLKW, &l); |
|
|
400 | } |
|
|
401 | |