+sub fetch_smsg { # long_response
+ my ($self, $tag, $msgs, $range_info, $ops) = @_;
+ while (!@$msgs) { # rare
+ if (my $end = refill_range($self, $msgs, $range_info)) {
+ $self->write(\"$tag $end\r\n");
+ return;
+ }
+ }
+ uo2m_extend($self, $msgs->[-1]->{num});
+ fetch_run_ops($self, $_, undef, $ops) for @$msgs;
+ @$msgs = ();
+ 1; # more
+}
+
+sub refill_uids ($$$;$) {
+ my ($self, $uids, $range_info, $sql) = @_;
+ my ($beg, $end, $range_csv) = @$range_info;
+ my $over = $self->{ibx}->over;
+ while (1) {
+ if (scalar(@$uids = @{$over->uid_range($beg, $end, $sql)})) {
+ $range_info->[0] = $uids->[-1] + 1; # update $beg
+ return;
+ } elsif (!$range_csv) {
+ return 0;
+ } else {
+ my $next_range = range_step($self, \$range_csv);
+ return $next_range if !ref($next_range); # error
+ ($beg, $end, $range_csv) = @$range_info = @$next_range;
+ # continue looping
+ }
+ }
+}
+
+sub fetch_uid { # long_response
+ my ($self, $tag, $uids, $range_info, $ops) = @_;
+ if (defined(my $err = refill_uids($self, $uids, $range_info))) {
+ $err ||= 'OK Fetch done';
+ $self->write("$tag $err\r\n");
+ return;
+ }
+ my $adj = $self->{uid_base} + 1;
+ my $uo2m = uo2m_extend($self, $uids->[-1]);
+ $uo2m = [ unpack('S*', $uo2m) ] if !ref($uo2m);
+ my ($i, $k);
+ for (@$uids) {
+ $self->msg_more("* $uo2m->[$_ - $adj] FETCH (UID $_");
+ for ($i = 0; $i < @$ops;) {
+ $k = $ops->[$i++];
+ $ops->[$i++]->($self, $k);
+ }
+ $self->msg_more(")\r\n");
+ }
+ @$uids = ();
+ 1; # more
+}
+
+sub cmd_status ($$$;@) {
+ my ($self, $tag, $mailbox, @items) = @_;
+ return "$tag BAD no items\r\n" if !scalar(@items);
+ ($items[0] !~ s/\A\(//s || $items[-1] !~ s/\)\z//s) and
+ return "$tag BAD invalid args\r\n";
+ my ($ibx, $exists, $uidnext) = inbox_lookup($self, $mailbox);
+ return "$tag NO Mailbox doesn't exist: $mailbox\r\n" if !$ibx;
+ my @it;
+ for my $it (@items) {
+ $it = uc($it);
+ push @it, $it;
+ if ($it =~ /\A(?:MESSAGES|UNSEEN|RECENT)\z/) {
+ push @it, $exists;
+ } elsif ($it eq 'UIDNEXT') {
+ push @it, $uidnext;
+ } elsif ($it eq 'UIDVALIDITY') {
+ push @it, $ibx->{uidvalidity};
+ } else {
+ return "$tag BAD invalid item\r\n";
+ }
+ }
+ return "$tag BAD no items\r\n" if !@it;
+ "* STATUS $mailbox (".join(' ', @it).")\r\n" .
+ "$tag OK Status done\r\n";
+}
+
+my %patmap = ('*' => '.*', '%' => '[^\.]*');
+sub cmd_list ($$$$) {
+ my ($self, $tag, $refname, $wildcard) = @_;
+ my $l = $self->{imapd}->{inboxlist};
+ if ($refname eq '' && $wildcard eq '') {
+ # request for hierarchy delimiter
+ $l = [ qq[* LIST (\\Noselect) "." ""\r\n] ];
+ } elsif ($refname ne '' || $wildcard ne '*') {
+ $wildcard =~ s!([^a-z0-9_])!$patmap{$1} // "\Q$1"!egi;
+ $l = [ grep(/ \Q$refname\E$wildcard\r\n\z/is, @$l) ];
+ }
+ \(join('', @$l, "$tag OK List done\r\n"));
+}
+
+sub cmd_lsub ($$$$) {
+ my (undef, $tag) = @_; # same args as cmd_list
+ "$tag OK Lsub done\r\n";
+}
+
+sub eml_index_offs_i { # PublicInbox::Eml::each_part callback
+ my ($p, $all) = @_;
+ my ($eml, undef, $idx) = @$p;
+ if ($idx && lc($eml->ct->{type}) eq 'multipart') {
+ $eml->{imap_bdy} = $eml->{bdy} // \'';
+ }
+ $all->{$idx} = $eml; # $idx => Eml
+}
+
+# prepares an index for BODY[$SECTION_IDX] fetches
+sub eml_body_idx ($$) {
+ my ($eml, $section_idx) = @_;
+ my $idx = $eml->{imap_all_parts} //= do {
+ my $all = {};
+ $eml->each_part(\&eml_index_offs_i, $all, 0, 1);
+ # top-level of multipart, BODY[0] not allowed (nz-number)
+ delete $all->{0};
+ $all;
+ };
+ $idx->{$section_idx};
+}
+
+# BODY[($SECTION_IDX)?(.$SECTION_NAME)?]<$offset.$bytes>
+sub partial_body {
+ my ($eml, $section_idx, $section_name) = @_;
+ if (defined $section_idx) {
+ $eml = eml_body_idx($eml, $section_idx) or return;
+ }
+ if (defined $section_name) {
+ if ($section_name eq 'MIME') {
+ # RFC 3501 6.4.5 states:
+ # The MIME part specifier MUST be prefixed
+ # by one or more numeric part specifiers
+ return unless defined $section_idx;
+ return $eml->header_obj->as_string . "\r\n";
+ }
+ my $bdy = $eml->{bdy} // $eml->{imap_bdy} // \'';
+ $eml = PublicInbox::Eml->new($$bdy);
+ if ($section_name eq 'TEXT') {
+ return $eml->body_raw;
+ } elsif ($section_name eq 'HEADER') {
+ return $eml->header_obj->as_string . "\r\n";
+ } else {
+ die "BUG: bad section_name=$section_name";
+ }
+ }
+ ${$eml->{bdy} // $eml->{imap_bdy} // \''};
+}
+
+# similar to what's in PublicInbox::Eml::re_memo, but doesn't memoize
+# to avoid OOM with malicious users
+sub hdrs_regexp ($) {
+ my ($hdrs) = @_;
+ my $names = join('|', map { "\Q$_" } split(/[ \t]+/, $hdrs));
+ qr/^(?:$names):[ \t]*[^\n]*\r?\n # 1st line
+ # continuation lines:
+ (?:[^:\n]*?[ \t]+[^\n]*\r?\n)*
+ /ismx;
+}
+
+# BODY[($SECTION_IDX.)?HEADER.FIELDS.NOT ($HDRS)]<$offset.$bytes>
+sub partial_hdr_not {
+ my ($eml, $section_idx, $hdrs_re) = @_;
+ if (defined $section_idx) {
+ $eml = eml_body_idx($eml, $section_idx) or return;
+ }
+ my $str = $eml->header_obj->as_string;
+ $str =~ s/$hdrs_re//g;
+ $str =~ s/(?<!\r)\n/\r\n/sg;
+ $str .= "\r\n";
+}
+
+# BODY[($SECTION_IDX.)?HEADER.FIELDS ($HDRS)]<$offset.$bytes>
+sub partial_hdr_get {
+ my ($eml, $section_idx, $hdrs_re) = @_;
+ if (defined $section_idx) {
+ $eml = eml_body_idx($eml, $section_idx) or return;
+ }
+ my $str = $eml->header_obj->as_string;
+ $str = join('', ($str =~ m/($hdrs_re)/g));
+ $str =~ s/(?<!\r)\n/\r\n/sg;
+ $str .= "\r\n";
+}
+
+sub partial_prepare ($$$$) {
+ my ($need, $partial, $want, $att) = @_;
+
+ # recombine [ "BODY[1.HEADER.FIELDS", "(foo", "bar)]" ]
+ # back to: "BODY[1.HEADER.FIELDS (foo bar)]"
+ return unless $att =~ /\ABODY\[/s;
+ until (rindex($att, ']') >= 0) {
+ my $next = shift @$want or return;
+ $att .= ' ' . uc($next);
+ }
+ if ($att =~ /\ABODY\[([0-9]+(?:\.[0-9]+)*)? # 1 - section_idx
+ (?:\.(HEADER|MIME|TEXT))? # 2 - section_name
+ \](?:<([0-9]+)(?:\.([0-9]+))?>)?\z/sx) { # 3, 4
+ $partial->{$att} = [ \&partial_body, $1, $2, $3, $4 ];
+ $$need |= CRLF_BREF|EML_HDR|EML_BDY;
+ } elsif ($att =~ /\ABODY\[(?:([0-9]+(?:\.[0-9]+)*)\.)? # 1 - section_idx
+ (?:HEADER\.FIELDS(\.NOT)?)\x20 # 2
+ \(([A-Z0-9\-\x20]+)\) # 3 - hdrs
+ \](?:<([0-9]+)(?:\.([0-9]+))?>)?\z/sx) { # 4 5
+ my $tmp = $partial->{$att} = [ $2 ? \&partial_hdr_not
+ : \&partial_hdr_get,
+ $1, undef, $4, $5 ];
+ $tmp->[2] = hdrs_regexp($3);
+
+ # don't emit CRLF_HDR instruction, here, partial_hdr_*
+ # will do CRLF conversion with only the extracted result
+ # and not waste time converting lines we don't care about.
+ $$need |= EML_HDR;