+sub partial_emit ($$$) {
+ my ($self, $partial, $eml) = @_;
+ for (@$partial) {
+ my ($k, $cb, @args) = @$_;
+ my ($offset, $len) = splice(@args, -2);
+ # $cb is partial_body|partial_hdr_get|partial_hdr_not
+ my $str = $cb->($eml, @args) // '';
+ if (defined $offset) {
+ if (defined $len) {
+ $str = substr($str, $offset, $len);
+ $k =~ s/\.$len>\z/>/ or warn
+"BUG: unable to remove `.$len>' from `$k'";
+ } else {
+ $str = substr($str, $offset);
+ $len = length($str);
+ }
+ } else {
+ $len = length($str);
+ }
+ $self->msg_more(" $k {$len}\r\n");
+ $self->msg_more($str);
+ }
+}
+
+sub fetch_compile ($) {
+ my ($want) = @_;
+ if ($want->[0] =~ s/\A\(//s) {
+ $want->[-1] =~ s/\)\z//s or return 'BAD no rparen';
+ }
+ my (%partial, %seen, @op);
+ my $need = 0;
+ while (defined(my $att = shift @$want)) {
+ $att = uc($att);
+ next if $att eq 'UID'; # always returned
+ $att =~ s/\ABODY\.PEEK\[/BODY\[/; # we're read-only
+ my $x = $FETCH_ATT{$att};
+ if ($x) {
+ while (my ($k, $fl_cb) = each %$x) {
+ next if $seen{$k}++;
+ $need |= $fl_cb->[0];
+ push @op, [ @$fl_cb, $k ];
+ }
+ } elsif (!partial_prepare(\$need, \%partial, $want, $att)) {
+ return "BAD param: $att";
+ }
+ }
+ my @r;
+
+ # stabilize partial order for consistency and ease-of-debugging:
+ if (scalar keys %partial) {
+ $need |= NEED_BLOB;
+ $r[2] = [ map { [ $_, @{$partial{$_}} ] } sort keys %partial ];
+ }
+
+ push @op, $OP_EML_NEW if ($need & (EML_HDR|EML_BDY));
+
+ # do we need CRLF conversion?
+ if ($need & CRLF_BREF) {
+ push @op, $OP_CRLF_BREF;
+ } elsif (my $crlf = ($need & (CRLF_HDR|CRLF_BDY))) {
+ if ($crlf == (CRLF_HDR|CRLF_BDY)) {
+ push @op, $OP_CRLF_BREF;
+ } elsif ($need & CRLF_HDR) {
+ push @op, $OP_CRLF_HDR;
+ } else {
+ push @op, $OP_CRLF_BDY;
+ }
+ }
+
+ $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
+ long_response($self, $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
+ long_response($self, $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 date_search {
+ my ($q, $k, $d) = @_;
+ my $sql = $q->{sql};
+
+ # Date: header
+ if ($k eq 'SENTON') {
+ my $end = $d + 86399; # no leap day...
+ my $da = strftime('%Y%m%d%H%M%S', gmtime($d));
+ my $db = strftime('%Y%m%d%H%M%S', gmtime($end));
+ $q->{xap} .= " dt:$da..$db";
+ $$sql .= " AND ds >= $d AND ds <= $end" if defined($sql);
+ } elsif ($k eq 'SENTBEFORE') {
+ $q->{xap} .= ' d:..'.strftime('%Y%m%d', gmtime($d));
+ $$sql .= " AND ds <= $d" if defined($sql);
+ } elsif ($k eq 'SENTSINCE') {
+ $q->{xap} .= ' d:'.strftime('%Y%m%d', gmtime($d)).'..';
+ $$sql .= " AND ds >= $d" if defined($sql);
+
+ # INTERNALDATE (Received)
+ } elsif ($k eq 'ON') {
+ my $end = $d + 86399; # no leap day...
+ $q->{xap} .= " ts:$d..$end";
+ $$sql .= " AND ts >= $d AND ts <= $end" if defined($sql);
+ } elsif ($k eq 'BEFORE') {
+ $q->{xap} .= " ts:..$d";
+ $$sql .= " AND ts <= $d" if defined($sql);
+ } elsif ($k eq 'SINCE') {
+ $q->{xap} .= " ts:$d..";
+ $$sql .= " AND ts >= $d" if defined($sql);