# Copyright (C) 2016 all contributors # License: AGPL-3.0+ package PublicInbox::WatchMaildir; use strict; use warnings; use Email::MIME; use Email::MIME::ContentType; $Email::MIME::ContentType::STRICT_PARAMS = 0; # user input is imperfect use PublicInbox::Git; use PublicInbox::Import; use PublicInbox::MDA; sub new { my ($class, $config) = @_; my (%mdmap, @mdir); foreach my $k (keys %$config) { $k =~ /\Apublicinbox\.([^\.]+)\.watch\z/ or next; my $name = $1; my $watch = $config->{$k}; if ($watch =~ s/\Amaildir://) { $watch =~ s!/+\z!!; my $inbox = $config->lookup_name($name); if (my $wm = $inbox->{watchheader}) { my ($k, $v) = split(/:/, $wm, 2); $inbox->{-watchheader} = [ $k, qr/\Q$v\E/ ]; } my $new = "$watch/new"; my $cur = "$watch/cur"; push @mdir, $new, $cur; $mdmap{$new} = $inbox; $mdmap{$cur} = $inbox; } else { warn "watch unsupported: $k=$watch\n"; } } return unless @mdir; my $mdre = join('|', map { quotemeta($_) } @mdir); $mdre = qr!\A($mdre)/!; bless { mdmap => \%mdmap, mdir => \@mdir, mdre => $mdre, importers => {}, }, $class; } sub _done_for_now { $_->done foreach values %{$_[0]->{importers}}; } sub _try_fsn_paths { my ($self, $paths) = @_; _try_path($self, $_->{path}) foreach @$paths; _done_for_now($self); } sub _try_path { my ($self, $path) = @_; if ($path !~ $self->{mdre}) { warn "unrecognized path: $path\n"; return; } my $inbox = $self->{mdmap}->{$1}; unless ($inbox) { warn "unmappable dir: $1\n"; return; } my $im = $inbox->{-import} ||= eval { my $git = $inbox->git; my $name = $inbox->{name}; my $addr = $inbox->{-primary_address}; PublicInbox::Import->new($git, $name, $addr); }; $self->{importers}->{"$im"} = $im; my $mime; if (open my $fh, '<', $path) { local $/; my $str = <$fh>; $str or return; $mime = Email::MIME->new(\$str); } elsif ($!{ENOENT}) { return; } else { warn "failed to open $path: $!\n"; return; } $mime->header_set($_) foreach @PublicInbox::MDA::BAD_HEADERS; my $wm = $inbox->{-watchheader}; if ($wm) { my $v = $mime->header_obj->header_raw($wm->[0]); return unless ($v && $v =~ $wm->[1]); } my $f = $inbox->{filter}; if ($f && $f =~ /::/) { eval "require $f"; if ($@) { warn $@; } else { $f = $f->new; $mime = $f->scrub($mime); } } $mime or return; my $mid = $mime->header_obj->header_raw('Message-Id'); $im->add($mime); } sub watch { my ($self) = @_; my $cb = sub { _try_fsn_paths($self, \@_) }; my $mdir = $self->{mdir}; require Filesys::Notify::Simple; my $watcher = Filesys::Notify::Simple->new($mdir); $watcher->wait($cb) while (1); } sub scan { my ($self) = @_; my $mdir = $self->{mdir}; foreach my $dir (@$mdir) { my $ok = opendir(my $dh, $dir); unless ($ok) { warn "failed to open $dir: $!\n"; next; } while (my $fn = readdir($dh)) { next unless $fn =~ /\A[a-zA-Z0-9][\w:,=\.]+\z/; $fn = "$dir/$fn"; if (-f $fn) { _try_path($self, $fn); } else { warn "not a file: $fn\n"; } } closedir $dh; } _done_for_now($self); } 1;