]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/IMAP.pm
imap: always send EXISTS on uo2m_extend
[public-inbox.git] / lib / PublicInbox / IMAP.pm
1 # Copyright (C) 2020 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 #
4 # Each instance of this represents an IMAP client connected to
5 # public-inbox-imapd.  Much of this was taken from NNTP, but
6 # further refined while experimenting on future ideas to handle
7 # slow storage.
8 #
9 # data notes:
10 #
11 # * NNTP article numbers are UIDs, mm->created_at is UIDVALIDITY
12 #
13 # * public-inboxes are sliced into mailboxes of 50K messages
14 #   to not overload MUAs: $NEWSGROUP_NAME.$SLICE_INDEX
15 #   Slices are similar in concept to v2 "epochs".  Epochs
16 #   are for the limitations of git clients, while slices are
17 #   for the limitations of IMAP clients.
18 #
19 # * We also take advantage of slices being only 50K to store
20 #   "UID offset" to message sequence number (MSN) mapping
21 #   as a 50K uint16_t array (via pack("S*", ...)).  "UID offset"
22 #   is the offset from {uid_base} which determines the start of
23 #   the mailbox slice.
24
25 package PublicInbox::IMAP;
26 use strict;
27 use base qw(PublicInbox::DS);
28 use fields qw(imapd ibx long_cb -login_tag
29         uid_base -idle_tag uo2m);
30 use PublicInbox::Eml;
31 use PublicInbox::EmlContentFoo qw(parse_content_disposition);
32 use PublicInbox::DS qw(now);
33 use PublicInbox::Syscall qw(EPOLLIN EPOLLONESHOT);
34 use PublicInbox::GitAsyncCat;
35 use Text::ParseWords qw(parse_line);
36 use Errno qw(EAGAIN);
37 use Hash::Util qw(unlock_hash); # dependency of fields for perl 5.10+, anyways
38 use PublicInbox::Search;
39 use PublicInbox::IMAPsearchqp;
40 *mdocid = \&PublicInbox::Search::mdocid;
41
42 my $Address;
43 for my $mod (qw(Email::Address::XS Mail::Address)) {
44         eval "require $mod" or next;
45         $Address = $mod and last;
46 }
47 die "neither Email::Address::XS nor Mail::Address loaded: $@" if !$Address;
48
49 sub LINE_MAX () { 8000 } # RFC 2683 3.2.1.5
50
51 # Changing UID_SLICE will cause grief for clients which cache.
52 # This also needs to be <64K: we pack it into a uint16_t
53 # for long_response UID (offset) => MSN mappings
54 sub UID_SLICE () { 50_000 }
55
56 # these values area also used for sorting
57 sub NEED_SMSG () { 1 }
58 sub NEED_BLOB () { NEED_SMSG|2 }
59 sub CRLF_BREF () { 4 }
60 sub EML_HDR () { 8 }
61 sub CRLF_HDR () { 16 }
62 sub EML_BDY () { 32 }
63 sub CRLF_BDY () { 64 }
64 my $OP_EML_NEW = [ EML_HDR - 1, \&op_eml_new ];
65 my $OP_CRLF_BREF = [ CRLF_BREF, \&op_crlf_bref ];
66 my $OP_CRLF_HDR = [ CRLF_HDR, \&op_crlf_hdr ];
67 my $OP_CRLF_BDY = [ CRLF_BDY, \&op_crlf_bdy ];
68
69 my %FETCH_NEED = (
70         'BODY[HEADER]' => [ NEED_BLOB|EML_HDR|CRLF_HDR, \&emit_rfc822_header ],
71         'BODY[TEXT]' => [ NEED_BLOB|EML_BDY|CRLF_BDY, \&emit_rfc822_text ],
72         'BODY[]' => [ NEED_BLOB|CRLF_BREF, \&emit_rfc822 ],
73         'RFC822.HEADER' => [ NEED_BLOB|EML_HDR|CRLF_HDR, \&emit_rfc822_header ],
74         'RFC822.TEXT' => [ NEED_BLOB|EML_BDY|CRLF_BDY, \&emit_rfc822_text ],
75         'RFC822.SIZE' => [ NEED_SMSG, \&emit_rfc822_size ],
76         RFC822 => [ NEED_BLOB|CRLF_BREF, \&emit_rfc822 ],
77         BODY => [ NEED_BLOB|EML_HDR|EML_BDY, \&emit_body ],
78         BODYSTRUCTURE => [ NEED_BLOB|EML_HDR|EML_BDY, \&emit_bodystructure ],
79         ENVELOPE => [ NEED_BLOB|EML_HDR, \&emit_envelope ],
80         FLAGS => [ 0, \&emit_flags ],
81         INTERNALDATE => [ NEED_SMSG, \&emit_internaldate ],
82 );
83 my %FETCH_ATT = map { $_ => [ $_ ] } keys %FETCH_NEED;
84
85 # aliases (RFC 3501 section 6.4.5)
86 $FETCH_ATT{FAST} = [ qw(FLAGS INTERNALDATE RFC822.SIZE) ];
87 $FETCH_ATT{ALL} = [ @{$FETCH_ATT{FAST}}, 'ENVELOPE' ];
88 $FETCH_ATT{FULL} = [ @{$FETCH_ATT{ALL}}, 'BODY' ];
89
90 for my $att (keys %FETCH_ATT) {
91         my %h = map { $_ => $FETCH_NEED{$_} } @{$FETCH_ATT{$att}};
92         $FETCH_ATT{$att} = \%h;
93 }
94 undef %FETCH_NEED;
95
96 my $valid_range = '[0-9]+|[0-9]+:[0-9]+|[0-9]+:\*';
97 $valid_range = qr/\A(?:$valid_range)(?:,(?:$valid_range))*\z/;
98
99 # RFC 3501 5.4. Autologout Timer needs to be >= 30min
100 $PublicInbox::DS::EXPTIME = 60 * 30;
101
102 sub greet ($) {
103         my ($self) = @_;
104         my $capa = capa($self);
105         $self->write(\"* OK [$capa] public-inbox-imapd ready\r\n");
106 }
107
108 sub new ($$$) {
109         my ($class, $sock, $imapd) = @_;
110         my $self = fields::new('PublicInbox::IMAP_preauth');
111         unlock_hash(%$self);
112         my $ev = EPOLLIN;
113         my $wbuf;
114         if ($sock->can('accept_SSL') && !$sock->accept_SSL) {
115                 return CORE::close($sock) if $! != EAGAIN;
116                 $ev = PublicInbox::TLS::epollbit();
117                 $wbuf = [ \&PublicInbox::DS::accept_tls_step, \&greet ];
118         }
119         $self->SUPER::new($sock, $ev | EPOLLONESHOT);
120         $self->{imapd} = $imapd;
121         if ($wbuf) {
122                 $self->{wbuf} = $wbuf;
123         } else {
124                 greet($self);
125         }
126         $self->update_idle_time;
127         $self;
128 }
129
130 sub logged_in { 1 }
131
132 sub capa ($) {
133         my ($self) = @_;
134
135         # dovecot advertises IDLE pre-login; perhaps because some clients
136         # depend on it, so we'll do the same
137         my $capa = 'CAPABILITY IMAP4rev1 IDLE';
138         if ($self->logged_in) {
139                 $capa .= ' COMPRESS=DEFLATE';
140         } else {
141                 if (!($self->{sock} // $self)->can('accept_SSL') &&
142                         $self->{imapd}->{accept_tls}) {
143                         $capa .= ' STARTTLS';
144                 }
145                 $capa .= ' AUTH=ANONYMOUS';
146         }
147 }
148
149 sub login_success ($$) {
150         my ($self, $tag) = @_;
151         bless $self, 'PublicInbox::IMAP';
152         my $capa = capa($self);
153         "$tag OK [$capa] Logged in\r\n";
154 }
155
156 sub auth_challenge_ok ($) {
157         my ($self) = @_;
158         my $tag = delete($self->{-login_tag}) or return;
159         login_success($self, $tag);
160 }
161
162 sub cmd_login ($$$$) {
163         my ($self, $tag) = @_; # ignore ($user, $password) = ($_[2], $_[3])
164         login_success($self, $tag);
165 }
166
167 sub cmd_close ($$) {
168         my ($self, $tag) = @_;
169         delete @$self{qw(uid_base uo2m)};
170         delete $self->{ibx} ? "$tag OK Close done\r\n"
171                                 : "$tag BAD No mailbox\r\n";
172 }
173
174 sub cmd_logout ($$) {
175         my ($self, $tag) = @_;
176         delete $self->{-idle_tag};
177         $self->write(\"* BYE logging out\r\n$tag OK Logout done\r\n");
178         $self->shutdn; # PublicInbox::DS::shutdn
179         undef;
180 }
181
182 sub cmd_authenticate ($$$) {
183         my ($self, $tag) = @_; # $method = $_[2], should be "ANONYMOUS"
184         $self->{-login_tag} = $tag;
185         "+\r\n"; # challenge
186 }
187
188 sub cmd_capability ($$) {
189         my ($self, $tag) = @_;
190         '* '.capa($self)."\r\n$tag OK Capability done\r\n";
191 }
192
193 # uo2m: UID Offset to MSN, this is an arrayref by default,
194 # but uo2m_hibernate can compact and deduplicate it
195 sub uo2m_ary_new ($) {
196         my ($self) = @_;
197         my $base = $self->{uid_base};
198         my $uids = $self->{ibx}->over->uid_range($base + 1, $base + UID_SLICE);
199
200         # convert UIDs to offsets from {base}
201         my @tmp; # [$UID_OFFSET] => $MSN
202         my $msn = 0;
203         ++$base;
204         $tmp[$_ - $base] = ++$msn for @$uids;
205         \@tmp;
206 }
207
208 # changes UID-offset-to-MSN mapping into a deduplicated scalar:
209 # uint16_t uo2m[UID_SLICE].
210 # May be swapped out for idle clients if THP is disabled.
211 sub uo2m_hibernate ($) {
212         my ($self) = @_;
213         ref(my $uo2m = $self->{uo2m}) or return;
214         my %dedupe = ( uo2m_pack($uo2m) => undef );
215         $self->{uo2m} = (keys(%dedupe))[0];
216         undef;
217 }
218
219 sub uo2m_last_uid ($) {
220         my ($self) = @_;
221         defined(my $uo2m = $self->{uo2m}) or die 'BUG: uo2m_last_uid w/o {uo2m}';
222         (ref($uo2m) ? @$uo2m : (length($uo2m) >> 1)) + $self->{uid_base};
223 }
224
225 sub uo2m_pack ($) {
226         # $_[0] is an arrayref of MSNs, it may have undef gaps if there
227         # are gaps in the corresponding UIDs: [ msn1, msn2, undef, msn3 ]
228         no warnings 'uninitialized';
229         pack('S*', @{$_[0]});
230 }
231
232 # extend {uo2m} to account for new messages which arrived since
233 # {uo2m} was created.
234 sub uo2m_extend ($$;$) {
235         my ($self, $new_uid_max) = @_;
236         defined(my $uo2m = $self->{uo2m}) or
237                 return($self->{uo2m} = uo2m_ary_new($self));
238         my $beg = uo2m_last_uid($self); # last UID we've learned
239         return $uo2m if $beg >= $new_uid_max; # fast path
240
241         # need to extend the current range:
242         my $base = $self->{uid_base};
243         ++$beg;
244         my $uids = $self->{ibx}->over->uid_range($beg, $base + UID_SLICE);
245         my @tmp; # [$UID_OFFSET] => $MSN
246         my $write_method = $_[2] // 'msg_more';
247         if (ref($uo2m)) {
248                 my $msn = $uo2m->[-1];
249                 $tmp[$_ - $beg] = ++$msn for @$uids;
250                 $self->$write_method("* $msn EXISTS\r\n");
251                 push @$uo2m, @tmp;
252                 $uo2m;
253         } else {
254                 my $msn = unpack('S', substr($uo2m, -2, 2));
255                 $tmp[$_ - $beg] = ++$msn for @$uids;
256                 $self->$write_method("* $msn EXISTS\r\n");
257                 $uo2m .= uo2m_pack(\@tmp);
258                 my %dedupe = ($uo2m => undef);
259                 $self->{uo2m} = (keys %dedupe)[0];
260         }
261 }
262
263 sub cmd_noop ($$) {
264         my ($self, $tag) = @_;
265         defined($self->{uid_base}) and
266                 uo2m_extend($self, $self->{uid_base} + UID_SLICE);
267         \"$tag OK Noop done\r\n";
268 }
269
270 # the flexible version which works on scalars and array refs.
271 # Must call uo2m_extend before this
272 sub uid2msn ($$) {
273         my ($self, $uid) = @_;
274         my $uo2m = $self->{uo2m};
275         my $off = $uid - $self->{uid_base} - 1;
276         ref($uo2m) ? $uo2m->[$off] : unpack('S', substr($uo2m, $off << 1, 2));
277 }
278
279 # returns an arrayref of UIDs, so MSNs can be translated to UIDs via:
280 # $msn2uid->[$MSN-1] => $UID.  The result of this is always ephemeral
281 # and does not live beyond the event loop.
282 sub msn2uid ($) {
283         my ($self) = @_;
284         my $base = $self->{uid_base};
285         my $uo2m = uo2m_extend($self, $base + UID_SLICE);
286         $uo2m = [ unpack('S*', $uo2m) ] if !ref($uo2m);
287
288         my $uo = 0;
289         my @msn2uid;
290         for my $msn (@$uo2m) {
291                 ++$uo;
292                 $msn2uid[$msn - 1] = $uo + $base if $msn;
293         }
294         \@msn2uid;
295 }
296
297 # converts a set of message sequence numbers in requests to UIDs:
298 sub msn_to_uid_range ($$) {
299         my $msn2uid = $_[0];
300         $_[1] =~ s!([0-9]+)!$msn2uid->[$1 - 1] // ($msn2uid->[-1] + 1)!sge;
301 }
302
303 # called by PublicInbox::InboxIdle
304 sub on_inbox_unlock {
305         my ($self, $ibx) = @_;
306         my $uid_end = $self->{uid_base} + UID_SLICE;
307         uo2m_extend($self, $uid_end, 'write');
308         my $new = uo2m_last_uid($self);
309         if ($new == $uid_end) { # max exceeded $uid_end
310                 # continue idling w/o inotify
311                 my $sock = $self->{sock} or return;
312                 $ibx->unsubscribe_unlock(fileno($sock));
313         }
314 }
315
316 # called every X minute(s) or so by PublicInbox::DS::later
317 my $IDLERS = {};
318 my $idle_timer;
319 sub idle_tick_all {
320         my $old = $IDLERS;
321         $IDLERS = {};
322         for my $i (values %$old) {
323                 next if ($i->{wbuf} || !exists($i->{-idle_tag}));
324                 $i->update_idle_time or next;
325                 $IDLERS->{fileno($i->{sock})} = $i;
326                 $i->write(\"* OK Still here\r\n");
327         }
328         $idle_timer = scalar keys %$IDLERS ?
329                         PublicInbox::DS::later(\&idle_tick_all) : undef;
330 }
331
332 sub cmd_idle ($$) {
333         my ($self, $tag) = @_;
334         # IDLE seems allowed by dovecot w/o a mailbox selected *shrug*
335         my $ibx = $self->{ibx} or return "$tag BAD no mailbox selected\r\n";
336         my $uid_end = $self->{uid_base} + UID_SLICE;
337         uo2m_extend($self, $uid_end);
338         my $sock = $self->{sock} or return;
339         my $fd = fileno($sock);
340         $self->{-idle_tag} = $tag;
341         # only do inotify on most recent slice
342         if ($ibx->over->max < $uid_end) {
343                 $ibx->subscribe_unlock($fd, $self);
344                 $self->{imapd}->idler_start;
345         }
346         $idle_timer //= PublicInbox::DS::later(\&idle_tick_all);
347         $IDLERS->{$fd} = $self;
348         \"+ idling\r\n"
349 }
350
351 sub stop_idle ($$) {
352         my ($self, $ibx) = @_;
353         my $sock = $self->{sock} or return;
354         my $fd = fileno($sock);
355         delete $IDLERS->{$fd};
356         $ibx->unsubscribe_unlock($fd);
357 }
358
359 sub idle_done ($$) {
360         my ($self, $tag) = @_; # $tag is "DONE" (case-insensitive)
361         defined(my $idle_tag = delete $self->{-idle_tag}) or
362                 return "$tag BAD not idle\r\n";
363         my $ibx = $self->{ibx} or do {
364                 warn "BUG: idle_tag set w/o inbox";
365                 return "$tag BAD internal bug\r\n";
366         };
367         stop_idle($self, $ibx);
368         "$idle_tag OK Idle done\r\n";
369 }
370
371 sub ensure_slices_exist ($$$) {
372         my ($imapd, $ibx, $max) = @_;
373         defined(my $mb_top = $ibx->{newsgroup}) or return;
374         my $mailboxes = $imapd->{mailboxes};
375         my @created;
376         for (my $i = int($max/UID_SLICE); $i >= 0; --$i) {
377                 my $sub_mailbox = "$mb_top.$i";
378                 last if exists $mailboxes->{$sub_mailbox};
379                 $mailboxes->{$sub_mailbox} = $ibx;
380                 $sub_mailbox =~ s/\Ainbox\./INBOX./i; # more familiar to users
381                 push @created, $sub_mailbox;
382         }
383         return unless @created;
384         my $l = $imapd->{inboxlist} or return;
385         push @$l, map { qq[* LIST (\\HasNoChildren) "." $_\r\n] } @created;
386 }
387
388 sub inbox_lookup ($$) {
389         my ($self, $mailbox) = @_;
390         my ($ibx, $exists, $uidnext, $uid_base);
391         if ($mailbox =~ /\A(.+)\.([0-9]+)\z/) {
392                 # old mail: inbox.comp.foo.$SLICE_IDX
393                 my $mb_top = $1;
394                 $uid_base = $2 * UID_SLICE;
395                 $ibx = $self->{imapd}->{mailboxes}->{lc $mailbox} or return;
396                 my $max;
397                 ($exists, $uidnext, $max) = $ibx->over->imap_status($uid_base,
398                                                         $uid_base + UID_SLICE);
399                 ensure_slices_exist($self->{imapd}, $ibx, $max);
400         } else { # check for dummy inboxes
401                 $mailbox = lc $mailbox;
402                 $ibx = $self->{imapd}->{mailboxes}->{$mailbox} or return;
403
404                 # if "INBOX.foo.bar" is selected and "INBOX.foo.bar.0",
405                 # check for new UID ranges (e.g. "INBOX.foo.bar.1")
406                 if (my $z = $self->{imapd}->{mailboxes}->{"$mailbox.0"}) {
407                         ensure_slices_exist($self->{imapd}, $z, $z->over->max);
408                 }
409
410                 $uid_base = $exists = 0;
411                 $uidnext = 1;
412         }
413         ($ibx, $exists, $uidnext, $uid_base);
414 }
415
416 sub cmd_examine ($$$) {
417         my ($self, $tag, $mailbox) = @_;
418         my ($ibx, $exists, $uidnext, $base) = inbox_lookup($self, $mailbox);
419         return "$tag NO Mailbox doesn't exist: $mailbox\r\n" if !$ibx;
420         $self->{uid_base} = $base;
421         delete $self->{uo2m};
422
423         # XXX: do we need this? RFC 5162/7162
424         my $ret = $self->{ibx} ? "* OK [CLOSED] previous closed\r\n" : '';
425         $self->{ibx} = $ibx;
426         $ret .= <<EOF;
427 * $exists EXISTS\r
428 * $exists RECENT\r
429 * FLAGS (\\Seen)\r
430 * OK [PERMANENTFLAGS ()] Read-only mailbox\r
431 * OK [UNSEEN $exists]\r
432 * OK [UIDNEXT $uidnext]\r
433 * OK [UIDVALIDITY $ibx->{uidvalidity}]\r
434 $tag OK [READ-ONLY] EXAMINE/SELECT done\r
435 EOF
436 }
437
438 sub _esc ($) {
439         my ($v) = @_;
440         if (!defined($v)) {
441                 'NIL';
442         } elsif ($v =~ /[{"\r\n%*\\\[]/) { # literal string
443                 '{' . length($v) . "}\r\n" . $v;
444         } else { # quoted string
445                 qq{"$v"}
446         }
447 }
448
449 sub addr_envelope ($$;$) {
450         my ($eml, $x, $y) = @_;
451         my $v = $eml->header_raw($x) //
452                 ($y ? $eml->header_raw($y) : undef) // return 'NIL';
453
454         my @x = $Address->parse($v) or return 'NIL';
455         '(' . join('',
456                 map { '(' . join(' ',
457                                 _esc($_->name), 'NIL',
458                                 _esc($_->user), _esc($_->host)
459                         ) . ')'
460                 } @x) .
461         ')';
462 }
463
464 sub eml_envelope ($) {
465         my ($eml) = @_;
466         '(' . join(' ',
467                 _esc($eml->header_raw('Date')),
468                 _esc($eml->header_raw('Subject')),
469                 addr_envelope($eml, 'From'),
470                 addr_envelope($eml, 'Sender', 'From'),
471                 addr_envelope($eml, 'Reply-To', 'From'),
472                 addr_envelope($eml, 'To'),
473                 addr_envelope($eml, 'Cc'),
474                 addr_envelope($eml, 'Bcc'),
475                 _esc($eml->header_raw('In-Reply-To')),
476                 _esc($eml->header_raw('Message-ID')),
477         ) . ')';
478 }
479
480 sub _esc_hash ($) {
481         my ($hash) = @_;
482         if ($hash && scalar keys %$hash) {
483                 $hash = [ %$hash ]; # flatten hash into 1-dimensional array
484                 '(' . join(' ', map { _esc($_) } @$hash) . ')';
485         } else {
486                 'NIL';
487         }
488 }
489
490 sub body_disposition ($) {
491         my ($eml) = @_;
492         my $cd = $eml->header_raw('Content-Disposition') or return 'NIL';
493         $cd = parse_content_disposition($cd);
494         my $buf = '('._esc($cd->{type});
495         $buf .= ' ' . _esc_hash(delete $cd->{attributes});
496         $buf .= ')';
497 }
498
499 sub body_leaf ($$;$) {
500         my ($eml, $structure, $hold) = @_;
501         my $buf = '';
502         $eml->{is_submsg} and # parent was a message/(rfc822|news|global)
503                 $buf .= eml_envelope($eml). ' ';
504         my $ct = $eml->ct;
505         $buf .= '('._esc($ct->{type}).' ';
506         $buf .= _esc($ct->{subtype});
507         $buf .= ' ' . _esc_hash(delete $ct->{attributes});
508         $buf .= ' ' . _esc($eml->header_raw('Content-ID'));
509         $buf .= ' ' . _esc($eml->header_raw('Content-Description'));
510         my $cte = $eml->header_raw('Content-Transfer-Encoding') // '7bit';
511         $buf .= ' ' . _esc($cte);
512         $buf .= ' ' . $eml->{imap_body_len};
513         $buf .= ' '.($eml->body_raw =~ tr/\n/\n/) if lc($ct->{type}) eq 'text';
514
515         # for message/(rfc822|global|news), $hold[0] should have envelope
516         $buf .= ' ' . (@$hold ? join('', @$hold) : 'NIL') if $hold;
517
518         if ($structure) {
519                 $buf .= ' '._esc($eml->header_raw('Content-MD5'));
520                 $buf .= ' '. body_disposition($eml);
521                 $buf .= ' '._esc($eml->header_raw('Content-Language'));
522                 $buf .= ' '._esc($eml->header_raw('Content-Location'));
523         }
524         $buf .= ')';
525 }
526
527 sub body_parent ($$$) {
528         my ($eml, $structure, $hold) = @_;
529         my $ct = $eml->ct;
530         my $type = lc($ct->{type});
531         if ($type eq 'multipart') {
532                 my $buf = '(';
533                 $buf .= @$hold ? join('', @$hold) : 'NIL';
534                 $buf .= ' '._esc($ct->{subtype});
535                 if ($structure) {
536                         $buf .= ' '._esc_hash(delete $ct->{attributes});
537                         $buf .= ' '.body_disposition($eml);
538                         $buf .= ' '._esc($eml->header_raw('Content-Language'));
539                         $buf .= ' '._esc($eml->header_raw('Content-Location'));
540                 }
541                 $buf .= ')';
542                 @$hold = ($buf);
543         } else { # message/(rfc822|global|news)
544                 @$hold = (body_leaf($eml, $structure, $hold));
545         }
546 }
547
548 # this is gross, but we need to process the parent part AFTER
549 # the child parts are done
550 sub bodystructure_prep {
551         my ($p, $q) = @_;
552         my ($eml, $depth) = @$p; # ignore idx
553         # set length here, as $eml->{bdy} gets deleted for message/rfc822
554         $eml->{imap_body_len} = length($eml->body_raw);
555         push @$q, $eml, $depth;
556 }
557
558 # for FETCH BODY and FETCH BODYSTRUCTURE
559 sub fetch_body ($;$) {
560         my ($eml, $structure) = @_;
561         my @q;
562         $eml->each_part(\&bodystructure_prep, \@q, 0, 1);
563         my $cur_depth = 0;
564         my @hold;
565         do {
566                 my ($part, $depth) = splice(@q, -2);
567                 my $is_mp_parent = $depth == ($cur_depth - 1);
568                 $cur_depth = $depth;
569
570                 if ($is_mp_parent) {
571                         body_parent($part, $structure, \@hold);
572                 } else {
573                         unshift @hold, body_leaf($part, $structure);
574                 }
575         } while (@q);
576         join('', @hold);
577 }
578
579 sub requeue_once ($) {
580         my ($self) = @_;
581         # COMPRESS users all share the same DEFLATE context.
582         # Flush it here to ensure clients don't see
583         # each other's data
584         $self->zflush;
585
586         # no recursion, schedule another call ASAP,
587         # but only after all pending writes are done.
588         # autovivify wbuf:
589         my $new_size = push(@{$self->{wbuf}}, \&long_step);
590
591         # wbuf may be populated by $cb, no need to rearm if so:
592         $self->requeue if $new_size == 1;
593 }
594
595 sub fetch_run_ops {
596         my ($self, $smsg, $bref, $ops, $partial) = @_;
597         my $uid = $smsg->{num};
598         $self->msg_more('* '.uid2msn($self, $uid)." FETCH (UID $uid");
599         my ($eml, $k);
600         for (my $i = 0; $i < @$ops;) {
601                 $k = $ops->[$i++];
602                 $ops->[$i++]->($self, $k, $smsg, $bref, $eml);
603         }
604         partial_emit($self, $partial, $eml) if $partial;
605         $self->msg_more(")\r\n");
606 }
607
608 sub fetch_blob_cb { # called by git->cat_async via git_async_cat
609         my ($bref, $oid, $type, $size, $fetch_arg) = @_;
610         my ($self, undef, $msgs, $range_info, $ops, $partial) = @$fetch_arg;
611         my $smsg = shift @$msgs or die 'BUG: no smsg';
612         if (!defined($oid)) {
613                 # it's possible to have TOCTOU if an admin runs
614                 # public-inbox-(edit|purge), just move onto the next message
615                 return requeue_once($self);
616         } else {
617                 $smsg->{blob} eq $oid or die "BUG: $smsg->{blob} != $oid";
618         }
619         fetch_run_ops($self, $smsg, $bref, $ops, $partial);
620         requeue_once($self);
621 }
622
623 sub emit_rfc822 {
624         my ($self, $k, undef, $bref) = @_;
625         $self->msg_more(" $k {" . length($$bref)."}\r\n");
626         $self->msg_more($$bref);
627 }
628
629 # Mail::IMAPClient::message_string cares about this by default,
630 # (->Ignoresizeerrors attribute).  Admins are encouraged to
631 # --reindex for IMAP support, anyways.
632 sub emit_rfc822_size {
633         my ($self, $k, $smsg) = @_;
634         $self->msg_more(' RFC822.SIZE ' . $smsg->{bytes});
635 }
636
637 sub emit_internaldate {
638         my ($self, undef, $smsg) = @_;
639         $self->msg_more(' INTERNALDATE "'.$smsg->internaldate.'"');
640 }
641
642 sub emit_flags { $_[0]->msg_more(' FLAGS ()') }
643
644 sub emit_envelope {
645         my ($self, undef, undef, undef, $eml) = @_;
646         $self->msg_more(' ENVELOPE '.eml_envelope($eml));
647 }
648
649 sub emit_rfc822_header {
650         my ($self, $k, undef, undef, $eml) = @_;
651         $self->msg_more(" $k {".length(${$eml->{hdr}})."}\r\n");
652         $self->msg_more(${$eml->{hdr}});
653 }
654
655 # n.b. this is sorted to be after any emit_eml_new ops
656 sub emit_rfc822_text {
657         my ($self, $k, undef, $bref) = @_;
658         $self->msg_more(" $k {".length($$bref)."}\r\n");
659         $self->msg_more($$bref);
660 }
661
662 sub emit_bodystructure {
663         my ($self, undef, undef, undef, $eml) = @_;
664         $self->msg_more(' BODYSTRUCTURE '.fetch_body($eml, 1));
665 }
666
667 sub emit_body {
668         my ($self, undef, undef, undef, $eml) = @_;
669         $self->msg_more(' BODY '.fetch_body($eml));
670 }
671
672 # set $eml once ($_[4] == $eml, $_[3] == $bref)
673 sub op_eml_new { $_[4] = PublicInbox::Eml->new($_[3]) }
674
675 # s/From / fixes old bug from import (pre-a0c07cba0e5d8b6a)
676 sub to_crlf_full {
677         ${$_[0]} =~ s/(?<!\r)\n/\r\n/sg;
678         ${$_[0]} =~ s/\A[\r\n]*From [^\r\n]*\r\n//s;
679 }
680
681 sub op_crlf_bref { to_crlf_full($_[3]) }
682
683 sub op_crlf_hdr { to_crlf_full($_[4]->{hdr}) }
684
685 sub op_crlf_bdy { ${$_[4]->{bdy}} =~ s/(?<!\r)\n/\r\n/sg if $_[4]->{bdy} }
686
687 sub uid_clamp ($$$) {
688         my ($self, $beg, $end) = @_;
689         my $uid_min = $self->{uid_base} + 1;
690         my $uid_end = $uid_min + UID_SLICE - 1;
691         $$beg = $uid_min if $$beg < $uid_min;
692         $$end = $uid_end if $$end > $uid_end;
693 }
694
695 sub range_step ($$) {
696         my ($self, $range_csv) = @_;
697         my ($beg, $end, $range);
698         if ($$range_csv =~ s/\A([^,]+),//) {
699                 $range = $1;
700         } else {
701                 $range = $$range_csv;
702                 $$range_csv = undef;
703         }
704         my $uid_base = $self->{uid_base};
705         my $uid_end = $uid_base + UID_SLICE;
706         if ($range =~ /\A([0-9]+):([0-9]+)\z/) {
707                 ($beg, $end) = ($1 + 0, $2 + 0);
708                 uid_clamp($self, \$beg, \$end);
709         } elsif ($range =~ /\A([0-9]+):\*\z/) {
710                 $beg = $1 + 0;
711                 $end = $self->{ibx}->over->max;
712                 $end = $uid_end if $end > $uid_end;
713                 $beg = $end if $beg > $end;
714                 uid_clamp($self, \$beg, \$end);
715         } elsif ($range =~ /\A[0-9]+\z/) {
716                 $beg = $end = $range + 0;
717                 # just let the caller do an out-of-range query if a single
718                 # UID is out-of-range
719                 ++$beg if ($beg <= $uid_base || $end > $uid_end);
720         } else {
721                 return 'BAD fetch range';
722         }
723         [ $beg, $end, $$range_csv ];
724 }
725
726 sub refill_range ($$$) {
727         my ($self, $msgs, $range_info) = @_;
728         my ($beg, $end, $range_csv) = @$range_info;
729         if (scalar(@$msgs = @{$self->{ibx}->over->query_xover($beg, $end)})) {
730                 $range_info->[0] = $msgs->[-1]->{num} + 1;
731                 return;
732         }
733         return 'OK Fetch done' if !$range_csv;
734         my $next_range = range_step($self, \$range_csv);
735         return $next_range if !ref($next_range); # error
736         @$range_info = @$next_range;
737         undef; # keep looping
738 }
739
740 sub fetch_blob { # long_response
741         my ($self, $tag, $msgs, $range_info, $ops, $partial) = @_;
742         while (!@$msgs) { # rare
743                 if (my $end = refill_range($self, $msgs, $range_info)) {
744                         $self->write(\"$tag $end\r\n");
745                         return;
746                 }
747         }
748         uo2m_extend($self, $msgs->[-1]->{num});
749         git_async_cat($self->{ibx}->git, $msgs->[0]->{blob},
750                         \&fetch_blob_cb, \@_);
751 }
752
753 sub fetch_smsg { # long_response
754         my ($self, $tag, $msgs, $range_info, $ops) = @_;
755         while (!@$msgs) { # rare
756                 if (my $end = refill_range($self, $msgs, $range_info)) {
757                         $self->write(\"$tag $end\r\n");
758                         return;
759                 }
760         }
761         uo2m_extend($self, $msgs->[-1]->{num});
762         fetch_run_ops($self, $_, undef, $ops) for @$msgs;
763         @$msgs = ();
764         1; # more
765 }
766
767 sub refill_uids ($$$;$) {
768         my ($self, $uids, $range_info, $sql) = @_;
769         my ($beg, $end, $range_csv) = @$range_info;
770         my $over = $self->{ibx}->over;
771         while (1) {
772                 if (scalar(@$uids = @{$over->uid_range($beg, $end, $sql)})) {
773                         $range_info->[0] = $uids->[-1] + 1; # update $beg
774                         return;
775                 } elsif (!$range_csv) {
776                         return 0;
777                 } else {
778                         my $next_range = range_step($self, \$range_csv);
779                         return $next_range if !ref($next_range); # error
780                         ($beg, $end, $range_csv) = @$range_info = @$next_range;
781                         # continue looping
782                 }
783         }
784 }
785
786 sub fetch_uid { # long_response
787         my ($self, $tag, $uids, $range_info, $ops) = @_;
788         if (defined(my $err = refill_uids($self, $uids, $range_info))) {
789                 $err ||= 'OK Fetch done';
790                 $self->write("$tag $err\r\n");
791                 return;
792         }
793         my $adj = $self->{uid_base} + 1;
794         my $uo2m = uo2m_extend($self, $uids->[-1]);
795         $uo2m = [ unpack('S*', $uo2m) ] if !ref($uo2m);
796         my ($i, $k);
797         for (@$uids) {
798                 $self->msg_more("* $uo2m->[$_ - $adj] FETCH (UID $_");
799                 for ($i = 0; $i < @$ops;) {
800                         $k = $ops->[$i++];
801                         $ops->[$i++]->($self, $k);
802                 }
803                 $self->msg_more(")\r\n");
804         }
805         @$uids = ();
806         1; # more
807 }
808
809 sub cmd_status ($$$;@) {
810         my ($self, $tag, $mailbox, @items) = @_;
811         return "$tag BAD no items\r\n" if !scalar(@items);
812         ($items[0] !~ s/\A\(//s || $items[-1] !~ s/\)\z//s) and
813                 return "$tag BAD invalid args\r\n";
814         my ($ibx, $exists, $uidnext) = inbox_lookup($self, $mailbox);
815         return "$tag NO Mailbox doesn't exist: $mailbox\r\n" if !$ibx;
816         my @it;
817         for my $it (@items) {
818                 $it = uc($it);
819                 push @it, $it;
820                 if ($it =~ /\A(?:MESSAGES|UNSEEN|RECENT)\z/) {
821                         push @it, $exists;
822                 } elsif ($it eq 'UIDNEXT') {
823                         push @it, $uidnext;
824                 } elsif ($it eq 'UIDVALIDITY') {
825                         push @it, $ibx->{uidvalidity};
826                 } else {
827                         return "$tag BAD invalid item\r\n";
828                 }
829         }
830         return "$tag BAD no items\r\n" if !@it;
831         "* STATUS $mailbox (".join(' ', @it).")\r\n" .
832         "$tag OK Status done\r\n";
833 }
834
835 my %patmap = ('*' => '.*', '%' => '[^\.]*');
836 sub cmd_list ($$$$) {
837         my ($self, $tag, $refname, $wildcard) = @_;
838         my $l = $self->{imapd}->{inboxlist};
839         if ($refname eq '' && $wildcard eq '') {
840                 # request for hierarchy delimiter
841                 $l = [ qq[* LIST (\\Noselect) "." ""\r\n] ];
842         } elsif ($refname ne '' || $wildcard ne '*') {
843                 $wildcard =~ s!([^a-z0-9_])!$patmap{$1} // "\Q$1"!egi;
844                 $l = [ grep(/ \Q$refname\E$wildcard\r\n\z/is, @$l) ];
845         }
846         \(join('', @$l, "$tag OK List done\r\n"));
847 }
848
849 sub cmd_lsub ($$$$) {
850         my (undef, $tag) = @_; # same args as cmd_list
851         "$tag OK Lsub done\r\n";
852 }
853
854 sub eml_index_offs_i { # PublicInbox::Eml::each_part callback
855         my ($p, $all) = @_;
856         my ($eml, undef, $idx) = @$p;
857         if ($idx && lc($eml->ct->{type}) eq 'multipart') {
858                 $eml->{imap_bdy} = $eml->{bdy} // \'';
859         }
860         $all->{$idx} = $eml; # $idx => Eml
861 }
862
863 # prepares an index for BODY[$SECTION_IDX] fetches
864 sub eml_body_idx ($$) {
865         my ($eml, $section_idx) = @_;
866         my $idx = $eml->{imap_all_parts} //= do {
867                 my $all = {};
868                 $eml->each_part(\&eml_index_offs_i, $all, 0, 1);
869                 # top-level of multipart, BODY[0] not allowed (nz-number)
870                 delete $all->{0};
871                 $all;
872         };
873         $idx->{$section_idx};
874 }
875
876 # BODY[($SECTION_IDX)?(.$SECTION_NAME)?]<$offset.$bytes>
877 sub partial_body {
878         my ($eml, $section_idx, $section_name) = @_;
879         if (defined $section_idx) {
880                 $eml = eml_body_idx($eml, $section_idx) or return;
881         }
882         if (defined $section_name) {
883                 if ($section_name eq 'MIME') {
884                         # RFC 3501 6.4.5 states:
885                         #       The MIME part specifier MUST be prefixed
886                         #       by one or more numeric part specifiers
887                         return unless defined $section_idx;
888                         return $eml->header_obj->as_string . "\r\n";
889                 }
890                 my $bdy = $eml->{bdy} // $eml->{imap_bdy} // \'';
891                 $eml = PublicInbox::Eml->new($$bdy);
892                 if ($section_name eq 'TEXT') {
893                         return $eml->body_raw;
894                 } elsif ($section_name eq 'HEADER') {
895                         return $eml->header_obj->as_string . "\r\n";
896                 } else {
897                         die "BUG: bad section_name=$section_name";
898                 }
899         }
900         ${$eml->{bdy} // $eml->{imap_bdy} // \''};
901 }
902
903 # similar to what's in PublicInbox::Eml::re_memo, but doesn't memoize
904 # to avoid OOM with malicious users
905 sub hdrs_regexp ($) {
906         my ($hdrs) = @_;
907         my $names = join('|', map { "\Q$_" } split(/[ \t]+/, $hdrs));
908         qr/^(?:$names):[ \t]*[^\n]*\r?\n # 1st line
909                 # continuation lines:
910                 (?:[^:\n]*?[ \t]+[^\n]*\r?\n)*
911                 /ismx;
912 }
913
914 # BODY[($SECTION_IDX.)?HEADER.FIELDS.NOT ($HDRS)]<$offset.$bytes>
915 sub partial_hdr_not {
916         my ($eml, $section_idx, $hdrs_re) = @_;
917         if (defined $section_idx) {
918                 $eml = eml_body_idx($eml, $section_idx) or return;
919         }
920         my $str = $eml->header_obj->as_string;
921         $str =~ s/$hdrs_re//g;
922         $str =~ s/(?<!\r)\n/\r\n/sg;
923         $str .= "\r\n";
924 }
925
926 # BODY[($SECTION_IDX.)?HEADER.FIELDS ($HDRS)]<$offset.$bytes>
927 sub partial_hdr_get {
928         my ($eml, $section_idx, $hdrs_re) = @_;
929         if (defined $section_idx) {
930                 $eml = eml_body_idx($eml, $section_idx) or return;
931         }
932         my $str = $eml->header_obj->as_string;
933         $str = join('', ($str =~ m/($hdrs_re)/g));
934         $str =~ s/(?<!\r)\n/\r\n/sg;
935         $str .= "\r\n";
936 }
937
938 sub partial_prepare ($$$$) {
939         my ($need, $partial, $want, $att) = @_;
940
941         # recombine [ "BODY[1.HEADER.FIELDS", "(foo", "bar)]" ]
942         # back to: "BODY[1.HEADER.FIELDS (foo bar)]"
943         return unless $att =~ /\ABODY\[/s;
944         until (rindex($att, ']') >= 0) {
945                 my $next = shift @$want or return;
946                 $att .= ' ' . uc($next);
947         }
948         if ($att =~ /\ABODY\[([0-9]+(?:\.[0-9]+)*)? # 1 - section_idx
949                         (?:\.(HEADER|MIME|TEXT))? # 2 - section_name
950                         \](?:<([0-9]+)(?:\.([0-9]+))?>)?\z/sx) { # 3, 4
951                 $partial->{$att} = [ \&partial_body, $1, $2, $3, $4 ];
952                 $$need |= CRLF_BREF|EML_HDR|EML_BDY;
953         } elsif ($att =~ /\ABODY\[(?:([0-9]+(?:\.[0-9]+)*)\.)? # 1 - section_idx
954                                 (?:HEADER\.FIELDS(\.NOT)?)\x20 # 2
955                                 \(([A-Z0-9\-\x20]+)\) # 3 - hdrs
956                         \](?:<([0-9]+)(?:\.([0-9]+))?>)?\z/sx) { # 4 5
957                 my $tmp = $partial->{$att} = [ $2 ? \&partial_hdr_not
958                                                 : \&partial_hdr_get,
959                                                 $1, undef, $4, $5 ];
960                 $tmp->[2] = hdrs_regexp($3);
961
962                 # don't emit CRLF_HDR instruction, here, partial_hdr_*
963                 # will do CRLF conversion with only the extracted result
964                 # and not waste time converting lines we don't care about.
965                 $$need |= EML_HDR;
966         } else {
967                 undef;
968         }
969 }
970
971 sub partial_emit ($$$) {
972         my ($self, $partial, $eml) = @_;
973         for (@$partial) {
974                 my ($k, $cb, @args) = @$_;
975                 my ($offset, $len) = splice(@args, -2);
976                 # $cb is partial_body|partial_hdr_get|partial_hdr_not
977                 my $str = $cb->($eml, @args) // '';
978                 if (defined $offset) {
979                         if (defined $len) {
980                                 $str = substr($str, $offset, $len);
981                                 $k =~ s/\.$len>\z/>/ or warn
982 "BUG: unable to remove `.$len>' from `$k'";
983                         } else {
984                                 $str = substr($str, $offset);
985                                 $len = length($str);
986                         }
987                 } else {
988                         $len = length($str);
989                 }
990                 $self->msg_more(" $k {$len}\r\n");
991                 $self->msg_more($str);
992         }
993 }
994
995 sub fetch_compile ($) {
996         my ($want) = @_;
997         if ($want->[0] =~ s/\A\(//s) {
998                 $want->[-1] =~ s/\)\z//s or return 'BAD no rparen';
999         }
1000         my (%partial, %seen, @op);
1001         my $need = 0;
1002         while (defined(my $att = shift @$want)) {
1003                 $att = uc($att);
1004                 next if $att eq 'UID'; # always returned
1005                 $att =~ s/\ABODY\.PEEK\[/BODY\[/; # we're read-only
1006                 my $x = $FETCH_ATT{$att};
1007                 if ($x) {
1008                         while (my ($k, $fl_cb) = each %$x) {
1009                                 next if $seen{$k}++;
1010                                 $need |= $fl_cb->[0];
1011                                 push @op, [ @$fl_cb, $k ];
1012                         }
1013                 } elsif (!partial_prepare(\$need, \%partial, $want, $att)) {
1014                         return "BAD param: $att";
1015                 }
1016         }
1017         my @r;
1018
1019         # stabilize partial order for consistency and ease-of-debugging:
1020         if (scalar keys %partial) {
1021                 $need |= NEED_BLOB;
1022                 $r[2] = [ map { [ $_, @{$partial{$_}} ] } sort keys %partial ];
1023         }
1024
1025         push @op, $OP_EML_NEW if ($need & (EML_HDR|EML_BDY));
1026
1027         # do we need CRLF conversion?
1028         if ($need & CRLF_BREF) {
1029                 push @op, $OP_CRLF_BREF;
1030         } elsif (my $crlf = ($need & (CRLF_HDR|CRLF_BDY))) {
1031                 if ($crlf == (CRLF_HDR|CRLF_BDY)) {
1032                         push @op, $OP_CRLF_BREF;
1033                 } elsif ($need & CRLF_HDR) {
1034                         push @op, $OP_CRLF_HDR;
1035                 } else {
1036                         push @op, $OP_CRLF_BDY;
1037                 }
1038         }
1039
1040         $r[0] = $need & NEED_BLOB ? \&fetch_blob :
1041                 ($need & NEED_SMSG ? \&fetch_smsg : \&fetch_uid);
1042
1043         # r[1] = [ $key1, $cb1, $key2, $cb2, ... ]
1044         use sort 'stable'; # makes output more consistent
1045         $r[1] = [ map { ($_->[2], $_->[1]) } sort { $a->[0] <=> $b->[0] } @op ];
1046         @r;
1047 }
1048
1049 sub cmd_uid_fetch ($$$$;@) {
1050         my ($self, $tag, $range_csv, @want) = @_;
1051         my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
1052         my ($cb, $ops, $partial) = fetch_compile(\@want);
1053         return "$tag $cb\r\n" unless $ops;
1054
1055         # cb is one of fetch_blob, fetch_smsg, fetch_uid
1056         $range_csv = 'bad' if $range_csv !~ $valid_range;
1057         my $range_info = range_step($self, \$range_csv);
1058         return "$tag $range_info\r\n" if !ref($range_info);
1059         uo2m_hibernate($self) if $cb == \&fetch_blob; # slow, save RAM
1060         long_response($self, $cb, $tag, [], $range_info, $ops, $partial);
1061 }
1062
1063 sub cmd_fetch ($$$$;@) {
1064         my ($self, $tag, $range_csv, @want) = @_;
1065         my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
1066         my ($cb, $ops, $partial) = fetch_compile(\@want);
1067         return "$tag $cb\r\n" unless $ops;
1068
1069         # cb is one of fetch_blob, fetch_smsg, fetch_uid
1070         $range_csv = 'bad' if $range_csv !~ $valid_range;
1071         msn_to_uid_range(msn2uid($self), $range_csv);
1072         my $range_info = range_step($self, \$range_csv);
1073         return "$tag $range_info\r\n" if !ref($range_info);
1074         uo2m_hibernate($self) if $cb == \&fetch_blob; # slow, save RAM
1075         long_response($self, $cb, $tag, [], $range_info, $ops, $partial);
1076 }
1077
1078 sub msn_convert ($$) {
1079         my ($self, $uids) = @_;
1080         my $adj = $self->{uid_base} + 1;
1081         my $uo2m = uo2m_extend($self, $uids->[-1]);
1082         $uo2m = [ unpack('S*', $uo2m) ] if !ref($uo2m);
1083         $_ = $uo2m->[$_ - $adj] for @$uids;
1084 }
1085
1086 sub search_uid_range { # long_response
1087         my ($self, $tag, $sql, $range_info, $want_msn) = @_;
1088         my $uids = [];
1089         if (defined(my $err = refill_uids($self, $uids, $range_info, $sql))) {
1090                 $err ||= 'OK Search done';
1091                 $self->write("\r\n$tag $err\r\n");
1092                 return;
1093         }
1094         msn_convert($self, $uids) if $want_msn;
1095         $self->msg_more(join(' ', '', @$uids));
1096         1; # more
1097 }
1098
1099 sub date_search {
1100         my ($q, $k, $d) = @_;
1101         my $sql = $q->{sql};
1102
1103         # Date: header
1104         if ($k eq 'SENTON') {
1105                 my $end = $d + 86399; # no leap day...
1106                 my $da = strftime('%Y%m%d%H%M%S', gmtime($d));
1107                 my $db = strftime('%Y%m%d%H%M%S', gmtime($end));
1108                 $q->{xap} .= " dt:$da..$db";
1109                 $$sql .= " AND ds >= $d AND ds <= $end" if defined($sql);
1110         } elsif ($k eq 'SENTBEFORE') {
1111                 $q->{xap} .= ' d:..'.strftime('%Y%m%d', gmtime($d));
1112                 $$sql .= " AND ds <= $d" if defined($sql);
1113         } elsif ($k eq 'SENTSINCE') {
1114                 $q->{xap} .= ' d:'.strftime('%Y%m%d', gmtime($d)).'..';
1115                 $$sql .= " AND ds >= $d" if defined($sql);
1116
1117         # INTERNALDATE (Received)
1118         } elsif ($k eq 'ON') {
1119                 my $end = $d + 86399; # no leap day...
1120                 $q->{xap} .= " ts:$d..$end";
1121                 $$sql .= " AND ts >= $d AND ts <= $end" if defined($sql);
1122         } elsif ($k eq 'BEFORE') {
1123                 $q->{xap} .= " ts:..$d";
1124                 $$sql .= " AND ts <= $d" if defined($sql);
1125         } elsif ($k eq 'SINCE') {
1126                 $q->{xap} .= " ts:$d..";
1127                 $$sql .= " AND ts >= $d" if defined($sql);
1128         } else {
1129                 die "BUG: $k not recognized";
1130         }
1131 }
1132
1133 # IMAP to Xapian search key mapping
1134 my %I2X = (
1135         SUBJECT => 's:',
1136         BODY => 'b:',
1137         FROM => 'f:',
1138         TEXT => '', # n.b. does not include all headers
1139         TO => 't:',
1140         CC => 'c:',
1141         # BCC => 'bcc:', # TODO
1142         # KEYWORD # TODO ? dfpre,dfpost,...
1143 );
1144
1145 # IMAP allows searching arbitrary headers via "HEADER $HDR_NAME $HDR_VAL"
1146 # which gets silly expensive.  We only allow the headers we already index.
1147 my %H2X = (%I2X, 'MESSAGE-ID' => 'm:', 'LIST-ID' => 'l:');
1148
1149 sub xap_append ($$$$) {
1150         my ($q, $rest, $k, $xk) = @_;
1151         delete $q->{sql}; # can't use over.sqlite3
1152         defined(my $arg = shift @$rest) or return "BAD $k no arg";
1153
1154         # AFAIK Xapian can't handle [*"] in probabilistic terms
1155         $arg =~ tr/*"//d;
1156         ${$q->{xap}} .= qq[ $xk"$arg"];
1157         undef;
1158 }
1159
1160 sub parse_query ($$) {
1161         my ($self, $query) = @_;
1162         my $q = PublicInbox::IMAPsearchqp::parse($self, $query);
1163         if (ref($q)) {
1164                 my $max = $self->{ibx}->over->max;
1165                 my $beg = 1;
1166                 uid_clamp($self, \$beg, \$max);
1167                 $q->{range_info} = [ $beg, $max ];
1168         }
1169         $q;
1170 }
1171
1172 sub refill_xap ($$$$) {
1173         my ($self, $uids, $range_info, $q) = @_;
1174         my ($beg, $end) = @$range_info;
1175         my $srch = $self->{ibx}->search;
1176         my $opt = { mset => 2, limit => 1000 };
1177         my $nshard = $srch->{nshard} // 1;
1178         my $mset = $srch->query("$q uid:$beg..$end", $opt);
1179         @$uids = map { mdocid($nshard, $_) } $mset->items;
1180         if (@$uids) {
1181                 $range_info->[0] = $uids->[-1] + 1; # update $beg
1182                 return; # possibly more
1183         }
1184         0; # all done
1185 }
1186
1187 sub search_xap_range { # long_response
1188         my ($self, $tag, $q, $range_info, $want_msn) = @_;
1189         my $uids = [];
1190         if (defined(my $err = refill_xap($self, $uids, $range_info, $q))) {
1191                 $err ||= 'OK Search done';
1192                 $self->write("\r\n$tag $err\r\n");
1193                 return;
1194         }
1195         msn_convert($self, $uids) if $want_msn;
1196         $self->msg_more(join(' ', '', @$uids));
1197         1; # more
1198 }
1199
1200 sub search_common {
1201         my ($self, $tag, $query, $want_msn) = @_;
1202         my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
1203         my $q = parse_query($self, $query);
1204         return "$tag $q\r\n" if !ref($q);
1205         my ($sql, $range_info) = delete @$q{qw(sql range_info)};
1206         if (!scalar(keys %$q)) { # overview.sqlite3
1207                 $self->msg_more('* SEARCH');
1208                 long_response($self, \&search_uid_range,
1209                                 $tag, $sql, $range_info, $want_msn);
1210         } elsif ($q = $q->{xap}) {
1211                 $self->{ibx}->search or
1212                         return "$tag BAD search not available for mailbox\r\n";
1213                 $self->msg_more('* SEARCH');
1214                 long_response($self, \&search_xap_range,
1215                                 $tag, $q, $range_info, $want_msn);
1216         } else {
1217                 "$tag BAD Error\r\n";
1218         }
1219 }
1220
1221 sub cmd_uid_search ($$$) {
1222         my ($self, $tag, $query) = @_;
1223         search_common($self, $tag, $query);
1224 }
1225
1226 sub cmd_search ($$$;) {
1227         my ($self, $tag, $query) = @_;
1228         search_common($self, $tag, $query, 1);
1229 }
1230
1231 sub args_ok ($$) { # duplicated from PublicInbox::NNTP
1232         my ($cb, $argc) = @_;
1233         my $tot = prototype $cb;
1234         my ($nreq, undef) = split(';', $tot);
1235         $nreq = ($nreq =~ tr/$//) - 1;
1236         $tot = ($tot =~ tr/$//) - 1;
1237         ($argc <= $tot && $argc >= $nreq);
1238 }
1239
1240 # returns 1 if we can continue, 0 if not due to buffered writes or disconnect
1241 sub process_line ($$) {
1242         my ($self, $l) = @_;
1243
1244         # TODO: IMAP allows literals for big requests to upload messages
1245         # (which we don't support) but maybe some big search queries use it.
1246         # RFC 3501 9 (2) doesn't permit TAB or multiple SP
1247         my ($tag, $req, @args) = parse_line('[ \t]+', 0, $l);
1248         pop(@args) if (@args && !defined($args[-1]));
1249         if (@args && uc($req) eq 'UID') {
1250                 $req .= "_".(shift @args);
1251         }
1252         my $res = eval {
1253                 if (defined(my $idle_tag = $self->{-idle_tag})) {
1254                         (uc($tag // '') eq 'DONE' && !defined($req)) ?
1255                                 idle_done($self, $tag) :
1256                                 "$idle_tag BAD expected DONE\r\n";
1257                 } elsif (my $cmd = $self->can('cmd_'.lc($req // ''))) {
1258                         if ($cmd == \&cmd_uid_search || $cmd == \&cmd_search) {
1259                                 # preserve user-supplied quotes for search
1260                                 (undef, @args) = split(/ search /i, $l, 2);
1261                         }
1262                         $cmd->($self, $tag, @args);
1263                 } else { # this is weird
1264                         auth_challenge_ok($self) //
1265                                         ($tag // '*') .
1266                                         ' BAD Error in IMAP command '.
1267                                         ($req // '(???)').
1268                                         ": Unknown command\r\n";
1269                 }
1270         };
1271         my $err = $@;
1272         if ($err && $self->{sock}) {
1273                 $l =~ s/\r?\n//s;
1274                 err($self, 'error from: %s (%s)', $l, $err);
1275                 $tag //= '*';
1276                 $res = "$tag BAD program fault - command not performed\r\n";
1277         }
1278         return 0 unless defined $res;
1279         $self->write($res);
1280 }
1281
1282 sub long_step {
1283         my ($self) = @_;
1284         # wbuf is unset or empty, here; {long} may add to it
1285         my ($fd, $cb, $t0, @args) = @{$self->{long_cb}};
1286         my $more = eval { $cb->($self, @args) };
1287         if ($@ || !$self->{sock}) { # something bad happened...
1288                 delete $self->{long_cb};
1289                 my $elapsed = now() - $t0;
1290                 if ($@) {
1291                         err($self,
1292                             "%s during long response[$fd] - %0.6f",
1293                             $@, $elapsed);
1294                 }
1295                 out($self, " deferred[$fd] aborted - %0.6f", $elapsed);
1296                 $self->close;
1297         } elsif ($more) { # $self->{wbuf}:
1298                 $self->update_idle_time;
1299
1300                 # control passed to git_async_cat if $more == \undef
1301                 requeue_once($self) if !ref($more);
1302         } else { # all done!
1303                 delete $self->{long_cb};
1304                 my $elapsed = now() - $t0;
1305                 my $fd = fileno($self->{sock});
1306                 out($self, " deferred[$fd] done - %0.6f", $elapsed);
1307                 my $wbuf = $self->{wbuf}; # do NOT autovivify
1308
1309                 $self->requeue unless $wbuf && @$wbuf;
1310         }
1311 }
1312
1313 sub err ($$;@) {
1314         my ($self, $fmt, @args) = @_;
1315         printf { $self->{imapd}->{err} } $fmt."\n", @args;
1316 }
1317
1318 sub out ($$;@) {
1319         my ($self, $fmt, @args) = @_;
1320         printf { $self->{imapd}->{out} } $fmt."\n", @args;
1321 }
1322
1323 sub long_response ($$;@) {
1324         my ($self, $cb, @args) = @_; # cb returns true if more, false if done
1325
1326         my $sock = $self->{sock} or return;
1327         # make sure we disable reading during a long response,
1328         # clients should not be sending us stuff and making us do more
1329         # work while we are stream a response to them
1330         $self->{long_cb} = [ fileno($sock), $cb, now(), @args ];
1331         long_step($self); # kick off!
1332         undef;
1333 }
1334
1335 # callback used by PublicInbox::DS for any (e)poll (in/out/hup/err)
1336 sub event_step {
1337         my ($self) = @_;
1338
1339         return unless $self->flush_write && $self->{sock} && !$self->{long_cb};
1340
1341         $self->update_idle_time;
1342         # only read more requests if we've drained the write buffer,
1343         # otherwise we can be buffering infinitely w/o backpressure
1344
1345         my $rbuf = $self->{rbuf} // \(my $x = '');
1346         my $line = index($$rbuf, "\n");
1347         while ($line < 0) {
1348                 if (length($$rbuf) >= LINE_MAX) {
1349                         $self->write(\"\* BAD request too long\r\n");
1350                         return $self->close;
1351                 }
1352                 $self->do_read($rbuf, LINE_MAX, length($$rbuf)) or
1353                                 return uo2m_hibernate($self);
1354                 $line = index($$rbuf, "\n");
1355         }
1356         $line = substr($$rbuf, 0, $line + 1, '');
1357         $line =~ s/\r?\n\z//s;
1358         return $self->close if $line =~ /[[:cntrl:]]/s;
1359         my $t0 = now();
1360         my $fd = fileno($self->{sock});
1361         my $r = eval { process_line($self, $line) };
1362         my $pending = $self->{wbuf} ? ' pending' : '';
1363         out($self, "[$fd] %s - %0.6f$pending - $r", $line, now() - $t0);
1364
1365         return $self->close if $r < 0;
1366         $self->rbuf_idle($rbuf);
1367         $self->update_idle_time;
1368
1369         # maybe there's more pipelined data, or we'll have
1370         # to register it for socket-readiness notifications
1371         $self->requeue unless $pending;
1372 }
1373
1374 sub compressed { undef }
1375
1376 sub zflush {} # overridden by IMAPdeflate
1377
1378 # RFC 4978
1379 sub cmd_compress ($$$) {
1380         my ($self, $tag, $alg) = @_;
1381         return "$tag BAD DEFLATE only\r\n" if uc($alg) ne "DEFLATE";
1382         return "$tag BAD COMPRESS active\r\n" if $self->compressed;
1383
1384         # CRIME made TLS compression obsolete
1385         # return "$tag NO [COMPRESSIONACTIVE]\r\n" if $self->tls_compressed;
1386
1387         PublicInbox::IMAPdeflate->enable($self, $tag);
1388         $self->requeue;
1389         undef
1390 }
1391
1392 sub cmd_starttls ($$) {
1393         my ($self, $tag) = @_;
1394         my $sock = $self->{sock} or return;
1395         if ($sock->can('stop_SSL') || $self->compressed) {
1396                 return "$tag BAD TLS or compression already enabled\r\n";
1397         }
1398         my $opt = $self->{imapd}->{accept_tls} or
1399                 return "$tag BAD can not initiate TLS negotiation\r\n";
1400         $self->write(\"$tag OK begin TLS negotiation now\r\n");
1401         $self->{sock} = IO::Socket::SSL->start_SSL($sock, %$opt);
1402         $self->requeue if PublicInbox::DS::accept_tls_step($self);
1403         undef;
1404 }
1405
1406 # for graceful shutdown in PublicInbox::Daemon:
1407 sub busy {
1408         my ($self, $now) = @_;
1409         if (defined($self->{-idle_tag})) {
1410                 $self->write(\"* BYE server shutting down\r\n");
1411                 return; # not busy anymore
1412         }
1413         ($self->{rbuf} || $self->{wbuf} || $self->not_idle_long($now));
1414 }
1415
1416 sub close {
1417         my ($self) = @_;
1418         if (my $ibx = delete $self->{ibx}) {
1419                 stop_idle($self, $ibx);
1420         }
1421         $self->SUPER::close; # PublicInbox::DS::close
1422 }
1423
1424 # we're read-only, so SELECT and EXAMINE do the same thing
1425 no warnings 'once';
1426 *cmd_select = \&cmd_examine;
1427
1428 package PublicInbox::IMAP_preauth;
1429 our @ISA = qw(PublicInbox::IMAP);
1430
1431 sub logged_in { 0 }
1432
1433 1;