-}
-
-# IMAP to Xapian search key mapping
-my %I2X = (
- SUBJECT => 's:',
- BODY => 'b:',
- FROM => 'f:',
- TEXT => '', # n.b. does not include all headers
- TO => 't:',
- CC => 'c:',
- # BCC => 'bcc:', # TODO
- # KEYWORD # TODO ? dfpre,dfpost,...
-);
-
-# IMAP allows searching arbitrary headers via "HEADER $HDR_NAME $HDR_VAL"
-# which gets silly expensive. We only allow the headers we already index.
-my %H2X = (%I2X, 'MESSAGE-ID' => 'm:', 'LIST-ID' => 'l:');
-
-sub xap_append ($$$$) {
- my ($q, $rest, $k, $xk) = @_;
- delete $q->{sql}; # can't use over.sqlite3
- defined(my $arg = shift @$rest) or return "BAD $k no arg";
-
- # AFAIK Xapian can't handle [*"] in probabilistic terms
- $arg =~ tr/*"//d;
- ${$q->{xap}} .= qq[ $xk"$arg"];
- undef;
-}
-
-sub parse_query {
- my ($self, $rest) = @_;
- if (uc($rest->[0]) eq 'CHARSET') {
- shift @$rest;
- defined(my $c = shift @$rest) or return 'BAD missing charset';
- $c =~ /\A(?:UTF-8|US-ASCII)\z/ or return 'NO [BADCHARSET]';
- }
-
- my $sql = ''; # date conditions, {sql} deleted if Xapian is needed
- my $xap = '';
- my $q = { sql => \$sql, xap => \$xap };
- while (@$rest) {
- my $k = uc(shift @$rest);
- # default criteria
- next if $k =~ /\A(?:ALL|RECENT|UNSEEN|NEW)\z/;
- next if $k eq 'AND'; # the default, until we support OR
- if ($k =~ $valid_range) { # convert sequence numbers to UIDs
- msn_to_uid_range($self, $k);
- push @{$q->{uid}}, $k;
- } elsif ($k eq 'UID') {
- $k = shift(@$rest) // '';
- $k =~ $valid_range or return 'BAD UID range';
- push @{$q->{uid}}, $k;
- } elsif ($k =~ /\A(?:SENT)?(?:SINCE|ON|BEFORE)\z/) {
- my $d = parse_date(shift(@$rest) // '');
- defined $d or return "BAD $k date format";
- date_search($q, $k, $d);
- } elsif ($k =~ /\A(?:SMALLER|LARGER)\z/) {
- delete $q->{sql}; # can't use over.sqlite3
- my $bytes = shift(@$rest) // '';
- $bytes =~ /\A[0-9]+\z/ or return "BAD $k not a number";
- $xap .= ' bytes:' . ($k eq 'SMALLER' ?
- '..'.(--$bytes) :
- (++$bytes).'..');
- } elsif ($k eq 'HEADER') {
- $k = uc(shift(@$rest) // '');
- my $xk = $H2X{$k} or
- return "BAD HEADER $k not supported";
- my $err = xap_append($q, $rest, $k, $xk);
- return $err if $err;
- } elsif (defined(my $xk = $I2X{$k})) {
- my $err = xap_append($q, $rest, $k, $xk);
- return $err if $err;
- } else {
- # TODO: parentheses, OR, NOT ...
- return "BAD $k not supported (yet?)";
- }
- }
-
- # favor using over.sqlite3 if possible, since Xapian is optional
- if (exists $q->{sql}) {
- delete($q->{xap});
- delete($q->{sql}) if $sql eq '';
- } elsif (!$self->{ibx}->search) {
- return 'BAD Xapian not configured for mailbox';
- }
- my $max = $self->{ibx}->over->max;
- if (my $uid = delete $q->{uid}) {
- my $range_csv = join(',', @$uid);
- do {
- my $nxt = range_step($self, \$range_csv);
- my ($beg, $end) = @$nxt;
- if ($xap) {
- $xap .= " uid:$beg..$end";
- } elsif ($beg == $end) {
- $sql .= " AND num = $beg";
- } else {
- $sql .= " AND num >= $beg AND num <= $end";
- }
- } while ($range_csv);
- }
- my $beg = 1;
- uid_clamp($self, \$beg, \$max);
- $q->{range_info} = [ $beg, $max ];