+ }
+
+ $r[0] = $need & NEED_BLOB ? \&fetch_blob :
+ ($need & NEED_SMSG ? \&fetch_smsg : \&fetch_uid);
+
+ # r[1] = [ $key1, $cb1, $key2, $cb2, ... ]
+ use sort 'stable'; # makes output more consistent
+ $r[1] = [ map { ($_->[2], $_->[1]) } sort { $a->[0] <=> $b->[0] } @op ];
+ @r;
+}
+
+sub cmd_uid_fetch ($$$$;@) {
+ my ($self, $tag, $range_csv, @want) = @_;
+ my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
+ my ($cb, $ops, $partial) = fetch_compile(\@want);
+ return "$tag $cb\r\n" unless $ops;
+
+ # cb is one of fetch_blob, fetch_smsg, fetch_uid
+ $range_csv = 'bad' if $range_csv !~ $valid_range;
+ my $range_info = range_step($self, \$range_csv);
+ return "$tag $range_info\r\n" if !ref($range_info);
+ uo2m_hibernate($self) if $cb == \&fetch_blob; # slow, save RAM
+ $self->long_response($cb, $tag, [], $range_info, $ops, $partial);
+}
+
+sub cmd_fetch ($$$$;@) {
+ my ($self, $tag, $range_csv, @want) = @_;
+ my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
+ my ($cb, $ops, $partial) = fetch_compile(\@want);
+ return "$tag $cb\r\n" unless $ops;
+
+ # cb is one of fetch_blob, fetch_smsg, fetch_uid
+ $range_csv = 'bad' if $range_csv !~ $valid_range;
+ msn_to_uid_range(msn2uid($self), $range_csv);
+ my $range_info = range_step($self, \$range_csv);
+ return "$tag $range_info\r\n" if !ref($range_info);
+ uo2m_hibernate($self) if $cb == \&fetch_blob; # slow, save RAM
+ $self->long_response($cb, $tag, [], $range_info, $ops, $partial);
+}
+
+sub msn_convert ($$) {
+ my ($self, $uids) = @_;
+ my $adj = $self->{uid_base} + 1;
+ my $uo2m = uo2m_extend($self, $uids->[-1]);
+ $uo2m = [ unpack('S*', $uo2m) ] if !ref($uo2m);
+ $_ = $uo2m->[$_ - $adj] for @$uids;
+}
+
+sub search_uid_range { # long_response
+ my ($self, $tag, $sql, $range_info, $want_msn) = @_;
+ my $uids = [];
+ if (defined(my $err = refill_uids($self, $uids, $range_info, $sql))) {
+ $err ||= 'OK Search done';
+ $self->write("\r\n$tag $err\r\n");
+ return;
+ }
+ msn_convert($self, $uids) if $want_msn;
+ $self->msg_more(join(' ', '', @$uids));
+ 1; # more
+}
+
+sub parse_imap_query ($$) {
+ my ($self, $query) = @_;
+ my $q = PublicInbox::IMAPsearchqp::parse($self, $query);
+ if (ref($q)) {
+ my $max = $self->{ibx}->over(1)->max;
+ my $beg = 1;
+ uid_clamp($self, \$beg, \$max);
+ $q->{range_info} = [ $beg, $max ];
+ }
+ $q;
+}
+
+sub search_common {
+ my ($self, $tag, $query, $want_msn) = @_;
+ my $ibx = $self->{ibx} or return "$tag BAD No mailbox selected\r\n";
+ my $q = parse_imap_query($self, $query);
+ return "$tag $q\r\n" if !ref($q);
+ my ($sql, $range_info) = delete @$q{qw(sql range_info)};
+ if (!scalar(keys %$q)) { # overview.sqlite3
+ $self->msg_more('* SEARCH');
+ $self->long_response(\&search_uid_range,
+ $tag, $sql, $range_info, $want_msn);
+ } elsif ($q = $q->{xap}) {
+ my $srch = $self->{ibx}->isrch or
+ return "$tag BAD search not available for mailbox\r\n";
+ my $opt = {
+ relevance => -1,
+ limit => UID_SLICE,
+ uid_range => $range_info
+ };
+ my $mset = $srch->mset($q, $opt);
+ my $uids = $srch->mset_to_artnums($mset, $opt);
+ msn_convert($self, $uids) if scalar(@$uids) && $want_msn;
+ "* SEARCH @$uids\r\n$tag OK Search done\r\n";