+sub _get_doc ($$) {
+ my ($self, $docid) = @_;
+ my $doc = eval { $self->{xdb}->get_document($docid) };
+ $doc // do {
+ warn "E: $@\n" if $@;
+ warn "E: #$docid missing in Xapian\n";
+ undef;
+ }
+}
+
+sub add_eidx_info {
+ my ($self, $docid, $eidx_key, $eml) = @_;
+ begin_txn_lazy($self);
+ my $doc = _get_doc($self, $docid) or return;
+ term_generator($self)->set_document($doc);
+
+ # '.' is special for lei_store
+ $doc->add_boolean_term('O'.$eidx_key) if $eidx_key ne '.';
+
+ index_list_id($self, $doc, $eml);
+ $self->{xdb}->replace_document($docid, $doc);
+}
+
+sub get_terms {
+ my ($self, $pfx, $docid) = @_;
+ begin_txn_lazy($self);
+ xap_terms($pfx, $self->{xdb}, $docid);
+}
+
+sub remove_eidx_info {
+ my ($self, $docid, $eidx_key, $eml) = @_;
+ begin_txn_lazy($self);
+ my $doc = _get_doc($self, $docid) or return;
+ eval { $doc->remove_term('O'.$eidx_key) };
+ warn "W: ->remove_term O$eidx_key: $@\n" if $@;
+ for my $l ($eml ? $eml->header_raw('List-Id') : ()) {
+ $l =~ /<([^>]+)>/ or next;
+ my $lid = lc $1;
+ eval { $doc->remove_term('G' . $lid) };
+ warn "W: ->remove_term G$lid: $@\n" if $@;
+
+ # nb: we don't remove the XL probabilistic terms
+ # since terms may overlap if cross-posted.
+ #
+ # IOW, a message which has both <foo.example.com>
+ # and <bar.example.com> would have overlapping
+ # "XLexample" and "XLcom" as terms and which we
+ # wouldn't know if they're safe to remove if we just
+ # unindex <foo.example.com> while preserving
+ # <bar.example.com>.
+ #
+ # In any case, this entire sub is will likely never
+ # be needed and users using the "l:" prefix are probably
+ # rarer.
+ }
+ $self->{xdb}->replace_document($docid, $doc);
+}
+
+sub set_vmd {
+ my ($self, $docid, $vmd) = @_;
+ begin_txn_lazy($self);
+ my $doc = _get_doc($self, $docid) or return;
+ my ($end, @rm, @add);
+ my @x = @VMD_MAP;
+ while (my ($field, $pfx) = splice(@x, 0, 2)) {
+ my $set = $vmd->{$field} // next;
+ my %keep = map { $_ => 1 } @$set;
+ my %add = %keep;
+ $end //= $doc->termlist_end;
+ for (my $cur = $doc->termlist_begin; $cur != $end; $cur++) {
+ $cur->skip_to($pfx);
+ last if $cur == $end;
+ my $v = $cur->get_termname;
+ $v =~ s/\A$pfx//s or next;
+ $keep{$v} ? delete($add{$v}) : push(@rm, $pfx.$v);