diff --git a/cmd/archivmail-export/main.go b/cmd/archivmail-export/main.go index fe3bb5c..96486ea 100644 --- a/cmd/archivmail-export/main.go +++ b/cmd/archivmail-export/main.go @@ -44,7 +44,7 @@ func main() { indexBackend := cfg.Index.Backend if indexBackend == "" { - indexBackend = "xapian" + indexBackend = "manticore" } batchSize := cfg.Index.BatchSize if batchSize <= 0 { diff --git a/cmd/archivmail-import/main.go b/cmd/archivmail-import/main.go index 2020608..c23b8e7 100644 --- a/cmd/archivmail-import/main.go +++ b/cmd/archivmail-import/main.go @@ -47,7 +47,7 @@ func main() { indexBackend := cfg.Index.Backend if indexBackend == "" { - indexBackend = "xapian" + indexBackend = "manticore" } batchSize := cfg.Index.BatchSize if batchSize <= 0 { diff --git a/cmd/archivmail/cmd_export.go b/cmd/archivmail/cmd_export.go index 76582a6..09449f0 100644 --- a/cmd/archivmail/cmd_export.go +++ b/cmd/archivmail/cmd_export.go @@ -105,7 +105,7 @@ func runExport(args []string) { } backend := cfg.Index.Backend if backend == "" { - backend = "xapian" + backend = "manticore" } // The fulltext index is only needed when exporting via search filters. var idx index.Indexer diff --git a/cmd/archivmail/cmd_import.go b/cmd/archivmail/cmd_import.go index ca20161..7d24c07 100644 --- a/cmd/archivmail/cmd_import.go +++ b/cmd/archivmail/cmd_import.go @@ -76,7 +76,7 @@ func runImport(args []string) { } backend := cfg.Index.Backend if backend == "" { - backend = "xapian" + backend = "manticore" } var idxMgr index.TenantIndexer diff --git a/cmd/archivmail/cmd_import_piler.go b/cmd/archivmail/cmd_import_piler.go index 045801c..3806a50 100644 --- a/cmd/archivmail/cmd_import_piler.go +++ b/cmd/archivmail/cmd_import_piler.go @@ -81,7 +81,7 @@ func runImportPiler(args []string) { } backend := cfg.Index.Backend if backend == "" { - backend = "xapian" + backend = "manticore" } var idxMgr index.TenantIndexer if backend == "manticore" { diff --git a/cmd/archivmail/cmd_reindex.go b/cmd/archivmail/cmd_reindex.go index 4c14e45..7df6152 100644 --- a/cmd/archivmail/cmd_reindex.go +++ b/cmd/archivmail/cmd_reindex.go @@ -44,7 +44,7 @@ func runReindex(args []string) { indexBackend := cfg.Index.Backend if indexBackend == "" { - indexBackend = "xapian" + indexBackend = "manticore" } batchSize := cfg.Index.BatchSize if batchSize <= 0 { diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 291a020..689f52a 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -40,9 +40,9 @@ func newTestEnv(t *testing.T) *testEnv { t.Fatal(err) } - idx, err := index.New(filepath.Join(dir, "index"), 100, "xapian") + idx, err := index.New(filepath.Join(dir, "index"), 100, "manticore") if err != nil { - t.Skip("xapian not available:", err) + t.Skip("manticore not available:", err) } dsn := os.Getenv("TEST_DATABASE_URL") diff --git a/internal/index/index.go b/internal/index/index.go index cc345dd..cfa6315 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -68,7 +68,7 @@ type Indexer interface { // updates of the OCR-extracted attachment text. Optional add-on to Indexer: // callers should type-assert and degrade gracefully if not supported. // -// PROJ-35: Manticore implements this; legacy Xapian does not. +// PROJ-35: implemented by the Manticore backend. type AttachmentTextUpdater interface { UpdateAttachmentText(mailID, text string) error } @@ -82,20 +82,29 @@ type AttachmentTextReader interface { } // TenantIndexer manages per-tenant Indexer instances. -// Implemented by ManticoreTenantManager (primary) and TenantIndexManager (legacy Xapian). +// Implemented by ManticoreTenantManager. type TenantIndexer interface { ForTenant(tenantID *int64) Indexer Global() Indexer Close() error } -// New creates an Indexer for the specified backend. -// Deprecated: use NewManticoreTenantManager instead. +// DefaultManticoreDSN is the default Manticore connection string used when no +// explicit DSN is configured. +const DefaultManticoreDSN = "manticore@tcp(127.0.0.1:9306)/?charset=utf8mb4" + +// New creates a single (global) Indexer for the specified backend. +// Only "manticore" is supported. For per-tenant indexing prefer +// NewManticoreTenantManager directly. func New(dir string, batchSize int, backend string) (Indexer, error) { switch backend { - case "xapian": - return newXapian(dir) + case "manticore": + m, err := NewManticoreTenantManager(DefaultManticoreDSN) + if err != nil { + return nil, err + } + return m.Global(), nil default: - return nil, fmt.Errorf("unknown index backend: %q (use manticore via NewManticoreTenantManager)", backend) + return nil, fmt.Errorf("unknown index backend: %q (only \"manticore\" is supported)", backend) } } diff --git a/internal/index/index_test.go b/internal/index/index_test.go index 390c297..a0d1d39 100644 --- a/internal/index/index_test.go +++ b/internal/index/index_test.go @@ -2,191 +2,13 @@ package index_test import ( "testing" - "time" "archivmail/internal/index" ) -// newXapianIndex creates a temporary Xapian index for testing. -func newXapianIndex(t *testing.T) index.Indexer { - t.Helper() - idx, err := index.New(t.TempDir(), 100, "xapian") - if err != nil { - t.Skip("xapian not available:", err) - } - t.Cleanup(func() { idx.Close() }) - return idx -} - -func seedDocs(t *testing.T, idx index.Indexer) { - t.Helper() - docs := []index.MailDocument{ - { - ID: "aaa111", - From: "alice@example.com", - To: "bob@example.com", - Subject: "Invoice Q1-2026", - Body: "Please find attached the invoice for January.", - Date: time.Date(2026, 1, 15, 10, 0, 0, 0, time.UTC), - Size: 1024, - }, - { - ID: "bbb222", - From: "bob@example.com", - To: "alice@example.com charlie@example.com", - Subject: "Meeting Agenda", - Body: "Agenda for the quarterly review meeting.", - Date: time.Date(2026, 2, 1, 9, 0, 0, 0, time.UTC), - Size: 512, - }, - { - ID: "ccc333", - From: "charlie@example.com", - To: "alice@example.com", - Subject: "Offer with attachment", - Body: "Please review the attached offer document.", - AttachNames: "offer.pdf", - HasAttachment: true, - Date: time.Date(2026, 3, 1, 14, 0, 0, 0, time.UTC), - Size: 8192, - }, - } - for _, d := range docs { - if err := idx.IndexSync(d); err != nil { - t.Fatalf("IndexSync %s: %v", d.ID, err) - } - } -} - -func TestIndexAndSearchFulltext(t *testing.T) { - idx := newXapianIndex(t) - seedDocs(t, idx) - - result, err := idx.Search(index.SearchRequest{Query: "invoice", PageSize: 10}) - if err != nil { - t.Fatalf("Search: %v", err) - } - if result.Total == 0 { - t.Error("expected at least 1 hit for 'invoice'") - } - if result.Hits[0].ID != "aaa111" { - t.Errorf("top hit = %q, want aaa111", result.Hits[0].ID) - } -} - -func TestSearchMatchAll(t *testing.T) { - idx := newXapianIndex(t) - seedDocs(t, idx) - - result, err := idx.Search(index.SearchRequest{PageSize: 25}) - if err != nil { - t.Fatalf("Search all: %v", err) - } - if result.Total != 3 { - t.Errorf("expected 3 total hits, got %d", result.Total) - } -} - -func TestSearchFromFilter(t *testing.T) { - idx := newXapianIndex(t) - seedDocs(t, idx) - - result, err := idx.Search(index.SearchRequest{ - From: "alice@example.com", - PageSize: 25, - }) - if err != nil { - t.Fatalf("Search from: %v", err) - } - if result.Total != 1 { - t.Errorf("expected 1 hit from alice, got %d", result.Total) - } -} - -func TestSearchDateRange(t *testing.T) { - idx := newXapianIndex(t) - seedDocs(t, idx) - - from := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) - to := time.Date(2026, 2, 1, 23, 59, 59, 0, time.UTC) - result, err := idx.Search(index.SearchRequest{ - DateFrom: &from, - DateTo: &to, - PageSize: 25, - }) - if err != nil { - t.Fatalf("Search date range: %v", err) - } - if result.Total != 2 { - t.Errorf("expected 2 hits in Jan-Feb 2026, got %d", result.Total) - } -} - -func TestSearchOwnEmail(t *testing.T) { - idx := newXapianIndex(t) - seedDocs(t, idx) - - // charlie@example.com sent 1 mail and received 1 mail = should see 2 - result, err := idx.Search(index.SearchRequest{ - OwnEmail: "charlie@example.com", - PageSize: 25, - }) - if err != nil { - t.Fatalf("Search OwnEmail: %v", err) - } - if result.Total < 1 { - t.Errorf("charlie should see at least 1 mail, got %d", result.Total) - } -} - -func TestSearchPagination(t *testing.T) { - idx := newXapianIndex(t) - seedDocs(t, idx) - - page0, _ := idx.Search(index.SearchRequest{PageSize: 2, Page: 0}) - page1, _ := idx.Search(index.SearchRequest{PageSize: 2, Page: 1}) - - if len(page0.Hits) != 2 { - t.Errorf("page 0: expected 2 hits, got %d", len(page0.Hits)) - } - if len(page1.Hits) != 1 { - t.Errorf("page 1: expected 1 hit, got %d", len(page1.Hits)) - } - // No overlap - if page0.Hits[0].ID == page1.Hits[0].ID { - t.Error("pagination returned duplicate results") - } -} - -func TestDelete(t *testing.T) { - idx := newXapianIndex(t) - seedDocs(t, idx) - - if err := idx.Delete("aaa111"); err != nil { - t.Fatalf("Delete: %v", err) - } - - result, _ := idx.Search(index.SearchRequest{Query: "invoice", PageSize: 10}) - for _, h := range result.Hits { - if h.ID == "aaa111" { - t.Error("deleted document still in results") - } - } -} - func TestUnknownBackend(t *testing.T) { _, err := index.New(t.TempDir(), 10, "elasticsearch") if err == nil { t.Error("expected error for unknown backend") } } - -func TestXapianNotCompiledError(t *testing.T) { - _, err := index.New(t.TempDir(), 10, "xapian") - // Without -tags xapian this must return a helpful error - if err == nil { - t.Log("xapian compiled in — skipping stub error test") - } else { - t.Logf("xapian stub error (expected): %v", err) - } -} diff --git a/internal/index/tenant_manager.go b/internal/index/tenant_manager.go index 82c8fa7..179f766 100644 --- a/internal/index/tenant_manager.go +++ b/internal/index/tenant_manager.go @@ -7,7 +7,8 @@ import ( "sync" ) -// TenantIndexManager manages a pool of Xapian indexes, one per tenant. +// TenantIndexManager manages a pool of single-Indexer instances, one per tenant, +// for filesystem-backed backends created via New(). // Tenant 0 / nil maps to the global index used by superadmin and as a fallback. type TenantIndexManager struct { basePath string diff --git a/internal/index/worker.go b/internal/index/worker.go index c498bca..cc1efbb 100644 --- a/internal/index/worker.go +++ b/internal/index/worker.go @@ -6,8 +6,8 @@ import ( ) // IndexWorker processes MailDocument indexing requests asynchronously via a -// buffered channel. It serialises writes to the underlying Indexer (important -// for Xapian which only allows one writer at a time). +// buffered channel. It serialises writes to the underlying Indexer so that +// backends requiring a single concurrent writer remain safe. type IndexWorker struct { idx Indexer queue chan MailDocument diff --git a/internal/index/xapian.go b/internal/index/xapian.go deleted file mode 100644 index 167057f..0000000 --- a/internal/index/xapian.go +++ /dev/null @@ -1,149 +0,0 @@ -//go:build xapian - -package index - -/* -#cgo pkg-config: xapian-core -#cgo LDFLAGS: -lstdc++ -#include "xapian_wrapper.h" -#include -*/ -import "C" -import ( - "encoding/json" - "fmt" - "unsafe" -) - -type xapianIndex struct { - db *C.XapianDB -} - -func newXapian(dir string) (Indexer, error) { - cdir := C.CString(dir) - defer C.free(unsafe.Pointer(cdir)) - var cerr *C.char - db := C.xapian_open(cdir, 1, &cerr) - if db == nil { - msg := C.GoString(cerr) - C.xapian_free_string(cerr) - return nil, fmt.Errorf("xapian open: %s", msg) - } - return &xapianIndex{db: db}, nil -} - -func (x *xapianIndex) IndexSync(doc MailDocument) error { - cid := C.CString(doc.ID) - defer C.free(unsafe.Pointer(cid)) - cfrom := C.CString(doc.From) - defer C.free(unsafe.Pointer(cfrom)) - cto := C.CString(doc.To) - defer C.free(unsafe.Pointer(cto)) - csubj := C.CString(doc.Subject) - defer C.free(unsafe.Pointer(csubj)) - cbody := C.CString(doc.Body) - defer C.free(unsafe.Pointer(cbody)) - hasAttach := C.int(0) - if doc.HasAttachment { - hasAttach = C.int(1) - } - var cerr *C.char - rc := C.xapian_index(x.db, cid, cfrom, cto, csubj, cbody, C.longlong(doc.Date.Unix()), hasAttach, &cerr) - if rc != 0 { - msg := C.GoString(cerr) - C.xapian_free_string(cerr) - return fmt.Errorf("xapian index: %s", msg) - } - return nil -} - -func (x *xapianIndex) Delete(id string) error { - cid := C.CString(id) - defer C.free(unsafe.Pointer(cid)) - var cerr *C.char - rc := C.xapian_delete(x.db, cid, &cerr) - if rc != 0 { - msg := C.GoString(cerr) - C.xapian_free_string(cerr) - return fmt.Errorf("xapian delete: %s", msg) - } - return nil -} - -func (x *xapianIndex) Search(req SearchRequest) (*SearchResult, error) { - cquery := C.CString(req.Query) - defer C.free(unsafe.Pointer(cquery)) - cfrom := C.CString(req.From) - defer C.free(unsafe.Pointer(cfrom)) - cown := C.CString(req.OwnEmail) - defer C.free(unsafe.Pointer(cown)) - cto := C.CString(req.To) - defer C.free(unsafe.Pointer(cto)) - - var dateFrom, dateTo C.longlong - if req.DateFrom != nil { - dateFrom = C.longlong(req.DateFrom.Unix()) - } - if req.DateTo != nil { - dateTo = C.longlong(req.DateTo.Unix()) - } - page := req.Page - if page < 1 { - page = 1 - } - offset := C.int((page - 1) * req.PageSize) - limit := C.int(req.PageSize) - if limit <= 0 { - limit = 25 - } - - // Sort mode: 0=relevance, 1=date_desc (default), 2=date_asc - sortMode := C.int(1) - switch req.Sort { - case "relevance": - sortMode = C.int(0) - case "date_asc": - sortMode = C.int(2) - } - - // Attachment filter: 0=all, 1=only with, -1=only without - attachFilter := C.int(0) - if req.HasAttachment != nil { - if *req.HasAttachment { - attachFilter = C.int(1) - } else { - attachFilter = C.int(-1) - } - } - - var cerr *C.char - cresult := C.xapian_search(x.db, cquery, cfrom, cown, cto, dateFrom, dateTo, offset, limit, sortMode, attachFilter, &cerr) - if cresult == nil { - msg := C.GoString(cerr) - C.xapian_free_string(cerr) - return nil, fmt.Errorf("xapian search: %s", msg) - } - defer C.xapian_free_string(cresult) - jsonStr := C.GoString(cresult) - - var raw struct { - Total int `json:"total"` - Hits []struct { - ID string `json:"id"` - Score float64 `json:"score"` - } `json:"hits"` - } - if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil { - return nil, fmt.Errorf("xapian parse result: %w", err) - } - hits := make([]Hit, len(raw.Hits)) - for i, h := range raw.Hits { - hits[i] = Hit{ID: h.ID, Score: h.Score} - } - return &SearchResult{Total: raw.Total, Hits: hits}, nil -} - -func (x *xapianIndex) Close() error { - C.xapian_close(x.db) - return nil -} diff --git a/internal/index/xapian_stub.go b/internal/index/xapian_stub.go deleted file mode 100644 index 5785e38..0000000 --- a/internal/index/xapian_stub.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !xapian - -package index - -import "errors" - -func newXapian(dir string) (Indexer, error) { - return nil, errors.New("xapian: not compiled in — rebuild with: go build -tags xapian") -} diff --git a/internal/index/xapian_wrapper.cpp b/internal/index/xapian_wrapper.cpp deleted file mode 100644 index 0a112e1..0000000 --- a/internal/index/xapian_wrapper.cpp +++ /dev/null @@ -1,223 +0,0 @@ -#include "xapian_wrapper.h" -#include -#include -#include -#include -#include -#include - -struct XapianDB { - Xapian::WritableDatabase* wdb; - Xapian::Database* rdb; - bool writable; -}; - -static char* dup_error(const std::string& msg) { - char* s = (char*)malloc(msg.size() + 1); - if (s) memcpy(s, msg.c_str(), msg.size() + 1); - return s; -} - -extern "C" { - -XapianDB* xapian_open(const char* path, int writable, char** err) { - try { - XapianDB* db = new XapianDB{nullptr, nullptr, (bool)writable}; - if (writable) { - db->wdb = new Xapian::WritableDatabase(path, Xapian::DB_CREATE_OR_OPEN); - } else { - db->rdb = new Xapian::Database(path); - } - return db; - } catch (const std::exception& e) { - if (err) *err = dup_error(e.what()); - return nullptr; - } -} - -void xapian_close(XapianDB* db) { - if (!db) return; - if (db->wdb) { db->wdb->close(); delete db->wdb; } - if (db->rdb) { db->rdb->close(); delete db->rdb; } - delete db; -} - -int xapian_index(XapianDB* db, const char* id, const char* from, - const char* to, const char* subject, const char* body, - long long timestamp, int has_attachment, char** err) { - try { - Xapian::Document doc; - Xapian::TermGenerator gen; - gen.set_document(doc); - gen.set_stemmer(Xapian::Stem("en")); - - // Prefix-indexed fields for filtering - gen.index_text(from, 1, "XF"); - gen.index_text(to, 1, "XT"); - gen.index_text(subject, 1, "XS"); - - // Free-text indexed fields - gen.index_text(subject); - gen.increase_termpos(); - gen.index_text(body); - gen.increase_termpos(); - gen.index_text(from); - gen.increase_termpos(); - gen.index_text(to); - - // Boolean term for attachment filter - if (has_attachment) { - doc.add_boolean_term("XHA"); - } - - // Store timestamp for date range queries (value slot 0) - doc.add_value(0, Xapian::sortable_serialise((double)timestamp)); - - // Store ID as document data - doc.set_data(id); - doc.add_boolean_term(std::string("Q") + id); - - db->wdb->replace_document(std::string("Q") + id, doc); - db->wdb->commit(); - return 0; - } catch (const std::exception& e) { - if (err) *err = dup_error(e.what()); - return -1; - } -} - -int xapian_delete(XapianDB* db, const char* id, char** err) { - try { - db->wdb->delete_document(std::string("Q") + id); - db->wdb->commit(); - return 0; - } catch (const std::exception& e) { - if (err) *err = dup_error(e.what()); - return -1; - } -} - -char* xapian_search(XapianDB* db, const char* query_str, - const char* from_filter, const char* own_email, - const char* to_filter, - long long date_from, long long date_to, - int offset, int limit, - int sort_mode, int has_attachment, - char** err) { - try { - Xapian::Database& xdb = db->wdb ? (Xapian::Database&)*db->wdb : *db->rdb; - Xapian::Enquire enquire(xdb); - - Xapian::Query main_query; - - // Full-text query - if (query_str && query_str[0] != '\0') { - Xapian::QueryParser qp; - qp.set_database(xdb); - qp.set_stemmer(Xapian::Stem("en")); - qp.set_stemming_strategy(Xapian::QueryParser::STEM_SOME); - qp.add_prefix("from", "XF"); - qp.add_prefix("to", "XT"); - qp.add_prefix("subject", "XS"); - main_query = qp.parse_query(query_str, - Xapian::QueryParser::FLAG_DEFAULT | - Xapian::QueryParser::FLAG_PARTIAL); - } else { - main_query = Xapian::Query::MatchAll; - } - - // From filter - if (from_filter && from_filter[0] != '\0') { - Xapian::QueryParser qp; - qp.set_database(xdb); - Xapian::Query fq = qp.parse_query(from_filter, - Xapian::QueryParser::FLAG_DEFAULT, "XF"); - main_query = Xapian::Query(Xapian::Query::OP_AND, main_query, fq); - } - - // OwnEmail filter: (from=own OR to=own) - if (own_email && own_email[0] != '\0') { - Xapian::QueryParser qp; - qp.set_database(xdb); - Xapian::Query fq = qp.parse_query(own_email, - Xapian::QueryParser::FLAG_DEFAULT, "XF"); - Xapian::Query tq = qp.parse_query(own_email, - Xapian::QueryParser::FLAG_DEFAULT, "XT"); - Xapian::Query owq(Xapian::Query::OP_OR, fq, tq); - main_query = Xapian::Query(Xapian::Query::OP_AND, main_query, owq); - } - - // To filter - if (to_filter && to_filter[0] != '\0') { - Xapian::QueryParser qp; - qp.set_database(xdb); - Xapian::Query tq = qp.parse_query(to_filter, - Xapian::QueryParser::FLAG_DEFAULT, "XT"); - main_query = Xapian::Query(Xapian::Query::OP_AND, main_query, tq); - } - - // Date range - if (date_from > 0 || date_to > 0) { - double lo = date_from > 0 ? (double)date_from : 0.0; - double hi = date_to > 0 ? (double)date_to : 1e18; - Xapian::Query drq(Xapian::Query::OP_VALUE_RANGE, 0, - Xapian::sortable_serialise(lo), - Xapian::sortable_serialise(hi)); - main_query = Xapian::Query(Xapian::Query::OP_AND, main_query, drq); - } - - // Attachment filter - if (has_attachment == 1) { - Xapian::Query aq("XHA"); - main_query = Xapian::Query(Xapian::Query::OP_AND, main_query, aq); - } else if (has_attachment == -1) { - Xapian::Query aq("XHA"); - main_query = Xapian::Query(Xapian::Query::OP_AND_NOT, main_query, aq); - } - - enquire.set_query(main_query); - - // Sort mode: 0=relevance, 1=date_desc, 2=date_asc - if (sort_mode == 2) { - enquire.set_sort_by_value(0, false); // date ascending - } else if (sort_mode == 0 && query_str && query_str[0] != '\0') { - // relevance: default BM25 ranking (no explicit sort) - } else { - enquire.set_sort_by_value(0, true); // date descending (default) - } - - // Get total count - Xapian::MSet all = enquire.get_mset(0, xdb.get_doccount()); - int total = (int)all.get_matches_estimated(); - - // Get page - Xapian::MSet mset = enquire.get_mset(offset, limit); - - std::ostringstream json; - json << "{\"total\":" << total << ",\"hits\":["; - bool first = true; - for (auto it = mset.begin(); it != mset.end(); ++it) { - if (!first) json << ","; - first = false; - std::string id = it.get_document().get_data(); - double score = it.get_weight(); - json << "{\"id\":\"" << id << "\",\"score\":" << score << "}"; - } - json << "]}"; - - std::string result = json.str(); - char* out = (char*)malloc(result.size() + 1); - memcpy(out, result.c_str(), result.size() + 1); - return out; - - } catch (const std::exception& e) { - if (err) *err = dup_error(e.what()); - return nullptr; - } -} - -void xapian_free_string(char* s) { - free(s); -} - -} // extern "C" diff --git a/internal/index/xapian_wrapper.h b/internal/index/xapian_wrapper.h deleted file mode 100644 index a88956a..0000000 --- a/internal/index/xapian_wrapper.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef XAPIAN_WRAPPER_H -#define XAPIAN_WRAPPER_H - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct XapianDB XapianDB; - -XapianDB* xapian_open(const char* path, int writable, char** err); -void xapian_close(XapianDB* db); - -/* has_attachment: 0=no attachment, 1=has attachment */ -int xapian_index(XapianDB* db, const char* id, const char* from, - const char* to, const char* subject, const char* body, - long long timestamp, int has_attachment, char** err); - -int xapian_delete(XapianDB* db, const char* id, char** err); - -/* Returns JSON string: {"total":N,"hits":[{"id":"...","score":0.9},...]} - Returns NULL on error, sets *err. Caller must free with xapian_free_string. - sort_mode: 0=relevance, 1=date_desc, 2=date_asc - has_attachment: 0=all, 1=only with attachment, -1=only without */ -char* xapian_search(XapianDB* db, const char* query, - const char* from_filter, const char* own_email, - const char* to_filter, - long long date_from, long long date_to, - int offset, int limit, - int sort_mode, int has_attachment, - char** err); - -void xapian_free_string(char* s); - -#ifdef __cplusplus -} -#endif -#endif