]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/LeiExportKw.pm
lei export-kw: relax IMAP URL matching
[public-inbox.git] / lib / PublicInbox / LeiExportKw.pm
1 # Copyright (C) 2021 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # front-end for the "lei export-kw" sub-command
5 package PublicInbox::LeiExportKw;
6 use strict;
7 use v5.10.1;
8 use parent qw(PublicInbox::IPC PublicInbox::LeiInput);
9 use Errno qw(EEXIST ENOENT);
10
11 sub export_kw_md { # LeiMailSync->each_src callback
12         my ($oidbin, $id, $self, $mdir) = @_;
13         my $oidhex = unpack('H*', $oidbin);
14         my $sto_kw = $self->{lse}->oid_keywords($oidhex) or return;
15         my $bn = $$id;
16         my ($md_kw, $unknown, @try);
17         if ($bn =~ s/:2,([a-zA-Z]*)\z//) {
18                 ($md_kw, $unknown) = PublicInbox::MdirReader::flags2kw($1);
19                 @try = qw(cur new);
20         } else {
21                 $unknown = [];
22                 @try = qw(new cur);
23         }
24         if ($self->{-merge_kw} && $md_kw) { # merging keywords is the default
25                 @$sto_kw{keys %$md_kw} = values(%$md_kw);
26         }
27         $bn .= ':2,'.
28                 PublicInbox::LeiToMail::kw2suffix([keys %$sto_kw], @$unknown);
29         my $dst = "$mdir/cur/$bn";
30         my @fail;
31         for my $d (@try) {
32                 my $src = "$mdir/$d/$$id";
33                 next if $src eq $dst;
34
35                 # we use link(2) + unlink(2) since rename(2) may
36                 # inadvertently clobber if the "uniquefilename" part wasn't
37                 # actually unique.
38                 if (link($src, $dst)) { # success
39                         # unlink(2) may ENOENT from parallel invocation,
40                         # ignore it, but not other serious errors
41                         if (!unlink($src) and $! != ENOENT) {
42                                 $self->{lei}->child_error(1,
43                                                         "E: unlink($src): $!");
44                         }
45                         $self->{lms}->mv_src("maildir:$mdir",
46                                                 $oidbin, $id, $bn) or die;
47                         return; # success anyways if link(2) worked
48                 }
49                 if ($! == ENOENT && !-e $src) { # some other process moved it
50                         $self->{lms}->clear_src("maildir:$mdir", $id);
51                         next;
52                 }
53                 push @fail, $src if $! != EEXIST;
54         }
55         return unless @fail;
56         # both tries failed
57         my $e = $!;
58         my $orig = '['.join('|', @fail).']';
59         $self->{lei}->child_error(1, "link($orig, $dst) ($oidhex): $e");
60 }
61
62 sub export_kw_imap { # LeiMailSync->each_src callback
63         my ($oidbin, $id, $self, $mic) = @_;
64         my $oidhex = unpack('H*', $oidbin);
65         my $sto_kw = $self->{lse}->oid_keywords($oidhex) or return;
66         $self->{imap_mod_kw}->($self->{nwr}, $mic, $id, [ keys %$sto_kw ]);
67 }
68
69 # overrides PublicInbox::LeiInput::input_path_url
70 sub input_path_url {
71         my ($self, $input, @args) = @_;
72         my $lms = $self->{lms} //= $self->{lse}->lms;
73         $lms->lms_begin;
74         if ($input =~ /\Amaildir:(.+)/i) {
75                 my $mdir = $1;
76                 require PublicInbox::LeiToMail; # kw2suffix
77                 $lms->each_src($input, \&export_kw_md, $self, $mdir);
78         } elsif ($input =~ m!\Aimaps?://!) {
79                 my $uri = PublicInbox::URIimap->new($input);
80                 my $mic = $self->{nwr}->mic_for_folder($uri);
81                 $lms->each_src($$uri, \&export_kw_imap, $self, $mic);
82                 $mic->expunge;
83         } else { die "BUG: $input not supported" }
84         $lms->lms_commit;
85 }
86
87 sub match_imap_url ($$) {
88         my ($all, $url) = @_; # $all = [ $lms->folders ];
89         require PublicInbox::URIimap;
90         my $cli = PublicInbox::URIimap->new($url)->canonical;
91         my ($s, $h, $mb) = ($cli->scheme, $cli->host, $cli->mailbox);
92         my @uri = map { PublicInbox::URIimap->new($_)->canonical }
93                 grep(m!\A\Q$s\E://.*?\Q$h\E\b.*?/\Q$mb\E\b!, @$all);
94         my @match;
95         for my $x (@uri) {
96                 next if $x->mailbox ne $cli->mailbox;
97                 next if $x->host ne $cli->host;
98                 next if $x->port != $cli->port;
99                 my $x_uidval = $x->uidvalidity;
100                 next if ($cli->uidvalidity // $x_uidval) != $x_uidval;
101
102                 # allow nothing in CLI to possibly match ";AUTH=ANONYMOUS"
103                 if (defined($x->auth) && !defined($cli->auth) &&
104                                 !defined($cli->user)) {
105                         push @match, $x;
106                 # or maybe user was forgotten on CLI:
107                 } elsif (defined($x->user) && !defined($cli->user)) {
108                         push @match, $x;
109                 } elsif (($x->user//"\0") eq ($cli->user//"\0")) {
110                         push @match, $x;
111                 }
112         }
113         return $match[0] if scalar(@match) <= 1;
114         warn "E: `$url' is ambiguous:\n\t", join("\n\t", @match), "\n";
115         undef;
116 }
117
118 sub lei_export_kw {
119         my ($lei, @folders) = @_;
120         my $sto = $lei->_lei_store or return $lei->fail(<<EOM);
121 lei/store uninitialized, see lei-import(1)
122 EOM
123         my $lse = $sto->search;
124         my $lms = $lse->lms or return $lei->fail(<<EOM);
125 lei mail_sync uninitialized, see lei-import(1)
126 EOM
127         my $opt = $lei->{opt};
128         my $all = $opt->{all};
129         my @all = $lms->folders;
130         if (defined $all) { # --all=<local|remote>
131                 my %x = map { $_ => $_ } split(/,/, $all);
132                 my @ok = grep(defined, delete(@x{qw(local remote), ''}));
133                 my @no = keys %x;
134                 if (@no) {
135                         @no = (join(',', @no));
136                         return $lei->fail(<<EOM);
137 --all=@no not accepted (must be `local' and/or `remote')
138 EOM
139                 }
140                 my (%seen, @inc);
141                 for my $ok (@ok) {
142                         if ($ok eq 'local') {
143                                 @inc = grep(!m!\A[a-z0-9\+]+://!i, @all);
144                         } elsif ($ok eq 'remote') {
145                                 @inc = grep(m!\A[a-z0-9\+]+://!i, @all);
146                         } elsif ($ok ne '') {
147                                 return $lei->fail("--all=$all not understood");
148                         } else {
149                                 @inc = @all;
150                         }
151                         for (@inc) {
152                                 push(@folders, $_) unless $seen{$_}++;
153                         }
154                 }
155                 return $lei->fail(<<EOM) if !@folders;
156 no --mail-sync folders known to lei
157 EOM
158         } else {
159                 my %all = map { $_ => 1 } @all;
160                 my @no;
161                 for (@folders) {
162                         next if $all{$_}; # ok
163                         if (-d "$_/new" && -d "$_/cur") {
164                                 my $d = 'maildir:'.$lei->rel2abs($_);
165                                 push(@no, $_) unless $all{$d};
166                                 $_ = $d;
167                         } elsif (m!\Aimaps?://!i) {
168                                 my $orig = $_;
169                                 if (my $canon = match_imap_url(\@all, $orig)) {
170                                         $lei->qerr(<<EOM);
171 # using `$canon' instead of `$orig'
172 EOM
173                                         $_ = $canon;
174                                 } else {
175                                         push @no, $orig;
176                                 }
177                         } else {
178                                 push @no, $_;
179                         }
180                 }
181                 my $no = join("\n\t", @no);
182                 return $lei->fail(<<EOF) if @no;
183 No sync information for: $no
184 Run `lei ls-mail-sync' to display valid choices
185 EOF
186         }
187         my $self = bless { lse => $lse }, __PACKAGE__;
188         $lei->{opt}->{'mail-sync'} = 1; # for prepare_inputs
189         $self->prepare_inputs($lei, \@folders) or return;
190         my $j = $opt->{jobs} // scalar(@{$self->{inputs}}) || 1;
191         if (my @ro = grep(!/\A(?:maildir|imaps?):/, @folders)) {
192                 return $lei->fail("cannot export to read-only folders: @ro");
193         }
194         my $m = $opt->{mode} // 'merge';
195         if ($m eq 'merge') { # default
196                 $self->{-merge_kw} = 1;
197         } elsif ($m eq 'set') {
198         } else {
199                 return $lei->fail(<<EOM);
200 --mode=$m not supported (`set' or `merge')
201 EOM
202         }
203         if (my $net = $lei->{net}) {
204                 require PublicInbox::NetWriter;
205                 $self->{nwr} = bless $net, 'PublicInbox::NetWriter';
206                 $self->{imap_mod_kw} = $net->can($self->{-merge_kw} ?
207                                         'imap_add_kw' : 'imap_set_kw');
208         }
209         undef $lms;
210         my $ops = {};
211         $lei->{auth}->op_merge($ops, $self) if $lei->{auth};
212         $self->{-wq_nr_workers} = $j // 1; # locked
213         (my $op_c, $ops) = $lei->workers_start($self, $j, $ops);
214         $lei->{wq1} = $self;
215         $lei->{-err_type} = 'non-fatal';
216         net_merge_all_done($self) unless $lei->{auth};
217         $op_c->op_wait_event($ops); # calls net_merge_all_done if $lei->{auth}
218 }
219
220 sub _complete_export_kw {
221         my ($lei, @argv) = @_;
222         my $sto = $lei->_lei_store or return;
223         my $lms = $sto->search->lms or return;
224         my $match_cb = $lei->complete_url_prepare(\@argv);
225         map { $match_cb->($_) } $lms->folders;
226 }
227
228 no warnings 'once';
229
230 *ipc_atfork_child = \&PublicInbox::LeiInput::input_only_atfork_child;
231 *net_merge_all_done = \&PublicInbox::LeiInput::input_only_net_merge_all_done;
232
233 # the following works even when LeiAuth is lazy-loaded
234 *net_merge_all = \&PublicInbox::LeiAuth::net_merge_all;
235
236 1;