bb963a796f
- seedDefaultUsers: generiert kryptographisch zufällige Passwörter (crypto/rand) statt hartkodiertes "archivmailrockz" — Passwörter werden einmalig im Terminal angezeigt und können danach nicht wiederhergestellt werden - generateJTI: verwendet crypto/rand (16 Byte, hex) statt time.UnixNano XOR deadbeef Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
224 lines
7.3 KiB
C++
224 lines
7.3 KiB
C++
#include "xapian_wrapper.h"
|
|
#include <xapian.h>
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
|
|
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"
|