+ 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 parse_date ($) { # 02-Oct-1993
+ my ($date_text) = @_;
+ my ($dd, $mon, $yyyy) = split(/-/, $_[0], 3);
+ defined($yyyy) or return;
+ my $mm = $MoY{$mon} // return;
+ $dd =~ /\A[0123]?[0-9]\z/ or return;
+ $yyyy =~ /\A[0-9]{4,}\z/ or return; # Y10K-compatible!
+ timegm(0, 0, 0, $dd, $mm, $yyyy);
+}
+
+sub search_uid_range { # long_response
+ my ($self, $tag, $sql, $range_info) = @_;
+ 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;
+ }
+ $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);