1 # Copyright (C) 2020 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
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
10 # * NNTP article numbers are UIDs and message sequence numbers (MSNs)
11 # * Message sequence numbers (MSNs) can be stable since we're read-only.
12 # Most IMAP clients use UIDs (I hope), and we can return a dummy
13 # message if a client requests a non-existent MSN.
15 package PublicInbox::IMAP;
17 use base qw(PublicInbox::DS);
18 use fields qw(imapd logged_in ibx long_cb -login_tag
21 use PublicInbox::EmlContentFoo qw(parse_content_disposition);
22 use PublicInbox::DS qw(now);
23 use PublicInbox::Syscall qw(EPOLLIN EPOLLONESHOT);
24 use Text::ParseWords qw(parse_line);
27 for my $mod (qw(Email::Address::XS Mail::Address)) {
28 eval "require $mod" or next;
29 $Address = $mod and last;
31 die "neither Email::Address::XS nor Mail::Address loaded: $@" if !$Address;
33 sub LINE_MAX () { 512 } # does RFC 3501 have a limit like RFC 977?
35 my %FETCH_NEED_BLOB = ( # for future optimization
40 'RFC822.SIZE' => 1, # needs CRLF conversion :<
50 my %FETCH_ATT = map { $_ => [ $_ ] } keys %FETCH_NEED_BLOB;
52 # aliases (RFC 3501 section 6.4.5)
53 $FETCH_ATT{FAST} = [ qw(FLAGS INTERNALDATE RFC822.SIZE) ];
54 $FETCH_ATT{ALL} = [ @{$FETCH_ATT{FAST}}, 'ENVELOPE' ];
55 $FETCH_ATT{FULL} = [ @{$FETCH_ATT{ALL}}, 'BODY' ];
57 for my $att (keys %FETCH_ATT) {
58 my %h = map { $_ => 1 } @{$FETCH_ATT{$att}};
59 $FETCH_ATT{$att} = \%h;
64 my $capa = capa($self);
65 $self->write(\"* OK [$capa] public-inbox-imapd ready\r\n");
69 my ($class, $sock, $imapd) = @_;
70 my $self = fields::new($class);
73 if ($sock->can('accept_SSL') && !$sock->accept_SSL) {
74 return CORE::close($sock) if $! != EAGAIN;
75 $ev = PublicInbox::TLS::epollbit();
76 $wbuf = [ \&PublicInbox::DS::accept_tls_step, \&greet ];
78 $self->SUPER::new($sock, $ev | EPOLLONESHOT);
79 $self->{imapd} = $imapd;
81 $self->{wbuf} = $wbuf;
85 $self->update_idle_time;
92 # dovecot advertises IDLE pre-login; perhaps because some clients
93 # depend on it, so we'll do the same
94 my $capa = 'CAPABILITY IMAP4rev1 IDLE';
95 if ($self->{logged_in}) {
96 $capa .= ' COMPRESS=DEFLATE';
98 if (!($self->{sock} // $self)->can('accept_SSL') &&
99 $self->{imapd}->{accept_tls}) {
100 $capa .= ' STARTTLS';
102 $capa .= ' AUTH=ANONYMOUS';
106 sub login_success ($$) {
107 my ($self, $tag) = @_;
108 $self->{logged_in} = 1;
109 my $capa = capa($self);
110 "$tag OK [$capa] Logged in\r\n";
113 sub auth_challenge_ok ($) {
115 my $tag = delete($self->{-login_tag}) or return;
116 login_success($self, $tag);
119 sub cmd_login ($$$$) {
120 my ($self, $tag) = @_; # ignore ($user, $password) = ($_[2], $_[3])
121 login_success($self, $tag);
124 sub cmd_logout ($$) {
125 my ($self, $tag) = @_;
126 delete $self->{logged_in};
127 $self->write(\"* BYE logging out\r\n$tag OK Logout done\r\n");
128 $self->shutdn; # PublicInbox::DS::shutdn
132 sub cmd_authenticate ($$$) {
133 my ($self, $tag) = @_; # $method = $_[2], should be "ANONYMOUS"
134 $self->{-login_tag} = $tag;
138 sub cmd_capability ($$) {
139 my ($self, $tag) = @_;
140 '* '.capa($self)."\r\n$tag OK Capability done\r\n";
143 sub cmd_noop ($$) { "$_[1] OK Noop done\r\n" }
145 # called by PublicInbox::InboxIdle
146 sub on_inbox_unlock {
147 my ($self, $ibx) = @_;
148 my $new = $ibx->mm->max;
149 defined(my $old = $self->{-idle_max}) or die 'BUG: -idle_max unset';
151 $self->{-idle_max} = $new;
152 $self->msg_more("* $_ EXISTS\r\n") for (($old + 1)..($new - 1));
153 $self->write(\"* $new EXISTS\r\n");
158 my ($self, $tag) = @_;
159 # IDLE seems allowed by dovecot w/o a mailbox selected *shrug*
160 my $ibx = $self->{ibx} or return "$tag BAD no mailbox selected\r\n";
161 $ibx->subscribe_unlock(fileno($self->{sock}), $self);
162 $self->{imapd}->idler_start;
163 $self->{-idle_tag} = $tag;
164 $self->{-idle_max} = $ibx->mm->max // 0;
169 my ($self, $tag) = @_; # $tag is "DONE" (case-insensitive)
170 defined(my $idle_tag = delete $self->{-idle_tag}) or
171 return "$tag BAD not idle\r\n";
172 my $ibx = $self->{ibx} or do {
173 warn "BUG: idle_tag set w/o inbox";
174 return "$tag BAD internal bug\r\n";
176 $ibx->unsubscribe_unlock(fileno($self->{sock}));
177 "$idle_tag OK Idle done\r\n";
180 sub cmd_examine ($$$) {
181 my ($self, $tag, $mailbox) = @_;
182 my $ibx = $self->{imapd}->{groups}->{$mailbox} or
183 return "$tag NO Mailbox doesn't exist: $mailbox\r\n";
185 my $max = $mm->max // 0;
186 # RFC 3501 2.3.1.1 - "A good UIDVALIDITY value to use in
187 # this case is a 32-bit representation of the creation
188 # date/time of the mailbox"
189 my $uidvalidity = $mm->created_at or return "$tag BAD UIDVALIDITY\r\n";
190 my $uidnext = $max + 1;
192 # XXX: do we need this? RFC 5162/7162
193 my $ret = $self->{ibx} ? "* OK [CLOSED] previous closed\r\n" : '';
199 * OK [PERMANENTFLAGS ()] Read-only mailbox\r
201 $ret .= "* OK [UNSEEN $max]\r\n" if $max;
202 $ret .= "* OK [UIDNEXT $uidnext]\r\n" if defined $uidnext;
203 $ret .= "* OK [UIDVALIDITY $uidvalidity]\r\n" if defined $uidvalidity;
204 $ret .= "$tag OK [READ-ONLY] EXAMINE/SELECT done\r\n";
211 } elsif ($v =~ /[{"\r\n%*\\\[]/) { # literal string
212 '{' . length($v) . "}\r\n" . $v;
213 } else { # quoted string
218 sub addr_envelope ($$;$) {
219 my ($eml, $x, $y) = @_;
220 my $v = $eml->header_raw($x) //
221 ($y ? $eml->header_raw($y) : undef) // return 'NIL';
223 my @x = $Address->parse($v) or return 'NIL';
225 map { '(' . join(' ',
226 _esc($_->name), 'NIL',
227 _esc($_->user), _esc($_->host)
233 sub eml_envelope ($) {
236 _esc($eml->header_raw('Date')),
237 _esc($eml->header_raw('Subject')),
238 addr_envelope($eml, 'From'),
239 addr_envelope($eml, 'Sender', 'From'),
240 addr_envelope($eml, 'Reply-To', 'From'),
241 addr_envelope($eml, 'To'),
242 addr_envelope($eml, 'Cc'),
243 addr_envelope($eml, 'Bcc'),
244 _esc($eml->header_raw('In-Reply-To')),
245 _esc($eml->header_raw('Message-ID')),
251 if ($hash && scalar keys %$hash) {
252 $hash = [ %$hash ]; # flatten hash into 1-dimensional array
253 '(' . join(' ', map { _esc($_) } @$hash) . ')';
259 sub body_disposition ($) {
261 my $cd = $eml->header_raw('Content-Disposition') or return 'NIL';
262 $cd = parse_content_disposition($cd);
263 my $buf = '('._esc($cd->{type});
264 $buf .= ' ' . _esc_hash(delete $cd->{attributes});
268 sub body_leaf ($$;$) {
269 my ($eml, $structure, $hold) = @_;
271 $eml->{is_submsg} and # parent was a message/(rfc822|news|global)
272 $buf .= eml_envelope($eml). ' ';
274 $buf .= '('._esc($ct->{type}).' ';
275 $buf .= _esc($ct->{subtype});
276 $buf .= ' ' . _esc_hash(delete $ct->{attributes});
277 $buf .= ' ' . _esc($eml->header_raw('Content-ID'));
278 $buf .= ' ' . _esc($eml->header_raw('Content-Description'));
279 my $cte = $eml->header_raw('Content-Transfer-Encoding') // '7bit';
280 $buf .= ' ' . _esc($cte);
281 $buf .= ' ' . $eml->{imap_body_len};
282 $buf .= ' '.($eml->body_raw =~ tr/\n/\n/) if lc($ct->{type}) eq 'text';
284 # for message/(rfc822|global|news), $hold[0] should have envelope
285 $buf .= ' ' . (@$hold ? join('', @$hold) : 'NIL') if $hold;
288 $buf .= ' '._esc($eml->header_raw('Content-MD5'));
289 $buf .= ' '. body_disposition($eml);
290 $buf .= ' '._esc($eml->header_raw('Content-Language'));
291 $buf .= ' '._esc($eml->header_raw('Content-Location'));
296 sub body_parent ($$$) {
297 my ($eml, $structure, $hold) = @_;
299 my $type = lc($ct->{type});
300 if ($type eq 'multipart') {
302 $buf .= @$hold ? join('', @$hold) : 'NIL';
303 $buf .= ' '._esc($ct->{subtype});
305 $buf .= ' '._esc_hash(delete $ct->{attributes});
306 $buf .= ' '.body_disposition($eml);
307 $buf .= ' '._esc($eml->header_raw('Content-Language'));
308 $buf .= ' '._esc($eml->header_raw('Content-Location'));
312 } else { # message/(rfc822|global|news)
313 @$hold = (body_leaf($eml, $structure, $hold));
317 # this is gross, but we need to process the parent part AFTER
318 # the child parts are done
319 sub bodystructure_prep {
321 my ($eml, $depth) = @$p; # ignore idx
322 # set length here, as $eml->{bdy} gets deleted for message/rfc822
323 $eml->{imap_body_len} = length($eml->body_raw);
324 push @$q, $eml, $depth;
327 # for FETCH BODY and FETCH BODYSTRUCTURE
328 sub fetch_body ($;$) {
329 my ($eml, $structure) = @_;
331 $eml->each_part(\&bodystructure_prep, \@q, 0, 1);
335 my ($part, $depth) = splice(@q, -2);
336 my $is_mp_parent = $depth == ($cur_depth - 1);
340 body_parent($part, $structure, \@hold);
342 unshift @hold, body_leaf($part, $structure);
348 sub dummy_message ($$) {
349 my ($seqno, $ibx) = @_;
351 From: nobody\@localhost\r
352 To: nobody\@localhost\r
353 Date: Thu, 01 Jan 1970 00:00:00 +0000\r
354 Message-ID: <dummy-$seqno\@$ibx->{newsgroup}>\r
355 Subject: dummy message #$seqno\r
357 You're seeing this message because your IMAP client didn't use UIDs.\r
358 The message which used to use this sequence number was likely spam\r
359 and removed by the administrator.\r
364 sub uid_fetch_cb { # called by git->cat_async
365 my ($bref, $oid, $type, $size, $fetch_m_arg) = @_;
366 my ($self, undef, $ibx, undef, undef, $msgs, $want) = @$fetch_m_arg;
367 my $smsg = shift @$msgs or die 'BUG: no smsg';
368 if (!defined($oid)) {
369 # it's possible to have TOCTOU if an admin runs
370 # public-inbox-(edit|purge), just move onto the next message
371 return unless defined $want->{-seqno};
372 $bref = dummy_message($smsg->{num}, $ibx);
374 $smsg->{blob} eq $oid or die "BUG: $smsg->{blob} != $oid";
376 $$bref =~ s/(?<!\r)\n/\r\n/sg; # make strict clients happy
378 # fixup old bug from import (pre-a0c07cba0e5d8b6a)
379 $$bref =~ s/\A[\r\n]*From [^\r\n]*\r\n//s;
381 $self->msg_more("* $smsg->{num} FETCH (UID $smsg->{num}");
383 $want->{'RFC822.SIZE'} and
384 $self->msg_more(' RFC822.SIZE '.length($$bref));
385 $want->{INTERNALDATE} and
386 $self->msg_more(' INTERNALDATE "'.$smsg->internaldate.'"');
387 $want->{FLAGS} and $self->msg_more(' FLAGS ()');
388 for ('RFC822', 'BODY[]') {
390 $self->msg_more(" $_ {".length($$bref)."}\r\n");
391 $self->msg_more($$bref);
394 my $eml = PublicInbox::Eml->new($bref);
396 $want->{ENVELOPE} and
397 $self->msg_more(' ENVELOPE '.eml_envelope($eml));
399 for ('RFC822.HEADER', 'BODY[HEADER]') {
401 $self->msg_more(" $_ {".length(${$eml->{hdr}})."}\r\n");
402 $self->msg_more(${$eml->{hdr}});
404 for ('RFC822.TEXT', 'BODY[TEXT]') {
406 $self->msg_more(" $_ {".length($$bref)."}\r\n");
407 $self->msg_more($$bref);
409 $want->{BODYSTRUCTURE} and
410 $self->msg_more(' BODYSTRUCTURE '.fetch_body($eml, 1));
412 $self->msg_more(' BODY '.fetch_body($eml));
413 if (my $partial = $want->{-partial}) {
414 partial_emit($self, $partial, $eml);
416 $self->msg_more(")\r\n");
419 sub uid_fetch_m { # long_response
420 my ($self, $tag, $ibx, $beg, $end, $msgs, $want) = @_;
421 if (!@$msgs) { # refill
422 @$msgs = @{$ibx->over->query_xover($$beg, $end)};
424 $self->write(\"$tag OK Fetch done\r\n");
427 $$beg = $msgs->[-1]->{num} + 1;
430 $git->cat_async_begin; # TODO: actually make async
431 $git->cat_async($msgs->[0]->{blob}, \&uid_fetch_cb, \@_);
432 $git->cat_async_wait;
436 sub cmd_status ($$$;@) {
437 my ($self, $tag, $mailbox, @items) = @_;
438 my $ibx = $self->{imapd}->{groups}->{$mailbox} or
439 return "$tag NO Mailbox doesn't exist: $mailbox\r\n";
440 return "$tag BAD no items\r\n" if !scalar(@items);
441 ($items[0] !~ s/\A\(//s || $items[-1] !~ s/\)\z//s) and
442 return "$tag BAD invalid args\r\n";
446 for my $it (@items) {
449 if ($it =~ /\A(?:MESSAGES|UNSEEN|RECENT)\z/) {
450 push(@it, ($max //= $mm->max // 0));
451 } elsif ($it eq 'UIDNEXT') {
452 push(@it, ($max //= $mm->max // 0) + 1);
453 } elsif ($it eq 'UIDVALIDITY') {
454 push(@it, $mm->created_at //
455 return("$tag BAD UIDVALIDITY\r\n"));
457 return "$tag BAD invalid item\r\n";
460 return "$tag BAD no items\r\n" if !@it;
461 "* STATUS $mailbox (".join(' ', @it).")\r\n" .
462 "$tag OK Status done\r\n";
465 my %patmap = ('*' => '.*', '%' => '[^\.]*');
466 sub cmd_list ($$$$) {
467 my ($self, $tag, $refname, $wildcard) = @_;
468 my $l = $self->{imapd}->{inboxlist};
469 if ($refname eq '' && $wildcard eq '') {
470 # request for hierarchy delimiter
471 $l = [ qq[* LIST (\\Noselect) "." ""\r\n] ];
472 } elsif ($refname ne '' || $wildcard ne '*') {
473 $wildcard =~ s!([^a-z0-9_])!$patmap{$1} // "\Q$1"!eig;
474 $l = [ grep(/ \Q$refname\E$wildcard\r\n\z/s, @$l) ];
476 \(join('', @$l, "$tag OK List done\r\n"));
479 sub eml_index_offs_i { # PublicInbox::Eml::each_part callback
481 my ($eml, undef, $idx) = @$p;
482 if ($idx && lc($eml->ct->{type}) eq 'multipart') {
483 $eml->{imap_bdy} = $eml->{bdy} // \'';
485 $all->{$idx} = $eml; # $idx => Eml
488 # prepares an index for BODY[$SECTION_IDX] fetches
489 sub eml_body_idx ($$) {
490 my ($eml, $section_idx) = @_;
491 my $idx = $eml->{imap_all_parts} //= do {
493 $eml->each_part(\&eml_index_offs_i, $all, 0, 1);
494 # top-level of multipart, BODY[0] not allowed (nz-number)
498 $idx->{$section_idx};
501 # BODY[($SECTION_IDX)?(.$SECTION_NAME)?]<$offset.$bytes>
503 my ($eml, $section_idx, $section_name) = @_;
504 if (defined $section_idx) {
505 $eml = eml_body_idx($eml, $section_idx) or return;
507 if (defined $section_name) {
508 if ($section_name eq 'MIME') {
509 # RFC 3501 6.4.5 states:
510 # The MIME part specifier MUST be prefixed
511 # by one or more numeric part specifiers
512 return unless defined $section_idx;
513 return $eml->header_obj->as_string . "\r\n";
515 my $bdy = $eml->{bdy} // $eml->{imap_bdy} // \'';
516 $eml = PublicInbox::Eml->new($$bdy);
517 if ($section_name eq 'TEXT') {
518 return $eml->body_raw;
519 } elsif ($section_name eq 'HEADER') {
520 return $eml->header_obj->as_string . "\r\n";
522 die "BUG: bad section_name=$section_name";
525 ${$eml->{bdy} // $eml->{imap_bdy} // \''};
528 # similar to what's in PublicInbox::Eml::re_memo, but doesn't memoize
529 # to avoid OOM with malicious users
530 sub hdrs_regexp ($) {
532 my $names = join('|', map { "\Q$_" } split(/[ \t]+/, $hdrs));
533 qr/^(?:$names):[ \t]*[^\n]*\r?\n # 1st line
534 # continuation lines:
535 (?:[^:\n]*?[ \t]+[^\n]*\r?\n)*
539 # BODY[($SECTION_IDX.)?HEADER.FIELDS.NOT ($HDRS)]<$offset.$bytes>
540 sub partial_hdr_not {
541 my ($eml, $section_idx, $hdrs) = @_;
542 if (defined $section_idx) {
543 $eml = eml_body_idx($eml, $section_idx) or return;
545 my $str = $eml->header_obj->as_string;
546 my $re = hdrs_regexp($hdrs);
551 # BODY[($SECTION_IDX.)?HEADER.FIELDS ($HDRS)]<$offset.$bytes>
552 sub partial_hdr_get {
553 my ($eml, $section_idx, $hdrs) = @_;
554 if (defined $section_idx) {
555 $eml = eml_body_idx($eml, $section_idx) or return;
557 my $str = $eml->header_obj->as_string;
558 my $re = hdrs_regexp($hdrs);
559 join('', ($str =~ m/($re)/g), "\r\n");
562 sub partial_prepare ($$$) {
563 my ($partial, $want, $att) = @_;
565 # recombine [ "BODY[1.HEADER.FIELDS", "(foo", "bar)]" ]
566 # back to: "BODY[1.HEADER.FIELDS (foo bar)]"
567 return unless $att =~ /\ABODY\[/s;
568 until (rindex($att, ']') >= 0) {
569 my $next = shift @$want or return;
570 $att .= ' ' . uc($next);
572 if ($att =~ /\ABODY\[([0-9]+(?:\.[0-9]+)*)? # 1 - section_idx
573 (?:\.(HEADER|MIME|TEXT))? # 2 - section_name
574 \](?:<([0-9]+)(?:\.([0-9]+))?>)?\z/sx) { # 3, 4
575 $partial->{$att} = [ \&partial_body, $1, $2, $3, $4 ];
576 } elsif ($att =~ /\ABODY\[(?:([0-9]+(?:\.[0-9]+)*)\.)? # 1 - section_idx
577 (?:HEADER\.FIELDS(\.NOT)?)\x20 # 2
578 \(([A-Z0-9\-\x20]+)\) # 3 - hdrs
579 \](?:<([0-9]+)(?:\.([0-9]+))?>)?\z/sx) { # 4 5
580 $partial->{$att} = [ $2 ? \&partial_hdr_not
588 sub partial_emit ($$$) {
589 my ($self, $partial, $eml) = @_;
591 my ($k, $cb, @args) = @$_;
592 my ($offset, $len) = splice(@args, -2);
593 # $cb is partial_body|partial_hdr_get|partial_hdr_not
594 my $str = $cb->($eml, @args) // '';
595 if (defined $offset) {
597 $str = substr($str, $offset, $len);
598 $k =~ s/\.$len>\z/>/ or warn
599 "BUG: unable to remove `.$len>' from `$k'";
601 $str = substr($str, $offset);
607 $self->msg_more(" $k {$len}\r\n");
608 $self->msg_more($str);
612 sub fetch_common ($$$$) {
613 my ($self, $tag, $range, $want) = @_;
614 my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
615 if ($want->[0] =~ s/\A\(//s) {
616 $want->[-1] =~ s/\)\z//s or return "$tag BAD no rparen\r\n";
618 my (%partial, %want);
619 while (defined(my $att = shift @$want)) {
621 $att =~ s/\ABODY\.PEEK\[/BODY\[/; # we're read-only
622 my $x = $FETCH_ATT{$att};
624 %want = (%want, %$x);
625 } elsif (!partial_prepare(\%partial, $want, $att)) {
626 return "$tag BAD param: $att\r\n";
630 # stabilize partial order for consistency and ease-of-debugging:
631 if (scalar keys %partial) {
632 $want{-partial} = [ map {;
633 [ $_, @{$partial{$_}} ]
634 } sort keys %partial ];
639 if ($range =~ /\A([0-9]+):([0-9]+)\z/s) {
640 ($beg, $end) = ($1, $2);
641 } elsif ($range =~ /\A([0-9]+):\*\z/s) {
642 ($beg, $end) = ($1, $ibx->mm->max // 0);
643 } elsif ($range =~ /\A[0-9]+\z/) {
644 my $smsg = $ibx->over->get_art($range) or
645 return "$tag OK Fetch done\r\n"; # really OK(!)
647 ($beg, $end) = ($range, 0);
649 return "$tag BAD fetch range\r\n";
651 [ $tag, $ibx, \$beg, $end, $msgs, \%want ];
654 sub cmd_uid_fetch ($$$;@) {
655 my ($self, $tag, $range, @want) = @_;
656 my $args = fetch_common($self, $tag, $range, \@want);
657 ref($args) eq 'ARRAY' ?
658 long_response($self, \&uid_fetch_m, @$args) :
662 sub seq_fetch_m { # long_response
663 my ($self, $tag, $ibx, $beg, $end, $msgs, $want) = @_;
664 if (!@$msgs) { # refill
665 @$msgs = @{$ibx->over->query_xover($$beg, $end)};
667 $self->write(\"$tag OK Fetch done\r\n");
670 $$beg = $msgs->[-1]->{num} + 1;
672 my $seq = $want->{-seqno}++;
673 my $cur_num = $msgs->[0]->{num};
674 if ($cur_num == $seq) { # as expected
676 $git->cat_async_begin; # TODO: actually make async
677 $git->cat_async($msgs->[0]->{blob}, \&uid_fetch_cb, \@_);
678 $git->cat_async_wait;
679 } elsif ($cur_num > $seq) {
680 # send dummy messages until $seq catches up to $cur_num
681 my $smsg = bless { num => $seq, ts => 0 }, 'PublicInbox::Smsg';
682 unshift @$msgs, $smsg;
683 my $bref = dummy_message($seq, $ibx);
684 uid_fetch_cb($bref, undef, undef, undef, \@_);
685 } else { # should not happen
686 die "BUG: cur_num=$cur_num < seq=$seq";
688 1; # more messages on the way
691 sub cmd_fetch ($$$;@) {
692 my ($self, $tag, $range, @want) = @_;
693 my $args = fetch_common($self, $tag, $range, \@want);
694 ref($args) eq 'ARRAY' ? do {
695 my $want = $args->[-1];
696 $want->{-seqno} = ${$args->[2]}; # $$beg
697 long_response($self, \&seq_fetch_m, @$args)
701 sub uid_search_all { # long_response
702 my ($self, $tag, $ibx, $num) = @_;
703 my $uids = $ibx->mm->ids_after($num);
704 if (scalar(@$uids)) {
705 $self->msg_more(join(' ', '', @$uids));
707 $self->write(\"\r\n$tag OK Search done\r\n");
712 sub uid_search_uid_range { # long_response
713 my ($self, $tag, $ibx, $beg, $end) = @_;
714 my $uids = $ibx->mm->msg_range($beg, $end, 'num');
716 $self->msg_more(join('', map { " $_->[0]" } @$uids));
718 $self->write(\"\r\n$tag OK Search done\r\n");
723 sub cmd_uid_search ($$$;) {
724 my ($self, $tag, $arg, @rest) = @_;
725 my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
727 if ($arg eq 'ALL' && !@rest) {
728 $self->msg_more('* SEARCH');
730 long_response($self, \&uid_search_all, $tag, $ibx, \$num);
731 } elsif ($arg eq 'UID' && scalar(@rest) == 1) {
732 if ($rest[0] =~ /\A([0-9]+):([0-9]+|\*)\z/s) {
733 my ($beg, $end) = ($1, $2);
734 $end = $ibx->mm->max if $end eq '*';
735 $self->msg_more('* SEARCH');
736 long_response($self, \&uid_search_uid_range,
737 $tag, $ibx, \$beg, $end);
738 } elsif ($rest[0] =~ /\A[0-9]+\z/s) {
740 $uid = $ibx->over->get_art($uid) ? " $uid" : '';
741 "* SEARCH$uid\r\n$tag OK Search done\r\n";
743 "$tag BAD Error\r\n";
746 "$tag BAD Error\r\n";
750 sub args_ok ($$) { # duplicated from PublicInbox::NNTP
751 my ($cb, $argc) = @_;
752 my $tot = prototype $cb;
753 my ($nreq, undef) = split(';', $tot);
754 $nreq = ($nreq =~ tr/$//) - 1;
755 $tot = ($tot =~ tr/$//) - 1;
756 ($argc <= $tot && $argc >= $nreq);
759 # returns 1 if we can continue, 0 if not due to buffered writes or disconnect
760 sub process_line ($$) {
762 my ($tag, $req, @args) = parse_line('[ \t]+', 0, $l);
763 pop(@args) if (@args && !defined($args[-1]));
764 if (@args && uc($req) eq 'UID') {
765 $req .= "_".(shift @args);
768 if (my $cmd = $self->can('cmd_'.lc($req // ''))) {
769 defined($self->{-idle_tag}) ?
770 "$self->{-idle_tag} BAD expected DONE\r\n" :
771 $cmd->($self, $tag, @args);
772 } elsif (uc($tag // '') eq 'DONE' && !defined($req)) {
773 cmd_done($self, $tag);
774 } else { # this is weird
775 auth_challenge_ok($self) //
776 "$tag BAD Error in IMAP command $req: ".
777 "Unknown command\r\n";
781 if ($err && $self->{sock}) {
783 err($self, 'error from: %s (%s)', $l, $err);
784 $res = "$tag BAD program fault - command not performed\r\n";
786 return 0 unless defined $res;
792 # wbuf is unset or empty, here; {long} may add to it
793 my ($fd, $cb, $t0, @args) = @{$self->{long_cb}};
794 my $more = eval { $cb->($self, @args) };
795 if ($@ || !$self->{sock}) { # something bad happened...
796 delete $self->{long_cb};
797 my $elapsed = now() - $t0;
800 "%s during long response[$fd] - %0.6f",
803 out($self, " deferred[$fd] aborted - %0.6f", $elapsed);
805 } elsif ($more) { # $self->{wbuf}:
806 $self->update_idle_time;
808 # COMPRESS users all share the same DEFLATE context.
809 # Flush it here to ensure clients don't see
813 # no recursion, schedule another call ASAP, but only after
814 # all pending writes are done. autovivify wbuf:
815 my $new_size = push(@{$self->{wbuf}}, \&long_step);
817 # wbuf may be populated by $cb, no need to rearm if so:
818 $self->requeue if $new_size == 1;
820 delete $self->{long_cb};
821 my $elapsed = now() - $t0;
822 my $fd = fileno($self->{sock});
823 out($self, " deferred[$fd] done - %0.6f", $elapsed);
824 my $wbuf = $self->{wbuf}; # do NOT autovivify
826 $self->requeue unless $wbuf && @$wbuf;
831 my ($self, $fmt, @args) = @_;
832 printf { $self->{imapd}->{err} } $fmt."\n", @args;
836 my ($self, $fmt, @args) = @_;
837 printf { $self->{imapd}->{out} } $fmt."\n", @args;
840 sub long_response ($$;@) {
841 my ($self, $cb, @args) = @_; # cb returns true if more, false if done
843 my $sock = $self->{sock} or return;
844 # make sure we disable reading during a long response,
845 # clients should not be sending us stuff and making us do more
846 # work while we are stream a response to them
847 $self->{long_cb} = [ fileno($sock), $cb, now(), @args ];
848 long_step($self); # kick off!
852 # callback used by PublicInbox::DS for any (e)poll (in/out/hup/err)
856 return unless $self->flush_write && $self->{sock};
858 $self->update_idle_time;
859 # only read more requests if we've drained the write buffer,
860 # otherwise we can be buffering infinitely w/o backpressure
862 my $rbuf = $self->{rbuf} // (\(my $x = ''));
865 if (index($$rbuf, "\n") < 0) {
866 my $off = length($$rbuf);
867 $r = $self->do_read($rbuf, LINE_MAX, $off) or return;
869 while ($r > 0 && $$rbuf =~ s/\A[ \t]*([^\n]*?)\r?\n//) {
871 return $self->close if $line =~ /[[:cntrl:]]/s;
873 my $fd = fileno($self->{sock});
874 $r = eval { process_line($self, $line) };
875 my $pending = $self->{wbuf} ? ' pending' : '';
876 out($self, "[$fd] %s - %0.6f$pending", $line, now() - $t0);
879 return $self->close if $r < 0;
880 my $len = length($$rbuf);
881 return $self->close if ($len >= LINE_MAX);
882 $self->rbuf_idle($rbuf);
883 $self->update_idle_time;
885 # maybe there's more pipelined data, or we'll have
886 # to register it for socket-readiness notifications
887 $self->requeue unless $self->{wbuf};
890 sub compressed { undef }
892 sub zflush {} # overridden by IMAPdeflate
895 sub cmd_compress ($$$) {
896 my ($self, $tag, $alg) = @_;
897 return "$tag BAD DEFLATE only\r\n" if uc($alg) ne "DEFLATE";
898 return "$tag BAD COMPRESS active\r\n" if $self->compressed;
900 # CRIME made TLS compression obsolete
901 # return "$tag NO [COMPRESSIONACTIVE]\r\n" if $self->tls_compressed;
903 PublicInbox::IMAPdeflate->enable($self, $tag);
908 sub cmd_starttls ($$) {
909 my ($self, $tag) = @_;
910 my $sock = $self->{sock} or return;
911 if ($sock->can('stop_SSL') || $self->compressed) {
912 return "$tag BAD TLS or compression already enabled\r\n";
914 my $opt = $self->{imapd}->{accept_tls} or
915 return "$tag BAD can not initiate TLS negotiation\r\n";
916 $self->write(\"$tag OK begin TLS negotiation now\r\n");
917 $self->{sock} = IO::Socket::SSL->start_SSL($sock, %$opt);
918 $self->requeue if PublicInbox::DS::accept_tls_step($self);
922 # for graceful shutdown in PublicInbox::Daemon:
924 my ($self, $now) = @_;
925 ($self->{rbuf} || $self->{wbuf} || $self->not_idle_long($now));
930 if (my $ibx = delete $self->{ibx}) {
931 if (my $sock = $self->{sock}) {;
932 $ibx->unsubscribe_unlock(fileno($sock));
935 $self->SUPER::close; # PublicInbox::DS::close
938 # we're read-only, so SELECT and EXAMINE do the same thing
940 *cmd_select = \&cmd_examine;