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);
125 my ($self, $tag) = @_;
126 delete $self->{ibx} ? "$tag OK Close done\r\n"
127 : "$tag BAD No mailbox\r\n";
130 sub cmd_logout ($$) {
131 my ($self, $tag) = @_;
132 delete $self->{logged_in};
133 $self->write(\"* BYE logging out\r\n$tag OK Logout done\r\n");
134 $self->shutdn; # PublicInbox::DS::shutdn
138 sub cmd_authenticate ($$$) {
139 my ($self, $tag) = @_; # $method = $_[2], should be "ANONYMOUS"
140 $self->{-login_tag} = $tag;
144 sub cmd_capability ($$) {
145 my ($self, $tag) = @_;
146 '* '.capa($self)."\r\n$tag OK Capability done\r\n";
149 sub cmd_noop ($$) { "$_[1] OK Noop done\r\n" }
151 # called by PublicInbox::InboxIdle
152 sub on_inbox_unlock {
153 my ($self, $ibx) = @_;
154 my $new = $ibx->mm->max;
155 defined(my $old = $self->{-idle_max}) or die 'BUG: -idle_max unset';
157 $self->{-idle_max} = $new;
158 $self->msg_more("* $_ EXISTS\r\n") for (($old + 1)..($new - 1));
159 $self->write(\"* $new EXISTS\r\n");
164 my ($self, $tag) = @_;
165 # IDLE seems allowed by dovecot w/o a mailbox selected *shrug*
166 my $ibx = $self->{ibx} or return "$tag BAD no mailbox selected\r\n";
167 $ibx->subscribe_unlock(fileno($self->{sock}), $self);
168 $self->{imapd}->idler_start;
169 $self->{-idle_tag} = $tag;
170 $self->{-idle_max} = $ibx->mm->max // 0;
175 my ($self, $tag) = @_; # $tag is "DONE" (case-insensitive)
176 defined(my $idle_tag = delete $self->{-idle_tag}) or
177 return "$tag BAD not idle\r\n";
178 my $ibx = $self->{ibx} or do {
179 warn "BUG: idle_tag set w/o inbox";
180 return "$tag BAD internal bug\r\n";
182 $ibx->unsubscribe_unlock(fileno($self->{sock}));
183 "$idle_tag OK Idle done\r\n";
186 sub cmd_examine ($$$) {
187 my ($self, $tag, $mailbox) = @_;
188 my $ibx = $self->{imapd}->{groups}->{$mailbox} or
189 return "$tag NO Mailbox doesn't exist: $mailbox\r\n";
191 my $max = $mm->max // 0;
192 # RFC 3501 2.3.1.1 - "A good UIDVALIDITY value to use in
193 # this case is a 32-bit representation of the creation
194 # date/time of the mailbox"
195 my $uidvalidity = $mm->created_at or return "$tag BAD UIDVALIDITY\r\n";
196 my $uidnext = $max + 1;
198 # XXX: do we need this? RFC 5162/7162
199 my $ret = $self->{ibx} ? "* OK [CLOSED] previous closed\r\n" : '';
205 * OK [PERMANENTFLAGS ()] Read-only mailbox\r
207 $ret .= "* OK [UNSEEN $max]\r\n" if $max;
208 $ret .= "* OK [UIDNEXT $uidnext]\r\n" if defined $uidnext;
209 $ret .= "* OK [UIDVALIDITY $uidvalidity]\r\n" if defined $uidvalidity;
210 $ret .= "$tag OK [READ-ONLY] EXAMINE/SELECT done\r\n";
217 } elsif ($v =~ /[{"\r\n%*\\\[]/) { # literal string
218 '{' . length($v) . "}\r\n" . $v;
219 } else { # quoted string
224 sub addr_envelope ($$;$) {
225 my ($eml, $x, $y) = @_;
226 my $v = $eml->header_raw($x) //
227 ($y ? $eml->header_raw($y) : undef) // return 'NIL';
229 my @x = $Address->parse($v) or return 'NIL';
231 map { '(' . join(' ',
232 _esc($_->name), 'NIL',
233 _esc($_->user), _esc($_->host)
239 sub eml_envelope ($) {
242 _esc($eml->header_raw('Date')),
243 _esc($eml->header_raw('Subject')),
244 addr_envelope($eml, 'From'),
245 addr_envelope($eml, 'Sender', 'From'),
246 addr_envelope($eml, 'Reply-To', 'From'),
247 addr_envelope($eml, 'To'),
248 addr_envelope($eml, 'Cc'),
249 addr_envelope($eml, 'Bcc'),
250 _esc($eml->header_raw('In-Reply-To')),
251 _esc($eml->header_raw('Message-ID')),
257 if ($hash && scalar keys %$hash) {
258 $hash = [ %$hash ]; # flatten hash into 1-dimensional array
259 '(' . join(' ', map { _esc($_) } @$hash) . ')';
265 sub body_disposition ($) {
267 my $cd = $eml->header_raw('Content-Disposition') or return 'NIL';
268 $cd = parse_content_disposition($cd);
269 my $buf = '('._esc($cd->{type});
270 $buf .= ' ' . _esc_hash(delete $cd->{attributes});
274 sub body_leaf ($$;$) {
275 my ($eml, $structure, $hold) = @_;
277 $eml->{is_submsg} and # parent was a message/(rfc822|news|global)
278 $buf .= eml_envelope($eml). ' ';
280 $buf .= '('._esc($ct->{type}).' ';
281 $buf .= _esc($ct->{subtype});
282 $buf .= ' ' . _esc_hash(delete $ct->{attributes});
283 $buf .= ' ' . _esc($eml->header_raw('Content-ID'));
284 $buf .= ' ' . _esc($eml->header_raw('Content-Description'));
285 my $cte = $eml->header_raw('Content-Transfer-Encoding') // '7bit';
286 $buf .= ' ' . _esc($cte);
287 $buf .= ' ' . $eml->{imap_body_len};
288 $buf .= ' '.($eml->body_raw =~ tr/\n/\n/) if lc($ct->{type}) eq 'text';
290 # for message/(rfc822|global|news), $hold[0] should have envelope
291 $buf .= ' ' . (@$hold ? join('', @$hold) : 'NIL') if $hold;
294 $buf .= ' '._esc($eml->header_raw('Content-MD5'));
295 $buf .= ' '. body_disposition($eml);
296 $buf .= ' '._esc($eml->header_raw('Content-Language'));
297 $buf .= ' '._esc($eml->header_raw('Content-Location'));
302 sub body_parent ($$$) {
303 my ($eml, $structure, $hold) = @_;
305 my $type = lc($ct->{type});
306 if ($type eq 'multipart') {
308 $buf .= @$hold ? join('', @$hold) : 'NIL';
309 $buf .= ' '._esc($ct->{subtype});
311 $buf .= ' '._esc_hash(delete $ct->{attributes});
312 $buf .= ' '.body_disposition($eml);
313 $buf .= ' '._esc($eml->header_raw('Content-Language'));
314 $buf .= ' '._esc($eml->header_raw('Content-Location'));
318 } else { # message/(rfc822|global|news)
319 @$hold = (body_leaf($eml, $structure, $hold));
323 # this is gross, but we need to process the parent part AFTER
324 # the child parts are done
325 sub bodystructure_prep {
327 my ($eml, $depth) = @$p; # ignore idx
328 # set length here, as $eml->{bdy} gets deleted for message/rfc822
329 $eml->{imap_body_len} = length($eml->body_raw);
330 push @$q, $eml, $depth;
333 # for FETCH BODY and FETCH BODYSTRUCTURE
334 sub fetch_body ($;$) {
335 my ($eml, $structure) = @_;
337 $eml->each_part(\&bodystructure_prep, \@q, 0, 1);
341 my ($part, $depth) = splice(@q, -2);
342 my $is_mp_parent = $depth == ($cur_depth - 1);
346 body_parent($part, $structure, \@hold);
348 unshift @hold, body_leaf($part, $structure);
354 sub dummy_message ($$) {
355 my ($seqno, $ibx) = @_;
357 From: nobody\@localhost\r
358 To: nobody\@localhost\r
359 Date: Thu, 01 Jan 1970 00:00:00 +0000\r
360 Message-ID: <dummy-$seqno\@$ibx->{newsgroup}>\r
361 Subject: dummy message #$seqno\r
363 You're seeing this message because your IMAP client didn't use UIDs.\r
364 The message which used to use this sequence number was likely spam\r
365 and removed by the administrator.\r
370 sub uid_fetch_cb { # called by git->cat_async
371 my ($bref, $oid, $type, $size, $fetch_m_arg) = @_;
372 my ($self, undef, $ibx, undef, undef, $msgs, $want) = @$fetch_m_arg;
373 my $smsg = shift @$msgs or die 'BUG: no smsg';
374 if (!defined($oid)) {
375 # it's possible to have TOCTOU if an admin runs
376 # public-inbox-(edit|purge), just move onto the next message
377 return unless defined $want->{-seqno};
378 $bref = dummy_message($smsg->{num}, $ibx);
380 $smsg->{blob} eq $oid or die "BUG: $smsg->{blob} != $oid";
382 $$bref =~ s/(?<!\r)\n/\r\n/sg; # make strict clients happy
384 # fixup old bug from import (pre-a0c07cba0e5d8b6a)
385 $$bref =~ s/\A[\r\n]*From [^\r\n]*\r\n//s;
387 $self->msg_more("* $smsg->{num} FETCH (UID $smsg->{num}");
389 $want->{'RFC822.SIZE'} and
390 $self->msg_more(' RFC822.SIZE '.length($$bref));
391 $want->{INTERNALDATE} and
392 $self->msg_more(' INTERNALDATE "'.$smsg->internaldate.'"');
393 $want->{FLAGS} and $self->msg_more(' FLAGS ()');
394 for ('RFC822', 'BODY[]') {
396 $self->msg_more(" $_ {".length($$bref)."}\r\n");
397 $self->msg_more($$bref);
400 my $eml = PublicInbox::Eml->new($bref);
402 $want->{ENVELOPE} and
403 $self->msg_more(' ENVELOPE '.eml_envelope($eml));
405 for ('RFC822.HEADER', 'BODY[HEADER]') {
407 $self->msg_more(" $_ {".length(${$eml->{hdr}})."}\r\n");
408 $self->msg_more(${$eml->{hdr}});
410 for ('RFC822.TEXT', 'BODY[TEXT]') {
412 $self->msg_more(" $_ {".length($$bref)."}\r\n");
413 $self->msg_more($$bref);
415 $want->{BODYSTRUCTURE} and
416 $self->msg_more(' BODYSTRUCTURE '.fetch_body($eml, 1));
418 $self->msg_more(' BODY '.fetch_body($eml));
419 if (my $partial = $want->{-partial}) {
420 partial_emit($self, $partial, $eml);
422 $self->msg_more(")\r\n");
425 sub uid_fetch_m { # long_response
426 my ($self, $tag, $ibx, $beg, $end, $msgs, $want) = @_;
427 if (!@$msgs) { # refill
428 @$msgs = @{$ibx->over->query_xover($$beg, $end)};
430 $self->write(\"$tag OK Fetch done\r\n");
433 $$beg = $msgs->[-1]->{num} + 1;
436 $git->cat_async_begin; # TODO: actually make async
437 $git->cat_async($msgs->[0]->{blob}, \&uid_fetch_cb, \@_);
438 $git->cat_async_wait;
442 sub cmd_status ($$$;@) {
443 my ($self, $tag, $mailbox, @items) = @_;
444 my $ibx = $self->{imapd}->{groups}->{$mailbox} or
445 return "$tag NO Mailbox doesn't exist: $mailbox\r\n";
446 return "$tag BAD no items\r\n" if !scalar(@items);
447 ($items[0] !~ s/\A\(//s || $items[-1] !~ s/\)\z//s) and
448 return "$tag BAD invalid args\r\n";
452 for my $it (@items) {
455 if ($it =~ /\A(?:MESSAGES|UNSEEN|RECENT)\z/) {
456 push(@it, ($max //= $mm->max // 0));
457 } elsif ($it eq 'UIDNEXT') {
458 push(@it, ($max //= $mm->max // 0) + 1);
459 } elsif ($it eq 'UIDVALIDITY') {
460 push(@it, $mm->created_at //
461 return("$tag BAD UIDVALIDITY\r\n"));
463 return "$tag BAD invalid item\r\n";
466 return "$tag BAD no items\r\n" if !@it;
467 "* STATUS $mailbox (".join(' ', @it).")\r\n" .
468 "$tag OK Status done\r\n";
471 my %patmap = ('*' => '.*', '%' => '[^\.]*');
472 sub cmd_list ($$$$) {
473 my ($self, $tag, $refname, $wildcard) = @_;
474 my $l = $self->{imapd}->{inboxlist};
475 if ($refname eq '' && $wildcard eq '') {
476 # request for hierarchy delimiter
477 $l = [ qq[* LIST (\\Noselect) "." ""\r\n] ];
478 } elsif ($refname ne '' || $wildcard ne '*') {
479 $wildcard =~ s!([^a-z0-9_])!$patmap{$1} // "\Q$1"!eig;
480 $l = [ grep(/ \Q$refname\E$wildcard\r\n\z/s, @$l) ];
482 \(join('', @$l, "$tag OK List done\r\n"));
485 sub eml_index_offs_i { # PublicInbox::Eml::each_part callback
487 my ($eml, undef, $idx) = @$p;
488 if ($idx && lc($eml->ct->{type}) eq 'multipart') {
489 $eml->{imap_bdy} = $eml->{bdy} // \'';
491 $all->{$idx} = $eml; # $idx => Eml
494 # prepares an index for BODY[$SECTION_IDX] fetches
495 sub eml_body_idx ($$) {
496 my ($eml, $section_idx) = @_;
497 my $idx = $eml->{imap_all_parts} //= do {
499 $eml->each_part(\&eml_index_offs_i, $all, 0, 1);
500 # top-level of multipart, BODY[0] not allowed (nz-number)
504 $idx->{$section_idx};
507 # BODY[($SECTION_IDX)?(.$SECTION_NAME)?]<$offset.$bytes>
509 my ($eml, $section_idx, $section_name) = @_;
510 if (defined $section_idx) {
511 $eml = eml_body_idx($eml, $section_idx) or return;
513 if (defined $section_name) {
514 if ($section_name eq 'MIME') {
515 # RFC 3501 6.4.5 states:
516 # The MIME part specifier MUST be prefixed
517 # by one or more numeric part specifiers
518 return unless defined $section_idx;
519 return $eml->header_obj->as_string . "\r\n";
521 my $bdy = $eml->{bdy} // $eml->{imap_bdy} // \'';
522 $eml = PublicInbox::Eml->new($$bdy);
523 if ($section_name eq 'TEXT') {
524 return $eml->body_raw;
525 } elsif ($section_name eq 'HEADER') {
526 return $eml->header_obj->as_string . "\r\n";
528 die "BUG: bad section_name=$section_name";
531 ${$eml->{bdy} // $eml->{imap_bdy} // \''};
534 # similar to what's in PublicInbox::Eml::re_memo, but doesn't memoize
535 # to avoid OOM with malicious users
536 sub hdrs_regexp ($) {
538 my $names = join('|', map { "\Q$_" } split(/[ \t]+/, $hdrs));
539 qr/^(?:$names):[ \t]*[^\n]*\r?\n # 1st line
540 # continuation lines:
541 (?:[^:\n]*?[ \t]+[^\n]*\r?\n)*
545 # BODY[($SECTION_IDX.)?HEADER.FIELDS.NOT ($HDRS)]<$offset.$bytes>
546 sub partial_hdr_not {
547 my ($eml, $section_idx, $hdrs_re) = @_;
548 if (defined $section_idx) {
549 $eml = eml_body_idx($eml, $section_idx) or return;
551 my $str = $eml->header_obj->as_string;
552 $str =~ s/$hdrs_re//g;
556 # BODY[($SECTION_IDX.)?HEADER.FIELDS ($HDRS)]<$offset.$bytes>
557 sub partial_hdr_get {
558 my ($eml, $section_idx, $hdrs_re) = @_;
559 if (defined $section_idx) {
560 $eml = eml_body_idx($eml, $section_idx) or return;
562 my $str = $eml->header_obj->as_string;
563 join('', ($str =~ m/($hdrs_re)/g), "\r\n");
566 sub partial_prepare ($$$) {
567 my ($partial, $want, $att) = @_;
569 # recombine [ "BODY[1.HEADER.FIELDS", "(foo", "bar)]" ]
570 # back to: "BODY[1.HEADER.FIELDS (foo bar)]"
571 return unless $att =~ /\ABODY\[/s;
572 until (rindex($att, ']') >= 0) {
573 my $next = shift @$want or return;
574 $att .= ' ' . uc($next);
576 if ($att =~ /\ABODY\[([0-9]+(?:\.[0-9]+)*)? # 1 - section_idx
577 (?:\.(HEADER|MIME|TEXT))? # 2 - section_name
578 \](?:<([0-9]+)(?:\.([0-9]+))?>)?\z/sx) { # 3, 4
579 $partial->{$att} = [ \&partial_body, $1, $2, $3, $4 ];
580 } elsif ($att =~ /\ABODY\[(?:([0-9]+(?:\.[0-9]+)*)\.)? # 1 - section_idx
581 (?:HEADER\.FIELDS(\.NOT)?)\x20 # 2
582 \(([A-Z0-9\-\x20]+)\) # 3 - hdrs
583 \](?:<([0-9]+)(?:\.([0-9]+))?>)?\z/sx) { # 4 5
584 my $tmp = $partial->{$att} = [ $2 ? \&partial_hdr_not
587 $tmp->[2] = hdrs_regexp($3);
593 sub partial_emit ($$$) {
594 my ($self, $partial, $eml) = @_;
596 my ($k, $cb, @args) = @$_;
597 my ($offset, $len) = splice(@args, -2);
598 # $cb is partial_body|partial_hdr_get|partial_hdr_not
599 my $str = $cb->($eml, @args) // '';
600 if (defined $offset) {
602 $str = substr($str, $offset, $len);
603 $k =~ s/\.$len>\z/>/ or warn
604 "BUG: unable to remove `.$len>' from `$k'";
606 $str = substr($str, $offset);
612 $self->msg_more(" $k {$len}\r\n");
613 $self->msg_more($str);
617 sub fetch_common ($$$$) {
618 my ($self, $tag, $range, $want) = @_;
619 my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
620 if ($want->[0] =~ s/\A\(//s) {
621 $want->[-1] =~ s/\)\z//s or return "$tag BAD no rparen\r\n";
623 my (%partial, %want);
624 while (defined(my $att = shift @$want)) {
626 $att =~ s/\ABODY\.PEEK\[/BODY\[/; # we're read-only
627 my $x = $FETCH_ATT{$att};
629 %want = (%want, %$x);
630 } elsif (!partial_prepare(\%partial, $want, $att)) {
631 return "$tag BAD param: $att\r\n";
635 # stabilize partial order for consistency and ease-of-debugging:
636 if (scalar keys %partial) {
637 $want{-partial} = [ map {;
638 [ $_, @{$partial{$_}} ]
639 } sort keys %partial ];
644 if ($range =~ /\A([0-9]+):([0-9]+)\z/s) {
645 ($beg, $end) = ($1, $2);
646 } elsif ($range =~ /\A([0-9]+):\*\z/s) {
647 ($beg, $end) = ($1, $ibx->mm->max // 0);
648 } elsif ($range =~ /\A[0-9]+\z/) {
649 my $smsg = $ibx->over->get_art($range) or
650 return "$tag OK Fetch done\r\n"; # really OK(!)
652 ($beg, $end) = ($range, 0);
654 return "$tag BAD fetch range\r\n";
656 [ $tag, $ibx, \$beg, $end, $msgs, \%want ];
659 sub cmd_uid_fetch ($$$;@) {
660 my ($self, $tag, $range, @want) = @_;
661 my $args = fetch_common($self, $tag, $range, \@want);
662 ref($args) eq 'ARRAY' ?
663 long_response($self, \&uid_fetch_m, @$args) :
667 sub seq_fetch_m { # long_response
668 my ($self, $tag, $ibx, $beg, $end, $msgs, $want) = @_;
669 if (!@$msgs) { # refill
670 @$msgs = @{$ibx->over->query_xover($$beg, $end)};
672 $self->write(\"$tag OK Fetch done\r\n");
675 $$beg = $msgs->[-1]->{num} + 1;
677 my $seq = $want->{-seqno}++;
678 my $cur_num = $msgs->[0]->{num};
679 if ($cur_num == $seq) { # as expected
681 $git->cat_async_begin; # TODO: actually make async
682 $git->cat_async($msgs->[0]->{blob}, \&uid_fetch_cb, \@_);
683 $git->cat_async_wait;
684 } elsif ($cur_num > $seq) {
685 # send dummy messages until $seq catches up to $cur_num
686 my $smsg = bless { num => $seq, ts => 0 }, 'PublicInbox::Smsg';
687 unshift @$msgs, $smsg;
688 my $bref = dummy_message($seq, $ibx);
689 uid_fetch_cb($bref, undef, undef, undef, \@_);
690 } else { # should not happen
691 die "BUG: cur_num=$cur_num < seq=$seq";
693 1; # more messages on the way
696 sub cmd_fetch ($$$;@) {
697 my ($self, $tag, $range, @want) = @_;
698 my $args = fetch_common($self, $tag, $range, \@want);
699 ref($args) eq 'ARRAY' ? do {
700 my $want = $args->[-1];
701 $want->{-seqno} = ${$args->[2]}; # $$beg
702 long_response($self, \&seq_fetch_m, @$args)
706 sub uid_search_all { # long_response
707 my ($self, $tag, $ibx, $num) = @_;
708 my $uids = $ibx->mm->ids_after($num);
709 if (scalar(@$uids)) {
710 $self->msg_more(join(' ', '', @$uids));
712 $self->write(\"\r\n$tag OK Search done\r\n");
717 sub uid_search_uid_range { # long_response
718 my ($self, $tag, $ibx, $beg, $end) = @_;
719 my $uids = $ibx->mm->msg_range($beg, $end, 'num');
721 $self->msg_more(join('', map { " $_->[0]" } @$uids));
723 $self->write(\"\r\n$tag OK Search done\r\n");
728 sub cmd_uid_search ($$$;) {
729 my ($self, $tag, $arg, @rest) = @_;
730 my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
732 if ($arg eq 'ALL' && !@rest) {
733 $self->msg_more('* SEARCH');
735 long_response($self, \&uid_search_all, $tag, $ibx, \$num);
736 } elsif ($arg eq 'UID' && scalar(@rest) == 1) {
737 if ($rest[0] =~ /\A([0-9]+):([0-9]+|\*)\z/s) {
738 my ($beg, $end) = ($1, $2);
739 $end = $ibx->mm->max if $end eq '*';
740 $self->msg_more('* SEARCH');
741 long_response($self, \&uid_search_uid_range,
742 $tag, $ibx, \$beg, $end);
743 } elsif ($rest[0] =~ /\A[0-9]+\z/s) {
745 $uid = $ibx->over->get_art($uid) ? " $uid" : '';
746 "* SEARCH$uid\r\n$tag OK Search done\r\n";
748 "$tag BAD Error\r\n";
751 "$tag BAD Error\r\n";
755 sub args_ok ($$) { # duplicated from PublicInbox::NNTP
756 my ($cb, $argc) = @_;
757 my $tot = prototype $cb;
758 my ($nreq, undef) = split(';', $tot);
759 $nreq = ($nreq =~ tr/$//) - 1;
760 $tot = ($tot =~ tr/$//) - 1;
761 ($argc <= $tot && $argc >= $nreq);
764 # returns 1 if we can continue, 0 if not due to buffered writes or disconnect
765 sub process_line ($$) {
767 my ($tag, $req, @args) = parse_line('[ \t]+', 0, $l);
768 pop(@args) if (@args && !defined($args[-1]));
769 if (@args && uc($req) eq 'UID') {
770 $req .= "_".(shift @args);
773 if (my $cmd = $self->can('cmd_'.lc($req // ''))) {
774 defined($self->{-idle_tag}) ?
775 "$self->{-idle_tag} BAD expected DONE\r\n" :
776 $cmd->($self, $tag, @args);
777 } elsif (uc($tag // '') eq 'DONE' && !defined($req)) {
778 cmd_done($self, $tag);
779 } else { # this is weird
780 auth_challenge_ok($self) //
781 "$tag BAD Error in IMAP command $req: ".
782 "Unknown command\r\n";
786 if ($err && $self->{sock}) {
788 err($self, 'error from: %s (%s)', $l, $err);
789 $res = "$tag BAD program fault - command not performed\r\n";
791 return 0 unless defined $res;
797 # wbuf is unset or empty, here; {long} may add to it
798 my ($fd, $cb, $t0, @args) = @{$self->{long_cb}};
799 my $more = eval { $cb->($self, @args) };
800 if ($@ || !$self->{sock}) { # something bad happened...
801 delete $self->{long_cb};
802 my $elapsed = now() - $t0;
805 "%s during long response[$fd] - %0.6f",
808 out($self, " deferred[$fd] aborted - %0.6f", $elapsed);
810 } elsif ($more) { # $self->{wbuf}:
811 $self->update_idle_time;
813 # COMPRESS users all share the same DEFLATE context.
814 # Flush it here to ensure clients don't see
818 # no recursion, schedule another call ASAP, but only after
819 # all pending writes are done. autovivify wbuf:
820 my $new_size = push(@{$self->{wbuf}}, \&long_step);
822 # wbuf may be populated by $cb, no need to rearm if so:
823 $self->requeue if $new_size == 1;
825 delete $self->{long_cb};
826 my $elapsed = now() - $t0;
827 my $fd = fileno($self->{sock});
828 out($self, " deferred[$fd] done - %0.6f", $elapsed);
829 my $wbuf = $self->{wbuf}; # do NOT autovivify
831 $self->requeue unless $wbuf && @$wbuf;
836 my ($self, $fmt, @args) = @_;
837 printf { $self->{imapd}->{err} } $fmt."\n", @args;
841 my ($self, $fmt, @args) = @_;
842 printf { $self->{imapd}->{out} } $fmt."\n", @args;
845 sub long_response ($$;@) {
846 my ($self, $cb, @args) = @_; # cb returns true if more, false if done
848 my $sock = $self->{sock} or return;
849 # make sure we disable reading during a long response,
850 # clients should not be sending us stuff and making us do more
851 # work while we are stream a response to them
852 $self->{long_cb} = [ fileno($sock), $cb, now(), @args ];
853 long_step($self); # kick off!
857 # callback used by PublicInbox::DS for any (e)poll (in/out/hup/err)
861 return unless $self->flush_write && $self->{sock};
863 $self->update_idle_time;
864 # only read more requests if we've drained the write buffer,
865 # otherwise we can be buffering infinitely w/o backpressure
867 my $rbuf = $self->{rbuf} // (\(my $x = ''));
870 if (index($$rbuf, "\n") < 0) {
871 my $off = length($$rbuf);
872 $r = $self->do_read($rbuf, LINE_MAX, $off) or return;
874 while ($r > 0 && $$rbuf =~ s/\A[ \t]*([^\n]*?)\r?\n//) {
876 return $self->close if $line =~ /[[:cntrl:]]/s;
878 my $fd = fileno($self->{sock});
879 $r = eval { process_line($self, $line) };
880 my $pending = $self->{wbuf} ? ' pending' : '';
881 out($self, "[$fd] %s - %0.6f$pending", $line, now() - $t0);
884 return $self->close if $r < 0;
885 my $len = length($$rbuf);
886 return $self->close if ($len >= LINE_MAX);
887 $self->rbuf_idle($rbuf);
888 $self->update_idle_time;
890 # maybe there's more pipelined data, or we'll have
891 # to register it for socket-readiness notifications
892 $self->requeue unless $self->{wbuf};
895 sub compressed { undef }
897 sub zflush {} # overridden by IMAPdeflate
900 sub cmd_compress ($$$) {
901 my ($self, $tag, $alg) = @_;
902 return "$tag BAD DEFLATE only\r\n" if uc($alg) ne "DEFLATE";
903 return "$tag BAD COMPRESS active\r\n" if $self->compressed;
905 # CRIME made TLS compression obsolete
906 # return "$tag NO [COMPRESSIONACTIVE]\r\n" if $self->tls_compressed;
908 PublicInbox::IMAPdeflate->enable($self, $tag);
913 sub cmd_starttls ($$) {
914 my ($self, $tag) = @_;
915 my $sock = $self->{sock} or return;
916 if ($sock->can('stop_SSL') || $self->compressed) {
917 return "$tag BAD TLS or compression already enabled\r\n";
919 my $opt = $self->{imapd}->{accept_tls} or
920 return "$tag BAD can not initiate TLS negotiation\r\n";
921 $self->write(\"$tag OK begin TLS negotiation now\r\n");
922 $self->{sock} = IO::Socket::SSL->start_SSL($sock, %$opt);
923 $self->requeue if PublicInbox::DS::accept_tls_step($self);
927 # for graceful shutdown in PublicInbox::Daemon:
929 my ($self, $now) = @_;
930 ($self->{rbuf} || $self->{wbuf} || $self->not_idle_long($now));
935 if (my $ibx = delete $self->{ibx}) {
936 if (my $sock = $self->{sock}) {;
937 $ibx->unsubscribe_unlock(fileno($sock));
940 $self->SUPER::close; # PublicInbox::DS::close
943 # we're read-only, so SELECT and EXAMINE do the same thing
945 *cmd_select = \&cmd_examine;