1 # Copyright (C) 2016 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 package PublicInbox::WatchMaildir;
7 use Email::MIME::ContentType;
8 $Email::MIME::ContentType::STRICT_PARAMS = 0; # user input is imperfect
10 use PublicInbox::Import;
12 use PublicInbox::Spawn qw(spawn);
15 my ($class, $config) = @_;
17 my $k = 'publicinboxlearn.watchspam';
18 if (my $spamdir = $config->{$k}) {
19 if ($spamdir =~ s/\Amaildir://) {
21 # skip "new", no MUA has seen it, yet.
22 my $cur = "$spamdir/cur";
24 $mdmap{$cur} = 'watchspam';
26 warn "unsupported $k=$spamdir\n";
29 foreach $k (keys %$config) {
30 $k =~ /\Apublicinbox\.([^\.]+)\.watch\z/ or next;
32 my $watch = $config->{$k};
33 if ($watch =~ s/\Amaildir://) {
35 my $inbox = $config->lookup_name($name);
36 if (my $wm = $inbox->{watchheader}) {
37 my ($k, $v) = split(/:/, $wm, 2);
38 $inbox->{-watchheader} = [ $k, qr/\Q$v\E/ ];
40 my $new = "$watch/new";
41 my $cur = "$watch/cur";
42 push @mdir, $new, $cur;
43 die "$new already in use\n" if $mdmap{$new};
44 die "$cur already in use\n" if $mdmap{$cur};
45 $mdmap{$new} = $mdmap{$cur} = $inbox;
47 warn "watch unsupported: $k=$watch\n";
52 my $mdre = join('|', map { quotemeta($_) } @mdir);
53 $mdre = qr!\A($mdre)/!;
63 $_->done foreach values %{$_[0]->{importers}};
67 my ($self, $paths) = @_;
68 _try_path($self, $_->{path}) foreach @$paths;
73 my ($self, $path) = @_;
74 $path =~ /:2,[A-R]*S[T-Z]*\z/ or return;
75 my $mime = _path_to_mime($path) or return;
77 foreach my $inbox (values %{$self->{mdmap}}) {
78 next unless ref $inbox;
79 my $im = _importer_for($self, $inbox);
81 if (my $scrub = _scrubber_for($inbox)) {
82 my $scrubbed = $scrub->scrub($mime) or next;
83 $im->remove($scrubbed);
88 # used to hash the relevant portions of a message when there are conflicts
92 my $dig = Digest::SHA->new('SHA-1');
93 $dig->add($mime->header_obj->header_raw('Subject'));
94 $dig->add($mime->body_raw);
100 # probably a bad idea, but we inject a Message-Id if
101 # one is missing, here..
102 my $mid = $mime->header_obj->header_raw('Message-Id');
103 if (!defined $mid || $mid =~ /\A\s*\z/) {
104 $mid = '<' . _hash_mime2($mime) . '@generated>';
105 $mime->header_set('Message-Id', $mid);
110 my ($self, $path) = @_;
111 my @p = split(m!/+!, $path);
112 return unless $p[-1] =~ /\A[a-zA-Z0-9][\w:,=\.]+\z/;
113 return unless -f $path;
114 if ($path !~ $self->{mdre}) {
115 warn "unrecognized path: $path\n";
118 my $inbox = $self->{mdmap}->{$1};
120 warn "unmappable dir: $1\n";
123 if (!ref($inbox) && $inbox eq 'watchspam') {
124 return _remove_spam($self, $path);
126 my $im = _importer_for($self, $inbox);
127 my $mime = _path_to_mime($path) or return;
128 $mime->header_set($_) foreach @PublicInbox::MDA::BAD_HEADERS;
129 my $wm = $inbox->{-watchheader};
131 my $v = $mime->header_obj->header_raw($wm->[0]);
132 return unless ($v && $v =~ $wm->[1]);
134 if (my $scrub = _scrubber_for($inbox)) {
135 $mime = $scrub->scrub($mime) or return;
144 my $cb = sub { _try_fsn_paths($self, \@_) };
145 my $mdir = $self->{mdir};
147 # lazy load here, we may support watching via IMAP IDLE
149 require Filesys::Notify::Simple;
150 my $watcher = Filesys::Notify::Simple->new($mdir);
151 $watcher->wait($cb) while (1);
156 my $mdir = $self->{mdir};
157 foreach my $dir (@$mdir) {
158 my $ok = opendir(my $dh, $dir);
160 warn "failed to open $dir: $!\n";
163 while (my $fn = readdir($dh)) {
164 _try_path($self, "$dir/$fn");
168 _done_for_now($self);
173 if (open my $fh, '<', $path) {
177 return Email::MIME->new(\$str);
178 } elsif ($!{ENOENT}) {
181 warn "failed to open $path: $!\n";
187 my ($self, $inbox) = @_;
188 my $im = $inbox->{-import} ||= eval {
189 my $git = $inbox->git;
190 my $name = $inbox->{name};
191 my $addr = $inbox->{-primary_address};
192 PublicInbox::Import->new($git, $name, $addr);
194 $self->{importers}->{"$im"} = $im;
199 my $f = $inbox->{filter};
200 if ($f && $f =~ /::/) {