X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FLeiInput.pm;h=d11d23d4064d374789b8200cab8c23101377e804;hb=b6b86cfd238c170ea3e2c4d4179f06c7af139086;hp=776b31515439ee0d6fb2a3fd86c395a016e282e4;hpb=d4b8980b9d2305c3004ff076a83006cc20502e56;p=public-inbox.git diff --git a/lib/PublicInbox/LeiInput.pm b/lib/PublicInbox/LeiInput.pm index 776b3151..d11d23d4 100644 --- a/lib/PublicInbox/LeiInput.pm +++ b/lib/PublicInbox/LeiInput.pm @@ -5,6 +5,39 @@ package PublicInbox::LeiInput; use strict; use v5.10.1; +use PublicInbox::DS; + +# JMAP RFC 8621 4.1.1 +# https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml +our @KW = (qw(seen answered flagged draft), # widely-compatible + qw(forwarded), # IMAP + Maildir + qw(phishing junk notjunk)); # rarely supported + +# note: RFC 8621 states "Users may add arbitrary keywords to an Email", +# but is it good idea? Stick to the system and reserved ones, for now. +# The widely-compatible ones map to IMAP system flags, Maildir flags +# and mbox Status/X-Status headers. +my %KW = map { $_ => 1 } @KW; +my $L_MAX = 244; # Xapian term limit - length('L') + +# RFC 8621, sec 2 (Mailboxes) a "label" for us is a JMAP Mailbox "name" +# "Servers MAY reject names that violate server policy" +my %ERR = ( + L => sub { + my ($label) = @_; + length($label) >= $L_MAX and + return "`$label' too long (must be <= $L_MAX)"; + $label =~ m{\A[a-z0-9_](?:[a-z0-9_\-\./\@,]*[a-z0-9])?\z}i ? + undef : "`$label' is invalid"; + }, + kw => sub { + my ($kw) = @_; + $KW{$kw} ? undef : <fail("--$opt_key unset for $err"); } + return 1 if $fmt eq 'eml'; require PublicInbox::MboxLock if $files; require PublicInbox::MboxReader; - return 1 if $fmt eq 'eml'; - # XXX: should this handle {gz,bz2,xz}? that's currently in LeiToMail PublicInbox::MboxReader->reads($fmt) or return $lei->fail("--$opt_key=$fmt unrecognized"); 1; } +# import a single file handle of $name +# Subclass must define ->input_eml_cb and ->input_mbox_cb +sub input_fh { + my ($self, $ifmt, $fh, $name, @args) = @_; + if ($ifmt eq 'eml') { + my $buf = do { local $/; <$fh> } // + return $self->{lei}->child_error(1 << 8, <<""); +error reading $name: $! + + # mutt pipes single RFC822 messages with a "From " line, + # but no Content-Length or "From " escaping. + # "git format-patch" also generates such files by default. + $buf =~ s/\A[\r\n]*From [^\r\n]*\r?\n//s; + $self->input_eml_cb(PublicInbox::Eml->new(\$buf), @args); + } else { + # prepare_inputs already validated $ifmt + my $cb = PublicInbox::MboxReader->reads($ifmt) // + die "BUG: bad fmt=$ifmt"; + $cb->(undef, $fh, $self->can('input_mbox_cb'), $self, @args); + } +} -sub prepare_inputs { +sub input_path_url { + my ($self, $input, @args) = @_; + my $lei = $self->{lei}; + my $ifmt = lc($lei->{opt}->{'in-format'} // ''); + # TODO auto-detect? + if ($input =~ m!\Aimaps?://!i) { + $lei->{net}->imap_each($input, $self->can('input_imap_cb') // + $self->can('input_net_cb'), + $self, @args); + return; + } elsif ($input =~ m!\A(?:nntps?|s?news)://!i) { + $lei->{net}->nntp_each($input, $self->can('input_nntp_cb') // + $self->can('input_net_cb'), + $self, @args); + return; + } + if ($input =~ s!\A([a-z0-9]+):!!i) { + $ifmt = lc($1); + } elsif ($input =~ /\.(?:patch|eml)\z/i) { + $ifmt = 'eml'; + } + my $devfd = $lei->path_to_fd($input) // return; + if ($devfd >= 0) { + $self->input_fh($ifmt, $lei->{$devfd}, $input, @args); + } elsif (-f $input && $ifmt eq 'eml') { + open my $fh, '<', $input or + return $lei->fail("open($input): $!"); + $self->input_fh($ifmt, $fh, $input, @args); + } elsif (-f _) { + my $m = $lei->{opt}->{'lock'} // + PublicInbox::MboxLock->defaults; + my $mbl = PublicInbox::MboxLock->acq($input, 0, $m); + my $zsfx = PublicInbox::MboxReader::zsfx($input); + if ($zsfx) { + my $in = delete $mbl->{fh}; + $mbl->{fh} = + PublicInbox::MboxReader::zsfxcat($in, $zsfx, $lei); + } + local $PublicInbox::DS::in_loop = 0 if $zsfx; # dwaitpid + $self->input_fh($ifmt, $mbl->{fh}, $input, @args); + } elsif (-d _ && (-d "$input/cur" || -d "$input/new")) { + return $lei->fail(<new->maildir_each_eml($input, + $self->can('input_maildir_cb'), + $self, @args); + } else { + $lei->fail("$input unsupported (TODO)"); + } +} + +sub prepare_inputs { # returns undef on error my ($self, $lei, $inputs) = @_; my $in_fmt = $lei->{opt}->{'in-format'}; + my $sync = $lei->{opt}->{sync} ? {} : undef; # using LeiMailSync if ($lei->{opt}->{stdin}) { @$inputs and return $lei->fail("--stdin and @$inputs do not mix"); check_input_format($lei) or return; - $self->{0} = $lei->{0}; + push @$inputs, '/dev/stdin'; + push @{$sync->{no}}, '/dev/stdin' if $sync; } my $net = $lei->{net}; # NetWriter may be created by l2m - my $fmt = $lei->{opt}->{'in-format'}; my (@f, @d); # e.g. Maildir:/home/user/Mail/ or imaps://example.com/INBOX for my $input (@$inputs) { @@ -43,6 +149,13 @@ sub prepare_inputs { require PublicInbox::NetReader; $net //= PublicInbox::NetReader->new; $net->add_url($input); + if ($sync) { + if ($input =~ m!\Aimaps?://!) { + push @{$sync->{ok}}, $input; + } else { + push @{$sync->{no}}, $input; + } + } } elsif ($input_path =~ s/\A([a-z0-9]+)://is) { my $ifmt = lc $1; if (($in_fmt // $ifmt) ne $ifmt) { @@ -50,26 +163,61 @@ sub prepare_inputs { --in-format=$in_fmt and `$ifmt:' conflict } - if (-f $input_path) { + if ($sync) { + if ($ifmt =~ /\A(?:maildir|mh)\z/i) { + push @{$sync->{ok}}, $input; + } else { + push @{$sync->{no}}, $input; + } + } + my $devfd = $lei->path_to_fd($input_path) // return; + if ($devfd >= 0 || (-f $input_path || -p _)) { require PublicInbox::MboxLock; require PublicInbox::MboxReader; PublicInbox::MboxReader->reads($ifmt) or return $lei->fail("$ifmt not supported"); - } elsif (-d _) { + } elsif (-d $input_path) { require PublicInbox::MdirReader; $ifmt eq 'maildir' or return $lei->fail("$ifmt not supported"); + $input = $lei->abs_path($input) if $sync; } else { return $lei->fail("Unable to handle $input"); } - } elsif (-f $input) { push @f, $input } - elsif (-d _) { push @d, $input } - else { return $lei->fail("Unable to handle $input") } + } elsif ($input =~ /\.(eml|patch)\z/i && -f $input) { + lc($in_fmt//'eml') eq 'eml' or return $lei->fail(<<""); +$input is `eml', not --in-format=$in_fmt + + require PublicInbox::Eml; + push @{$sync->{no}}, $input if $sync; + } else { + my $devfd = $lei->path_to_fd($input) // return; + if ($devfd >= 0 || -f $input || -p _) { + push @{$sync->{no}}, $input if $sync; + push @f, $input; + } elsif (-d $input) { + if ($sync) { + $input = $lei->abs_path($input); + push @{$sync->{ok}}, $input; + } + push @d, $input; + } else { + return $lei->fail("Unable to handle $input") + } + } } if (@f) { check_input_format($lei, \@f) or return } if (@d) { # TODO: check for MH vs Maildir, here require PublicInbox::MdirReader; } + if ($sync && $sync->{no}) { + return $lei->fail(<<"") if !$sync->{ok}; +--sync specified but no inputs support it + + # non-fatal if some inputs support support sync + $lei->err("# --sync will only be used for @{$sync->{ok}}"); + $lei->err("# --sync is not supported for: @{$sync->{no}}"); + } if ($net) { if (my $err = $net->errors) { return $lei->fail($err); @@ -82,4 +230,43 @@ sub prepare_inputs { $self->{inputs} = $inputs; } +sub process_inputs { + my ($self) = @_; + for my $input (@{$self->{inputs}}) { + $self->input_path_url($input); + } + my $wait = $self->{lei}->{sto}->ipc_do('done') if $self->{lei}->{sto}; +} + +sub input_only_atfork_child { + my ($self) = @_; + my $lei = $self->{lei}; + $lei->_lei_atfork_child; + PublicInbox::IPC::ipc_atfork_child($self); + $lei->{auth}->do_auth_atfork($self) if $lei->{auth}; + undef; +} + +# like Getopt::Long, but for +kw:FOO and -kw:FOO to prepare +# for update_xvmd -> update_vmd +sub vmd_mod_extract { + my $argv = $_[-1]; + my $vmd_mod = {}; + my @new_argv; + for my $x (@$argv) { + if ($x =~ /\A(\+|\-)(kw|L):(.+)\z/) { + my ($op, $pfx, $val) = ($1, $2, $3); + if (my $err = $ERR{$pfx}->($val)) { + push @{$vmd_mod->{err}}, $err; + } else { # set "+kw", "+L", "-L", "-kw" + push @{$vmd_mod->{$op.$pfx}}, $val; + } + } else { + push @new_argv, $x; + } + } + @$argv = @new_argv; + $vmd_mod; +} + 1;