]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/LeiLcat.pm
0902c213deee71a070c434e7c2948075ef444b75
[public-inbox.git] / lib / PublicInbox / LeiLcat.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 # lcat: local cat, display a local message by Message-ID or blob,
5 # extracting from URL necessary
6 # "lei lcat <URL|SPEC>"
7 package PublicInbox::LeiLcat;
8 use strict;
9 use v5.10.1;
10 use PublicInbox::LeiViewText;
11 use URI::Escape qw(uri_unescape);
12 use PublicInbox::MID qw($MID_EXTRACT);
13
14 sub lcat_folder ($$;$$) {
15         my ($lei, $folder, $beg, $end) = @_;
16         my $lms = $lei->{-lms_ro} //= $lei->lms // return;
17         my $folders = [ $folder ];
18         eval { $lms->arg2folder($lei, $folders) };
19         return $lei->child_error(0, "# unknown folder: $folder") if $@;
20         my %range;
21         if (defined($beg)) { # NNTP article range
22                 $range{min} = $beg;
23                 $range{max} = $end // $beg;
24         }
25         for my $f (@$folders) {
26                 my $fid = $lms->fid_for($f);
27                 push @{$lei->{lcat_todo}}, { fid => $fid, %range };
28         }
29 }
30
31 sub lcat_imap_uri ($$) {
32         my ($lei, $uri) = @_;
33         # cf. LeiXSearch->lcat_dump
34         my $lms = $lei->{-lms_ro} //= $lei->lms // return;
35         if (defined $uri->uid) {
36                 push @{$lei->{lcat_todo}}, $lms->imap_oidhex($lei, $uri);
37         } elsif (defined(my $fid = $lms->fid_for($$uri))) {
38                 push @{$lei->{lcat_todo}}, { fid => $fid };
39         } else {
40                 lcat_folder($lei, $$uri);
41         }
42 }
43
44 sub lcat_nntp_uri ($$) {
45         my ($lei, $uri) = @_;
46         my $mid = $uri->message; # already unescaped by URI::news
47         return "mid:$mid" if defined($mid);
48         my $lms = $lei->{-lms_ro} //= $lei->lms // return;
49         my ($ng, $beg, $end) = $uri->group;
50         $uri->group($ng);
51         lcat_folder($lei, $$uri, $beg, $end);
52         '""';
53 }
54
55 sub extract_1 ($$) {
56         my ($lei, $x) = @_;
57         if ($x =~ m!\b(maildir:.+)!i) {
58                 lcat_folder($lei, $1);
59                 '""'; # blank query, using {lcat_todo}
60         } elsif ($x =~ m!\b(([a-z]+)://\S+)!i) {
61                 my ($u, $scheme) = ($1, $2);
62                 $u =~ s/[\>\]\)\,\.\;]+\z//;
63                 if ($scheme =~ m!\A(imaps?)\z!i) {
64                         require PublicInbox::URIimap;
65                         lcat_imap_uri($lei, PublicInbox::URIimap->new($u));
66                         return '""'; # blank query, using {lcat_todo}
67                 } elsif ($scheme =~ m!\A(?:nntps?|s?news)\z!i) {
68                         require PublicInbox::URInntps;
69                         $u = PublicInbox::URInntps->new($u);
70                         return lcat_nntp_uri($lei, $u);
71                 } # http, or something else:
72                 require URI;
73                 $u = URI->new($u);
74                 my $p = $u->path;
75                 my $term;
76                 if ($p =~ m!([^/]+\@[^/]+)!) { # common msgid pattern
77                         $term = 'mid:'.uri_unescape($1);
78
79                         # is it a URL which returns the full thread?
80                         if ($u->scheme =~ /\Ahttps?/i &&
81                                 $p =~ m!/(?:T/?|t/?|t\.mbox\.gz|t\.atom)\b!) {
82
83                                 $lei->{mset_opt}->{threads} = 1;
84                         }
85                 } elsif ($u->scheme =~ /\Ahttps?/i &&
86                                 # some msgids don't have '@', see if it looks like
87                                 # a public-inbox URL:
88                                 $p =~ m!/([^/]+)/(raw|t/?|T/?|
89                                         t\.mbox\.gz|t\.atom)\z!x) {
90                         $lei->{mset_opt}->{threads} = 1 if $2 && $2 ne 'raw';
91                         $term = 'mid:'.uri_unescape($1);
92                 }
93                 $term;
94         } elsif ($x =~ $MID_EXTRACT) { # <$MSGID>
95                 "mid:$1";
96         } elsif ($x =~ /\b((?:m|mid):\S+)/) { # our own prefixes (and mairix)
97                 $1;
98         } elsif ($x =~ /\bid:(\S+)/) { # notmuch convention
99                 "mid:$1";
100         } elsif ($x =~ /\bblob:([0-9a-f]{7,})\b/) {
101                 push @{$lei->{lcat_todo}}, $1; # cf. LeiToMail->wq_atexit_child
102                 '""'; # blank query
103         } else {
104                 undef;
105         }
106 }
107
108 sub extract_all {
109         my ($lei, @argv) = @_;
110         my $strict = !$lei->{opt}->{stdin};
111         my @q;
112         for my $x (@argv) {
113                 if (my $term = extract_1($lei, $x)) {
114                         push @q, $term;
115                 } elsif ($strict) {
116                         return $lei->fail(<<"");
117 could not extract Message-ID from $x
118
119                 }
120         }
121         delete $lei->{-lms_ro};
122         @q ? join(' OR ', @q) : $lei->fail("no Message-ID in: @argv");
123 }
124
125 sub _stdin { # PublicInbox::InputPipe::consume callback for --stdin
126         my ($lei) = @_; # $_[1] = $rbuf
127         if (defined($_[1])) {
128                 $_[1] eq '' and return eval {
129                         $lei->fchdir or return;
130                         my @argv = split(/\s+/, $lei->{mset_opt}->{qstr});
131                         $lei->{mset_opt}->{qstr} = extract_all($lei, @argv)
132                                 or return;
133                         $lei->_start_query;
134                 };
135                 $lei->{mset_opt}->{qstr} .= $_[1];
136         } else {
137                 $lei->fail("error reading stdin: $!");
138         }
139 }
140
141 sub lei_lcat {
142         my ($lei, @argv) = @_;
143         my $lxs = $lei->lxs_prepare or return;
144         $lei->ale->refresh_externals($lxs, $lei);
145         $lei->_lei_store(1);
146         my $opt = $lei->{opt};
147         my %mset_opt = map { $_ => $opt->{$_} } qw(threads limit offset);
148         $mset_opt{asc} = $opt->{'reverse'} ? 1 : 0;
149         $mset_opt{limit} //= 10000;
150         $opt->{sort} //= 'relevance';
151         $mset_opt{relevance} = 1;
152         $lei->{mset_opt} = \%mset_opt;
153         $opt->{'format'} //= 'text' unless defined($opt->{output});
154         if ($lei->{opt}->{stdin}) {
155                 return $lei->fail(<<'') if @argv;
156 no args allowed on command-line with --stdin
157
158                 require PublicInbox::InputPipe;
159                 PublicInbox::InputPipe::consume($lei->{0}, \&_stdin, $lei);
160                 return;
161         }
162         $lei->{mset_opt}->{qstr} = extract_all($lei, @argv) or return;
163         $lei->_start_query;
164 }
165
166 sub _complete_lcat {
167         my ($lei, @argv) = @_;
168         my $lms = $lei->lms or return;
169         my $match_cb = $lei->complete_url_prepare(\@argv);
170         map { $match_cb->($_) } $lms->folders;
171 }
172
173 1;