@blamejs/core 0.11.24 → 0.11.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/index.js +5 -0
- package/lib/auth/bot-challenge.js +573 -0
- package/lib/framework-error.js +6 -0
- package/lib/fsm.js +469 -0
- package/lib/guard-mail-query.js +14 -0
- package/lib/mail-agent.js +24 -10
- package/lib/mail-server-mx.js +12 -7
- package/lib/mail-server-submission.js +199 -11
- package/lib/mail-store-fts.js +394 -0
- package/lib/mail-store.js +142 -4
- package/lib/money.js +699 -0
- package/lib/webhook.js +229 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-store.js
CHANGED
|
@@ -63,6 +63,7 @@ var cryptoField = require("./crypto-field");
|
|
|
63
63
|
var safeMime = require("./safe-mime");
|
|
64
64
|
var safeSql = require("./safe-sql");
|
|
65
65
|
var guardMessageId = require("./guard-message-id");
|
|
66
|
+
var mailStoreFts = require("./mail-store-fts");
|
|
66
67
|
var { defineClass } = require("./framework-error");
|
|
67
68
|
|
|
68
69
|
var MailStoreError = defineClass("MailStoreError", { alwaysPermanent: true });
|
|
@@ -126,6 +127,7 @@ function create(opts) {
|
|
|
126
127
|
var qFolders = safeSql.quoteIdentifier(prefix + "_folders", "sqlite");
|
|
127
128
|
var qFlags = safeSql.quoteIdentifier(prefix + "_flags", "sqlite");
|
|
128
129
|
var qQuota = safeSql.quoteIdentifier(prefix + "_quota", "sqlite");
|
|
130
|
+
var qFts = safeSql.quoteIdentifier(prefix + "_messages_fts", "sqlite");
|
|
129
131
|
var messagesTable = prefix + "_messages";
|
|
130
132
|
|
|
131
133
|
var maxMessageBytes = opts.maxMessageBytes !== undefined ? opts.maxMessageBytes : DEFAULT_MAX_MESSAGE_BYTES;
|
|
@@ -147,7 +149,7 @@ function create(opts) {
|
|
|
147
149
|
});
|
|
148
150
|
|
|
149
151
|
if (doInit) {
|
|
150
|
-
_ensureSchema(db, qMsgs, qFolders, qFlags, qQuota);
|
|
152
|
+
_ensureSchema(db, qMsgs, qFolders, qFlags, qQuota, qFts);
|
|
151
153
|
_ensureDefaultFolders(db, qFolders);
|
|
152
154
|
}
|
|
153
155
|
|
|
@@ -195,12 +197,27 @@ function create(opts) {
|
|
|
195
197
|
" WHERE folder_id = ? AND objectid IN (SELECT value FROM json_each(?))");
|
|
196
198
|
var stmtDeleteMsg = db.prepare("DELETE FROM " + qMsgs + " WHERE objectid = ?");
|
|
197
199
|
var stmtDeleteFlags = db.prepare("DELETE FROM " + qFlags + " WHERE objectid = ?");
|
|
200
|
+
// Sealed-token FTS5 prepared statements — index sync runs in the
|
|
201
|
+
// same transaction window as the canonical row mutation so a crash
|
|
202
|
+
// between the two cannot leave the FTS index out of step with the
|
|
203
|
+
// messages table. See lib/mail-store-fts.js for the tokenize +
|
|
204
|
+
// vault-salted-hash transform applied here.
|
|
205
|
+
var stmtInsertFts = db.prepare(
|
|
206
|
+
"INSERT INTO " + qFts + " (objectid, subject_toks, addr_toks, body_toks) VALUES (?, ?, ?, ?)");
|
|
207
|
+
var stmtDeleteFts = db.prepare("DELETE FROM " + qFts + " WHERE objectid = ?");
|
|
198
208
|
|
|
199
209
|
return {
|
|
200
210
|
appendMessage: function (folderName, rawBytes, appendOpts) {
|
|
201
|
-
|
|
211
|
+
// Wrap canonical row insert + FTS row insert in a single backend
|
|
212
|
+
// transaction so a crash / FTS-row failure CANNOT leave a message
|
|
213
|
+
// persisted but unsearchable (state drift). better-sqlite3-style
|
|
214
|
+
// backends expose `.transaction(fn)()`; backends without
|
|
215
|
+
// transactions fall back to per-statement (the FTS insert is the
|
|
216
|
+
// last write, so partial state == still consistent to the reader).
|
|
217
|
+
var args = {
|
|
202
218
|
db: db, qMsgs: qMsgs, qFlags: qFlags, messagesTable: messagesTable,
|
|
203
219
|
stmtInsertMsg: stmtInsertMsg,
|
|
220
|
+
stmtInsertFts: stmtInsertFts,
|
|
204
221
|
stmtBumpFolderModseq: stmtBumpFolderModseq,
|
|
205
222
|
stmtGetFolderByName: stmtGetFolderByName,
|
|
206
223
|
stmtFindThreadByMsgId: stmtFindThreadByMsgId,
|
|
@@ -209,7 +226,13 @@ function create(opts) {
|
|
|
209
226
|
safeMimeOpts: safeMimeOpts,
|
|
210
227
|
maxMessageBytes: maxMessageBytes,
|
|
211
228
|
maxBodyBytes: maxBodyBytes,
|
|
212
|
-
}
|
|
229
|
+
};
|
|
230
|
+
if (typeof db.transaction === "function") {
|
|
231
|
+
var result;
|
|
232
|
+
db.transaction(function () { result = _appendMessage(args); })();
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
return _appendMessage(args);
|
|
213
236
|
},
|
|
214
237
|
fetchByObjectId: function (folderName, objectid) {
|
|
215
238
|
return _fetchByObjectId({
|
|
@@ -220,6 +243,97 @@ function create(opts) {
|
|
|
220
243
|
folderName: folderName, objectid: objectid,
|
|
221
244
|
});
|
|
222
245
|
},
|
|
246
|
+
/**
|
|
247
|
+
* search — sealed-token full-text search inside a single folder.
|
|
248
|
+
*
|
|
249
|
+
* Composes the FTS5 virtual table populated by `appendMessage`.
|
|
250
|
+
* Each filter term is tokenized + vault-salted-hashed exactly like
|
|
251
|
+
* the index side, then issued as an FTS5 `MATCH` expression
|
|
252
|
+
* intersected with the modseq + flag window. Result rows carry the
|
|
253
|
+
* SAME shape as `queryByModseq` so operators iterate either path
|
|
254
|
+
* symmetrically.
|
|
255
|
+
*
|
|
256
|
+
* `filter` accepts (any subset; all present terms AND-combine):
|
|
257
|
+
* - text: match across subject + addr + body
|
|
258
|
+
* - subject: match against `subject_toks` column only
|
|
259
|
+
* - body: match against `body_toks` column only
|
|
260
|
+
* - from / to: match against `addr_toks`
|
|
261
|
+
* - sinceModseq: integer floor
|
|
262
|
+
* - limit: result cap (default 100, hard cap 1000)
|
|
263
|
+
*
|
|
264
|
+
* When NO text-side filter is present, falls through to the
|
|
265
|
+
* `queryByModseq` path — search is purely additive on the existing
|
|
266
|
+
* modseq cursor.
|
|
267
|
+
*/
|
|
268
|
+
search: function (folderName, filter) {
|
|
269
|
+
var folder = stmtGetFolderByName.get(folderName);
|
|
270
|
+
if (!folder) {
|
|
271
|
+
throw new MailStoreError("mail-store/no-folder",
|
|
272
|
+
"search: folder '" + folderName + "' not found");
|
|
273
|
+
}
|
|
274
|
+
var f = filter || {};
|
|
275
|
+
var sinceModseq = f.sinceModseq || 0;
|
|
276
|
+
var limit = f.limit || 100;
|
|
277
|
+
if (limit > 1000) limit = 1000; // allow:raw-byte-literal — query row cap, not bytes
|
|
278
|
+
|
|
279
|
+
var matchClauses = [];
|
|
280
|
+
function addMatch(filterKey, term) {
|
|
281
|
+
if (!term) return;
|
|
282
|
+
var m = mailStoreFts.columnAndFieldFor(filterKey);
|
|
283
|
+
if (!m) return;
|
|
284
|
+
var expr = mailStoreFts.buildMatchExpression(messagesTable, m.field, term);
|
|
285
|
+
if (expr) matchClauses.push(m.column + ":(" + expr + ")");
|
|
286
|
+
}
|
|
287
|
+
if (f.subject) addMatch("subject", f.subject);
|
|
288
|
+
if (f.body) addMatch("body", f.body);
|
|
289
|
+
if (f.from) addMatch("from", f.from);
|
|
290
|
+
if (f.to) addMatch("to", f.to);
|
|
291
|
+
if (f.text) {
|
|
292
|
+
var perCol = ["subject", "body", "from"].map(function (key) {
|
|
293
|
+
var m = mailStoreFts.columnAndFieldFor(key);
|
|
294
|
+
var perColExpr = mailStoreFts.buildMatchExpression(messagesTable, m.field, f.text);
|
|
295
|
+
return perColExpr ? "(" + m.column + ":(" + perColExpr + "))" : null;
|
|
296
|
+
}).filter(Boolean);
|
|
297
|
+
if (perCol.length > 0) {
|
|
298
|
+
matchClauses.push("(" + perCol.join(" OR ") + ")");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (matchClauses.length === 0) {
|
|
303
|
+
var fallback = stmtQueryByModseq.all(folder.id, sinceModseq, limit);
|
|
304
|
+
return {
|
|
305
|
+
rows: fallback.map(function (r) {
|
|
306
|
+
return {
|
|
307
|
+
objectid: r.objectid, modseq: r.modseq, sizeBytes: r.size_bytes,
|
|
308
|
+
internalDate: r.internal_date, legalHold: r.legal_hold === 1,
|
|
309
|
+
};
|
|
310
|
+
}),
|
|
311
|
+
nextModseq: fallback.length > 0 ? fallback[fallback.length - 1].modseq : sinceModseq,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
var matchExpr = matchClauses.join(" AND ");
|
|
316
|
+
// FTS5 MATCH binds to the virtual-table name — aliases / joined-
|
|
317
|
+
// table refs are parsed as ordinary column refs and fail. The
|
|
318
|
+
// IN-subquery shape sidesteps that.
|
|
319
|
+
var sql =
|
|
320
|
+
"SELECT objectid, modseq, size_bytes, internal_date, legal_hold " +
|
|
321
|
+
"FROM " + qMsgs + " " +
|
|
322
|
+
"WHERE folder_id = ? AND modseq > ? " +
|
|
323
|
+
"AND objectid IN (SELECT objectid FROM " + qFts + " WHERE " + qFts + " MATCH ?) " +
|
|
324
|
+
"ORDER BY modseq ASC LIMIT ?";
|
|
325
|
+
var rows = db.prepare(sql).all(folder.id, sinceModseq, matchExpr, limit);
|
|
326
|
+
return {
|
|
327
|
+
rows: rows.map(function (r) {
|
|
328
|
+
return {
|
|
329
|
+
objectid: r.objectid, modseq: r.modseq, sizeBytes: r.size_bytes,
|
|
330
|
+
internalDate: r.internal_date, legalHold: r.legal_hold === 1,
|
|
331
|
+
};
|
|
332
|
+
}),
|
|
333
|
+
nextModseq: rows.length > 0 ? rows[rows.length - 1].modseq : sinceModseq,
|
|
334
|
+
matchExpr: matchExpr,
|
|
335
|
+
};
|
|
336
|
+
},
|
|
223
337
|
queryByModseq: function (folderName, queryOpts) {
|
|
224
338
|
var folder = stmtGetFolderByName.get(folderName);
|
|
225
339
|
if (!folder) {
|
|
@@ -368,6 +482,7 @@ function create(opts) {
|
|
|
368
482
|
function _runTxn() {
|
|
369
483
|
for (var di = 0; di < toDelete.length; di += 1) {
|
|
370
484
|
stmtDeleteFlags.run(toDelete[di].objectid);
|
|
485
|
+
stmtDeleteFts.run(toDelete[di].objectid);
|
|
371
486
|
stmtDeleteMsg.run(toDelete[di].objectid);
|
|
372
487
|
totalBytes += toDelete[di].size_bytes || 0;
|
|
373
488
|
}
|
|
@@ -512,6 +627,18 @@ function _appendMessage(args) {
|
|
|
512
627
|
args.stmtBumpFolderModseq.run(modseq, args.folderName);
|
|
513
628
|
args.stmtBumpQuota.run(folder.id, buf.length, 1);
|
|
514
629
|
|
|
630
|
+
// FTS index update — tokenize the PRE-seal plaintext, hash each
|
|
631
|
+
// token with the per-deployment vault salt, insert into the FTS5
|
|
632
|
+
// virtual table.
|
|
633
|
+
var ftsRow = mailStoreFts.rowFromMessage(args.messagesTable, {
|
|
634
|
+
objectid: objectid,
|
|
635
|
+
subject: subject,
|
|
636
|
+
from: fromAddr,
|
|
637
|
+
to: toAddrs,
|
|
638
|
+
body: bodyText,
|
|
639
|
+
});
|
|
640
|
+
args.stmtInsertFts.run(ftsRow.objectid, ftsRow.subject_toks, ftsRow.addr_toks, ftsRow.body_toks);
|
|
641
|
+
|
|
515
642
|
return { objectid: objectid, modseq: modseq, sizeBytes: buf.length, threadRootId: threadRootId };
|
|
516
643
|
}
|
|
517
644
|
|
|
@@ -706,7 +833,7 @@ function _normalizeMsgId(s) {
|
|
|
706
833
|
|
|
707
834
|
// ---- Schema bootstrap ----------------------------------------------------
|
|
708
835
|
|
|
709
|
-
function _ensureSchema(db, qMsgs, qFolders, qFlags, qQuota) {
|
|
836
|
+
function _ensureSchema(db, qMsgs, qFolders, qFlags, qQuota, qFts) {
|
|
710
837
|
// Folders table — created first since messages reference folder_id.
|
|
711
838
|
db.prepare(
|
|
712
839
|
"CREATE TABLE IF NOT EXISTS " + qFolders + " (" +
|
|
@@ -775,6 +902,13 @@ function _ensureSchema(db, qMsgs, qFolders, qFlags, qQuota) {
|
|
|
775
902
|
"cap_count INTEGER, " +
|
|
776
903
|
"FOREIGN KEY(folder_id) REFERENCES " + qFolders + "(id))"
|
|
777
904
|
).run();
|
|
905
|
+
|
|
906
|
+
// Sealed-token FTS5 virtual table. The token-hash transform lives in
|
|
907
|
+
// `lib/mail-store-fts.js`; this is the storage layer. Tokenizer is
|
|
908
|
+
// `unicode61 remove_diacritics 2` so FTS5's segmenter splits hash-
|
|
909
|
+
// tokens on whitespace exactly — hashes are ASCII-hex-only, so no
|
|
910
|
+
// Unicode case-fold runs at MATCH time.
|
|
911
|
+
db.prepare(mailStoreFts.createSql(qFts)).run();
|
|
778
912
|
}
|
|
779
913
|
|
|
780
914
|
function _ensureDefaultFolders(db, qFolders) {
|
|
@@ -788,4 +922,8 @@ module.exports = {
|
|
|
788
922
|
create: create,
|
|
789
923
|
DEFAULT_FOLDERS: DEFAULT_FOLDERS,
|
|
790
924
|
MailStoreError: MailStoreError,
|
|
925
|
+
// Sealed-token FTS substrate. Exposed for adjacent primitives (e.g.
|
|
926
|
+
// wire-protocol adapters translating IMAP SEARCH TEXT into the
|
|
927
|
+
// store's FTS5 column expression).
|
|
928
|
+
fts: mailStoreFts,
|
|
791
929
|
};
|