preparation for mail filtering
[chiroptera.git] / chibackend / sqbase.d
blob705763a78b83a206b00a6238430eb6b003bae5c0
1 /* E-Mail Client
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module chibackend.sqbase is aliced;
18 private:
20 //version = fts5_use_porter;
22 // do not use, for testing only!
23 // and it seems to generate bigger files, lol
24 //version = use_balz;
26 // use libdeflate instead of zlib
27 // see https://github.com/ebiggers/libdeflate
28 // around 2 times slower on level 9 than zlib, resulting size is 5MB less
29 // around 3 times slower on level 12, resulting size it 10MB less
30 // totally doesn't worth it
31 //version = use_libdeflate;
33 // use libxpack instead of zlib
34 // see https://github.com/ebiggers/xpack
35 // it supports buffers up to 2^19 (524288) bytes (see https://github.com/ebiggers/xpack/issues/1)
36 // therefore it is useless (the resulting file is bigger than with zlib)
37 //version = use_libxpack;
39 // just for fun
40 // see https://github.com/jibsen/brieflz
41 // it has spurious slowdowns, and so is 4 times slower than zlib, with worser compression
42 //version = use_libbrieflz;
44 // apple crap; i just wanted to see how bad it is ;-)
45 // speed is comparable with zlib, compression is shittier by 60MB; crap
46 //version = use_liblzfse;
48 // just for fun
49 //version = use_lzjb;
51 // just for fun, slightly better than lzjb
52 //version = use_lz4;
54 // some compressors from wimlib
55 // see https://wimlib.net/
56 // only one can be selected!
57 // 15 times slower than zlib, much worser compression (~100MB bigger)
58 //version = use_libwim_lzms; // this supports chunks up to our maximum blob size
59 // two times faster than lzms, compression is still awful
60 //version = use_libwim_lzx; // this supports chunks up to 2MB; more-or-less useful
61 // quite fast (because it refuses to compress anything bigger than 64KB); compression is most awful
62 //version = use_libwim_xpress; // this supports chunks up to 64KB; useless
64 // oh, because why not?
65 // surprisingly good (but not as good as zlib), and lightning fast on default compression level
66 // sadly, requires external lib
67 //version = use_zstd;
69 private import std.digest.ripemd;
70 private import iv.cmdcon;
71 private import iv.strex;
72 private import iv.sq3;
73 private import iv.timer;
74 private import iv.vfs.io;
75 private import iv.vfs.util;
77 private import chibackend.mbuilder : DynStr;
78 private import chibackend.parse;
79 private import chibackend.decode;
80 //private import iv.utfutil;
81 //private import iv.vfs.io;
83 version(use_libdeflate) private import chibackend.pack.libdeflate;
84 else version(use_balz) private import iv.balz;
85 else version(use_libxpack) private import chibackend.pack.libxpack;
86 else version(use_libbrieflz) private import chibackend.pack.libbrieflz;
87 else version(use_liblzfse) private import chibackend.pack.liblzfse;
88 else version(use_lzjb) private import chibackend.pack.lzjb;
89 else version(use_libwim_lzms) private import chibackend.pack.libwim;
90 else version(use_libwim_lzx) private import chibackend.pack.libwim;
91 else version(use_libwim_xpress) private import chibackend.pack.libwim;
92 else version(use_lz4) private import chibackend.pack.liblz4;
93 else version(use_zstd) private import chibackend.pack.libzstd;
96 version(use_zstd) {
97 public enum ChiroDefaultPackLevel = 6;
98 } else {
99 public enum ChiroDefaultPackLevel = 9;
103 // use `MailDBPath()` to get/set it
104 private __gshared string ExpandedMailDBPath = null;
105 public __gshared int ChiroCompressionLevel = ChiroDefaultPackLevel;
106 public __gshared bool ChiroSQLiteSilent = false;
108 public __gshared bool ChiroTimerEnabled = false;
109 private __gshared Timer chiTimer = Timer(false);
110 private __gshared char[] chiTimerMsg = null;
112 // opened databases
113 public __gshared Database dbStore; // message store db
114 public __gshared Database dbView; // message view db
115 public __gshared Database dbConf; // config/options db
118 public enum Appearance {
119 Ignore = -1, // can be used to ignore messages in thread view
120 Unread = 0,
121 Read = 1,
122 SoftDeleteFilter = 2, // soft-delete from filter
123 SoftDeleteUser = 3, // soft-delete by user
124 SoftDeletePurge = 4, // soft-delete by user (will be purged on folder change)
127 public enum Mute {
128 Normal = 0,
129 Message = 1, /* single message */
130 ThreadStart = 2, /* all children starting from this */
131 ThreadOther = 3, /* muted by some parent */
134 public bool isSoftDeleted (const int appearance) pure nothrow @safe @nogc {
135 pragma(inline, true);
136 return
137 appearance >= Appearance.SoftDeleteFilter &&
138 appearance <= Appearance.SoftDeletePurge;
143 There are several added SQLite functions:
145 ChiroPack(data[, compratio])
146 ===============
148 This tries to compress the given data, and returns a compressed blob.
149 If `compratio` is negative or zero, do not compress anything.
152 ChiroUnpack(data)
153 =================
155 This decompresses the blob compressed with `ChiroPack()`. It is (usually) safe to pass
156 non-compressed data to this function.
159 ChiroNormCRLF(content)
160 ======================
162 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
163 Removes trailing blanks.
166 ChiroNormHeaders(content)
167 =========================
169 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
170 Then replaces 'space, LF' with a single space (joins multiline headers).
171 Removes trailing blanks.
174 ChiroExtractHeaders(content)
175 ============================
177 Can be used to extract headers from the message.
178 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
179 Then replaces 'space, LF' with a single space (joins multiline headers).
180 Removes trailing blanks.
183 ChiroExtractBody(content)
184 =========================
186 Can be used to extract body from the message.
187 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
188 Then replaces 'space, LF' with a single space (joins multiline headers).
189 Removes trailing blanks and final dot.
193 public enum OptionsDBName = "chiroptera.db";
194 public enum StorageDBName = "chistore.db";
195 public enum SupportDBName = "chiview.db";
198 // ////////////////////////////////////////////////////////////////////////// //
199 private enum CommonPragmas = `
200 PRAGMA case_sensitive_like = OFF;
201 PRAGMA foreign_keys = OFF;
202 PRAGMA locking_mode = NORMAL; /*EXCLUSIVE;*/
203 PRAGMA secure_delete = OFF;
204 PRAGMA threads = 3;
205 PRAGMA trusted_schema = OFF;
206 PRAGMA writable_schema = OFF;
209 enum CommonPragmasRO = CommonPragmas~`
210 PRAGMA temp_store = MEMORY; /*DEFAULT*/ /*FILE*/
213 enum CommonPragmasRW = CommonPragmas~`
214 PRAGMA application_id = 1128810834; /*CHIR*/
215 PRAGMA auto_vacuum = NONE;
216 PRAGMA encoding = "UTF-8";
217 PRAGMA temp_store = DEFAULT;
218 --PRAGMA journal_mode = WAL; /*OFF;*/
219 --PRAGMA journal_mode = DELETE; /*OFF;*/
220 PRAGMA synchronous = NORMAL; /*OFF;*/
223 enum CommonPragmasRecreate = `
224 PRAGMA locking_mode = EXCLUSIVE;
225 PRAGMA journal_mode = OFF;
226 PRAGMA synchronous = OFF;
229 static immutable dbpragmasRO = CommonPragmasRO;
231 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
232 // use the smallest page size, because we don't need to perform alot of selects here
233 static immutable dbpragmasRWStorage = "PRAGMA page_size = 512;"~CommonPragmasRW~"PRAGMA journal_mode = DELETE;";
234 static immutable dbpragmasRWStorageRecreate = dbpragmasRWStorage~CommonPragmasRecreate;
236 // use slightly bigger pages
237 // funny, smaller pages leads to bigger files
238 static immutable dbpragmasRWSupport = "PRAGMA page_size = 4096;"~CommonPragmasRW~"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;";
239 static immutable dbpragmasRWSupportRecreate = dbpragmasRWSupport~CommonPragmasRecreate;
241 // smaller page size is ok
242 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
243 static immutable dbpragmasRWOptions = "PRAGMA page_size = 512;"~CommonPragmasRW~"PRAGMA journal_mode = /*DELETE*/WAL; PRAGMA synchronous = NORMAL;";
244 static immutable dbpragmasRWOptionsRecreate = dbpragmasRWOptions~CommonPragmasRecreate;
247 enum msgTagNameCheckSQL = `
248 WITH RECURSIVE tagtable(tag, rest) AS (
249 VALUES('', NEW.tags||'|')
250 UNION ALL
251 SELECT
252 SUBSTR(rest, 0, INSTR(rest, '|')),
253 SUBSTR(rest, INSTR(rest, '|')+1)
254 FROM tagtable
255 WHERE rest <> '')
256 SELECT
257 (CASE
258 WHEN tag = '/' THEN RAISE(FAIL, 'tag name violation (root tags are not allowed)')
259 WHEN LENGTH(tag) = 1 THEN RAISE(FAIL, 'tag name violation (too short tag name)')
260 WHEN SUBSTR(tag, LENGTH(tag)) = '/' THEN RAISE(FAIL, 'tag name violation (tag should not end with a slash)')
261 END)
262 FROM tagtable
263 WHERE tag <> '';
266 // main storage and support databases will be in different files
267 static immutable string schemaStorage = `
268 -- deleted messages have empty headers and body
269 -- this is so uids will remain unique on inserting
270 -- tags are used to associate the message with various folders, and stored here for rebuild purposes
271 -- the frontend will use the separate "tags" table to select messages
272 -- deleted messages must not have any tags, and should contain no other data
273 -- (keeping the data is harmless, it simply sits there and takes space)
274 CREATE TABLE IF NOT EXISTS messages (
275 uid INTEGER PRIMARY KEY /* rowid, never zero */
276 , tags TEXT DEFAULT NULL /* associated message tags, '|'-separated; case-sensitive, no extra whitespaces or '||'! */
277 -- article data; MUST contain the ending dot, and be properly dot-stuffed
278 -- basically, this is "what we had received, as is" (*WITH* the ending dot!)
279 -- there is no need to normalize it in any way (and you *SHOULD NOT* do it!)
280 -- it should be compressed with "ChiroPack()", and extracted with "ChiroUnpack()"
281 , data BLOB
284 -- check tag constraints
285 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_insert
286 BEFORE INSERT ON messages
287 FOR EACH ROW
288 BEGIN`~msgTagNameCheckSQL~`
289 END;
291 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_update_tags
292 BEFORE UPDATE OF tags ON messages
293 FOR EACH ROW
294 BEGIN`~msgTagNameCheckSQL~`
295 END;
298 static immutable string schemaStorageIndex = `
302 static immutable string schemaOptions = `
303 -- use "autoincrement" to allow account deletion
304 CREATE TABLE IF NOT EXISTS accounts (
305 accid INTEGER PRIMARY KEY AUTOINCREMENT /* unique, never zero */
306 , checktime INTEGER NOT NULL DEFAULT 15 /* check time, in minutes */
307 , nosendauth INTEGER NOT NULL DEFAULT 0 /* turn off authentication on sending? */
308 , debuglog INTEGER NOT NULL DEFAULT 0 /* do debug logging? */
309 , nocheck INTEGER NOT NULL DEFAULT 0 /* disable checking? */
310 , nntplastindex INTEGER NOT NULL DEFAULT 0 /* last seen article index for NNTP groups */
311 , name TEXT NOT NULL UNIQUE /* account name; lowercase alphanum, '_', '-', '.' */
312 , recvserver TEXT NOT NULL /* server for receiving messages */
313 , sendserver TEXT NOT NULL /* server for sending messages */
314 , user TEXT NOT NULL /* pop3 user name */
315 , pass TEXT NOT NULL /* pop3 password, empty for no authorisation */
316 , realname TEXT NOT NULL /* user name for e-mail headers */
317 , email TEXT NOT NULL /* account e-mail address (full, name@host) */
318 , inbox TEXT NOT NULL /* inbox tag, usually "/accname/inbox", or folder for nntp */
319 , nntpgroup TEXT NOT NULL DEFAULT '' /* nntp group name for NNTP accounts; if empty, this is POP3 account */
323 CREATE TABLE IF NOT EXISTS options (
324 name TEXT NOT NULL UNIQUE
325 , value TEXT
329 CREATE TABLE IF NOT EXISTS addressbook (
330 nick TEXT NOT NULL UNIQUE /* short nick for this address book entry */
331 , name TEXT NOT NULL DEFAULT ''
332 , email TEXT NOT NULL
333 , notes TEXT DEFAULT NULL
337 -- twits by email/name
338 CREATE TABLE IF NOT EXISTS emailtwits (
339 etwitid INTEGER PRIMARY KEY
340 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
341 , email TEXT /* if both name and email present, use only email */
342 , name TEXT /* name to twit by */
343 , title TEXT /* optional title */
344 , notes TEXT /* notes; often URL */
347 -- twits by msgids
348 CREATE TABLE IF NOT EXISTS msgidtwits (
349 mtwitid INTEGER PRIMARY KEY
350 , etwitid INTEGER /* parent mail twit, if any */
351 , automatic INTEGER DEFAULT 1 /* added by message filtering, not from .rc? */
352 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
353 , msgid TEXT /* message used to set twit */
357 -- message filters
358 CREATE TABLE IF NOT EXISTS filters (
359 filterid INTEGER PRIMARY KEY
360 , valid INTEGER NOT NULL DEFAULT 1 /* is this filter valid? used to skip bad filters */
361 , idx INTEGER NOT NULL DEFAULT 0 /* used for ordering */
362 , post INTEGER NOT NULL DEFAULT 0 /* post-spamcheck filter? */
363 , hitcount INTEGER NOT NULL DEFAULT 0 /* for statistics */
364 , name TEXT NOT NULL UNIQUE /* filter name */
365 , body TEXT /* filter text */
368 CREATE TRIGGER IF NOT EXISTS filters_new_index
369 AFTER INSERT ON filters
370 FOR EACH ROW
371 BEGIN
372 UPDATE filters SET idx=(SELECT MAX(idx)+10 FROM filters)
373 WHERE NEW.idx=0 AND filterid=NEW.filterid;
374 END;
377 static immutable string schemaOptionsIndex = `
378 -- no need to, "UNIQUE" automaptically creates it
379 --CREATE INDEX IF NOT EXISTS accounts_name ON accounts(name);
381 -- this index in implicit
382 --CREATE INDEX IF NOT EXISTS options_name ON options(name);
384 CREATE INDEX IF NOT EXISTS emailtwits_email ON emailtwits(email);
385 CREATE INDEX IF NOT EXISTS emailtwits_name ON emailtwits(name);
386 CREATE UNIQUE INDEX IF NOT EXISTS emailtwits_email_name ON emailtwits(email, name);
388 CREATE INDEX IF NOT EXISTS msgidtwits_msgid ON msgidtwits(msgid);
390 CREATE INDEX IF NOT EXISTS filters_idx_post_valid ON filters(idx, post, valid);
394 enum schemaSupportTable = `
395 -- tag <-> messageid correspondence
396 -- note that one message can be tagged with more than one tag
397 -- there is always tag with "uid=0", to keep all tags alive
398 -- special tags:
399 -- account:name -- received via this account
400 -- #spam -- this is spam message
401 -- #hobo -- will be autoassigned to messages without any tags (created on demand)
402 CREATE TABLE IF NOT EXISTS tagnames (
403 tagid INTEGER PRIMARY KEY
404 , hidden INTEGER NOT NULL DEFAULT 0 /* deleting tags may cause 'tagid' reuse, so it's better to hide them instead */
405 , threading INTEGER NOT NULL DEFAULT 1 /* enable threaded view? */
406 , noattaches INTEGER NOT NULL DEFAULT 0 /* ignore non-text attachments? */
407 , tag TEXT NOT NULL UNIQUE
410 -- it is here, because we don't have a lot of tags, and inserts are slightly faster this way
411 -- it's not required, because "UNIQUE" constraint will create automatic index
412 --CREATE INDEX IF NOT EXISTS tagname_tag ON tagnames(tag);
414 --CREATE INDEX IF NOT EXISTS tagname_tag_uid ON tagnames(tag, tagid);
417 -- each tag has its own unique threads (so uids can be duplicated, but (uid,tagid) paris cannot
418 -- see above for "apearance" and "mute" values
419 CREATE TABLE IF NOT EXISTS threads (
420 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
421 , tagid INTEGER /* we need separate threads for each tag */
422 , time INTEGER DEFAULT 0 /* unixtime -- creation/send/receive */
423 /* threading info */
424 , parent INTEGER DEFAULT 0 /* uid: parent message in thread, or 0 */
425 /* flags */
426 , appearance INTEGER DEFAULT 0 /* how the message should look */
427 , mute INTEGER DEFAULT 0 /* 1: only this message, 2: the whole thread */
428 , title TEXT DEFAULT NULL /* title from the filter */
432 -- WARNING!
433 -- for FTS5 to work, this table must be:
434 -- updated LAST on INSERT
435 -- updated FIRST on DELETE
436 -- this is due to FTS5 triggers
437 -- message texts should NEVER be updated!
438 -- if you want to do update a message:
439 -- first, DELETE the old one from this table
440 -- then, update textx
441 -- then, INSERT here again
442 -- doing it like that will keep FTS5 in sync
443 CREATE TABLE IF NOT EXISTS info (
444 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
445 , from_name TEXT /* can be empty */
446 , from_mail TEXT /* can be empty */
447 , subj TEXT /* can be empty */
448 , to_name TEXT /* can be empty */
449 , to_mail TEXT /* can be empty */
453 -- this holds msgid
454 -- moved to separate table, because this info is used only when inserting new messages
455 CREATE TABLE IF NOT EXISTS msgids (
456 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
457 , time INTEGER /* so we can select the most recent message */
458 , msgid TEXT /* message id */
462 -- this holds in-reply-to, and references
463 -- moved to separate table, because this info is used only when inserting new messages
464 CREATE TABLE IF NOT EXISTS refids (
465 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
466 , idx INTEGER /* internal index in headers, cannot have gaps, starts from 0 */
467 , msgid TEXT /* message id */
471 -- this ALWAYS contain an entry (yet content may be empty string)
472 CREATE TABLE IF NOT EXISTS content_text (
473 uid INTEGER PRIMARY KEY /* owner message uid */
474 , format TEXT NOT NULL /* optional format, like 'flowed' */
475 , content TEXT NOT NULL /* properly decoded */
479 -- this ALWAYS contain an entry (yet content may be empty string)
480 CREATE TABLE IF NOT EXISTS content_html (
481 uid INTEGER PRIMARY KEY /* owner message uid */
482 , format TEXT NOT NULL /* optional format, like 'flowed' */
483 , content TEXT NOT NULL /* properly decoded */
487 -- this DOES NOT include text and html contents (and may exclude others)
488 CREATE TABLE IF NOT EXISTS attaches (
489 uid INTEGER /* owner message uid */
490 , idx INTEGER /* code should take care of proper autoincrementing this */
491 , mime TEXT NOT NULL /* always lowercased */
492 , name TEXT NOT NULL /* attachment name; always empty for inline content, never empty for non-inline content */
493 , format TEXT NOT NULL /* optional format, like 'flowed' */
494 , content BLOB /* properly decoded; NULL if the attach was dropped */
498 -- this view is used for FTS5 content queries
499 -- it is harmless to keep it here even if FTS5 is not used
500 --DROP VIEW IF EXISTS fts5_msgview;
501 CREATE VIEW IF NOT EXISTS fts5_msgview (uid, sender, subj, text, html)
503 SELECT
504 info.uid AS uid
505 , info.from_name||' '||CHAR(26)||' '||info.from_mail AS sender
506 , info.subj AS subj
507 , ChiroUnpack(content_text.content) AS text
508 , ChiroUnpack(content_html.content) AS html
509 FROM info
510 INNER JOIN content_text USING(uid)
511 INNER JOIN content_html USING(uid)
515 static immutable string schemaSupportTempTables = `
516 --DROP TABLE IF EXISTS treepane;
517 CREATE TEMP TABLE IF NOT EXISTS treepane (
518 iid INTEGER PRIMARY KEY
519 , uid INTEGER
520 , level INTEGER
521 -- to make joins easier
522 , tagid INTEGER
525 CREATE INDEX IF NOT EXISTS treepane_uid ON treepane(uid);
528 enum schemaSupportIndex = `
529 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_tag_uid ON threads(tagid, uid);
530 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_uid_tag ON threads(uid, tagid);
532 -- this is for views where threading is disabled
533 CREATE INDEX IF NOT EXISTS trd_by_tag_time ON threads(tagid, time);
534 --CREATE INDEX IF NOT EXISTS trd_by_tag_time_parent ON threads(tagid, time, parent);
535 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent_time ON threads(tagid, parent, time);
536 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent ON threads(tagid, parent);
537 CREATE INDEX IF NOT EXISTS trd_by_parent_tag ON threads(parent, tagid);
539 -- this is for test if we have any unread articles (we don't mind the exact numbers, tho)
540 CREATE INDEX IF NOT EXISTS trd_by_appearance ON threads(appearance);
541 -- this is for removing purged messages
542 CREATE INDEX IF NOT EXISTS trd_by_tag_appearance ON threads(tagid, appearance);
543 -- was used in table view creation, not used anymore
544 --CREATE INDEX IF NOT EXISTS trd_by_parent_tag_appearance ON threads(parent, tagid, appearance);
546 -- for theadmsgview
547 -- was used in table view creation, not used anymore
548 --CREATE INDEX IF NOT EXISTS trd_by_tag_appearance_time ON threads(tagid, appearance, time);
550 CREATE INDEX IF NOT EXISTS msgid_by_msgid_time ON msgids(msgid, time DESC);
552 CREATE INDEX IF NOT EXISTS refid_by_refids_idx ON refids(msgid, idx);
553 CREATE INDEX IF NOT EXISTS refid_by_uid_idx ON refids(uid, idx);
555 CREATE INDEX IF NOT EXISTS content_text_by_uid ON content_text(uid);
556 CREATE INDEX IF NOT EXISTS content_html_by_uid ON content_html(uid);
558 CREATE INDEX IF NOT EXISTS attaches_by_uid_name ON attaches(uid, name);
559 CREATE INDEX IF NOT EXISTS attaches_by_uid_idx ON attaches(uid, idx);
561 -- "info" indicies for twits
562 --CREATE INDEX IF NOT EXISTS info_by_from_mail_name ON info(from_mail, from_name);
565 static immutable string schemaSupport = schemaSupportTable~schemaSupportIndex;
568 version(fts5_use_porter) {
569 enum FTS5_Tokenizer = "porter unicode61 remove_diacritics 2";
570 } else {
571 enum FTS5_Tokenizer = "unicode61 remove_diacritics 2";
574 static immutable string recreateFTS5 = `
575 DROP TABLE IF EXISTS fts5_messages;
576 CREATE VIRTUAL TABLE fts5_messages USING fts5(
577 sender /* sender name and email, separated by " \x1a " (dec 26) (substitute char) */
578 , subj /* email subject */
579 , text /* email body, text/plain */
580 , html /* email body, text/html */
581 --, uid UNINDEXED /* message uid this comes from (not needed, use "rowid" instead */
582 , tokenize = '`~FTS5_Tokenizer~`'
583 , content = 'fts5_msgview'
584 , content_rowid = 'uid'
586 /* sender, subj, text, html */
587 INSERT INTO fts5_messages(fts5_messages, rank) VALUES('rank', 'bm25(1.0, 3.0, 10.0, 6.0)');
590 static immutable string repopulateFTS5 = `
591 SELECT ChiroTimerStart('updating FTS5');
592 BEGIN TRANSACTION;
594 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
595 SELECT uid, sender, subj, text, html
596 FROM fts5_msgview
597 WHERE
598 EXISTS (
599 SELECT threads.tagid FROM threads
600 INNER JOIN tagnames USING(tagid)
601 WHERE
602 threads.uid=fts5_msgview.uid AND
603 tagnames.hidden=0 AND SUBSTR(tagnames.tag, 1, 1)='/'
606 COMMIT TRANSACTION;
607 SELECT ChiroTimerStop();
611 static immutable string recreateFTS5Triggers = `
612 -- triggers to keep the FTS index up to date
614 -- this rely on the proper "info" table update order
615 -- info must be inserted LAST
616 DROP TRIGGER IF EXISTS fts5xtrig_insert;
617 CREATE TRIGGER fts5xtrig_insert
618 AFTER INSERT ON info
619 BEGIN
620 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
621 SELECT uid, sender, subj, text, html FROM fts5_msgview WHERE uid=NEW.uid LIMIT 1;
622 END;
624 -- not AFTER, because we still need a valid view!
625 -- this rely on the proper "info" table update order
626 -- info must be deleted FIRST
627 DROP TRIGGER IF EXISTS fts5xtrig_delete;
628 CREATE TRIGGER fts5xtrig_delete
629 BEFORE DELETE ON info
630 BEGIN
631 INSERT INTO fts5_messages(fts5_messages, rowid, sender, subj, text, html)
632 SELECT 'delete', uid, sender, subj, text, html FROM fts5_msgview WHERE uid=OLD.uid LIMIT 1;
633 END;
635 -- message texts should NEVER be updated, so no ON UPDATE trigger
639 // ////////////////////////////////////////////////////////////////////////// //
640 // not properly implemented yet
641 //version = lazy_mt_safe;
643 version(lazy_mt_safe) {
644 enum lazy_mt_safe_flag = true;
645 } else {
646 enum lazy_mt_safe_flag = false;
649 public struct LazyStatement(string dbname) {
650 public:
651 enum DB {
652 Store,
653 View,
654 Conf,
657 private:
658 static struct Data {
659 DBStatement st = void;
660 version(lazy_mt_safe) {
661 sqlite3_mutex* mutex = void;
663 char* sql = void;
664 usize sqlsize = void;
665 uint compiled = void;
668 private:
669 usize udata = 0;
670 DB dbtype;
671 string delayInit = null;
673 private:
674 inout(Data)* datap () inout pure nothrow @trusted @nogc { pragma(inline, true); return cast(Data*)udata; }
675 void datap (Data *v) pure nothrow @trusted @nogc { pragma(inline, true); udata = cast(usize)v; }
677 public:
678 //@disable this ();
679 @disable this (this);
681 this (string sql) {
682 delayInit = sql;
684 assert(sql.length);
685 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
686 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
687 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
688 else static assert(0, "invalid db name: '"~dbname~"'");
689 import core.stdc.stdlib : calloc;
690 Data* dp = cast(Data*)calloc(1, Data.sizeof);
691 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
692 datap = dp;
693 dp.sql = cast(char*)calloc(1, sql.length);
694 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
695 dp.sql[0..sql.length] = sql[];
696 dp.sqlsize = sql.length;
697 version(lazy_mt_safe) {
698 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
699 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
702 //dbtype = adb;
703 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
706 ~this () {
707 import core.stdc.stdlib : free;
708 if (!udata) return;
709 Data* dp = datap;
710 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===DEINIT===\n%s\n============\n", dp.sql); }
711 dp.st = DBStatement.init;
712 free(dp.sql);
713 version(lazy_mt_safe) {
714 sqlite3_mutex_free(dp.mutex);
716 free(dp);
717 udata = 0;
720 bool valid () pure nothrow @safe @nogc { pragma(inline, true); return (udata != 0 || delayInit.length); }
722 private void setupWith (const(char)[] sql) {
723 if (udata) throw new Exception("statement already inited");
724 assert(sql.length);
725 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
726 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
727 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
728 else static assert(0, "invalid db name: '"~dbname~"'");
729 import core.stdc.stdlib : calloc;
730 Data* dp = cast(Data*)calloc(1, Data.sizeof);
731 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
732 datap = dp;
733 dp.sql = cast(char*)calloc(1, sql.length);
734 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
735 dp.sql[0..sql.length] = sql[];
736 dp.sqlsize = sql.length;
737 version(lazy_mt_safe) {
738 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
739 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
741 //dbtype = adb;
742 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
745 ref DBStatement st () {
746 if (!udata) {
747 //throw new Exception("no statement set");
748 setupWith(delayInit);
750 Data* dp = datap;
751 if (!dp.compiled) {
752 version(lazy_mt_safe) {
753 sqlite3_mutex_enter(dp.mutex);
755 scope(exit) {
756 version(lazy_mt_safe) {
757 sqlite3_mutex_leave(dp.mutex);
760 //{ import core.stdc.stdio : printf; printf("***compiling:\n%s\n=====\n", dp.sql); }
761 final switch (dbtype) {
762 case DB.Store: dp.st = dbStore.persistentStatement(dp.sql[0..dp.sqlsize]); break;
763 case DB.View: dp.st = dbView.persistentStatement(dp.sql[0..dp.sqlsize]); break;
764 case DB.Conf: dp.st = dbConf.persistentStatement(dp.sql[0..dp.sqlsize]); break;
766 dp.compiled = 1;
767 //assert(dp.st.valid);
769 //assert(dp.st.valid);
770 return dp.st;
775 // ////////////////////////////////////////////////////////////////////////// //
776 private bool isGoodText (const(void)[] buf) pure nothrow @safe @nogc {
777 foreach (immutable ubyte ch; cast(const(ubyte)[])buf) {
778 if (ch < 32) {
779 if (ch != 9 && ch != 10 && ch != 13 && ch != 27) return false;
780 } else {
781 if (ch == 127) return false;
784 return true;
785 //return utf8ValidText(buf);
789 // ////////////////////////////////////////////////////////////////////////// //
790 private bool isBadPrefix (const(char)[] buf) pure nothrow @trusted @nogc {
791 if (buf.length < 5) return false;
792 return
793 buf.ptr[0] == '\x1b' &&
794 buf.ptr[1] >= 'A' && buf.ptr[1] <= 'Z' &&
795 buf.ptr[2] >= 'A' && buf.ptr[2] <= 'Z' &&
796 buf.ptr[3] >= 'A' && buf.ptr[3] <= 'Z' &&
797 buf.ptr[4] >= 'A' && buf.ptr[4] <= 'Z';
801 /* two high bits of the first byte holds the size:
802 00: fit into 6 bits: [0.. 0x3f] (1 byte)
803 01: fit into 14 bits: [0.. 0x3fff] (2 bytes)
804 10: fit into 22 bits: [0.. 0x3f_ffff] (3 bytes)
805 11: fit into 30 bits: [0..0x3fff_ffff] (4 bytes)
807 number is stored as big-endian.
808 will not write anything to `dest` if there is not enough room.
810 returns number of bytes, or 0 if the number is too big.
812 private uint encodeUInt (void[] dest, uint v) nothrow @trusted @nogc {
813 if (v > 0x3fff_ffffU) return 0;
814 ubyte[] d = cast(ubyte[])dest;
815 // 4 bytes?
816 if (v > 0x3f_ffffU) {
817 v |= 0xc000_0000U;
818 if (d.length >= 4) {
819 d.ptr[0] = cast(ubyte)(v>>24);
820 d.ptr[1] = cast(ubyte)(v>>16);
821 d.ptr[2] = cast(ubyte)(v>>8);
822 d.ptr[3] = cast(ubyte)v;
824 return 4;
826 // 3 bytes?
827 if (v > 0x3fffU) {
828 v |= 0x80_0000U;
829 if (d.length >= 3) {
830 d.ptr[0] = cast(ubyte)(v>>16);
831 d.ptr[1] = cast(ubyte)(v>>8);
832 d.ptr[2] = cast(ubyte)v;
834 return 3;
836 // 2 bytes?
837 if (v > 0x3fU) {
838 v |= 0x4000U;
839 if (d.length >= 2) {
840 d.ptr[0] = cast(ubyte)(v>>8);
841 d.ptr[1] = cast(ubyte)v;
843 return 2;
845 // 1 byte
846 if (d.length >= 1) d.ptr[0] = cast(ubyte)v;
847 return 1;
851 private uint decodeUIntLength (const(void)[] dest) pure nothrow @trusted @nogc {
852 const(ubyte)[] d = cast(const(ubyte)[])dest;
853 if (d.length == 0) return 0;
854 switch (d.ptr[0]&0xc0) {
855 case 0x00: return 1;
856 case 0x40: return (d.length >= 2 ? 2 : 0);
857 case 0x80: return (d.length >= 3 ? 3 : 0);
858 default:
860 return (d.length >= 4 ? 4 : 0);
864 // returns uint.max on error (impossible value)
865 private uint decodeUInt (const(void)[] dest) pure nothrow @trusted @nogc {
866 const(ubyte)[] d = cast(const(ubyte)[])dest;
867 if (d.length == 0) return uint.max;
868 uint res = void;
869 switch (d.ptr[0]&0xc0) {
870 case 0x00:
871 res = d.ptr[0];
872 break;
873 case 0x40:
874 if (d.length < 2) return uint.max;
875 res = ((d.ptr[0]&0x3fU)<<8)|d.ptr[1];
876 break;
877 case 0x80:
878 if (d.length < 3) return uint.max;
879 res = ((d.ptr[0]&0x3fU)<<16)|(d.ptr[1]<<8)|d.ptr[2];
880 break;
881 default:
882 if (d.length < 4) return uint.max;
883 res = ((d.ptr[0]&0x3fU)<<24)|(d.ptr[1]<<16)|(d.ptr[2]<<8)|d.ptr[3];
884 break;
886 return res;
890 // returns position AFTER the headers (empty line is skipped too)
891 // returned value is safe for slicing
892 private int sq3Supp_FindHeadersEnd (const(char)* vs, const int sz) {
893 import core.stdc.string : memchr;
894 if (sz <= 0) return 0;
895 const(char)* eptr = cast(const(char)*)memchr(vs, '\n', cast(uint)sz);
896 while (eptr !is null) {
897 ++eptr;
898 int epos = cast(int)cast(usize)(eptr-vs);
899 if (sz-epos < 1) break;
900 if (*eptr == '\r') {
901 if (sz-epos < 2) break;
902 ++epos;
903 ++eptr;
905 if (*eptr == '\n') return epos+1;
906 assert(epos < sz);
907 eptr = cast(const(char)*)memchr(eptr, '\n', cast(uint)(sz-epos));
909 return sz;
913 // hack for some invalid dates
914 uint parseMailDate (const(char)[] s) nothrow {
915 import std.datetime;
916 if (s.length == 0) return 0;
917 try {
918 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
919 } catch (Exception) {}
920 // sometimes this helps
921 usize dcount = 0;
922 foreach_reverse (immutable char ch; s) {
923 if (ch < '0' || ch > '9') break;
924 ++dcount;
926 if (dcount > 4) return 0;
927 s ~= "0000"[0..4-dcount];
928 try {
929 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
930 } catch (Exception) {}
931 return 0;
935 // ////////////////////////////////////////////////////////////////////////// //
936 extern(C) {
939 ** ChiroPack(content)
940 ** ChiroPack(content, packflag)
942 ** second form accepts int flag; 0 means "don't pack"
944 private void sq3Fn_ChiroPackCommon (sqlite3_context *ctx, sqlite3_value *val, int packlevel) nothrow @trusted {
945 immutable int sz = sqlite3_value_bytes(val);
946 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
948 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
950 const(char)* vs = cast(const(char) *)sqlite3_value_blob(val);
951 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroPack()`", -1); return; }
953 if (sz >= 0x3fffffff-8) {
954 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_toobig(ctx); return; }
955 sqlite3_result_value(ctx, val);
956 return;
959 import core.stdc.stdlib : malloc, free;
960 import core.stdc.string : memcpy;
962 if (packlevel > 0 && sz > 8) {
963 import core.stdc.stdio : snprintf;
964 char[16] xsz = void;
965 version(use_balz) {
966 xsz[0..5] = "\x1bBALZ";
967 } else version(use_libxpack) {
968 xsz[0..5] = "\x1bXPAK";
969 } else version(use_libbrieflz) {
970 xsz[0..5] = "\x1bBRLZ";
971 } else version(use_liblzfse) {
972 xsz[0..5] = "\x1bLZFS";
973 } else version(use_lzjb) {
974 xsz[0..5] = "\x1bLZJB";
975 } else version(use_libwim_lzms) {
976 xsz[0..5] = "\x1bLZMS";
977 } else version(use_libwim_lzx) {
978 xsz[0..5] = "\x1bLZMX";
979 } else version(use_libwim_xpress) {
980 xsz[0..5] = "\x1bXPRS";
981 } else version(use_lz4) {
982 xsz[0..5] = "\x1bLZ4D";
983 } else version(use_zstd) {
984 xsz[0..5] = "\x1bZSTD";
985 } else {
986 xsz[0..5] = "\x1bZLIB";
988 uint xszlen = encodeUInt(xsz[5..$], cast(uint)sz);
989 if (xszlen) {
990 xszlen += 5;
991 //xsz[xszlen++] = ':';
992 version(use_libbrieflz) {
993 immutable usize bsz = blz_max_packed_size(cast(usize)sz);
994 } else version(use_lzjb) {
995 immutable uint bsz = cast(uint)sz+1024;
996 } else version(use_lz4) {
997 immutable uint bsz = cast(uint)LZ4_compressBound(sz)+1024;
998 } else {
999 immutable uint bsz = cast(uint)sz;
1001 char* cbuf = cast(char*)malloc(bsz+xszlen);
1002 if (cbuf is null) {
1003 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_nomem(ctx); return; }
1004 } else {
1005 cbuf[0..xszlen] = xsz[0..xszlen];
1006 version(use_balz) {
1007 Balz bz;
1008 usize spos = 0;
1009 usize dpos = xszlen;
1010 try {
1011 bz.compress(
1012 // reader
1013 (buf) {
1014 if (spos >= cast(usize)sz) return 0;
1015 usize left = cast(usize)sz-spos;
1016 if (left > buf.length) left = buf.length;
1017 if (left) memcpy(buf.ptr, vs+spos, left);
1018 spos += left;
1019 return left;
1021 // writer
1022 (buf) {
1023 if (dpos+buf.length >= cast(usize)sz) throw new Exception("uncompressible");
1024 memcpy(cbuf+dpos, buf.ptr, buf.length);
1025 dpos += buf.length;
1027 // maximum compression?
1028 true
1030 } catch(Exception) {
1031 dpos = usize.max;
1033 if (dpos < cast(usize)sz) {
1034 sqlite3_result_blob(ctx, cbuf, dpos, &free);
1035 return;
1037 } else version(use_libdeflate) {
1038 if (packlevel > 12) packlevel = 12;
1039 libdeflate_compressor *cpr = libdeflate_alloc_compressor(packlevel);
1040 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1041 usize dsize = libdeflate_zlib_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
1042 libdeflate_free_compressor(cpr);
1043 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1044 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1045 return;
1047 } else version(use_libxpack) {
1048 // 2^19 (524288) bytes. This is definitely a big problem and I am planning to address it.
1049 // https://github.com/ebiggers/xpack/issues/1
1050 if (sz < 524288-64) {
1051 if (packlevel > 9) packlevel = 9;
1052 xpack_compressor *cpr = xpack_alloc_compressor(cast(usize)sz, packlevel);
1053 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1054 usize dsize = xpack_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
1055 xpack_free_compressor(cpr);
1056 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1057 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1058 return;
1061 } else version(use_libbrieflz) {
1062 if (packlevel > 10) packlevel = 10;
1063 immutable usize wbsize = blz_workmem_size_level(cast(usize)sz, packlevel);
1064 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1065 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1066 uint dsize = blz_pack_level(vs, cbuf+xszlen, cast(uint)sz, wbuf, packlevel);
1067 free(wbuf);
1068 if (dsize+xszlen < cast(usize)sz) {
1069 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1070 return;
1072 } else version(use_liblzfse) {
1073 immutable usize wbsize = lzfse_encode_scratch_size();
1074 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1075 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1076 usize dsize = lzfse_encode_buffer(cbuf+xszlen, bsz, vs, cast(uint)sz, wbuf);
1077 free(wbuf);
1078 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1079 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1080 return;
1082 } else version(use_lzjb) {
1083 usize dsize = lzjb_compress(vs, cast(usize)sz, cbuf+xszlen, bsz);
1084 if (dsize == usize.max) dsize = 0;
1085 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1086 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1087 return;
1089 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "LZJB FAILED!\n"); }
1090 } else version(use_libwim_lzms) {
1091 wimlib_compressor* cpr;
1092 uint clevel = (packlevel < 10 ? 50 : 1000);
1093 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZMS, cast(usize)sz, clevel, &cpr);
1094 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1095 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1096 wimlib_free_compressor(cpr);
1097 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1098 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1099 return;
1101 } else version(use_libwim_lzx) {
1102 if (sz <= WIMLIB_LZX_MAX_CHUNK) {
1103 wimlib_compressor* cpr;
1104 uint clevel = (packlevel < 10 ? 50 : 1000);
1105 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZX, cast(usize)sz, clevel, &cpr);
1106 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1107 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1108 wimlib_free_compressor(cpr);
1109 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1110 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1111 return;
1114 } else version(use_libwim_xpress) {
1115 if (sz <= WIMLIB_XPRESS_MAX_CHUNK) {
1116 wimlib_compressor* cpr;
1117 uint clevel = (packlevel < 10 ? 50 : 1000);
1118 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
1119 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < cast(uint)sz) csz *= 2U;
1120 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, clevel, &cpr);
1121 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1122 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1123 wimlib_free_compressor(cpr);
1124 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1125 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1126 return;
1129 } else version(use_lz4) {
1130 int dsize = LZ4_compress_default(vs, cbuf+xszlen, sz, cast(int)bsz);
1131 if (dsize > 0 && dsize+xszlen < sz) {
1132 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1133 return;
1135 } else version(use_zstd) {
1136 immutable int clev =
1137 packlevel <= 3 ? ZSTD_minCLevel() :
1138 packlevel <= 6 ? ZSTD_defaultCLevel() :
1139 packlevel < 10 ? 19 :
1140 ZSTD_maxCLevel();
1141 usize dsize = ZSTD_compress(cbuf+xszlen, cast(int)bsz, vs, sz, clev);
1142 if (!ZSTD_isError(dsize) && dsize > 0 && dsize+xszlen < sz) {
1143 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1144 return;
1146 } else {
1147 import etc.c.zlib : /*compressBound,*/ compress2, Z_OK;
1148 //uint bsz = cast(uint)compressBound(cast(uint)sz);
1149 if (packlevel > 9) packlevel = 9;
1150 usize dsize = bsz;
1151 int zres = compress2(cast(ubyte *)(cbuf+xszlen), &dsize, cast(const(ubyte) *)vs, sz, packlevel);
1152 if (zres == Z_OK && dsize+xszlen < cast(usize)sz) {
1153 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1154 return;
1157 free(cbuf);
1162 if (isBadPrefix(vs[0..cast(uint)sz])) {
1163 char *res = cast(char *)malloc(sz+4);
1164 if (res is null) { sqlite3_result_error_nomem(ctx); return; }
1165 res[0..5] = "\x1bRAWB";
1166 res[5..sz+5] = vs[0..sz];
1167 if (isGoodText(vs[0..cast(usize)sz])) {
1168 sqlite3_result_text(ctx, res, sz+5, &free);
1169 } else {
1170 sqlite3_result_blob(ctx, res, sz+5, &free);
1172 } else {
1173 immutable bool wantBlob = !isGoodText(vs[0..cast(usize)sz]);
1174 immutable int tp = sqlite3_value_type(val);
1175 if ((wantBlob && tp == SQLITE_BLOB) || (!wantBlob && tp == SQLITE3_TEXT)) {
1176 sqlite3_result_value(ctx, val);
1177 } else if (wantBlob) {
1178 sqlite3_result_blob(ctx, vs, sz, SQLITE_TRANSIENT);
1179 } else {
1180 sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1187 ** ChiroPack(content)
1189 private void sq3Fn_ChiroPack (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
1190 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
1191 return sq3Fn_ChiroPackCommon(ctx, argv[0], ChiroCompressionLevel);
1196 ** ChiroPack(content, packlevel)
1198 ** `packlevel` == 0 means "don't pack"
1199 ** `packlevel` == 9 means "maximum compression"
1201 private void sq3Fn_ChiroPackDPArg (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
1202 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
1203 return sq3Fn_ChiroPackCommon(ctx, argv[0], sqlite3_value_int(argv[1]));
1208 ** ChiroUnpack(content)
1210 ** it is (almost) safe to pass non-packed content here
1212 private void sq3Fn_ChiroUnpack (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1213 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!000\n"); }
1214 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroUnpack()`", -1); return; }
1216 int sz = sqlite3_value_bytes(argv[0]);
1217 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
1219 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1221 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1222 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1224 if (!isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_value(ctx, argv[0]); return; }
1225 if (vs[0..5] == "\x1bRAWB") { sqlite3_result_blob(ctx, vs+5, sz-5, SQLITE_TRANSIENT); return; }
1226 if (sz < 6) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1228 enum {
1229 Codec_ZLIB,
1230 Codec_BALZ,
1231 Codec_XPAK,
1232 Codec_BRLZ,
1233 Codec_LZFS,
1234 Codec_LZJB,
1235 Codec_LZMS,
1236 Codec_LZMX,
1237 Codec_XPRS,
1238 Codec_LZ4D,
1239 Codec_ZSTD,
1242 int codec = Codec_ZLIB;
1243 if (vs[0..5] != "\x1bZLIB") {
1244 version(use_balz) {
1245 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBALZ") codec = Codec_BALZ;
1247 version(use_libxpack) {
1248 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1250 version(use_libxpack) {
1251 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1253 version(use_libbrieflz) {
1254 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBRLZ") codec = Codec_BRLZ;
1256 version(use_liblzfse) {
1257 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZFS") codec = Codec_LZFS;
1259 version(use_lzjb) {
1260 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZJB") codec = Codec_LZJB;
1262 version(use_libwim_lzms) {
1263 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMS") codec = Codec_LZMS;
1265 version(use_libwim_lzx) {
1266 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMX") codec = Codec_LZMX;
1268 version(use_libwim_xpress) {
1269 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPRS") codec = Codec_XPRS;
1271 version(use_lz4) {
1272 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZ4D") codec = Codec_LZ4D;
1274 version(use_zstd) {
1275 if (codec == Codec_ZLIB && vs[0..5] == "\x1bZSTD") codec = Codec_ZSTD;
1277 if (codec == Codec_ZLIB) { sqlite3_result_error(ctx, "invalid codec in `ChiroUnpack()`", -1); return; }
1280 // skip codec id
1281 // size is guaranteed to be at least 6 here
1282 vs += 5;
1283 sz -= 5;
1285 immutable uint numsz = decodeUIntLength(vs[0..cast(uint)sz]);
1286 //{ import core.stdc.stdio : printf; printf("sz=%d; numsz=%u; %02X %02X %02X %02X\n", sz, numsz, cast(uint)vs[5], cast(uint)vs[6], cast(uint)vs[7], cast(uint)vs[8]); }
1287 //writeln("sq3Fn_ChiroUnpack: nsz=", sz-5);
1288 if (numsz == 0 || numsz > cast(uint)sz) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1289 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!100\n"); }
1290 immutable uint rsize = decodeUInt(vs[0..cast(uint)sz]);
1291 if (rsize == uint.max) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1292 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!101:rsize=%u\n", rsize); }
1293 if (rsize == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1294 // skip number
1295 vs += numsz;
1296 sz -= cast(int)numsz;
1297 //{ import core.stdc.stdio : printf; printf("sz=%d; rsize=%u\n", sz, rsize, dpos); }
1299 import core.stdc.stdlib : malloc, free;
1300 import core.stdc.string : memcpy;
1302 char* cbuf = cast(char*)malloc(rsize);
1303 if (cbuf is null) { sqlite3_result_error_nomem(ctx); return; }
1304 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz-dpos);
1306 usize dsize = rsize;
1307 final switch (codec) {
1308 case Codec_ZLIB:
1309 version(use_libdeflate) {
1310 libdeflate_decompressor *dcp = libdeflate_alloc_decompressor();
1311 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1312 auto rc = libdeflate_zlib_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1313 if (rc != LIBDEFLATE_SUCCESS) {
1314 free(cbuf);
1315 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1316 return;
1318 } else {
1319 import etc.c.zlib : uncompress, Z_OK;
1320 int zres = uncompress(cast(ubyte *)cbuf, &dsize, cast(const(ubyte) *)vs, sz);
1321 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz, "; dsize=", dsize, "; zres=", zres);
1322 if (zres != Z_OK || dsize != rsize) {
1323 free(cbuf);
1324 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1325 return;
1328 break;
1329 case Codec_BALZ:
1330 version(use_balz) {
1331 uint spos = 0;
1332 uint outpos = 0;
1333 try {
1334 Unbalz bz;
1335 auto dc = bz.decompress(
1336 // reader
1337 (buf) {
1338 uint left = cast(uint)sz-spos;
1339 if (left > buf.length) left = cast(uint)buf.length;
1340 if (left != 0) memcpy(buf.ptr, vs, left);
1341 spos += left;
1342 return left;
1344 // writer
1345 (buf) {
1346 uint left = rsize-outpos;
1347 if (left == 0) throw new Exception("broken data");
1348 if (left > buf.length) left = cast(uint)buf.length;
1349 if (left) memcpy(cbuf+outpos, buf.ptr, left);
1350 outpos += left;
1353 if (dc != rsize) throw new Exception("broken data");
1354 } catch (Exception) {
1355 outpos = uint.max;
1357 if (outpos == uint.max) {
1358 free(cbuf);
1359 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1360 return;
1362 dsize = outpos;
1363 } else {
1364 free(cbuf);
1365 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1366 return;
1368 break;
1369 case Codec_XPAK:
1370 version(use_libxpack) {
1371 xpack_decompressor *dcp = xpack_alloc_decompressor();
1372 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1373 auto rc = xpack_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1374 if (rc != DECOMPRESS_SUCCESS) {
1375 free(cbuf);
1376 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1377 return;
1379 } else {
1380 free(cbuf);
1381 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1382 return;
1384 break;
1385 case Codec_BRLZ:
1386 version(use_libbrieflz) {
1387 dsize = blz_depack_safe(vs, cast(uint)sz, cbuf, rsize);
1388 if (dsize != rsize) {
1389 free(cbuf);
1390 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1391 return;
1393 } else {
1394 free(cbuf);
1395 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1396 return;
1398 break;
1399 case Codec_LZFS:
1400 version(use_liblzfse) {
1401 immutable usize wbsize = lzfse_decode_scratch_size();
1402 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1403 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1404 dsize = lzfse_decode_buffer(cbuf, cast(usize)rsize, vs, cast(usize)sz, wbuf);
1405 free(wbuf);
1406 if (dsize == 0 || dsize != rsize) {
1407 free(cbuf);
1408 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1409 return;
1411 } else {
1412 free(cbuf);
1413 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1414 return;
1416 break;
1417 case Codec_LZJB:
1418 version(use_lzjb) {
1419 dsize = lzjb_decompress(vs, cast(usize)sz, cbuf, rsize);
1420 if (dsize != rsize) {
1421 free(cbuf);
1422 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1423 return;
1425 } else {
1426 free(cbuf);
1427 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1428 return;
1430 break;
1431 case Codec_LZMS:
1432 version(use_libwim_lzms) {
1433 wimlib_decompressor* dpr;
1434 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZMS, rsize, &dpr);
1435 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1436 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1437 wimlib_free_decompressor(dpr);
1438 if (rc != 0) {
1439 free(cbuf);
1440 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1441 return;
1443 } else {
1444 free(cbuf);
1445 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1446 return;
1448 break;
1449 case Codec_LZMX:
1450 version(use_libwim_lzx) {
1451 wimlib_decompressor* dpr;
1452 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZX, rsize, &dpr);
1453 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1454 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1455 wimlib_free_decompressor(dpr);
1456 if (rc != 0) {
1457 free(cbuf);
1458 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1459 return;
1461 } else {
1462 free(cbuf);
1463 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1464 return;
1466 break;
1467 case Codec_XPRS:
1468 version(use_libwim_xpress) {
1469 wimlib_decompressor* dpr;
1470 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
1471 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < rsize) csz *= 2U;
1472 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, &dpr);
1473 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1474 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1475 wimlib_free_decompressor(dpr);
1476 if (rc != 0) {
1477 free(cbuf);
1478 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1479 return;
1481 } else {
1482 free(cbuf);
1483 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1484 return;
1486 break;
1487 case Codec_LZ4D:
1488 version(use_lz4) {
1489 dsize = LZ4_decompress_safe(vs, cbuf, sz, rsize);
1490 if (dsize != rsize) {
1491 free(cbuf);
1492 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1493 return;
1495 } else {
1496 free(cbuf);
1497 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1498 return;
1500 break;
1501 case Codec_ZSTD:
1502 version(use_zstd) {
1503 dsize = ZSTD_decompress(cbuf, rsize, vs, sz);
1504 if (ZSTD_isError(dsize) || dsize != rsize) {
1505 free(cbuf);
1506 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1507 return;
1509 } else {
1510 free(cbuf);
1511 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1512 return;
1514 break;
1517 if (isGoodText(cbuf[0..dsize])) {
1518 sqlite3_result_text(ctx, cbuf, cast(int)dsize, &free);
1519 } else {
1520 sqlite3_result_blob(ctx, cbuf, cast(int)dsize, &free);
1526 ** ChiroNormCRLF(content)
1528 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1529 ** Removes trailing blanks.
1531 private void sq3Fn_ChiroNormCRLF (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1532 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormCRLF()`", -1); return; }
1534 int sz = sqlite3_value_bytes(argv[0]);
1535 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1537 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1539 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1540 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormCRLF()`", -1); return; }
1542 // check if we have something to do, and calculate new string size
1543 bool needwork = false;
1544 if (vs[cast(uint)sz-1] <= 32) {
1545 needwork = true;
1546 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1547 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1549 uint newsz = cast(uint)sz;
1550 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1551 if (ch == 13) {
1552 needwork = true;
1553 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1554 } else if (!needwork) {
1555 needwork = ((ch < 32 && ch != 9 && ch != 10) || ch == 127);
1559 if (!needwork) {
1560 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1561 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1562 return;
1565 assert(newsz && newsz <= cast(uint)sz);
1567 // need a new string
1568 import core.stdc.stdlib : malloc, free;
1569 char* newstr = cast(char*)malloc(newsz);
1570 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1571 char* dest = newstr;
1572 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1573 if (ch == 13) {
1574 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1575 } else {
1576 if (ch == 127) *dest++ = '~';
1577 else if (ch == 11 || ch == 12) *dest++ = '\n';
1578 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1579 else *dest++ = ch;
1582 assert(dest == newstr+newsz);
1584 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1589 ** ChiroNormHeaders(content)
1591 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1592 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1593 ** Removes trailing blanks.
1595 private void sq3Fn_ChiroNormHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1596 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormHeaders()`", -1); return; }
1598 int sz = sqlite3_value_bytes(argv[0]);
1599 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1601 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1603 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1604 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormHeaders()`", -1); return; }
1606 // check if we have something to do, and calculate new string size
1607 bool needwork = false;
1608 if (vs[cast(uint)sz-1] <= 32) {
1609 needwork = true;
1610 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1611 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1613 uint newsz = cast(uint)sz;
1614 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1615 if (ch == 13) {
1616 needwork = true;
1617 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1618 } else if (ch == 10) {
1619 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) { needwork = true; --newsz; }
1620 } else if (!needwork) {
1621 needwork = ((ch < 32 && ch != 10) || ch == 127);
1625 if (!needwork) {
1626 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1627 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1628 return;
1631 assert(newsz && newsz <= cast(uint)sz);
1633 // need a new string
1634 import core.stdc.stdlib : malloc, free;
1635 char* newstr = cast(char*)malloc(newsz);
1636 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1637 char* dest = newstr;
1638 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1639 if (ch == 13) {
1640 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1641 } else if (ch == 10) {
1642 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1643 } else {
1644 if (ch == 127) *dest++ = '~';
1645 else if (ch < 32 && ch != 10) *dest++ = ' ';
1646 else *dest++ = ch;
1649 assert(dest == newstr+newsz);
1651 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1656 ** ChiroExtractHeaders(content)
1658 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1659 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1660 ** Removes trailing blanks.
1662 private void sq3Fn_ChiroExtractHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1663 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1665 int sz = sqlite3_value_bytes(argv[0]);
1666 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1668 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1670 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1671 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1673 // slice headers
1674 sz = sq3Supp_FindHeadersEnd(vs, sz);
1676 // strip trailing blanks
1677 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1678 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1680 // allocate new string (it can be smaller, but will never be bigger)
1681 import core.stdc.stdlib : malloc, free;
1682 char* newstr = cast(char*)malloc(cast(uint)sz);
1683 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1684 char* dest = newstr;
1685 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1686 if (ch == 13) {
1687 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1688 } else if (ch == 10) {
1689 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1690 } else {
1691 if (ch == 127) *dest++ = '~';
1692 else if (ch < 32 && ch != 10) *dest++ = ' ';
1693 else *dest++ = ch;
1696 assert(dest <= newstr+cast(uint)sz);
1697 sz = cast(int)cast(usize)(dest-newstr);
1698 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1699 sqlite3_result_text(ctx, newstr, sz, &free);
1704 ** ChiroExtractBody(content)
1706 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1707 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1708 ** Removes trailing blanks and final dot.
1710 private void sq3Fn_ChiroExtractBody (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1711 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1713 int sz = sqlite3_value_bytes(argv[0]);
1714 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1716 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1718 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1719 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1721 // slice body
1722 immutable int bstart = sq3Supp_FindHeadersEnd(vs, sz);
1723 if (bstart >= sz) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1724 vs += bstart;
1725 sz -= bstart;
1727 // strip trailing dot
1728 if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '\n') sz -= 2;
1729 else if (sz >= 1 && vs[cast(uint)sz-1U] == '\n') --sz;
1730 if (sz == 1 && vs[0] == '.') sz = 0;
1731 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\n' && vs[cast(uint)sz-1U] == '.') --sz;
1732 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '.') --sz;
1734 // strip trailing blanks
1735 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1736 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1738 // allocate new string (it can be smaller, but will never be bigger)
1739 import core.stdc.stdlib : malloc, free;
1740 char* newstr = cast(char*)malloc(cast(uint)sz);
1741 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1742 char* dest = newstr;
1743 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1744 if (ch == 13) {
1745 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1746 } else {
1747 if (ch == 127) *dest++ = '~';
1748 else if (ch == 11 || ch == 12) *dest++ = '\n';
1749 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1750 else *dest++ = ch;
1753 assert(dest <= newstr+cast(uint)sz);
1754 sz = cast(int)cast(usize)(dest-newstr);
1755 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1756 sqlite3_result_text(ctx, newstr, sz, &free);
1761 ** ChiroRIPEMD160(content)
1763 ** Calculates RIPEMD160 hash over the given content.
1765 ** Returns BINARY BLOB! You can use `tolower(hex(ChiroRIPEMD160(contents)))`
1766 ** to get lowercased hex hash string.
1768 private void sq3Fn_ChiroRIPEMD160 (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1769 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroRIPEMD160()`", -1); return; }
1771 immutable int sz = sqlite3_value_bytes(argv[0]);
1772 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1774 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1775 if (!vs && sz == 0) vs = "";
1776 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroRIPEMD160()`", -1); return; }
1778 ubyte[20] hash = ripemd160Of(vs[0..cast(uint)sz]);
1779 sqlite3_result_blob(ctx, cast(const(char)*)hash.ptr, cast(int)hash.length, SQLITE_TRANSIENT);
1783 enum HeaderProcStartTpl(string fnname) = `
1784 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"`~fnname~`()\"", -1); return; }
1786 immutable int sz = sqlite3_value_bytes(argv[0]);
1787 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1789 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1790 if (!vs && sz == 0) vs = "";
1791 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"`~fnname~`()\"", -1); return; }
1793 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
1798 ** ChiroHdr_NNTPIndex(headers)
1800 ** The content must be email with headers (or headers only).
1801 ** Returns "NNTP-Index" field or zero (int).
1803 private void sq3Fn_ChiroHdr_NNTPIndex (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1804 mixin(HeaderProcStartTpl!"ChiroHdr_NNTPIndex");
1806 uint nntpidx = 0;
1808 auto nntpidxfld = findHeaderField(hdrs, "NNTP-Index");
1809 if (nntpidxfld.length) {
1810 auto id = nntpidxfld.getFieldValue;
1811 if (id.length) {
1812 foreach (immutable ch; id) {
1813 if (ch < '0' || ch > '9') { nntpidx = 0; break; }
1814 if (nntpidx == 0 && ch == '0') continue;
1815 immutable uint nn = nntpidx*10u+(ch-'0');
1816 if (nn <= nntpidx) nntpidx = 0x7fffffff; else nntpidx = nn;
1821 // it is safe, it can't overflow
1822 sqlite3_result_int(ctx, cast(int)nntpidx);
1827 ** ChiroHdr_RecvTime(headers)
1829 ** The content must be email with headers (or headers only).
1830 ** Returns unixtime (can be zero).
1832 private void sq3Fn_ChiroHdr_RecvTime (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1833 mixin(HeaderProcStartTpl!"ChiroHdr_RecvTime");
1835 uint msgtime = 0; // message receiving time
1837 auto datefld = findHeaderField(hdrs, "Injection-Date");
1838 if (datefld.length != 0) {
1839 auto v = datefld.getFieldValue;
1840 try {
1841 msgtime = parseMailDate(v);
1842 } catch (Exception) {
1843 //writeln("UID=", uid, ": FUCKED INJECTION-DATE: |", v, "|");
1844 msgtime = 0; // just in case
1848 if (!msgtime) {
1849 // obsolete NNTP date field, because why not?
1850 datefld = findHeaderField(hdrs, "NNTP-Posting-Date");
1851 if (datefld.length != 0) {
1852 auto v = datefld.getFieldValue;
1853 try {
1854 msgtime = parseMailDate(v);
1855 } catch (Exception) {
1856 //writeln("UID=", uid, ": FUCKED NNTP-POSTING-DATE: |", v, "|");
1857 msgtime = 0; // just in case
1862 if (!msgtime) {
1863 datefld = findHeaderField(hdrs, "Date");
1864 if (datefld.length != 0) {
1865 auto v = datefld.getFieldValue;
1866 try {
1867 msgtime = parseMailDate(v);
1868 } catch (Exception) {
1869 //writeln("UID=", uid, ": FUCKED DATE: |", v, "|");
1870 msgtime = 0; // just in case
1875 // finally, try to get time from "Received:"
1876 //Received: from dns9.fly.us ([131.103.96.154]) by np5-d2.fly.us with Microsoft SMTPSVC(5.0.2195.6824); Tue, 21 Mar 2017 17:35:54 -0400
1877 if (!msgtime) {
1878 //writeln("!!! --- !!!");
1879 uint lowesttime = uint.max;
1880 foreach (uint fidx; 0..uint.max) {
1881 auto recvfld = findHeaderField(hdrs, "Received", fidx);
1882 if (recvfld.length == 0) break;
1883 auto lsemi = recvfld.lastIndexOf(';');
1884 if (lsemi >= 0) recvfld = recvfld[lsemi+1..$].xstrip;
1885 if (recvfld.length != 0) {
1886 auto v = recvfld.getFieldValue;
1887 uint tm = 0;
1888 try {
1889 tm = parseMailDate(v);
1890 } catch (Exception) {
1891 //writeln("UID=", uid, ": FUCKED RECV DATE: |", v, "|");
1892 tm = 0; // just in case
1894 //writeln(tm, " : ", lowesttime);
1895 if (tm && tm < lowesttime) lowesttime = tm;
1898 if (lowesttime != uint.max) msgtime = lowesttime;
1901 sqlite3_result_int64(ctx, msgtime);
1906 ** ChiroHdr_FromEmail(headers)
1908 ** The content must be email with headers (or headers only).
1909 ** Returns email "From" field.
1911 private void sq3Fn_ChiroHdr_FromEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1912 mixin(HeaderProcStartTpl!"ChiroHdr_FromEmail");
1913 auto from = findHeaderField(hdrs, "From").extractMail;
1914 if (from.length == 0) {
1915 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
1916 } else {
1917 sqlite3_result_text(ctx, from.ptr, cast(int)from.length, SQLITE_TRANSIENT);
1923 ** ChiroHdr_ToEmail(headers)
1925 ** The content must be email with headers (or headers only).
1926 ** Returns email "From" field.
1928 private void sq3Fn_ChiroHdr_ToEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1929 mixin(HeaderProcStartTpl!"ChiroHdr_ToEmail");
1930 auto to = findHeaderField(hdrs, "To").extractMail;
1931 if (to.length == 0) {
1932 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
1933 } else {
1934 sqlite3_result_text(ctx, to.ptr, cast(int)to.length, SQLITE_TRANSIENT);
1940 ** ChiroHdr_Subj(headers)
1942 ** The content must be email with headers (or headers only).
1943 ** Returns email "From" field.
1945 private void sq3Fn_ChiroHdr_Subj (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1946 mixin(HeaderProcStartTpl!"sq3Fn_ChiroHdr_Subj");
1947 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
1948 if (subj.length == 0) {
1949 sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
1950 } else {
1951 sqlite3_result_text(ctx, subj.ptr, cast(int)subj.length, SQLITE_TRANSIENT);
1957 ** ChiroHdr_Field(headers, fieldname)
1959 ** The content must be email with headers (or headers only).
1960 ** Returns field value as text, or NULL if there is no such field.
1962 private void sq3Fn_ChiroHdr_Field (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1963 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroHdr_Field()\"", -1); return; }
1965 immutable int sz = sqlite3_value_bytes(argv[0]);
1966 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1968 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1969 if (!vs && sz == 0) vs = "";
1970 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
1972 immutable int fldsz = sqlite3_value_bytes(argv[1]);
1973 if (fldsz < 0) { sqlite3_result_error_toobig(ctx); return; }
1975 const(char)* fldname = cast(const(char) *)sqlite3_value_blob(argv[1]);
1976 if (!fldname && fldsz == 0) fldname = "";
1977 if (!fldname) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
1979 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
1980 auto value = findHeaderField(hdrs, fldname[0..fldsz]);
1981 if (value is null) {
1982 sqlite3_result_null(ctx);
1983 } else if (value.length == 0) {
1984 sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
1985 } else {
1986 sqlite3_result_text(ctx, value.ptr, cast(int)value.length, SQLITE_TRANSIENT);
1992 ** ChiroTimerStart([msg])
1994 ** The content must be email with headers (or headers only).
1995 ** Returns email "From" field.
1997 private void sq3Fn_ChiroTimerStart (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1998 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStart()\"", -1); return; }
2000 delete chiTimerMsg;
2002 if (argc == 1) {
2003 immutable int sz = sqlite3_value_bytes(argv[0]);
2004 if (sz > 0) {
2005 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2006 if (vs) {
2007 chiTimerMsg = new char[cast(usize)sz];
2008 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
2009 writeln("started ", chiTimerMsg, "...");
2014 sqlite3_result_int(ctx, 1);
2015 chiTimer.restart();
2020 ** ChiroTimerStop([msg])
2022 ** The content must be email with headers (or headers only).
2023 ** Returns email "From" field.
2025 private void sq3Fn_ChiroTimerStop (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2026 chiTimer.stop;
2027 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStop()\"", -1); return; }
2029 if (ChiroTimerEnabled) {
2030 if (argc == 1) {
2031 delete chiTimerMsg;
2032 immutable int sz = sqlite3_value_bytes(argv[0]);
2033 if (sz > 0) {
2034 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2035 if (vs) {
2036 chiTimerMsg = new char[cast(usize)sz];
2037 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
2042 char[128] buf;
2043 auto tstr = chiTimer.toBuffer(buf[]);
2044 if (chiTimerMsg.length) {
2045 writeln("done ", chiTimerMsg, ": ", tstr);
2046 } else {
2047 writeln("time: ", tstr);
2051 delete chiTimerMsg;
2053 sqlite3_result_int(ctx, 1);
2057 // ////////////////////////////////////////////////////////////////////////// //
2061 // ////////////////////////////////////////////////////////////////////////// //
2062 private void registerFunctions (ref Database db) {
2063 sqlite3_busy_timeout(db.getHandle, 20000); // busy timeout: 20 seconds
2065 immutable int rc = sqlite3_extended_result_codes(db.getHandle, 1);
2066 if (rc != SQLITE_OK) {
2067 import core.stdc.stdio : stderr, fprintf;
2068 fprintf(stderr, "SQLITE WARNING: cannot enable extended result codes (this is harmless).\n");
2070 db.createFunction("ChiroPack", 1, &sq3Fn_ChiroPack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2071 db.createFunction("ChiroPack", 2, &sq3Fn_ChiroPackDPArg, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2072 db.createFunction("ChiroUnpack", 1, &sq3Fn_ChiroUnpack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2073 db.createFunction("ChiroNormCRLF", 1, &sq3Fn_ChiroNormCRLF, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2074 db.createFunction("ChiroNormHeaders", 1, &sq3Fn_ChiroNormHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2075 db.createFunction("ChiroExtractHeaders", 1, &sq3Fn_ChiroExtractHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2076 db.createFunction("ChiroExtractBody", 1, &sq3Fn_ChiroExtractBody, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2077 db.createFunction("ChiroRIPEMD160", 1, &sq3Fn_ChiroRIPEMD160, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2079 db.createFunction("ChiroHdr_NNTPIndex", 1, &sq3Fn_ChiroHdr_NNTPIndex, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2080 db.createFunction("ChiroHdr_RecvTime", 1, &sq3Fn_ChiroHdr_RecvTime, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2081 db.createFunction("ChiroHdr_FromEmail", 1, &sq3Fn_ChiroHdr_FromEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2082 db.createFunction("ChiroHdr_ToEmail", 1, &sq3Fn_ChiroHdr_ToEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2083 db.createFunction("ChiroHdr_Subj", 1, &sq3Fn_ChiroHdr_Subj, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2084 db.createFunction("ChiroHdr_Field", 2, &sq3Fn_ChiroHdr_Field, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2086 db.createFunction("ChiroTimerStart", 0, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2087 db.createFunction("ChiroTimerStart", 1, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2088 db.createFunction("ChiroTimerStop", 0, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2089 db.createFunction("ChiroTimerStop", 1, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2093 // ////////////////////////////////////////////////////////////////////////// //
2094 public void chiroRecreateStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName) {
2095 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2096 dbStore = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWStorageRecreate, schemaStorage);
2097 registerFunctions(dbStore);
2098 dbStore.setOnClose(schemaStorageIndex~dbpragmasRWStorage~"ANALYZE;");
2102 // ////////////////////////////////////////////////////////////////////////// //
2103 public void chiroRecreateViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName) {
2104 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2105 dbView = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWSupportRecreate, schemaSupportTable);
2106 registerFunctions(dbView);
2107 dbView.setOnClose(schemaSupportIndex~dbpragmasRWSupport~"ANALYZE;");
2111 public void chiroCreateViewIndiciesDB () {
2112 dbView.setOnClose(dbpragmasRWSupport~"ANALYZE;");
2113 dbView.execute(schemaSupportIndex);
2117 // ////////////////////////////////////////////////////////////////////////// //
2118 public void chiroRecreateConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName) {
2119 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2120 dbConf = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWOptionsRecreate, schemaOptions);
2121 registerFunctions(dbConf);
2122 dbConf.setOnClose(schemaOptionsIndex~dbpragmasRWOptions~"ANALYZE;");
2126 // ////////////////////////////////////////////////////////////////////////// //
2127 public void chiroOpenStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName, bool readonly=false) {
2128 dbStore = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWStorage), schemaStorage);
2129 registerFunctions(dbStore);
2130 if (!readonly) dbStore.setOnClose("PRAGMA optimize;");
2134 // ////////////////////////////////////////////////////////////////////////// //
2135 public void chiroOpenViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName, bool readonly=false) {
2136 dbView = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWSupport), schemaSupport);
2137 registerFunctions(dbView);
2138 if (!readonly) {
2139 dbView.execute(schemaSupportTempTables);
2140 dbView.setOnClose("PRAGMA optimize;");
2145 // ////////////////////////////////////////////////////////////////////////// //
2146 public void chiroOpenConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName, bool readonly=false) {
2147 dbConf = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWOptions), schemaOptions);
2148 registerFunctions(dbConf);
2149 if (!readonly) dbConf.setOnClose("PRAGMA optimize;");
2153 // ////////////////////////////////////////////////////////////////////////// //
2155 recreates FTS5 (full-text search) info.
2157 public void chiroRecreateFTS5 (bool repopulate=true) {
2158 dbView.execute(recreateFTS5);
2159 if (repopulate) dbView.execute(repopulateFTS5);
2160 dbView.execute(recreateFTS5Triggers);
2164 // ////////////////////////////////////////////////////////////////////////// //
2165 extern(C) {
2166 static void errorLogCallback (void *pArg, int rc, const char *zMsg) {
2167 if (ChiroSQLiteSilent) return;
2168 import core.stdc.stdio : stderr, fprintf;
2169 switch (rc) {
2170 case SQLITE_NOTICE: fprintf(stderr, "***SQLITE NOTICE: %s\n", zMsg); break;
2171 case SQLITE_NOTICE_RECOVER_WAL: fprintf(stderr, "***SQLITE NOTICE (WAL RECOVER): %s\n", zMsg); break;
2172 case SQLITE_NOTICE_RECOVER_ROLLBACK: fprintf(stderr, "***SQLITE NOTICE (ROLLBACK RECOVER): %s\n", zMsg); break;
2173 /* */
2174 case SQLITE_WARNING: fprintf(stderr, "***SQLITE WARNING: %s\n", zMsg); break;
2175 case SQLITE_WARNING_AUTOINDEX: fprintf(stderr, "***SQLITE AUTOINDEX WARNING: %s\n", zMsg); break;
2176 /* */
2177 case SQLITE_CANTOPEN:
2178 case SQLITE_SCHEMA:
2179 break; // ignore those
2180 /* */
2181 default: fprintf(stderr, "***SQLITE LOG(%d) [%s]: %s\n", rc, sqlite3_errstr(rc), zMsg); break;
2187 static string sqerrstr (immutable int rc) nothrow @trusted {
2188 const(char)* msg = sqlite3_errstr(rc);
2189 if (!msg || !msg[0]) return null;
2190 import core.stdc.string : strlen;
2191 return msg[0..strlen(msg)].idup;
2195 static void sqconfigcheck (immutable int rc, string msg, bool fatal) {
2196 if (rc == SQLITE_OK) return;
2197 if (fatal) {
2198 string errmsg = sqerrstr(rc);
2199 throw new Exception("FATAL: "~msg~": "~errmsg);
2200 } else {
2201 if (msg is null) msg = "";
2202 import core.stdc.stdio : stderr, fprintf;
2203 fprintf(stderr, "SQLITE WARNING: %.*s (this is harmless): %s\n", cast(uint)msg.length, msg.ptr, sqlite3_errstr(rc));
2208 // call this BEFORE opening any SQLite database connection!
2209 public void chiroSwitchToSingleThread () {
2210 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SINGLETHREAD), "cannot set single-threaded mode", fatal:false);
2214 public string MailDBPath () nothrow @trusted @nogc { return ExpandedMailDBPath; }
2217 public void MailDBPath(T:const(char)[]) (T mailpath) nothrow @trusted {
2218 while (mailpath.length > 1 && mailpath[$-1] == '/') mailpath = mailpath[0..$-1];
2220 if (mailpath.length == 0 || mailpath == ".") {
2221 ExpandedMailDBPath = "";
2222 return;
2225 if (mailpath[0] == '~') {
2226 char[] dpath = new char[mailpath.length+4096];
2227 dpath = expandTilde(dpath, mailpath);
2229 while (dpath.length > 1 && dpath[$-1] == '/') dpath = dpath[0..$-1];
2230 dpath ~= '/';
2231 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
2232 } else {
2233 char[] dpath = new char[mailpath.length+1];
2234 dpath[0..$-1] = mailpath[];
2235 dpath[$-1] = '/';
2236 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
2241 shared static this () {
2242 enum {
2243 SQLITE_CONFIG_STMTJRNL_SPILL = 26, /* int nByte */
2244 SQLITE_CONFIG_SMALL_MALLOC = 27, /* boolean */
2247 if (!sqlite3_threadsafe()) {
2248 throw new Exception("FATAL: SQLite must be compiled with threading support!");
2251 // we are interested in all errors
2252 sqlite3_config(SQLITE_CONFIG_LOG, &errorLogCallback, null);
2254 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SERIALIZED), "cannot set SQLite serialized threading mode", fatal:true);
2255 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SMALL_MALLOC, 0), "cannot enable SQLite unrestriced malloc mode", fatal:false);
2256 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_URI, 1), "cannot enable SQLite URI handling", fatal:false);
2257 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_COVERING_INDEX_SCAN, 1), "cannot enable SQLite covering index scan", fatal:false);
2258 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_STMTJRNL_SPILL, 512*1024), "cannot set SQLite statement journal spill threshold", fatal:false);
2260 MailDBPath = "~/Mail";
2264 shared static ~this () {
2265 dbConf.close();
2266 dbView.close();
2267 dbStore.close();
2271 // ////////////////////////////////////////////////////////////////////////// //
2272 public void transacted(string dbname) (void delegate () dg) {
2273 if (dg is null) return;
2274 static if (dbname == "View" || dbname == "view") alias db = dbView;
2275 else static if (dbname == "Store" || dbname == "store") alias db = dbStore;
2276 else static if (dbname == "Conf" || dbname == "conf") alias db = dbConf;
2277 else static assert(0, "invalid db name: '"~dbname~"'");
2278 db.transacted(dg);
2282 // ////////////////////////////////////////////////////////////////////////// //
2283 public void chiroSetOption(T) (const(char)[] name, T value)
2284 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2286 assert(name.length != 0);
2287 static auto stat = LazyStatement!"Conf"(`
2288 INSERT INTO options
2289 ( name, value)
2290 VALUES(:name,:value)
2291 ON CONFLICT(name)
2292 DO UPDATE SET value=:value
2293 ;`);
2294 stat.st.bindConstText(":name", name);
2295 static if (is(T == typeof(null))) {
2296 stat.st.bindConstText(":value", "");
2297 } else static if (__traits(isIntegral, T)) {
2298 stat.st.bind(":value", value);
2299 } else static if (is(T:const(char)[])) {
2300 stat.st.bindConstText(":value", value);
2301 } else {
2302 static assert(0, "oops");
2304 stat.st.doAll();
2307 public void chiroSetOption (const(char)[] name, DynStr value) {
2308 assert(name.length != 0);
2309 //{ import std.stdio; writeln("SETOPTION(", name, "): <", value.getData, ">"); }
2310 static auto stat = LazyStatement!"Conf"(`
2311 INSERT INTO options
2312 ( name, value)
2313 VALUES(:name,:value)
2314 ON CONFLICT(name)
2315 DO UPDATE SET value=:value
2316 ;`);
2317 stat.st
2318 .bindConstText(":name", name)
2319 .bindConstText(":value", value.getData)
2320 .doAll();
2324 public void chiroSetOptionUInts (const(char)[] name, uint v0, uint v1) {
2325 assert(name.length != 0);
2326 static auto stat = LazyStatement!"Conf"(`
2327 INSERT INTO options
2328 ( name, value)
2329 VALUES(:name,:value)
2330 ON CONFLICT(name)
2331 DO UPDATE SET value=:value
2332 ;`);
2333 import core.stdc.stdio : snprintf;
2334 char[64] value = void;
2335 auto vlen = snprintf(value.ptr, value.sizeof, "%u,%u", v0, v1);
2336 stat.st
2337 .bindConstText(":name", name)
2338 .bindConstText(":value", value[0..vlen])
2339 .doAll();
2343 public T chiroGetOptionEx(T) (const(char)[] name, out bool exists, T defval=T.init)
2344 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2346 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2347 assert(name.length != 0);
2348 exists = false;
2349 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2350 exists = true;
2351 return row.value!T;
2353 return defval;
2356 public T chiroGetOption(T) (const(char)[] name, T defval=T.init)
2357 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2359 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2360 assert(name.length != 0);
2361 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2362 return row.value!T;
2364 return defval;
2367 public void chiroGetOption (ref DynStr s, const(char)[] name, const(char)[] defval=null) {
2368 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2369 assert(name.length != 0);
2370 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2371 s = row.value!SQ3Text;
2372 return;
2374 s = defval;
2378 private uint parseUInt (ref SQ3Text s) {
2379 s = s.xstrip;
2380 if (s.length == 0 || !isdigit(s[0])) return uint.max;
2381 uint res = 0;
2382 while (s.length) {
2383 immutable int dg = s[0].digitInBase(10);
2384 if (dg < 0) break;
2385 immutable uint nr = res*10U+cast(uint)dg;
2386 if (nr < res) return uint.max;
2387 res = nr;
2388 s = s[1..$];
2390 if (s.length && s[0] == ',') s = s[1..$];
2391 s = s.xstrip;
2392 return res;
2396 public void chiroGetOptionUInts (ref uint v0, ref uint v1, const(char)[] name) {
2397 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2398 assert(name.length != 0);
2399 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2400 auto s = row.value!SQ3Text;
2401 immutable uint rv0 = parseUInt(s);
2402 immutable uint rv1 = parseUInt(s);
2403 if (rv0 != uint.max && rv1 != uint.max && s.length == 0) {
2404 v0 = rv0;
2405 v1 = rv1;
2407 return;
2412 // ////////////////////////////////////////////////////////////////////////// //
2413 // append tag if necessary, return tagid
2414 // tag name must be valid: not empty, and not end with a '/'
2415 // returns 0 on invalid tag name
2416 uint chiroAppendTag (const(char)[] tagname, int hidden=0) {
2417 tagname = tagname.xstrip;
2418 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2419 tagname = tagname.xstrip;
2420 if (tagname.length == 0) return 0;
2421 if (tagname.indexOf('|') >= 0) return 0;
2423 static auto stAppendTag = LazyStatement!"View"(`
2424 INSERT INTO tagnames(tag, hidden, threading) VALUES(:tagname,:hidden,:threading)
2425 ON CONFLICT(tag)
2426 DO UPDATE SET hidden=hidden -- this is for "returning"
2427 RETURNING tagid AS tagid
2428 ;`);
2430 // alphanum tags must start with '/'
2431 DynStr tn;
2432 if (tagname[0].isalnum) {
2433 tn = "/";
2434 tn ~= tagname;
2435 stAppendTag.st.bindConstText(":tagname", tn);
2436 } else {
2437 stAppendTag.st.bindConstText(":tagname", tagname);
2439 stAppendTag.st
2440 .bind(":hidden", hidden)
2441 .bind(":threading", (hidden ? 0 : 1));
2442 foreach (auto row; stAppendTag.st.range) return row.tagid!uint;
2444 return 0;
2448 // ////////////////////////////////////////////////////////////////////////// //
2449 /// returns `true` if we need to update pane
2450 /// if message is left without any tags, it will be tagged with "#hobo"
2451 public bool chiroMessageRemoveTag (uint uid, const(char)[] tagname) {
2452 if (uid == 0) return false;
2453 tagname = tagname.xstrip;
2454 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2455 tagname = tagname.xstrip;
2456 if (tagname.length == 0) return false;
2457 if (tagname.indexOf('|') >= 0) return false;
2459 immutable tagid = chiroGetTagUid(tagname);
2460 if (tagid == 0) return false;
2462 static auto stUpdateStorageTags = LazyStatement!"Store"(`
2463 UPDATE SET tags=:tags WHERE uid=:uid
2464 ;`);
2466 static auto stUidHasTag = LazyStatement!"View"(`
2467 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2468 ;`);
2470 static auto stInsertIntoThreads = LazyStatement!"View"(`
2471 INSERT INTO threads(uid, tagid,appearance,time)
2472 VALUES(:uid, :tagid, :appr, (SELECT time FROM info WHERE uid=:uid LIMIT 1))
2473 ;`);
2475 // delete message from threads
2476 static auto stClearThreads = LazyStatement!"View"(`
2477 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2478 ;`);
2480 static auto stGetMsgTags = LazyStatement!"View"(`
2481 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
2482 FROM threads
2483 WHERE uid=:uid
2484 INNER JOIN tagnames AS tt USING(tagid)
2485 ;`);
2488 immutable bool updatePane = (chiroGetTreePaneTableTagId() == tagid);
2489 bool wasChanges = false;
2491 transacted!"View"{
2492 // get tagid (possibly appending the tag)
2493 bool hasit = false;
2494 foreach (auto row; stUidHasTag.st.bind(":uid", uid).bind(":tagid", tagid).range) hasit = true;
2495 if (!hasit) return;
2497 stClearThreads.st.bind(":uid", uid).bind(":tagid", tagid).doAll((stmt) { wasChanges = true; });
2499 // if there were any changes, rebuild message tags
2500 if (!wasChanges) return;
2502 DynStr newtags;
2503 foreach (auto trow; stGetMsgTags.st.bind(":uid", uid).range) {
2504 auto tname = trow.name!SQ3Text;
2505 if (tname.length == 0) continue;
2506 if (newtags.length) newtags ~= "|";
2507 newtags ~= tname;
2510 // if there is no tags, assign "#hobo"
2511 // this should not happen, but...
2512 if (newtags.length == 0) {
2513 newtags = "#hobo";
2514 auto hobo = chiroAppendTag(newtags, hidden:1);
2515 assert(hobo != 0);
2516 // append record for this tag to threads
2517 // note that there is no need to relink hobos, they should not be threaded
2518 //FIXME: this clears message appearance
2519 stInsertIntoThreads.st
2520 .bind(":uid", uid)
2521 .bind(":tagid", hobo)
2522 .bind(":appr", Appearance.Read)
2523 .doAll();
2526 // update storage with new tag names
2527 assert(newtags.length);
2528 stUpdateStorageTags.st.bindConstText(":tags", newtags).doAll();
2530 // and relink threads for this tagid
2531 chiroSupportRelinkTagThreads(tagid);
2534 return (wasChanges && updatePane);
2538 // ////////////////////////////////////////////////////////////////////////// //
2539 /// returns `true` if we need to update pane
2540 public bool chiroMessageAddTag (uint uid, const(char)[] tagname) {
2541 if (uid == 0) return false;
2542 tagname = tagname.xstrip;
2543 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2544 tagname = tagname.xstrip;
2545 if (tagname.length == 0) return false;
2546 if (tagname.indexOf('|') >= 0) return false;
2548 static auto stUpdateStorageTags = LazyStatement!"Store"(`
2549 UPDATE SET tags=tags||'|'||:tagname WHERE uid=:uid
2550 ;`);
2552 static auto stUidExists = LazyStatement!"View"(`
2553 SELECT uid AS uid FROM threads WHERE uid=:uid LIMIT 1
2554 ;`);
2556 static auto stUidHasTag = LazyStatement!"View"(`
2557 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2558 ;`);
2560 static auto stInsertIntoThreads = LazyStatement!"View"(`
2561 INSERT INTO threads(uid, tagid,appearance,time)
2562 VALUES(:uid, :tagid, :appr, (SELECT time FROM info WHERE uid=:uid LIMIT 1))
2563 ;`);
2565 static auto stUnHobo = LazyStatement!"View"(`
2566 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2567 ;`);
2569 bool hasuid = false;
2570 foreach (auto row; stUidExists.st.bind(":uid", uid).range) hasuid = true;
2571 if (!hasuid) return false; // nothing to do
2573 immutable paneTagId = chiroGetTreePaneTableTagId();
2574 bool updatePane = false;
2576 transacted!"View"{
2577 // get tagid (possibly appending the tag)
2578 uint tagid = chiroAppendTag(tagname);
2579 if (tagid == 0) {
2580 conwriteln("ERROR: cannot append tag name '", tagname, "'!");
2581 return;
2584 bool hasit = false;
2585 foreach (auto row; stUidHasTag.st.bind(":uid", uid).bind(":tagid", tagid).range) hasit = true;
2586 if (hasit) return;
2588 // append this tag to the message in the storage
2589 stUpdateStorageTags.st.bind(":uid", uid).bindConstText(":tagname", tagname).doAll();
2591 // append record for this tag to threads
2592 stInsertIntoThreads.st
2593 .bind(":uid", uid)
2594 .bind(":tagid", tagid)
2595 .bind(":appr", Appearance.Read)
2596 .doAll();
2598 // and relink threads for this tagid
2599 chiroSupportRelinkTagThreads(tagid);
2601 // remove this message from "#hobo", if there is any
2602 auto hobo = chiroGetTagUid("#hobo");
2603 if (hobo && hobo != tagid) {
2604 stUnHobo.st.bind(":tagid", hobo).bind(":uid", uid).doAll();
2605 // there's no need to relink hobos, because they should have no links
2608 updatePane = (tagid == paneTagId);
2611 return updatePane;
2616 inserts the one message from the message storage with the given id into view storage.
2617 parses it and such, and optionally updates threads.
2619 doesn't updates NNTP indicies and such, never relinks anything.
2621 invalid (unknown) tags will be ignored.
2623 returns number of processed messages.
2625 doesn't start/end any transactions, so wrap it yourself.
2627 public bool chiroParseAndInsertOneMessage (uint uid, uint msgtime, int appearance,
2628 const(char)[] hdrs, const(char)[] body, const(char)[] tags)
2630 auto stInsThreads = dbView.statement(`
2631 INSERT INTO threads
2632 ( uid, tagid, time, appearance)
2633 VALUES(:uid,:tagid,:time,:appearance)
2634 ;`);
2636 auto stInsInfo = dbView.statement(`
2637 INSERT INTO info
2638 ( uid, from_name, from_mail, subj, to_name, to_mail)
2639 VALUES(:uid,:from_name,:from_mail,:subj,:to_name,:to_mail)
2640 ;`);
2642 auto stInsMsgId = dbView.statement(`
2643 INSERT INTO msgids
2644 ( uid, msgid, time)
2645 VALUES(:uid,:msgid,:time)
2646 ;`);
2648 auto stInsMsgRefId = dbView.statement(`
2649 INSERT INTO refids
2650 ( uid, idx, msgid)
2651 VALUES(:uid,:idx,:msgid)
2652 ;`);
2654 auto stInsContentText = dbView.statement(`
2655 INSERT INTO content_text
2656 ( uid, format, content)
2657 VALUES(:uid,:format, ChiroPack(:content))
2658 ;`);
2660 auto stInsContentHtml = dbView.statement(`
2661 INSERT INTO content_html
2662 ( uid, format, content)
2663 VALUES(:uid,:format, ChiroPack(:content))
2664 ;`);
2666 auto stInsAttach = dbView.statement(`
2667 INSERT INTO attaches
2668 ( uid, idx, mime, name, format, content)
2669 VALUES(:uid,:idx,:mime,:name,:format, ChiroPack(:content))
2670 ;`);
2672 bool noattaches = false; // do not store attaches?
2674 // create thread record for each tag (and update max nntp index)
2675 int tagCount = 0;
2676 int noAttachCount = 0;
2677 while (tags.length) {
2678 auto eep = tags.indexOf('|');
2679 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
2680 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
2681 if (tagname.length == 0) continue;
2683 immutable uint tuid = chiroGetTagUid(tagname);
2684 if (tuid == 0) continue;
2686 /* nope
2687 if (nntpidx > 0 && tagname.startsWith("account:")) {
2688 auto accname = tagname[8..$];
2689 stInsNNTPIdx
2690 .bindConstText(":accname", accname)
2691 .bind(":nntpidx", nntpidx)
2692 .doAll();
2696 if (!chiroIsTagAllowAttaches(tuid)) ++noAttachCount;
2697 ++tagCount;
2699 stInsThreads
2700 .bind(":uid", uid)
2701 .bind(":tagid", tuid)
2702 .bind(":time", msgtime)
2703 .bind(":appearance", appearance)
2704 .doAll();
2706 if (!tagCount) return false;
2707 noattaches = (noAttachCount && noAttachCount == tagCount);
2709 // insert msgid
2711 bool hasmsgid = false;
2712 auto msgidfield = findHeaderField(hdrs, "Message-Id");
2713 if (msgidfield.length) {
2714 auto id = msgidfield.getFieldValue;
2715 if (id.length) {
2716 hasmsgid = true;
2717 stInsMsgId
2718 .bind(":uid", uid)
2719 .bind("time", msgtime)
2720 .bindConstText(":msgid", id)
2721 .doAll();
2724 // if there is no msgid, create one
2725 if (!hasmsgid) {
2726 RIPEMD160 hash;
2727 hash.start();
2728 hash.put(cast(const(ubyte)[])hdrs);
2729 hash.put(cast(const(ubyte)[])body);
2730 ubyte[20] digest = hash.finish();
2731 char[20*2+2+16] buf;
2732 import core.stdc.stdio : snprintf;
2733 import core.stdc.string : strcat;
2734 foreach (immutable idx, ubyte b; digest[]) snprintf(buf.ptr+idx*2, 3, "%02x", b);
2735 strcat(buf.ptr, "@artificial"); // it is safe, there is enough room for it
2736 stInsMsgId
2737 .bind(":uid", uid)
2738 .bind("time", msgtime)
2739 .bindConstText(":msgid", buf[0..20*2])
2740 .doAll();
2744 // insert references
2746 uint refidx = 0;
2747 auto inreplyfld = findHeaderField(hdrs, "In-Reply-To");
2748 while (inreplyfld.length) {
2749 auto id = getNextFieldValue(inreplyfld);
2750 if (id.length) {
2751 stInsMsgRefId
2752 .bind(":uid", uid)
2753 .bind(":idx", refidx++)
2754 .bind(":msgid", id)
2755 .doAll();
2759 inreplyfld = findHeaderField(hdrs, "References");
2760 while (inreplyfld.length) {
2761 auto id = getNextFieldValue(inreplyfld);
2762 if (id.length) {
2763 stInsMsgRefId
2764 .bind(":uid", uid)
2765 .bind(":idx", refidx++)
2766 .bind(":msgid", id)
2767 .doAll();
2772 // insert base content and attaches
2774 Content[] content;
2775 parseContent(ref content, hdrs, body, noattaches);
2776 // insert text and html
2777 bool wasText = false, wasHtml = false;
2778 foreach (const ref Content cc; content) {
2779 if (cc.name.length) continue;
2780 if (noattaches && !cc.mime.startsWith("text/")) continue;
2781 if (!wasText && cc.mime == "text/plain") {
2782 wasText = true;
2783 stInsContentText
2784 .bind(":uid", uid)
2785 .bindConstText(":format", cc.format)
2786 .bindConstBlob(":content", cc.data)
2787 .doAll();
2788 } else if (!wasHtml && cc.mime == "text/html") {
2789 wasHtml = true;
2790 stInsContentHtml
2791 .bind(":uid", uid)
2792 .bindConstText(":format", cc.format)
2793 .bindConstBlob(":content", cc.data)
2794 .doAll();
2797 if (!wasText) {
2798 stInsContentText
2799 .bind(":uid", uid)
2800 .bindConstText(":format", "")
2801 .bindConstBlob(":content", "")
2802 .doAll();
2804 if (!wasHtml) {
2805 stInsContentHtml
2806 .bind(":uid", uid)
2807 .bindConstText(":format", "")
2808 .bindConstBlob(":content", "")
2809 .doAll();
2811 // insert everything
2812 uint cidx = 0;
2813 foreach (const ref Content cc; content) {
2814 if (cc.name.length == 0 && cc.mime.startsWith("text/")) continue;
2815 // for "no attaches" mode, still record the attach, but ignore its contents
2816 stInsAttach
2817 .bind(":uid", uid)
2818 .bind(":idx", cidx++)
2819 .bindConstText(":mime", cc.mime)
2820 .bindConstText(":name", cc.name)
2821 .bindConstText(":format", cc.name)
2822 .bindConstBlob(":content", (noattaches ? null : cc.data), allowNull:true)
2823 .doAll();
2827 // insert from/to/subj info
2828 // this must be done last to keep FTS5 in sync
2830 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
2831 auto from = findHeaderField(hdrs, "From");
2832 auto to = findHeaderField(hdrs, "To");
2833 stInsInfo
2834 .bind(":uid", uid)
2835 .bind(":from_name", from.extractName)
2836 .bind(":from_mail", from.extractMail)
2837 .bind(":subj", subj)
2838 .bind(":to_name", to.extractName)
2839 .bind(":to_mail", to.extractMail)
2840 .doAll();
2843 return true;
2848 inserts the messages from the message storage with the given id into view storage.
2849 parses it and such, and optionally updates threads.
2851 WARNING! DOESN'T UPDATE NNTP INDICIES! this should be done by the downloader.
2853 invalid (unknown) tags will be ignored.
2855 returns number of processed messages.
2857 public uint chiroParseAndInsertMessages (uint stmsgid,
2858 void delegate (uint count, uint total, uint nntpidx, const(char)[] tags) progresscb=null,
2859 uint emsgid=uint.max, bool relink=true, bool asread=false)
2861 if (emsgid < stmsgid) return 0; // nothing to do
2863 uint count = 0;
2864 uint total = 0;
2865 if (progresscb !is null) {
2866 // find total number of messages to process
2867 foreach (auto row; dbStore.statement(`
2868 SELECT count(uid) AS total FROM messages WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
2869 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
2871 total = row.total!uint;
2872 break;
2874 if (total == 0) return 0; // why not?
2877 transacted!"View"{
2878 uint[] uptagids;
2879 if (relink) uptagids.reserve(128);
2880 scope(exit) delete uptagids;
2882 foreach (auto mrow; dbStore.statement(`
2883 -- this should cache unpack results
2884 WITH msgunpacked(msguid, msgdata, msgtags) AS (
2885 SELECT uid AS msguid, ChiroUnpack(data) AS msgdata, tags AS msgtags
2886 FROM messages
2887 WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
2888 ORDER BY uid
2890 SELECT
2891 msguid AS uid
2892 , msgtags AS tags
2893 , ChiroExtractHeaders(msgdata) AS headers
2894 , ChiroExtractBody(msgdata) AS body
2895 , ChiroHdr_NNTPIndex(msgdata) AS nntpidx
2896 , ChiroHdr_RecvTime(msgdata) AS msgtime
2897 FROM msgunpacked
2898 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
2900 ++count;
2901 auto hdrs = mrow.headers!SQ3Text;
2902 auto body = mrow.body!SQ3Text;
2903 auto tags = mrow.tags!SQ3Text;
2904 uint uid = mrow.uid!uint;
2905 uint nntpidx = mrow.nntpidx!uint;
2906 uint msgtime = mrow.msgtime!uint;
2907 assert(tags.length);
2909 chiroParseAndInsertOneMessage(uid, msgtime, (asread ? 1 : 0), hdrs, body, tags);
2911 if (progresscb !is null) progresscb(count, total, nntpidx, tags);
2913 if (relink) {
2914 while (tags.length) {
2915 auto eep = tags.indexOf('|');
2916 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
2917 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
2918 if (tagname.length == 0) continue;
2920 immutable uint tuid = chiroGetTagUid(tagname);
2921 if (tuid == 0) continue;
2923 bool found = false;
2924 foreach (immutable n; uptagids) if (n == tuid) { found = true; break; }
2925 if (!found) uptagids ~= tuid;
2930 if (relink && uptagids.length) {
2931 foreach (immutable tagid; uptagids) chiroSupportRelinkTagThreads(tagid);
2935 return count;
2940 returns accouint uid (accid) or 0.
2942 public uint chiroGetAccountUid (const(char)[] accname) {
2943 static auto stat = LazyStatement!"Conf"(`SELECT accid AS accid FROM accounts WHERE name=:accname LIMIT 1;`);
2944 foreach (auto row; stat.st.bindConstText(":accname", accname).range) return row.accid!uint;
2945 return 0;
2950 returns accouint name, or empty string.
2952 public DynStr chiroGetAccountName (uint accid) {
2953 static auto stat = LazyStatement!"Conf"(`SELECT name AS name FROM accounts WHERE accid=:accid LIMIT 1;`);
2954 DynStr res;
2955 if (accid == 0) return res;
2956 foreach (auto row; stat.st.bind(":accid", accid).range) {
2957 res = row.name!SQ3Text;
2958 break;
2960 return res;
2965 returns list of known tags, sorted by name.
2967 public string[] chiroGetTagList () {
2968 static auto stat = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE hidden=0 ORDER BY tag;`);
2969 string[] res;
2970 foreach (auto row; stat.st.range) res ~= row.tagname!string;
2971 return res;
2976 returns tag uid (tagid) or 0.
2978 public uint chiroGetTagUid (const(char)[] tagname) {
2979 static auto stat = LazyStatement!"View"(`SELECT tagid AS tagid FROM tagnames WHERE tag=:tagname LIMIT 1;`);
2980 foreach (auto row; stat.st.bindConstText(":tagname", tagname).range) {
2981 return row.tagid!uint;
2983 return 0;
2988 returns tag name or empty string.
2990 public DynStr chiroGetTagName (uint tagid) {
2991 static auto stat = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
2992 DynStr s;
2993 foreach (auto row; stat.st.bind(":tagid", tagid).range) {
2994 s = row.tagname!SQ3Text;
2995 break;
2997 return s;
3002 returns `true` if the given tag supports threads.
3004 this is used only when adding new messages, to set all parents to 0.
3006 public bool chiroIsTagThreaded(T) (T tagnameid)
3007 if (is(T:const(char)[]) || is(T:uint))
3009 static if (is(T:const(char)[])) {
3010 static auto stat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3011 foreach (auto row; stat.st.bindConstText(":tagname", tagnameid).range) {
3012 return (row.threading!uint == 1);
3014 } else {
3015 static auto xstat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3016 foreach (auto row; xstat.st.bind(":tagid", tagnameid).range) {
3017 return (row.threading!uint == 1);
3020 return false;
3025 returns `true` if the given tag allows attaches.
3027 this is used only when adding new messages, to set all parents to 0.
3029 public bool chiroIsTagAllowAttaches(T) (T tagnameid)
3030 if (is(T:const(char)[]) || is(T:uint))
3032 static if (is(T:const(char)[])) {
3033 static auto stat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3034 foreach (auto row; stat.st.bindConstText(":tagname", tagnameid).range) {
3035 return (row.threading!uint == 1);
3037 } else {
3038 static auto xstat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3039 foreach (auto row; xstat.st.bind(":tagid", tagnameid).range) {
3040 return (row.threading!uint == 1);
3043 return false;
3048 relinks all messages in all threads suitable for relinking, and
3049 sets parents to zero otherwise.
3051 public void chiroSupportRelinkAllThreads () {
3052 // yeah, that's it: a single SQL statement
3053 dbView.execute(`
3054 -- clear parents where threading is disabled
3055 SELECT ChiroTimerStart('clearing parents');
3056 UPDATE threads
3058 parent = 0
3059 WHERE
3060 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=0)
3061 AND parent <> 0
3063 SELECT ChiroTimerStop();
3065 SELECT ChiroTimerStart('relinking threads');
3066 UPDATE threads
3068 parent=ifnull(
3070 SELECT uid FROM msgids
3071 WHERE
3072 -- find MSGID for any of our current references
3073 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3074 -- check if UID for that MSGID has the valid tag
3075 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=threads.tagid)
3076 ORDER BY time DESC
3077 LIMIT 1
3079 , 0)
3080 WHERE
3081 -- do not process messages with non-threading tags
3082 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=1)
3084 SELECT ChiroTimerStop();
3090 relinks all messages for the given tag, or sets parents to zero if
3091 threading for that tag is disabled.
3093 public void chiroSupportRelinkTagThreads(T) (T tagnameid)
3094 if (is(T:const(char)[]) || is(T:uint))
3096 static if (is(T:const(char)[])) {
3097 immutable uint tid = chiroGetTagUid(tagnameid);
3098 if (!tid) return;
3099 } else {
3100 alias tid = tagnameid;
3103 static auto statNoTrd = LazyStatement!"View"(`
3104 UPDATE threads
3106 parent = 0
3107 WHERE
3108 tagid = :tagid AND parent <> 0
3109 ;`);
3111 static auto statTrd = LazyStatement!"View"(`
3112 UPDATE threads
3114 parent=ifnull(
3116 SELECT uid FROM msgids
3117 WHERE
3118 -- find MSGID for any of our current references
3119 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3120 -- check if UID for that MSGID has the valid tag
3121 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=:tagid)
3122 ORDER BY time DESC
3123 LIMIT 1
3125 , 0)
3126 WHERE
3127 threads.tagid = :tagid
3128 ;`);
3130 if (!chiroIsTagThreaded(tid)) {
3131 // clear parents (just in case)
3132 statNoTrd.st.bind(":tagid", tid).doAll();
3133 } else {
3134 // yeah, that's it: a single SQL statement
3135 statTrd.st.bind(":tagid", tid).doAll();
3141 gets twit title and state for the given (tagid, uid) message.
3143 returns -666 if there is no such message.
3145 public DynStr chiroGetMessageTwit(T) (T tagidname, uint uid, out bool twited)
3146 if (is(T:const(char)[]) || is(T:uint))
3148 twited = false;
3149 DynStr res;
3150 if (!uid) return res;
3152 static if (is(T:const(char)[])) {
3153 immutable uint tid = chiroGetTagUid(tagidname);
3154 if (!tid) return 0;
3155 enum selHdr = ``;
3156 } else {
3157 alias tid = tagidname;
3160 if (!tid) return res;
3162 static auto statGetTwit = LazyStatement!"View"(`
3163 SELECT title AS title
3164 FROM threads
3165 WHERE uid=:uid AND tagid=:tagid AND mute>0
3166 LIMIT 1
3167 ;`);
3169 statGetTwit.st
3170 .bind(":uid", uid)
3171 .bind(":tagid", tid);
3172 foreach (auto row; statGetTwit.st.range) {
3173 twited = true;
3174 res = row.title!SQ3Text;
3177 return res;
3182 gets mute state for the given (tagid, uid) message.
3184 returns -666 if there is no such message.
3186 public int chiroGetMessageMute(T) (T tagidname, uint uid)
3187 if (is(T:const(char)[]) || is(T:uint))
3189 if (!uid) return -666;
3191 static if (is(T:const(char)[])) {
3192 immutable uint tid = chiroGetTagUid(tagidname);
3193 if (!tid) return 0;
3194 enum selHdr = ``;
3195 } else {
3196 alias tid = tagidname;
3199 if (!tid) return -666;
3201 static auto statGetApp = LazyStatement!"View"(`
3202 SELECT mute AS mute
3203 FROM threads
3204 WHERE uid=:uid AND tagid=:tagid
3205 LIMIT 1
3206 ;`);
3208 statGetApp.st
3209 .bind(":uid", uid)
3210 .bind(":tagid", tid);
3211 foreach (auto row; statGetApp.st.range) return row.mute!int;
3212 return -666;
3217 sets mute state the given (tagid, uid) message.
3219 doesn't change children states.
3221 public void chiroSetMessageMute(T) (T tagidname, uint uid, Mute mute)
3222 if (is(T:const(char)[]) || is(T:uint))
3224 if (!uid) return;
3226 static if (is(T:const(char)[])) {
3227 immutable uint tid = chiroGetTagUid(tagidname);
3228 if (!tid) return 0;
3229 enum selHdr = ``;
3230 } else {
3231 alias tid = tagidname;
3234 if (!tid) return;
3236 static auto statSetApp = LazyStatement!"View"(`
3237 UPDATE threads
3239 mute=:mute
3240 WHERE
3241 uid=:uid AND tagid=:tagid
3242 ;`);
3244 statSetApp.st
3245 .bind(":mute", cast(int)mute)
3246 .bind(":uid", uid)
3247 .bind(":tagid", tid)
3248 .doAll();
3253 gets appearance for the given (tagid, uid) message.
3255 returns -666 if there is no such message.
3257 public int chiroGetMessageAppearance(T) (T tagidname, uint uid)
3258 if (is(T:const(char)[]) || is(T:uint))
3260 if (!uid) return -666;
3262 static if (is(T:const(char)[])) {
3263 immutable uint tid = chiroGetTagUid(tagidname);
3264 if (!tid) return 0;
3265 enum selHdr = ``;
3266 } else {
3267 alias tid = tagidname;
3270 if (!tid) return -666;
3272 static auto statGetApp = LazyStatement!"View"(`
3273 SELECT appearance AS appearance
3274 FROM threads
3275 WHERE uid=:uid AND tagid=:tagid
3276 LIMIT 1
3277 ;`);
3279 statGetApp.st
3280 .bind(":uid", uid)
3281 .bind(":tagid", tid);
3282 foreach (auto row; statGetApp.st.range) return row.appearance!int;
3283 return -666;
3288 gets appearance for the given (tagid, uid) message.
3290 public bool chiroGetMessageUnread(T) (T tagidname, uint uid)
3291 if (is(T:const(char)[]) || is(T:uint))
3293 return (chiroGetMessageAppearance(tagidname, uid) == Appearance.Unread);
3298 gets appearance for the given (tagid, uid) message.
3300 public bool chiroGetMessageExactRead(T) (T tagidname, uint uid)
3301 if (is(T:const(char)[]) || is(T:uint))
3303 return (chiroGetMessageAppearance(tagidname, uid) == Appearance.Read);
3308 sets appearance for the given (tagid, uid) message.
3310 public void chiroSetMessageAppearance(T) (T tagidname, uint uid, Appearance appearance)
3311 if (is(T:const(char)[]) || is(T:uint))
3313 if (!uid) return;
3315 static if (is(T:const(char)[])) {
3316 immutable uint tid = chiroGetTagUid(tagidname);
3317 if (!tid) return 0;
3318 enum selHdr = ``;
3319 } else {
3320 alias tid = tagidname;
3323 if (!tid) return;
3325 static auto statSetApp = LazyStatement!"View"(`
3326 UPDATE threads
3328 appearance=:appearance
3329 WHERE
3330 uid=:uid AND tagid=:tagid
3331 ;`);
3333 statSetApp.st
3334 .bind(":appearance", cast(int)appearance)
3335 .bind(":uid", uid)
3336 .bind(":tagid", tid)
3337 .doAll();
3342 mark (tagid, uid) message as read.
3344 public void chiroSetReadOrUnreadMessageAppearance(T) (T tagidname, uint uid, Appearance appearance)
3345 if (is(T:const(char)[]) || is(T:uint))
3347 if (!uid) return;
3349 static if (is(T:const(char)[])) {
3350 immutable uint tid = chiroGetTagUid(tagidname);
3351 if (!tid) return 0;
3352 enum selHdr = ``;
3353 } else {
3354 alias tid = tagidname;
3357 if (!tid) return;
3359 static auto statSetApp = LazyStatement!"View"(`
3360 UPDATE threads
3362 appearance=:setapp
3363 WHERE
3364 uid=:uid AND tagid=:tagid AND (appearance=:checkapp0 OR appearance=:checkapp1)
3365 ;`);
3367 statSetApp.st
3368 .bind(":uid", uid)
3369 .bind(":tagid", tid)
3370 .bind(":setapp", cast(int)appearance)
3371 .bind(":checkapp0", Appearance.Read)
3372 .bind(":checkapp1", Appearance.Unread)
3373 .doAll();
3378 mark (tagid, uid) message as read.
3380 public void chiroSetMessageRead(T) (T tagidname, uint uid)
3381 if (is(T:const(char)[]) || is(T:uint))
3383 chiroSetReadOrUnreadMessageAppearance(tagidname, uid, Appearance.Read);
3387 public void chiroSetMessageUnread(T) (T tagidname, uint uid)
3388 if (is(T:const(char)[]) || is(T:uint))
3390 chiroSetReadOrUnreadMessageAppearance(tagidname, uid, Appearance.Unread);
3395 purge all messages with the given tag.
3397 this removes messages from all view tables, removes content from
3398 the "messages" table, and sets "messages" table tags to NULL.
3400 public void chiroDeletePurgedWithTag(T) (T tagidname)
3401 if (is(T:const(char)[]) || is(T:uint))
3403 static if (is(T:const(char)[])) {
3404 immutable uint tid = chiroGetTagUid(tagidname);
3405 if (!tid) return 0;
3406 enum selHdr = ``;
3407 } else {
3408 alias tid = tagidname;
3411 if (!tid) return;
3413 static auto statCountPurged = LazyStatement!"View"(`
3414 SELECT COUNT(uid) AS pcount FROM threads
3415 WHERE tagid=:tagid AND appearance=:appr
3418 uint purgedCount = 0;
3419 foreach (auto row; statCountPurged.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).range) {
3420 purgedCount = row.pcount!uint;
3422 if (!purgedCount) return;
3424 // we will need this to clear storage
3425 uint[] plist;
3426 scope(exit) delete plist;
3427 plist.reserve(purgedCount);
3429 static auto statListPurged = LazyStatement!"View"(`
3430 SELECT uid AS uid FROM threads
3431 WHERE tagid=:tagid AND appearance=:appr
3432 ORDER BY uid
3435 foreach (auto row; statListPurged.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).range) {
3436 plist ~= row.uid!uint;
3438 if (plist.length == 0) return; // just in case
3440 static auto statClearStorage = LazyStatement!"Store"(`
3441 UPDATE messages
3442 SET tags=NULL, data=NULL
3443 WHERE uid=:uid
3444 ;`);
3446 enum BulkClearSQL(string table) = `
3447 DELETE FROM `~table~`
3448 WHERE
3449 uid IN (SELECT uid FROM threads WHERE tagid=:tagid AND appearance=:appr)
3452 // bulk clearing of info
3453 static auto statClearInfo = LazyStatement!"View"(BulkClearSQL!"info");
3454 // bulk clearing of msgids
3455 static auto statClearMsgids = LazyStatement!"View"(BulkClearSQL!"msgids");
3456 // bulk clearing of refids
3457 static auto statClearRefids = LazyStatement!"View"(BulkClearSQL!"refids");
3458 // bulk clearing of text
3459 static auto statClearText = LazyStatement!"View"(BulkClearSQL!"content_text");
3460 // bulk clearing of html
3461 static auto statClearHtml = LazyStatement!"View"(BulkClearSQL!"content_html");
3462 // bulk clearing of attaches
3463 static auto statClearAttach = LazyStatement!"View"(BulkClearSQL!"attaches");
3464 // bulk clearing of threads
3465 static auto statClearThreads = LazyStatement!"View"(`
3466 DELETE FROM threads
3467 WHERE tagid=:tagid AND appearance=:appr
3468 ;`);
3470 static if (is(T:const(char)[])) {
3471 conwriteln("removing ", plist.length, " message", (plist.length != 1 ? "s" : ""), " from '", tagidname, "'...");
3472 } else {
3473 DynStr tname = chiroGetTagName(tid);
3474 conwriteln("removing ", plist.length, " message", (plist.length != 1 ? "s" : ""), " from '", tname.getData, "'...");
3477 // WARNING! "info" must be cleared FIRST, and "threads" LAST
3478 transacted!"View"{
3479 statClearInfo.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3480 statClearMsgids.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3481 statClearRefids.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3482 statClearText.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3483 statClearHtml.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3484 statClearAttach.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3485 statClearThreads.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3486 // relink tag threads
3487 chiroSupportRelinkTagThreads(tid);
3490 // now clear the storage
3491 conwriteln("clearing the storage...");
3492 transacted!"Store"{
3493 foreach (immutable uint uid; plist) {
3494 statClearStorage.st.bind(":uid", uid).doAll();
3498 conwriteln("done purging.");
3503 creates "treepane" table for the given tag. that table can be used to
3504 render threaded listview.
3506 returns max id of the existing item. can be used for pagination.
3507 item ids are guaranteed to be sequential, and without any holes.
3508 the first id is `1`.
3510 returned table has "rowid", and two integer fields: "uid" (message uid), and
3511 "level" (message depth, starting from 0).
3513 public uint chiroCreateTreePaneTable(T) (T tagidname, int lastmonthes=12, bool allowThreading=true)
3514 if (is(T:const(char)[]) || is(T:uint))
3516 auto ctm = Timer(true);
3518 // shrink temp table to the bare minimum, because each field costs several msecs
3519 // we don't need parent and time here, because we can easily select them with inner joins
3521 dbView.execute(`
3522 DROP TABLE IF EXISTS treepane;
3523 CREATE TEMP TABLE IF NOT EXISTS treepane (
3524 iid INTEGER PRIMARY KEY
3525 , uid INTEGER
3526 , level INTEGER
3527 -- to make joins easier
3528 , tagid INTEGER
3533 // clear it (should be faster than dropping and recreating)
3534 dbView.execute(`DELETE FROM treepane;`);
3536 static auto statTrd = LazyStatement!"View"(`
3537 INSERT INTO treepane
3538 (uid, level, tagid)
3539 WITH tree(uid, parent, level, time, path) AS (
3540 WITH RECURSIVE fulltree(uid, parent, level, time, path) AS (
3541 SELECT t.uid AS uid, t.parent AS parent, 1 AS level, t.time AS time, printf('%08X', t.time) AS path
3542 FROM threads t
3543 WHERE t.time>=:starttime AND parent=0 AND t.tagid=:tagidname AND t.appearance <> -1
3544 UNION ALL
3545 SELECT t.uid AS uid, t.parent AS parent, ft.level+1 AS level, t.time AS time, printf('%s|%08X', ft.path, t.time) AS path
3546 FROM threads t, fulltree ft
3547 WHERE t.time>=:starttime AND t.parent=ft.uid AND t.tagid=:tagidname AND t.appearance <> -1
3549 SELECT * FROM fulltree
3551 SELECT
3552 tree.uid AS uid
3553 , tree.level-1 AS level
3554 , :tagidname AS tagid
3555 FROM tree
3556 ORDER BY path
3557 ;`);
3559 static auto statNoTrd = LazyStatement!"View"(`
3560 INSERT INTO treepane
3561 (uid, level, tagid)
3562 SELECT
3563 threads.uid AS uid
3564 , 0 AS level
3565 , :tagidname AS tagid
3566 FROM threads
3567 WHERE
3568 threads.time>=:starttime AND threads.tagid=:tagidname AND threads.appearance <> -1
3569 ORDER BY
3570 threads.time
3571 ;`);
3573 static if (is(T:const(char)[])) {
3574 immutable uint tid = chiroGetTagUid(tagidname);
3575 if (!tid) return 0;
3576 enum selHdr = ``;
3577 } else {
3578 alias tid = tagidname;
3581 uint startTime = 0;
3583 if (lastmonthes > 0) {
3584 if (lastmonthes > 12*100) {
3585 startTime = 0;
3586 } else {
3587 // show last `lastmonthes` (full monthes)
3588 import std.datetime;
3589 import core.time : Duration;
3591 SysTime now = Clock.currTime().toUTC();
3592 int year = now.year;
3593 int month = now.month; // from 1
3594 --lastmonthes;
3595 // yes, i am THAT lazy
3596 while (lastmonthes > 0) {
3597 if (month > lastmonthes) { month -= lastmonthes; break; }
3598 lastmonthes -= month;
3599 month = 12;
3600 --year;
3602 // construct unix time
3603 now.fracSecs = Duration.zero;
3604 now.second = 0;
3605 now.hour = 0;
3606 now.minute = 0;
3607 now.day = 1;
3608 now.month = cast(Month)month;
3609 now.year = year;
3610 startTime = cast(uint)now.toUnixTime();
3614 // this "%08X" will do up to 2038; i'm fine with it
3615 if (allowThreading) {
3616 statTrd.st.bind(":tagidname", tid).bind(":starttime", startTime).doAll();
3617 } else {
3618 statNoTrd.st.bind(":tagidname", tid).bind(":starttime", startTime).doAll();
3620 ctm.stop;
3621 if (ChiroTimerEnabled) writeln("creating treepane time: ", ctm);
3623 immutable uint res = cast(uint)dbView.lastRowId;
3625 version(chidb_drop_pane_table) {
3626 dbView.execute(`CREATE INDEX treepane_uid ON treepane(uid);`);
3629 return res;
3634 returns current treepane tagid.
3636 public uint chiroGetTreePaneTableTagId () {
3637 static auto stat = LazyStatement!"View"(`SELECT tagid AS tagid FROM treepane WHERE iid=1 LIMIT 1;`);
3638 foreach (auto row; stat.st.range) return row.tagid!uint;
3639 return 0;
3644 returns current treepane max uid.
3646 public uint chiroGetTreePaneTableMaxUId () {
3647 static auto stat = LazyStatement!"View"(`SELECT MAX(uid) AS uid FROM treepane LIMIT 1;`);
3648 foreach (auto row; stat.st.range) return row.uid!uint;
3649 return 0;
3654 returns number of items in the current treepane.
3656 public uint chiroGetTreePaneTableCount () {
3657 static auto stat = LazyStatement!"View"(`SELECT COUNT(*) AS total FROM treepane;`);
3658 foreach (auto row; stat.st.range) return row.total!uint;
3659 return 0;
3664 returns index of the given uid in the treepane.
3666 public bool chiroIsTreePaneTableUidValid (uint uid) {
3667 static auto stat = LazyStatement!"View"(`SELECT iid AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
3668 if (uid == 0) return false;
3669 foreach (auto row; stat.st.bind(":uid", uid).range) return true;
3670 return false;
3675 returns first treepane uid.
3677 public uint chiroGetTreePaneTableFirstUid () {
3678 static auto stmt = LazyStatement!"View"(`SELECT uid AS uid FROM treepane WHERE iid=1 LIMIT 1;`);
3679 foreach (auto row; stmt.st.range) return row.uid!uint;
3680 return 0;
3685 returns last treepane uid.
3687 public uint chiroGetTreePaneTableLastUid () {
3688 static auto stmt = LazyStatement!"View"(`SELECT MAX(iid), uid AS uid FROM treepane LIMIT 1;`);
3689 foreach (auto row; stmt.st.range) return row.uid!uint;
3690 return 0;
3695 returns index of the given uid in the treepane.
3697 public int chiroGetTreePaneTableUid2Index (uint uid) {
3698 static auto stmt = LazyStatement!"View"(`SELECT iid-1 AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
3699 if (uid == 0) return -1;
3700 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.idx!int;
3701 return -1;
3706 returns uid of the given index in the treepane.
3708 public uint chiroGetTreePaneTableIndex2Uid (int index) {
3709 static auto stmt = LazyStatement!"View"(`SELECT uid AS uid FROM treepane WHERE iid=:idx+1 LIMIT 1;`);
3710 if (index < 0 || index == int.max) return 0;
3711 foreach (auto row; stmt.st.bind(":idx", index).range) return row.uid!uint;
3712 return 0;
3717 returns previous uid in the treepane.
3719 public uint chiroGetTreePaneTablePrevUid (uint uid) {
3720 static auto stmt = LazyStatement!"View"(`
3721 SELECT uid AS uid FROM treepane
3722 WHERE iid IN (SELECT iid-1 FROM treepane WHERE uid=:uid LIMIT 1)
3723 LIMIT 1
3724 ;`);
3725 if (uid == 0) return chiroGetTreePaneTableFirstUid();
3726 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.uid!uint;
3727 return 0;
3732 returns uid of the given index in the treepane.
3734 public uint chiroGetTreePaneTableNextUid (uint uid) {
3735 static auto stmt = LazyStatement!"View"(`
3736 SELECT uid AS uid FROM treepane
3737 WHERE iid IN (SELECT iid+1 FROM treepane WHERE uid=:uid LIMIT 1)
3738 LIMIT 1
3739 ;`);
3740 if (uid == 0) return chiroGetTreePaneTableFirstUid();
3741 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.uid!uint;
3742 return 0;
3747 releases (drops) "treepane" table.
3749 can be called several times, but usually you don't need to call this at all.
3751 public void chiroClearTreePaneTable () {
3752 //dbView.execute(`DROP TABLE IF EXISTS treepane;`);
3753 dbView.execute(`DELETE FROM treepane;`);
3758 return next unread message uid in treepane, or 0.
3760 public uint chiroGetPaneNextUnread (uint curruid) {
3761 static auto stmtNext = LazyStatement!"View"(`
3762 SELECT treepane.uid AS uid FROM treepane
3763 INNER JOIN threads USING(uid, tagid)
3764 WHERE treepane.iid-1 > :cidx AND threads.appearance=:appr
3765 ORDER BY iid
3766 LIMIT 1
3767 ;`);
3768 immutable int cidx = chiroGetTreePaneTableUid2Index(curruid);
3769 foreach (auto row; stmtNext.st.bind(":cidx", cidx).bind(":appr", Appearance.Unread).range) return row.uid!uint;
3770 if (curruid) {
3771 // try from the beginning
3772 foreach (auto row; stmtNext.st.bind(":cidx", -1).bind(":appr", Appearance.Unread).range) return row.uid!uint;
3774 return 0;
3779 selects given number of items starting with the given item id.
3781 returns numer of selected items.
3783 `stiid` counts from zero
3785 WARNING! "treepane" table must be prepared with `chiroCreateTreePaneTable()`!
3787 WARNING! [i]dup `SQ3Text` arguments if necessary, they won't survive the `cb` return!
3789 public int chiroGetPaneTablePage (int stiid, int limit,
3790 void delegate (int pgofs, /* offset from the page start, from zero and up to `limit` */
3791 int iid, /* item id, counts from zero*/
3792 uint uid, /* msguid, never zero */
3793 uint parentuid, /* parent msguid, may be zero */
3794 uint level, /* threading level, from zero */
3795 Appearance appearance, /* see above */
3796 Mute mute, /* see above */
3797 SQ3Text date, /* string representation of receiving date and time */
3798 SQ3Text subj, /* message subject, can be empty string */
3799 SQ3Text fromName, /* message sender name, can be empty string */
3800 SQ3Text fromMail, /* message sender email, can be empty string */
3801 SQ3Text title) cb /* title from twiting */
3803 static auto stat = LazyStatement!"View"(`
3804 SELECT
3805 treepane.iid AS iid
3806 , treepane.uid AS uid
3807 , treepane.level AS level
3808 , threads.parent AS parent
3809 , threads.appearance AS appearance
3810 , threads.mute AS mute
3811 , datetime(threads.time, 'unixepoch') AS time
3812 , info.subj AS subj
3813 , info.from_name AS from_name
3814 , info.from_mail AS from_mail
3815 , threads.title AS title
3816 FROM treepane
3817 INNER JOIN info USING(uid)
3818 INNER JOIN threads USING(uid, tagid)
3819 WHERE treepane.iid >= :stiid
3820 ORDER BY treepane.iid
3821 LIMIT :limit
3824 if (limit <= 0) return 0;
3825 if (stiid < 0) {
3826 if (stiid == int.min) return 0;
3827 limit += stiid;
3828 if (limit <= 0) return 0;
3829 stiid = 0;
3831 int total = 0;
3832 foreach (auto row; stat.st.bind(":stiid", stiid+1).bind(":limit", limit).range)
3834 if (cb !is null) {
3835 cb(total, row.iid!int, row.uid!uint, row.parent!uint, row.level!uint,
3836 cast(Appearance)row.appearance!int, cast(Mute)row.mute!int,
3837 row.time!SQ3Text, row.subj!SQ3Text, row.from_name!SQ3Text, row.from_mail!SQ3Text, row.title!SQ3Text);
3839 ++total;
3841 return total;
3845 // ////////////////////////////////////////////////////////////////////////// //
3846 /** returns full content of the messare or `null` if no message found (or it was deleted).
3848 * you can safely `delete` the result
3850 public char[] GetFullMessageContent (uint uid) {
3851 if (uid == 0) return null;
3852 foreach (auto row; dbStore.statement(`SELECT ChiroUnpack(data) AS content FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid).range) {
3853 auto ct = row.content!SQ3Text;
3854 if (!ct.length) return null;
3855 char[] content = new char[ct.length];
3856 content[] = ct[];
3857 return content;
3859 return null;
3863 // ////////////////////////////////////////////////////////////////////////// //
3864 public enum Bogo {
3865 Error, // some error occured
3866 Ham,
3867 Unsure,
3868 Spam,
3871 public Bogo messageBogoCheck (uint uid) {
3872 if (uid == 0) return Bogo.Error;
3873 char[] content = GetFullMessageContent(uid);
3874 scope(exit) delete content;
3875 if (content is null) return Bogo.Error;
3877 try {
3878 import std.process;
3879 //{ auto fo = VFile("/tmp/zzzz", "w"); fo.rawWriteExact(art.data); }
3880 auto pipes = pipeProcess(["/usr/bin/bogofilter", "-T"]);
3881 //foreach (string s; art.headers) pipes.stdin.writeln(s);
3882 //pipes.stdin.writeln();
3883 //foreach (string s; art.text) pipes.stdin.writeln(s);
3884 pipes.stdin.writeln(content.xstripright);
3885 pipes.stdin.flush();
3886 pipes.stdin.close();
3887 auto res = pipes.stdout.readln();
3888 wait(pipes.pid);
3889 //conwriteln("RESULT: [", res, "]");
3890 if (res.length == 0) {
3891 //conwriteln("ERROR: bogofilter returned nothing");
3892 return Bogo.Error;
3894 if (res[0] == 'H') return Bogo.Ham;
3895 if (res[0] == 'U') return Bogo.Unsure;
3896 if (res[0] == 'S') return Bogo.Spam;
3897 //while (res.length && res[$-1] <= ' ') res = res[0..$-1];
3898 //conwriteln("ERROR: bogofilter returned some shit: [", res, "]");
3899 } catch (Exception e) { // sorry
3900 //conwriteln("ERROR bogofiltering: ", e.msg);
3903 return Bogo.Error;
3907 // ////////////////////////////////////////////////////////////////////////// //
3908 private void messageBogoMarkSpamHam(bool spam) (uint uid) {
3909 if (uid == 0) return;
3910 char[] content = GetFullMessageContent(uid);
3911 scope(exit) delete content;
3912 if (content is null) return;
3914 static if (spam) enum arg = "-s"; else enum arg = "-n";
3915 try {
3916 import std.process;
3917 auto pipes = pipeProcess(["/usr/bin/bogofilter", arg]);
3918 //foreach (string s; art.headers) pipes.stdin.writeln(s);
3919 //pipes.stdin.writeln();
3920 //foreach (string s; art.text) pipes.stdin.writeln(s);
3921 pipes.stdin.writeln(content.xstripright);
3922 pipes.stdin.flush();
3923 pipes.stdin.close();
3924 wait(pipes.pid);
3925 } catch (Exception e) { // sorry
3926 //conwriteln("ERROR bogofiltering: ", e.msg);
3931 public void messageBogoMarkHam (uint uid) { messageBogoMarkSpamHam!false(uid); }
3932 public void messageBogoMarkSpam (uint uid) { messageBogoMarkSpamHam!true(uid); }