efgx: some cosmetix
[chiroptera.git] / chibackend / sqbase.d
blobdeda9f9450e5ca0cedd01d98aa60559e73abfe7d
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.encoding;
71 private import iv.cmdcon;
72 private import iv.strex;
73 private import iv.sq3;
74 private import iv.timer;
75 private import iv.vfs.io;
76 private import iv.vfs.util;
78 private import iv.dlzma;
80 private import chibackend.mbuilder : DynStr;
81 private import chibackend.parse;
82 private import chibackend.decode;
83 //private import iv.utfutil;
84 //private import iv.vfs.io;
86 version(use_libdeflate) private import chibackend.pack.libdeflate;
87 else version(use_balz) private import iv.balz;
88 else version(use_libxpack) private import chibackend.pack.libxpack;
89 else version(use_libbrieflz) private import chibackend.pack.libbrieflz;
90 else version(use_liblzfse) private import chibackend.pack.liblzfse;
91 else version(use_lzjb) private import chibackend.pack.lzjb;
92 else version(use_libwim_lzms) private import chibackend.pack.libwim;
93 else version(use_libwim_lzx) private import chibackend.pack.libwim;
94 else version(use_libwim_xpress) private import chibackend.pack.libwim;
95 else version(use_lz4) private import chibackend.pack.liblz4;
96 else version(use_zstd) private import chibackend.pack.libzstd;
99 version(use_zstd) {
100 public enum ChiroDefaultPackLevel = 6;
101 } else {
102 public enum ChiroDefaultPackLevel = 9;
106 // use `MailDBPath()` to get/set it
107 private __gshared string ExpandedMailDBPath = null;
108 public __gshared int ChiroCompressionLevel = ChiroDefaultPackLevel;
109 public __gshared bool ChiroSQLiteSilent = false;
111 // if `true`, will try both ZLib and LZMA
112 public __gshared bool ChiroPackTryHard = false;
114 public __gshared bool ChiroTimerEnabled = false;
115 private __gshared Timer chiTimer = Timer(false);
116 private __gshared char[] chiTimerMsg = null;
118 // opened databases
119 public __gshared Database dbStore; // message store db
120 public __gshared Database dbView; // message view db
121 public __gshared Database dbConf; // config/options db
124 public enum Appearance {
125 Ignore = -1, // can be used to ignore messages in thread view
126 Unread = 0,
127 Read = 1,
128 SoftDeleteFilter = 2, // soft-delete from filter
129 SoftDeleteUser = 3, // soft-delete by user
130 SoftDeletePurge = 4, // soft-delete by user (will be purged on folder change)
133 public enum Mute {
134 Never = -1,
135 Normal = 0,
136 Message = 1, /* single message */
137 ThreadStart = 2, /* all children starting from this */
138 ThreadOther = 3, /* muted by some parent */
141 public bool isSoftDeleted (const int appearance) pure nothrow @safe @nogc {
142 pragma(inline, true);
143 return
144 appearance >= Appearance.SoftDeleteFilter &&
145 appearance <= Appearance.SoftDeletePurge;
150 There are several added SQLite functions:
152 ChiroPack(data[, compratio])
153 ===============
155 This tries to compress the given data, and returns a compressed blob.
156 If `compratio` is negative or zero, do not compress anything.
159 ChiroUnpack(data)
160 =================
162 This decompresses the blob compressed with `ChiroPack()`. It is (usually) safe to pass
163 non-compressed data to this function.
166 ChiroNormCRLF(content)
167 ======================
169 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
170 Removes trailing blanks.
173 ChiroNormHeaders(content)
174 =========================
176 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
177 Then replaces 'space, LF' with a single space (joins multiline headers).
178 Removes trailing blanks.
181 ChiroExtractHeaders(content)
182 ============================
184 Can be used to extract headers from the message.
185 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
186 Then replaces 'space, LF' with a single space (joins multiline headers).
187 Removes trailing blanks.
190 ChiroExtractBody(content)
191 =========================
193 Can be used to extract body from the message.
194 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
195 Then replaces 'space, LF' with a single space (joins multiline headers).
196 Removes trailing blanks and final dot.
200 public enum OptionsDBName = "chiroptera.db";
201 public enum StorageDBName = "chistore.db";
202 public enum SupportDBName = "chiview.db";
205 // ////////////////////////////////////////////////////////////////////////// //
206 private enum CommonPragmas = `
207 PRAGMA case_sensitive_like = OFF;
208 PRAGMA foreign_keys = OFF;
209 PRAGMA locking_mode = NORMAL; /*EXCLUSIVE;*/
210 PRAGMA secure_delete = OFF;
211 PRAGMA threads = 3;
212 PRAGMA trusted_schema = OFF;
213 PRAGMA writable_schema = OFF;
216 enum CommonPragmasRO = CommonPragmas~`
217 PRAGMA temp_store = MEMORY; /*DEFAULT*/ /*FILE*/
220 enum CommonPragmasRW = CommonPragmas~`
221 PRAGMA application_id = 1128810834; /*CHIR*/
222 PRAGMA auto_vacuum = NONE;
223 PRAGMA encoding = "UTF-8";
224 PRAGMA temp_store = DEFAULT;
225 --PRAGMA journal_mode = WAL; /*OFF;*/
226 --PRAGMA journal_mode = DELETE; /*OFF;*/
227 PRAGMA synchronous = NORMAL; /*OFF;*/
230 enum CommonPragmasRecreate = `
231 PRAGMA locking_mode = EXCLUSIVE;
232 PRAGMA journal_mode = OFF;
233 PRAGMA synchronous = OFF;
236 static immutable dbpragmasRO = CommonPragmasRO;
238 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
239 // use the smallest page size, because we don't need to perform alot of selects here
240 static immutable dbpragmasRWStorage = "PRAGMA page_size = 512;"~CommonPragmasRW~"PRAGMA journal_mode = DELETE;";
241 static immutable dbpragmasRWStorageRecreate = dbpragmasRWStorage~CommonPragmasRecreate;
243 // use slightly bigger pages
244 // funny, smaller pages leads to bigger files
245 static immutable dbpragmasRWSupport = "PRAGMA page_size = 4096;"~CommonPragmasRW~"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;";
246 static immutable dbpragmasRWSupportRecreate = dbpragmasRWSupport~CommonPragmasRecreate;
248 // smaller page size is ok
249 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
250 static immutable dbpragmasRWOptions = "PRAGMA page_size = 512;"~CommonPragmasRW~"PRAGMA journal_mode = /*DELETE*/WAL; PRAGMA synchronous = NORMAL;";
251 static immutable dbpragmasRWOptionsRecreate = dbpragmasRWOptions~CommonPragmasRecreate;
254 enum msgTagNameCheckSQL = `
255 WITH RECURSIVE tagtable(tag, rest) AS (
256 VALUES('', NEW.tags||'|')
257 UNION ALL
258 SELECT
259 SUBSTR(rest, 0, INSTR(rest, '|')),
260 SUBSTR(rest, INSTR(rest, '|')+1)
261 FROM tagtable
262 WHERE rest <> '')
263 SELECT
264 (CASE
265 WHEN tag = '/' THEN RAISE(FAIL, 'tag name violation (root tags are not allowed)')
266 WHEN LENGTH(tag) = 1 THEN RAISE(FAIL, 'tag name violation (too short tag name)')
267 WHEN SUBSTR(tag, LENGTH(tag)) = '/' THEN RAISE(FAIL, 'tag name violation (tag should not end with a slash)')
268 END)
269 FROM tagtable
270 WHERE tag <> '';
273 // main storage and support databases will be in different files
274 static immutable string schemaStorage = `
275 -- deleted messages have empty headers and body
276 -- this is so uids will remain unique on inserting
277 -- tags are used to associate the message with various folders, and stored here for rebuild purposes
278 -- the frontend will use the separate "tags" table to select messages
279 -- deleted messages must not have any tags, and should contain no other data
280 -- (keeping the data is harmless, it simply sits there and takes space)
281 CREATE TABLE IF NOT EXISTS messages (
282 uid INTEGER PRIMARY KEY /* rowid, never zero */
283 , tags TEXT DEFAULT NULL /* associated message tags, '|'-separated; case-sensitive, no extra whitespaces or '||'! */
284 -- article data; MUST contain the ending dot, and be properly dot-stuffed
285 -- basically, this is "what we had received, as is" (*WITH* the ending dot!)
286 -- there is no need to normalize it in any way (and you *SHOULD NOT* do it!)
287 -- it should be compressed with "ChiroPack()", and extracted with "ChiroUnpack()"
288 , data BLOB
291 -- check tag constraints
292 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_insert
293 BEFORE INSERT ON messages
294 FOR EACH ROW
295 BEGIN`~msgTagNameCheckSQL~`
296 END;
298 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_update_tags
299 BEFORE UPDATE OF tags ON messages
300 FOR EACH ROW
301 BEGIN`~msgTagNameCheckSQL~`
302 END;
305 static immutable string schemaStorageIndex = `
309 static immutable string schemaOptions = `
310 -- use "autoincrement" to allow account deletion
311 CREATE TABLE IF NOT EXISTS accounts (
312 accid INTEGER PRIMARY KEY AUTOINCREMENT /* unique, never zero */
313 , checktime INTEGER NOT NULL DEFAULT 15 /* check time, in minutes */
314 , nosendauth INTEGER NOT NULL DEFAULT 0 /* turn off authentication on sending? */
315 , debuglog INTEGER NOT NULL DEFAULT 0 /* do debug logging? */
316 , nocheck INTEGER NOT NULL DEFAULT 0 /* disable checking? */
317 , nntplastindex INTEGER NOT NULL DEFAULT 0 /* last seen article index for NNTP groups */
318 , name TEXT NOT NULL UNIQUE /* account name; lowercase alphanum, '_', '-', '.' */
319 , recvserver TEXT NOT NULL /* server for receiving messages */
320 , sendserver TEXT NOT NULL /* server for sending messages */
321 , user TEXT NOT NULL /* pop3 user name */
322 , pass TEXT NOT NULL /* pop3 password, empty for no authorisation */
323 , realname TEXT NOT NULL /* user name for e-mail headers */
324 , email TEXT NOT NULL /* account e-mail address (full, name@host) */
325 , inbox TEXT NOT NULL /* inbox tag, usually "/accname/inbox", or folder for nntp */
326 , nntpgroup TEXT NOT NULL DEFAULT '' /* nntp group name for NNTP accounts; if empty, this is POP3 account */
330 CREATE TABLE IF NOT EXISTS options (
331 name TEXT NOT NULL UNIQUE
332 , value TEXT
336 CREATE TABLE IF NOT EXISTS addressbook (
337 nick TEXT NOT NULL UNIQUE /* short nick for this address book entry */
338 , name TEXT NOT NULL DEFAULT ''
339 , email TEXT NOT NULL
340 , notes TEXT DEFAULT NULL
344 -- twits by email/name
345 CREATE TABLE IF NOT EXISTS emailtwits (
346 etwitid INTEGER PRIMARY KEY
347 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
348 , email TEXT /* if both name and email present, use only email */
349 , name TEXT /* name to twit by */
350 , title TEXT /* optional title */
351 , notes TEXT /* notes; often URL */
354 -- twits by msgids
355 CREATE TABLE IF NOT EXISTS msgidtwits (
356 mtwitid INTEGER PRIMARY KEY
357 , etwitid INTEGER /* parent mail twit, if any */
358 , automatic INTEGER DEFAULT 1 /* added by message filtering, not from .rc? */
359 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
360 , msgid TEXT /* message used to set twit */
364 -- message filters
365 CREATE TABLE IF NOT EXISTS filters (
366 filterid INTEGER PRIMARY KEY
367 , valid INTEGER NOT NULL DEFAULT 1 /* is this filter valid? used to skip bad filters */
368 , idx INTEGER NOT NULL DEFAULT 0 /* used for ordering */
369 , post INTEGER NOT NULL DEFAULT 0 /* post-spamcheck filter? */
370 , hitcount INTEGER NOT NULL DEFAULT 0 /* for statistics */
371 , name TEXT NOT NULL UNIQUE /* filter name */
372 , body TEXT /* filter text */
375 CREATE TRIGGER IF NOT EXISTS filters_new_index
376 AFTER INSERT ON filters
377 FOR EACH ROW
378 BEGIN
379 UPDATE filters SET idx=(SELECT MAX(idx)+10 FROM filters)
380 WHERE NEW.idx=0 AND filterid=NEW.filterid;
381 END;
384 static immutable string schemaOptionsIndex = `
385 -- no need to, "UNIQUE" automaptically creates it
386 --CREATE INDEX IF NOT EXISTS accounts_name ON accounts(name);
388 -- this index in implicit
389 --CREATE INDEX IF NOT EXISTS options_name ON options(name);
391 CREATE INDEX IF NOT EXISTS emailtwits_email ON emailtwits(email);
392 CREATE INDEX IF NOT EXISTS emailtwits_name ON emailtwits(name);
393 CREATE UNIQUE INDEX IF NOT EXISTS emailtwits_email_name ON emailtwits(email, name);
395 CREATE INDEX IF NOT EXISTS msgidtwits_msgid ON msgidtwits(msgid);
397 CREATE INDEX IF NOT EXISTS filters_idx_post_valid ON filters(idx, post, valid);
401 enum schemaSupportTable = `
402 -- tag <-> messageid correspondence
403 -- note that one message can be tagged with more than one tag
404 -- there is always tag with "uid=0", to keep all tags alive
405 -- special tags:
406 -- account:name -- received via this account
407 -- #spam -- this is spam message
408 -- #hobo -- will be autoassigned to messages without any tags (created on demand)
409 CREATE TABLE IF NOT EXISTS tagnames (
410 tagid INTEGER PRIMARY KEY
411 , hidden INTEGER NOT NULL DEFAULT 0 /* deleting tags may cause 'tagid' reuse, so it's better to hide them instead */
412 , threading INTEGER NOT NULL DEFAULT 1 /* enable threaded view? */
413 , noattaches INTEGER NOT NULL DEFAULT 0 /* ignore non-text attachments? */
414 , tag TEXT NOT NULL UNIQUE
417 -- it is here, because we don't have a lot of tags, and inserts are slightly faster this way
418 -- it's not required, because "UNIQUE" constraint will create automatic index
419 --CREATE INDEX IF NOT EXISTS tagname_tag ON tagnames(tag);
421 --CREATE INDEX IF NOT EXISTS tagname_tag_uid ON tagnames(tag, tagid);
424 -- each tag has its own unique threads (so uids can be duplicated, but (uid,tagid) paris cannot
425 -- see above for "apearance" and "mute" values
426 CREATE TABLE IF NOT EXISTS threads (
427 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
428 , tagid INTEGER /* we need separate threads for each tag */
429 , time INTEGER DEFAULT 0 /* unixtime -- creation/send/receive */
430 /* threading info */
431 , parent INTEGER DEFAULT 0 /* uid: parent message in thread, or 0 */
432 /* flags */
433 , appearance INTEGER DEFAULT 0 /* how the message should look */
434 , mute INTEGER DEFAULT 0 /* 1: only this message, 2: the whole thread */
435 , title TEXT DEFAULT NULL /* title from the filter */
439 -- WARNING!
440 -- for FTS5 to work, this table must be:
441 -- updated LAST on INSERT
442 -- updated FIRST on DELETE
443 -- this is due to FTS5 triggers
444 -- message texts should NEVER be updated!
445 -- if you want to do update a message:
446 -- first, DELETE the old one from this table
447 -- then, update textx
448 -- then, INSERT here again
449 -- doing it like that will keep FTS5 in sync
450 CREATE TABLE IF NOT EXISTS info (
451 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
452 , from_name TEXT /* can be empty */
453 , from_mail TEXT /* can be empty */
454 , subj TEXT /* can be empty */
455 , to_name TEXT /* can be empty */
456 , to_mail TEXT /* can be empty */
460 -- this holds msgid
461 -- moved to separate table, because this info is used only when inserting new messages
462 CREATE TABLE IF NOT EXISTS msgids (
463 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
464 , time INTEGER /* so we can select the most recent message */
465 , msgid TEXT /* message id */
469 -- this holds in-reply-to, and references
470 -- moved to separate table, because this info is used only when inserting new messages
471 CREATE TABLE IF NOT EXISTS refids (
472 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
473 , idx INTEGER /* internal index in headers, cannot have gaps, starts from 0 */
474 , msgid TEXT /* message id */
478 -- this ALWAYS contain an entry (yet content may be empty string)
479 CREATE TABLE IF NOT EXISTS content_text (
480 uid INTEGER PRIMARY KEY /* owner message uid */
481 , format TEXT NOT NULL /* optional format, like 'flowed' */
482 , content TEXT NOT NULL /* properly decoded; packed */
486 -- this ALWAYS contain an entry (yet content may be empty string)
487 CREATE TABLE IF NOT EXISTS content_html (
488 uid INTEGER PRIMARY KEY /* owner message uid */
489 , format TEXT NOT NULL /* optional format, like 'flowed' */
490 , content TEXT NOT NULL /* properly decoded; packed */
494 -- this DOES NOT include text and html contents (and may exclude others)
495 CREATE TABLE IF NOT EXISTS attaches (
496 uid INTEGER /* owner message uid */
497 , idx INTEGER /* code should take care of proper autoincrementing this */
498 , mime TEXT NOT NULL /* always lowercased */
499 , name TEXT NOT NULL /* attachment name; always empty for inline content, never empty for non-inline content */
500 , format TEXT NOT NULL /* optional format, like 'flowed' */
501 , content BLOB /* properly decoded; packed; NULL if the attach was dropped */
505 -- this view is used for FTS5 content queries
506 -- it is harmless to keep it here even if FTS5 is not used
507 --DROP VIEW IF EXISTS fts5_msgview;
508 CREATE VIEW IF NOT EXISTS fts5_msgview (uid, sender, subj, text, html)
510 SELECT
511 info.uid AS uid
512 , info.from_name||' '||CHAR(26)||' '||info.from_mail AS sender
513 , info.subj AS subj
514 , ChiroUnpack(content_text.content) AS text
515 , ChiroUnpack(content_html.content) AS html
516 FROM info
517 INNER JOIN content_text USING(uid)
518 INNER JOIN content_html USING(uid)
522 -- this table holds all unsent messages
523 -- they are put in the storage and properly inserted,
524 -- but also put in this table, for the receiver to send them
525 -- also note that NNTP messages will be put in the storage without any tags (but with the contents)
526 -- (this is because we will receive them back from NNTP server later)
527 -- succesfully sent messages will be simply DELETEd
528 CREATE TABLE IF NOT EXISTS unsent (
529 uid INTEGER PRIMARY KEY /* the same as in the storage, not automatic */
530 , accid INTEGER /* account from which this message should be sent */
531 , from_pop3 TEXT /* "from" for POP3 */
532 , to_pop3 TEXT /* "to" for POP3 */
533 , data TEXT /* PACKED data to send */
537 static immutable string schemaSupportTempTables = `
538 --DROP TABLE IF EXISTS treepane;
539 CREATE TEMP TABLE IF NOT EXISTS treepane (
540 iid INTEGER PRIMARY KEY
541 , uid INTEGER
542 , level INTEGER
543 -- to make joins easier
544 , tagid INTEGER
547 CREATE INDEX IF NOT EXISTS treepane_uid ON treepane(uid);
550 enum schemaSupportIndex = `
551 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_tag_uid ON threads(tagid, uid);
552 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_uid_tag ON threads(uid, tagid);
554 -- this is for views where threading is disabled
555 CREATE INDEX IF NOT EXISTS trd_by_tag_time ON threads(tagid, time);
556 --CREATE INDEX IF NOT EXISTS trd_by_tag_time_parent ON threads(tagid, time, parent);
557 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent_time ON threads(tagid, parent, time);
558 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent ON threads(tagid, parent);
559 CREATE INDEX IF NOT EXISTS trd_by_parent_tag ON threads(parent, tagid);
561 -- this is for test if we have any unread articles (we don't mind the exact numbers, tho)
562 CREATE INDEX IF NOT EXISTS trd_by_appearance ON threads(appearance);
563 -- this is for removing purged messages
564 CREATE INDEX IF NOT EXISTS trd_by_tag_appearance ON threads(tagid, appearance);
565 -- was used in table view creation, not used anymore
566 --CREATE INDEX IF NOT EXISTS trd_by_parent_tag_appearance ON threads(parent, tagid, appearance);
568 -- for theadmsgview
569 -- was used in table view creation, not used anymore
570 --CREATE INDEX IF NOT EXISTS trd_by_tag_appearance_time ON threads(tagid, appearance, time);
572 CREATE INDEX IF NOT EXISTS msgid_by_msgid_time ON msgids(msgid, time DESC);
574 CREATE INDEX IF NOT EXISTS refid_by_refids_idx ON refids(msgid, idx);
575 CREATE INDEX IF NOT EXISTS refid_by_uid_idx ON refids(uid, idx);
577 CREATE INDEX IF NOT EXISTS content_text_by_uid ON content_text(uid);
578 CREATE INDEX IF NOT EXISTS content_html_by_uid ON content_html(uid);
580 CREATE INDEX IF NOT EXISTS attaches_by_uid_name ON attaches(uid, name);
581 CREATE INDEX IF NOT EXISTS attaches_by_uid_idx ON attaches(uid, idx);
583 -- "info" indicies for twits
584 CREATE INDEX IF NOT EXISTS info_by_from_mail_name ON info(from_mail, from_name);
585 --CREATE INDEX IF NOT EXISTS info_by_from_mail ON info(from_mail);
586 CREATE INDEX IF NOT EXISTS info_by_from_name ON info(from_name);
589 CREATE INDEX IF NOT EXISTS unsent_by_accid ON unsent(accid);
592 static immutable string schemaSupport = schemaSupportTable~schemaSupportIndex;
595 version(fts5_use_porter) {
596 enum FTS5_Tokenizer = "porter unicode61 remove_diacritics 2";
597 } else {
598 enum FTS5_Tokenizer = "unicode61 remove_diacritics 2";
601 static immutable string recreateFTS5 = `
602 DROP TABLE IF EXISTS fts5_messages;
603 CREATE VIRTUAL TABLE fts5_messages USING fts5(
604 sender /* sender name and email, separated by " \x1a " (dec 26) (substitute char) */
605 , subj /* email subject */
606 , text /* email body, text/plain */
607 , html /* email body, text/html */
608 --, uid UNINDEXED /* message uid this comes from (not needed, use "rowid" instead */
609 , tokenize = '`~FTS5_Tokenizer~`'
610 , content = 'fts5_msgview'
611 , content_rowid = 'uid'
613 /* sender, subj, text, html */
614 INSERT INTO fts5_messages(fts5_messages, rank) VALUES('rank', 'bm25(1.0, 3.0, 10.0, 6.0)');
617 static immutable string repopulateFTS5 = `
618 SELECT ChiroTimerStart('updating FTS5');
619 BEGIN TRANSACTION;
621 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
622 SELECT uid, sender, subj, text, html
623 FROM fts5_msgview
624 WHERE
625 EXISTS (
626 SELECT threads.tagid FROM threads
627 INNER JOIN tagnames USING(tagid)
628 WHERE
629 threads.uid=fts5_msgview.uid AND
630 tagnames.hidden=0 AND SUBSTR(tagnames.tag, 1, 1)='/'
633 COMMIT TRANSACTION;
634 SELECT ChiroTimerStop();
638 static immutable string recreateFTS5Triggers = `
639 -- triggers to keep the FTS index up to date
641 -- this rely on the proper "info" table update order
642 -- info must be inserted LAST
643 DROP TRIGGER IF EXISTS fts5xtrig_insert;
644 CREATE TRIGGER fts5xtrig_insert
645 AFTER INSERT ON info
646 BEGIN
647 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
648 SELECT uid, sender, subj, text, html FROM fts5_msgview WHERE uid=NEW.uid LIMIT 1;
649 END;
651 -- not AFTER, because we still need a valid view!
652 -- this rely on the proper "info" table update order
653 -- info must be deleted FIRST
654 DROP TRIGGER IF EXISTS fts5xtrig_delete;
655 CREATE TRIGGER fts5xtrig_delete
656 BEFORE DELETE ON info
657 BEGIN
658 INSERT INTO fts5_messages(fts5_messages, rowid, sender, subj, text, html)
659 SELECT 'delete', uid, sender, subj, text, html FROM fts5_msgview WHERE uid=OLD.uid LIMIT 1;
660 END;
662 -- message texts should NEVER be updated, so no ON UPDATE trigger
666 // ////////////////////////////////////////////////////////////////////////// //
667 // not properly implemented yet
668 //version = lazy_mt_safe;
670 version(lazy_mt_safe) {
671 enum lazy_mt_safe_flag = true;
672 } else {
673 enum lazy_mt_safe_flag = false;
676 public struct LazyStatement(string dbname) {
677 public:
678 enum DB {
679 Store,
680 View,
681 Conf,
684 private:
685 static struct Data {
686 DBStatement st = void;
687 version(lazy_mt_safe) {
688 sqlite3_mutex* mutex = void;
690 char* sql = void;
691 usize sqlsize = void;
692 uint compiled = void;
695 private:
696 usize udata = 0;
697 DB dbtype;
698 string delayInit = null;
700 private:
701 inout(Data)* datap () inout pure nothrow @trusted @nogc { pragma(inline, true); return cast(Data*)udata; }
702 void datap (Data *v) pure nothrow @trusted @nogc { pragma(inline, true); udata = cast(usize)v; }
704 public:
705 //@disable this ();
706 @disable this (this);
708 this (string sql) {
709 delayInit = sql;
711 assert(sql.length);
712 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
713 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
714 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
715 else static assert(0, "invalid db name: '"~dbname~"'");
716 import core.stdc.stdlib : calloc;
717 Data* dp = cast(Data*)calloc(1, Data.sizeof);
718 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
719 datap = dp;
720 dp.sql = cast(char*)calloc(1, sql.length);
721 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
722 dp.sql[0..sql.length] = sql[];
723 dp.sqlsize = sql.length;
724 version(lazy_mt_safe) {
725 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
726 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
729 //dbtype = adb;
730 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
733 ~this () {
734 import core.stdc.stdlib : free;
735 if (!udata) return;
736 Data* dp = datap;
737 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===DEINIT===\n%s\n============\n", dp.sql); }
738 dp.st = DBStatement.init;
739 free(dp.sql);
740 version(lazy_mt_safe) {
741 sqlite3_mutex_free(dp.mutex);
743 free(dp);
744 udata = 0;
747 bool valid () pure nothrow @safe @nogc { pragma(inline, true); return (udata != 0 || delayInit.length); }
749 private void setupWith (const(char)[] sql) {
750 if (udata) throw new Exception("statement already inited");
751 assert(sql.length);
752 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
753 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
754 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
755 else static assert(0, "invalid db name: '"~dbname~"'");
756 import core.stdc.stdlib : calloc;
757 Data* dp = cast(Data*)calloc(1, Data.sizeof);
758 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
759 datap = dp;
760 dp.sql = cast(char*)calloc(1, sql.length);
761 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
762 dp.sql[0..sql.length] = sql[];
763 dp.sqlsize = sql.length;
764 version(lazy_mt_safe) {
765 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
766 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
768 //dbtype = adb;
769 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
772 ref DBStatement st () {
773 if (!udata) {
774 //throw new Exception("no statement set");
775 setupWith(delayInit);
777 Data* dp = datap;
778 if (!dp.compiled) {
779 version(lazy_mt_safe) {
780 sqlite3_mutex_enter(dp.mutex);
782 scope(exit) {
783 version(lazy_mt_safe) {
784 sqlite3_mutex_leave(dp.mutex);
787 //{ import core.stdc.stdio : printf; printf("***compiling:\n%s\n=====\n", dp.sql); }
788 final switch (dbtype) {
789 case DB.Store: dp.st = dbStore.persistentStatement(dp.sql[0..dp.sqlsize]); break;
790 case DB.View: dp.st = dbView.persistentStatement(dp.sql[0..dp.sqlsize]); break;
791 case DB.Conf: dp.st = dbConf.persistentStatement(dp.sql[0..dp.sqlsize]); break;
793 dp.compiled = 1;
794 //assert(dp.st.valid);
796 //assert(dp.st.valid);
797 return dp.st;
802 // ////////////////////////////////////////////////////////////////////////// //
803 private bool isGoodText (const(void)[] buf) pure nothrow @safe @nogc {
804 foreach (immutable ubyte ch; cast(const(ubyte)[])buf) {
805 if (ch < 32) {
806 if (ch != 9 && ch != 10 && ch != 13 && ch != 27) return false;
807 } else {
808 if (ch == 127) return false;
811 return true;
812 //return utf8ValidText(buf);
816 // ////////////////////////////////////////////////////////////////////////// //
817 private bool isBadPrefix (const(char)[] buf) pure nothrow @trusted @nogc {
818 if (buf.length < 5) return false;
819 return
820 buf.ptr[0] == '\x1b' &&
821 buf.ptr[1] >= 'A' && buf.ptr[1] <= 'Z' &&
822 buf.ptr[2] >= 'A' && buf.ptr[2] <= 'Z' &&
823 buf.ptr[3] >= 'A' && buf.ptr[3] <= 'Z' &&
824 buf.ptr[4] >= 'A' && buf.ptr[4] <= 'Z';
828 /* two high bits of the first byte holds the size:
829 00: fit into 6 bits: [0.. 0x3f] (1 byte)
830 01: fit into 14 bits: [0.. 0x3fff] (2 bytes)
831 10: fit into 22 bits: [0.. 0x3f_ffff] (3 bytes)
832 11: fit into 30 bits: [0..0x3fff_ffff] (4 bytes)
834 number is stored as big-endian.
835 will not write anything to `dest` if there is not enough room.
837 returns number of bytes, or 0 if the number is too big.
839 private uint encodeUInt (void[] dest, uint v) nothrow @trusted @nogc {
840 if (v > 0x3fff_ffffU) return 0;
841 ubyte[] d = cast(ubyte[])dest;
842 // 4 bytes?
843 if (v > 0x3f_ffffU) {
844 v |= 0xc000_0000U;
845 if (d.length >= 4) {
846 d.ptr[0] = cast(ubyte)(v>>24);
847 d.ptr[1] = cast(ubyte)(v>>16);
848 d.ptr[2] = cast(ubyte)(v>>8);
849 d.ptr[3] = cast(ubyte)v;
851 return 4;
853 // 3 bytes?
854 if (v > 0x3fffU) {
855 v |= 0x80_0000U;
856 if (d.length >= 3) {
857 d.ptr[0] = cast(ubyte)(v>>16);
858 d.ptr[1] = cast(ubyte)(v>>8);
859 d.ptr[2] = cast(ubyte)v;
861 return 3;
863 // 2 bytes?
864 if (v > 0x3fU) {
865 v |= 0x4000U;
866 if (d.length >= 2) {
867 d.ptr[0] = cast(ubyte)(v>>8);
868 d.ptr[1] = cast(ubyte)v;
870 return 2;
872 // 1 byte
873 if (d.length >= 1) d.ptr[0] = cast(ubyte)v;
874 return 1;
878 private uint decodeUIntLength (const(void)[] dest) pure nothrow @trusted @nogc {
879 const(ubyte)[] d = cast(const(ubyte)[])dest;
880 if (d.length == 0) return 0;
881 switch (d.ptr[0]&0xc0) {
882 case 0x00: return 1;
883 case 0x40: return (d.length >= 2 ? 2 : 0);
884 case 0x80: return (d.length >= 3 ? 3 : 0);
885 default:
887 return (d.length >= 4 ? 4 : 0);
891 // returns uint.max on error (impossible value)
892 private uint decodeUInt (const(void)[] dest) pure nothrow @trusted @nogc {
893 const(ubyte)[] d = cast(const(ubyte)[])dest;
894 if (d.length == 0) return uint.max;
895 uint res = void;
896 switch (d.ptr[0]&0xc0) {
897 case 0x00:
898 res = d.ptr[0];
899 break;
900 case 0x40:
901 if (d.length < 2) return uint.max;
902 res = ((d.ptr[0]&0x3fU)<<8)|d.ptr[1];
903 break;
904 case 0x80:
905 if (d.length < 3) return uint.max;
906 res = ((d.ptr[0]&0x3fU)<<16)|(d.ptr[1]<<8)|d.ptr[2];
907 break;
908 default:
909 if (d.length < 4) return uint.max;
910 res = ((d.ptr[0]&0x3fU)<<24)|(d.ptr[1]<<16)|(d.ptr[2]<<8)|d.ptr[3];
911 break;
913 return res;
917 // returns position AFTER the headers (empty line is skipped too)
918 // returned value is safe for slicing
919 private int sq3Supp_FindHeadersEnd (const(char)* vs, const int sz) {
920 import core.stdc.string : memchr;
921 if (sz <= 0) return 0;
922 const(char)* eptr = cast(const(char)*)memchr(vs, '\n', cast(uint)sz);
923 while (eptr !is null) {
924 ++eptr;
925 int epos = cast(int)cast(usize)(eptr-vs);
926 if (sz-epos < 1) break;
927 if (*eptr == '\r') {
928 if (sz-epos < 2) break;
929 ++epos;
930 ++eptr;
932 if (*eptr == '\n') return epos+1;
933 assert(epos < sz);
934 eptr = cast(const(char)*)memchr(eptr, '\n', cast(uint)(sz-epos));
936 return sz;
940 // hack for some invalid dates
941 uint parseMailDate (const(char)[] s) nothrow {
942 import std.datetime;
943 if (s.length == 0) return 0;
944 try {
945 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
946 } catch (Exception) {}
947 // sometimes this helps
948 usize dcount = 0;
949 foreach_reverse (immutable char ch; s) {
950 if (ch < '0' || ch > '9') break;
951 ++dcount;
953 if (dcount > 4) return 0;
954 s ~= "0000"[0..4-dcount];
955 try {
956 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
957 } catch (Exception) {}
958 return 0;
962 // ////////////////////////////////////////////////////////////////////////// //
963 extern(C) {
966 ** ChiroPackLZMA(content)
967 ** ChiroPackLZMA(content, packflag)
969 ** second form accepts int flag; 0 means "don't pack"
971 private void sq3Fn_ChiroPackLZMA (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
972 if (argc < 1 || argc > 2) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPackLZMA()`", -1); return; }
973 int packlevel = (argc > 1 ? sqlite3_value_int(argv[1]) : ChiroDefaultPackLevel);
974 if (packlevel < 0) packlevel = 5/*lzma default*/; else if (packlevel > 9) packlevel = 9;
976 sqlite3_value *val = argv[0];
978 immutable int sz = sqlite3_value_bytes(val);
979 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
981 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
983 const(char)* vs = cast(const(char) *)sqlite3_value_blob(val);
984 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroPackLZMA()`", -1); return; }
986 if (sz >= 0x3fffffff-8) {
987 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_toobig(ctx); return; }
988 sqlite3_result_value(ctx, val);
989 return;
992 import core.stdc.stdlib : malloc, free;
993 import core.stdc.string : memcpy;
995 if (packlevel > 0 && sz > 8) {
996 import core.stdc.stdio : snprintf;
997 char[16] xsz = void;
998 xsz[0..5] = "\x1bLZMA";
999 uint xszlen = encodeUInt(xsz[5..$], cast(uint)sz);
1000 if (xszlen) {
1001 xszlen += 5;
1002 immutable uint bsz = cast(uint)sz;
1003 char* cbuf = cast(char*)malloc(bsz+xszlen+LZMA_PROPS_SIZE+1+16);
1004 if (cbuf is null) {
1005 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_nomem(ctx); return; }
1006 } else {
1007 cbuf[0..xszlen] = xsz[0..xszlen];
1008 usize destLen = bsz;
1009 ubyte[LZMA_PROPS_SIZE+8] hdr = void;
1010 uint hdrSize = cast(uint)hdr.sizeof;
1012 CLzmaEncProps props;
1013 props.level = packlevel;
1014 props.dictSize = 1<<22; //4MB
1015 props.reduceSize = bsz;
1017 SRes res = LzmaEncode(cast(ubyte*)cbuf+xszlen+LZMA_PROPS_SIZE+1, &destLen, cast(const(ubyte)*)vs, bsz, &props, hdr.ptr, &hdrSize, 0/*writeEndMark*/, null, &lzmaDefAllocator, &lzmaDefAllocator);
1018 assert(hdrSize == LZMA_PROPS_SIZE);
1019 if (res == SZ_OK && destLen+xszlen+LZMA_PROPS_SIZE+1 < cast(usize)sz) {
1020 import core.stdc.string : memcpy;
1021 cbuf[xszlen] = LZMA_PROPS_SIZE;
1022 memcpy(cbuf+xszlen+1, hdr.ptr, LZMA_PROPS_SIZE);
1023 sqlite3_result_blob(ctx, cbuf, destLen+xszlen+LZMA_PROPS_SIZE+1, &free);
1024 return;
1026 free(cbuf);
1031 if (isBadPrefix(vs[0..cast(uint)sz])) {
1032 char *res = cast(char *)malloc(sz+5);
1033 if (res is null) { sqlite3_result_error_nomem(ctx); return; }
1034 res[0..5] = "\x1bRAWB";
1035 res[5..sz+5] = vs[0..sz];
1036 if (isGoodText(vs[0..cast(usize)sz])) {
1037 sqlite3_result_text(ctx, res, sz+5, &free);
1038 } else {
1039 sqlite3_result_blob(ctx, res, sz+5, &free);
1041 } else {
1042 immutable bool wantBlob = !isGoodText(vs[0..cast(usize)sz]);
1043 immutable int tp = sqlite3_value_type(val);
1044 if ((wantBlob && tp == SQLITE_BLOB) || (!wantBlob && tp == SQLITE3_TEXT)) {
1045 sqlite3_result_value(ctx, val);
1046 } else if (wantBlob) {
1047 sqlite3_result_blob(ctx, vs, sz, SQLITE_TRANSIENT);
1048 } else {
1049 sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1056 ** ChiroPack(content)
1057 ** ChiroPack(content, packflag)
1059 ** second form accepts int flag; 0 means "don't pack"
1061 private void sq3Fn_ChiroPackCommon (sqlite3_context *ctx, sqlite3_value *val, int packlevel) nothrow @trusted {
1062 immutable int sz = sqlite3_value_bytes(val);
1063 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
1065 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1067 const(char)* vs = cast(const(char) *)sqlite3_value_blob(val);
1068 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroPack()`", -1); return; }
1070 if (sz >= 0x3fffffff-8) {
1071 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_toobig(ctx); return; }
1072 sqlite3_result_value(ctx, val);
1073 return;
1076 import core.stdc.stdlib : malloc, free;
1077 import core.stdc.string : memcpy;
1079 if (packlevel > 0 && sz > 8) {
1080 import core.stdc.stdio : snprintf;
1081 char[16] xsz = void;
1082 version(use_balz) {
1083 xsz[0..5] = "\x1bBALZ";
1084 } else version(use_libxpack) {
1085 xsz[0..5] = "\x1bXPAK";
1086 } else version(use_libbrieflz) {
1087 xsz[0..5] = "\x1bBRLZ";
1088 } else version(use_liblzfse) {
1089 xsz[0..5] = "\x1bLZFS";
1090 } else version(use_lzjb) {
1091 xsz[0..5] = "\x1bLZJB";
1092 } else version(use_libwim_lzms) {
1093 xsz[0..5] = "\x1bLZMS";
1094 } else version(use_libwim_lzx) {
1095 xsz[0..5] = "\x1bLZMX";
1096 } else version(use_libwim_xpress) {
1097 xsz[0..5] = "\x1bXPRS";
1098 } else version(use_lz4) {
1099 xsz[0..5] = "\x1bLZ4D";
1100 } else version(use_zstd) {
1101 xsz[0..5] = "\x1bZSTD";
1102 } else {
1103 xsz[0..5] = "\x1bZLIB";
1105 immutable uint xszlenNum = encodeUInt(xsz[5..$], cast(uint)sz);
1106 if (xszlenNum) {
1107 immutable uint xszlen = xszlenNum+5;
1108 //xsz[xszlen++] = ':';
1109 version(use_libbrieflz) {
1110 immutable usize bsz = blz_max_packed_size(cast(usize)sz);
1111 } else version(use_lzjb) {
1112 immutable uint bsz = cast(uint)sz+1024;
1113 } else version(use_lz4) {
1114 immutable uint bsz = cast(uint)LZ4_compressBound(sz)+1024;
1115 } else {
1116 immutable uint bsz = cast(uint)sz;
1118 char* cbuf = cast(char*)malloc(bsz+xszlen+64);
1119 if (cbuf is null) {
1120 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_nomem(ctx); return; }
1121 } else {
1122 cbuf[0..xszlen] = xsz[0..xszlen];
1123 version(use_balz) {
1124 Balz bz;
1125 usize spos = 0;
1126 usize dpos = xszlen;
1127 try {
1128 bz.compress(
1129 // reader
1130 (buf) {
1131 if (spos >= cast(usize)sz) return 0;
1132 usize left = cast(usize)sz-spos;
1133 if (left > buf.length) left = buf.length;
1134 if (left) memcpy(buf.ptr, vs+spos, left);
1135 spos += left;
1136 return left;
1138 // writer
1139 (buf) {
1140 if (dpos+buf.length >= cast(usize)sz) throw new Exception("uncompressible");
1141 memcpy(cbuf+dpos, buf.ptr, buf.length);
1142 dpos += buf.length;
1144 // maximum compression?
1145 true
1147 } catch(Exception) {
1148 dpos = usize.max;
1150 if (dpos < cast(usize)sz) {
1151 sqlite3_result_blob(ctx, cbuf, dpos, &free);
1152 return;
1154 } else version(use_libdeflate) {
1155 if (packlevel > 12) packlevel = 12;
1156 libdeflate_compressor *cpr = libdeflate_alloc_compressor(packlevel);
1157 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1158 usize dsize = libdeflate_zlib_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
1159 libdeflate_free_compressor(cpr);
1160 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1161 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1162 return;
1164 } else version(use_libxpack) {
1165 // 2^19 (524288) bytes. This is definitely a big problem and I am planning to address it.
1166 // https://github.com/ebiggers/xpack/issues/1
1167 if (sz < 524288-64) {
1168 if (packlevel > 9) packlevel = 9;
1169 xpack_compressor *cpr = xpack_alloc_compressor(cast(usize)sz, packlevel);
1170 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1171 usize dsize = xpack_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
1172 xpack_free_compressor(cpr);
1173 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1174 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1175 return;
1178 } else version(use_libbrieflz) {
1179 if (packlevel > 10) packlevel = 10;
1180 immutable usize wbsize = blz_workmem_size_level(cast(usize)sz, packlevel);
1181 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1182 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1183 uint dsize = blz_pack_level(vs, cbuf+xszlen, cast(uint)sz, wbuf, packlevel);
1184 free(wbuf);
1185 if (dsize+xszlen < cast(usize)sz) {
1186 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1187 return;
1189 } else version(use_liblzfse) {
1190 immutable usize wbsize = lzfse_encode_scratch_size();
1191 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1192 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1193 usize dsize = lzfse_encode_buffer(cbuf+xszlen, bsz, vs, cast(uint)sz, wbuf);
1194 free(wbuf);
1195 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1196 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1197 return;
1199 } else version(use_lzjb) {
1200 usize dsize = lzjb_compress(vs, cast(usize)sz, cbuf+xszlen, bsz);
1201 if (dsize == usize.max) dsize = 0;
1202 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1203 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1204 return;
1206 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "LZJB FAILED!\n"); }
1207 } else version(use_libwim_lzms) {
1208 wimlib_compressor* cpr;
1209 uint clevel = (packlevel < 10 ? 50 : 1000);
1210 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZMS, cast(usize)sz, clevel, &cpr);
1211 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1212 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1213 wimlib_free_compressor(cpr);
1214 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1215 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1216 return;
1218 } else version(use_libwim_lzx) {
1219 if (sz <= WIMLIB_LZX_MAX_CHUNK) {
1220 wimlib_compressor* cpr;
1221 uint clevel = (packlevel < 10 ? 50 : 1000);
1222 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZX, cast(usize)sz, clevel, &cpr);
1223 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1224 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1225 wimlib_free_compressor(cpr);
1226 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1227 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1228 return;
1231 } else version(use_libwim_xpress) {
1232 if (sz <= WIMLIB_XPRESS_MAX_CHUNK) {
1233 wimlib_compressor* cpr;
1234 uint clevel = (packlevel < 10 ? 50 : 1000);
1235 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
1236 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < cast(uint)sz) csz *= 2U;
1237 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, clevel, &cpr);
1238 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1239 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1240 wimlib_free_compressor(cpr);
1241 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1242 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1243 return;
1246 } else version(use_lz4) {
1247 int dsize = LZ4_compress_default(vs, cbuf+xszlen, sz, cast(int)bsz);
1248 if (dsize > 0 && dsize+xszlen < sz) {
1249 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1250 return;
1252 } else version(use_zstd) {
1253 immutable int clev =
1254 packlevel <= 3 ? ZSTD_minCLevel() :
1255 packlevel <= 6 ? ZSTD_defaultCLevel() :
1256 packlevel < 10 ? 19 :
1257 ZSTD_maxCLevel();
1258 usize dsize = ZSTD_compress(cbuf+xszlen, cast(int)bsz, vs, sz, clev);
1259 if (!ZSTD_isError(dsize) && dsize > 0 && dsize+xszlen < sz) {
1260 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1261 return;
1263 } else {
1264 import etc.c.zlib : /*compressBound,*/ compress2, Z_OK;
1265 //uint bsz = cast(uint)compressBound(cast(uint)sz);
1266 if (packlevel > 9) packlevel = 9;
1267 usize dsize = bsz;
1268 immutable int zres = compress2(cast(ubyte *)(cbuf+xszlen), &dsize, cast(const(ubyte) *)vs, sz, packlevel);
1269 if (zres == Z_OK && dsize+xszlen < cast(usize)sz) {
1270 if (!ChiroPackTryHard) {
1271 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1272 return;
1274 } else {
1275 free(cbuf);
1276 cbuf = null;
1278 // try LZMA?
1279 if (ChiroPackTryHard) {
1280 char* lzmabuf = cast(char*)malloc(bsz+xszlen+LZMA_PROPS_SIZE+1+64);
1281 if (lzmabuf !is null) {
1282 lzmabuf[0..xszlen] = xsz[0..xszlen];
1283 lzmabuf[1..5] = "LZMA";
1284 usize destLen = (cbuf is null ? bsz : dsize); // do not take more than zlib
1285 if (destLen > bsz) destLen = bsz; // just in case
1286 ubyte[LZMA_PROPS_SIZE+8] hdr = void;
1287 uint hdrSize = cast(uint)hdr.sizeof;
1289 CLzmaEncProps props;
1290 props.level = packlevel;
1291 props.dictSize = 1<<22; //4MB
1292 props.reduceSize = bsz;
1294 immutable SRes nres = LzmaEncode(cast(ubyte*)(lzmabuf+xszlen+LZMA_PROPS_SIZE+1), &destLen, cast(const(ubyte)*)vs, bsz, &props, hdr.ptr, &hdrSize, 0/*writeEndMark*/, null, &lzmaDefAllocator, &lzmaDefAllocator);
1295 assert(hdrSize == LZMA_PROPS_SIZE);
1296 if (nres == SZ_OK && destLen+xszlen+LZMA_PROPS_SIZE+1 < cast(usize)sz) {
1297 if (cbuf is null || destLen+xszlen+LZMA_PROPS_SIZE+1 < dsize+xszlen) {
1298 if (cbuf !is null) free(cbuf); // free zlib result
1299 import core.stdc.string : memcpy;
1300 lzmabuf[xszlen] = LZMA_PROPS_SIZE;
1301 memcpy(lzmabuf+xszlen+1, hdr.ptr, LZMA_PROPS_SIZE);
1302 sqlite3_result_blob(ctx, lzmabuf, destLen+xszlen+LZMA_PROPS_SIZE+1, &free);
1303 return;
1306 free(lzmabuf);
1309 // return zlib result?
1310 if (cbuf !is null) {
1311 assert(dsize < bsz);
1312 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1313 return;
1316 if (cbuf !is null) free(cbuf);
1321 if (isBadPrefix(vs[0..cast(uint)sz])) {
1322 char *res = cast(char *)malloc(sz+5);
1323 if (res is null) { sqlite3_result_error_nomem(ctx); return; }
1324 res[0..5] = "\x1bRAWB";
1325 res[5..sz+5] = vs[0..sz];
1326 if (isGoodText(vs[0..cast(usize)sz])) {
1327 sqlite3_result_text(ctx, res, sz+5, &free);
1328 } else {
1329 sqlite3_result_blob(ctx, res, sz+5, &free);
1331 } else {
1332 immutable bool wantBlob = !isGoodText(vs[0..cast(usize)sz]);
1333 immutable int tp = sqlite3_value_type(val);
1334 if ((wantBlob && tp == SQLITE_BLOB) || (!wantBlob && tp == SQLITE3_TEXT)) {
1335 sqlite3_result_value(ctx, val);
1336 } else if (wantBlob) {
1337 sqlite3_result_blob(ctx, vs, sz, SQLITE_TRANSIENT);
1338 } else {
1339 sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1346 ** ChiroPack(content)
1348 private void sq3Fn_ChiroPack (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
1349 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
1350 return sq3Fn_ChiroPackCommon(ctx, argv[0], ChiroCompressionLevel);
1355 ** ChiroPack(content, packlevel)
1357 ** `packlevel` == 0 means "don't pack"
1358 ** `packlevel` == 9 means "maximum compression"
1360 private void sq3Fn_ChiroPackDPArg (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
1361 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
1362 return sq3Fn_ChiroPackCommon(ctx, argv[0], sqlite3_value_int(argv[1]));
1367 ** ChiroGetPackType(content)
1369 private void sq3Fn_ChiroGetPackType (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1370 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroGetPackType()`", -1); return; }
1372 int sz = sqlite3_value_bytes(argv[0]);
1373 if (sz < 5 || sz > 0x3fffffff-4) { sqlite3_result_text(ctx, "RAWB", 4, SQLITE_STATIC); return; }
1375 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1376 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1378 if (!isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_text(ctx, "RAWB", 4, SQLITE_STATIC); return; }
1380 sqlite3_result_text(ctx, vs+1, 4, SQLITE_TRANSIENT);
1385 ** ChiroUnpack(content)
1387 ** it is (almost) safe to pass non-packed content here
1389 private void sq3Fn_ChiroUnpack (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1390 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!000\n"); }
1391 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroUnpack()`", -1); return; }
1393 int sz = sqlite3_value_bytes(argv[0]);
1394 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
1396 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1398 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1399 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1401 if (!isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_value(ctx, argv[0]); return; }
1402 if (vs[0..5] == "\x1bRAWB") { sqlite3_result_blob(ctx, vs+5, sz-5, SQLITE_TRANSIENT); return; }
1403 if (sz < 6) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1405 enum {
1406 Codec_ZLIB,
1407 Codec_LZMA,
1408 Codec_BALZ,
1409 Codec_XPAK,
1410 Codec_BRLZ,
1411 Codec_LZFS,
1412 Codec_LZJB,
1413 Codec_LZMS,
1414 Codec_LZMX,
1415 Codec_XPRS,
1416 Codec_LZ4D,
1417 Codec_ZSTD,
1420 int codec = Codec_ZLIB;
1421 if (vs[0..5] != "\x1bZLIB") {
1422 if (vs[0..5] == "\x1bLZMA") codec = Codec_LZMA;
1423 version(use_balz) {
1424 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBALZ") codec = Codec_BALZ;
1426 version(use_libxpack) {
1427 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1429 version(use_libxpack) {
1430 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1432 version(use_libbrieflz) {
1433 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBRLZ") codec = Codec_BRLZ;
1435 version(use_liblzfse) {
1436 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZFS") codec = Codec_LZFS;
1438 version(use_lzjb) {
1439 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZJB") codec = Codec_LZJB;
1441 version(use_libwim_lzms) {
1442 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMS") codec = Codec_LZMS;
1444 version(use_libwim_lzx) {
1445 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMX") codec = Codec_LZMX;
1447 version(use_libwim_xpress) {
1448 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPRS") codec = Codec_XPRS;
1450 version(use_lz4) {
1451 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZ4D") codec = Codec_LZ4D;
1453 version(use_zstd) {
1454 if (codec == Codec_ZLIB && vs[0..5] == "\x1bZSTD") codec = Codec_ZSTD;
1456 if (codec == Codec_ZLIB) { sqlite3_result_error(ctx, "invalid codec in `ChiroUnpack()`", -1); return; }
1459 // skip codec id
1460 // size is guaranteed to be at least 6 here
1461 vs += 5;
1462 sz -= 5;
1464 immutable uint numsz = decodeUIntLength(vs[0..cast(uint)sz]);
1465 //{ 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]); }
1466 //writeln("sq3Fn_ChiroUnpack: nsz=", sz-5);
1467 if (numsz == 0 || numsz > cast(uint)sz) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1468 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!100\n"); }
1469 immutable uint rsize = decodeUInt(vs[0..cast(uint)sz]);
1470 if (rsize == uint.max) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1471 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!101:rsize=%u\n", rsize); }
1472 if (rsize == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1473 // skip number
1474 vs += numsz;
1475 sz -= cast(int)numsz;
1476 //{ import core.stdc.stdio : printf; printf("sz=%d; rsize=%u\n", sz, rsize, dpos); }
1478 import core.stdc.stdlib : malloc, free;
1479 import core.stdc.string : memcpy;
1481 char* cbuf = cast(char*)malloc(rsize);
1482 if (cbuf is null) { sqlite3_result_error_nomem(ctx); return; }
1483 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz-dpos);
1485 usize dsize = rsize;
1486 final switch (codec) {
1487 case Codec_ZLIB:
1488 version(use_libdeflate) {
1489 libdeflate_decompressor *dcp = libdeflate_alloc_decompressor();
1490 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1491 auto rc = libdeflate_zlib_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1492 if (rc != LIBDEFLATE_SUCCESS) {
1493 free(cbuf);
1494 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1495 return;
1497 } else {
1498 import etc.c.zlib : uncompress, Z_OK;
1499 int zres = uncompress(cast(ubyte *)cbuf, &dsize, cast(const(ubyte) *)vs, sz);
1500 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz, "; dsize=", dsize, "; zres=", zres);
1501 if (zres != Z_OK || dsize != rsize) {
1502 free(cbuf);
1503 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1504 return;
1507 break;
1508 case Codec_LZMA:
1510 if (sz < LZMA_PROPS_SIZE+1 || vs[0] != LZMA_PROPS_SIZE) {
1511 free(cbuf);
1512 sqlite3_result_error(ctx, "broken LZMA data in `ChiroUnpack()`", -1);
1513 return;
1516 usize srcSize = sz-vs[0]-1;
1517 ELzmaStatus status;
1518 SRes zres = LzmaDecode(cast(ubyte *)cbuf, &dsize, cast(const(ubyte) *)vs+vs[0]+1, &srcSize,
1519 cast(const(ubyte)*)(vs+1)/*propData*/, vs[0]/*propSize*/, LZMA_FINISH_ANY, &status, &lzmaDefAllocator);
1520 if (zres != SZ_OK || dsize != rsize || status == LZMA_STATUS_FINISHED_WITH_MARK || status == LZMA_STATUS_NEEDS_MORE_INPUT) {
1521 free(cbuf);
1522 sqlite3_result_error(ctx, "broken LZMA data in `ChiroUnpack()`", -1);
1523 return;
1526 break;
1527 case Codec_BALZ:
1528 version(use_balz) {
1529 uint spos = 0;
1530 uint outpos = 0;
1531 try {
1532 Unbalz bz;
1533 auto dc = bz.decompress(
1534 // reader
1535 (buf) {
1536 uint left = cast(uint)sz-spos;
1537 if (left > buf.length) left = cast(uint)buf.length;
1538 if (left != 0) memcpy(buf.ptr, vs, left);
1539 spos += left;
1540 return left;
1542 // writer
1543 (buf) {
1544 uint left = rsize-outpos;
1545 if (left == 0) throw new Exception("broken data");
1546 if (left > buf.length) left = cast(uint)buf.length;
1547 if (left) memcpy(cbuf+outpos, buf.ptr, left);
1548 outpos += left;
1551 if (dc != rsize) throw new Exception("broken data");
1552 } catch (Exception) {
1553 outpos = uint.max;
1555 if (outpos == uint.max) {
1556 free(cbuf);
1557 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1558 return;
1560 dsize = outpos;
1561 } else {
1562 free(cbuf);
1563 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1564 return;
1566 break;
1567 case Codec_XPAK:
1568 version(use_libxpack) {
1569 xpack_decompressor *dcp = xpack_alloc_decompressor();
1570 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1571 auto rc = xpack_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1572 if (rc != DECOMPRESS_SUCCESS) {
1573 free(cbuf);
1574 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1575 return;
1577 } else {
1578 free(cbuf);
1579 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1580 return;
1582 break;
1583 case Codec_BRLZ:
1584 version(use_libbrieflz) {
1585 dsize = blz_depack_safe(vs, cast(uint)sz, cbuf, rsize);
1586 if (dsize != rsize) {
1587 free(cbuf);
1588 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1589 return;
1591 } else {
1592 free(cbuf);
1593 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1594 return;
1596 break;
1597 case Codec_LZFS:
1598 version(use_liblzfse) {
1599 immutable usize wbsize = lzfse_decode_scratch_size();
1600 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1601 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1602 dsize = lzfse_decode_buffer(cbuf, cast(usize)rsize, vs, cast(usize)sz, wbuf);
1603 free(wbuf);
1604 if (dsize == 0 || dsize != rsize) {
1605 free(cbuf);
1606 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1607 return;
1609 } else {
1610 free(cbuf);
1611 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1612 return;
1614 break;
1615 case Codec_LZJB:
1616 version(use_lzjb) {
1617 dsize = lzjb_decompress(vs, cast(usize)sz, cbuf, rsize);
1618 if (dsize != rsize) {
1619 free(cbuf);
1620 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1621 return;
1623 } else {
1624 free(cbuf);
1625 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1626 return;
1628 break;
1629 case Codec_LZMS:
1630 version(use_libwim_lzms) {
1631 wimlib_decompressor* dpr;
1632 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZMS, rsize, &dpr);
1633 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1634 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1635 wimlib_free_decompressor(dpr);
1636 if (rc != 0) {
1637 free(cbuf);
1638 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1639 return;
1641 } else {
1642 free(cbuf);
1643 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1644 return;
1646 break;
1647 case Codec_LZMX:
1648 version(use_libwim_lzx) {
1649 wimlib_decompressor* dpr;
1650 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZX, rsize, &dpr);
1651 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1652 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1653 wimlib_free_decompressor(dpr);
1654 if (rc != 0) {
1655 free(cbuf);
1656 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1657 return;
1659 } else {
1660 free(cbuf);
1661 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1662 return;
1664 break;
1665 case Codec_XPRS:
1666 version(use_libwim_xpress) {
1667 wimlib_decompressor* dpr;
1668 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
1669 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < rsize) csz *= 2U;
1670 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, &dpr);
1671 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1672 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1673 wimlib_free_decompressor(dpr);
1674 if (rc != 0) {
1675 free(cbuf);
1676 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1677 return;
1679 } else {
1680 free(cbuf);
1681 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1682 return;
1684 break;
1685 case Codec_LZ4D:
1686 version(use_lz4) {
1687 dsize = LZ4_decompress_safe(vs, cbuf, sz, rsize);
1688 if (dsize != rsize) {
1689 free(cbuf);
1690 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1691 return;
1693 } else {
1694 free(cbuf);
1695 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1696 return;
1698 break;
1699 case Codec_ZSTD:
1700 version(use_zstd) {
1701 dsize = ZSTD_decompress(cbuf, rsize, vs, sz);
1702 if (ZSTD_isError(dsize) || dsize != rsize) {
1703 free(cbuf);
1704 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1705 return;
1707 } else {
1708 free(cbuf);
1709 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1710 return;
1712 break;
1715 if (isGoodText(cbuf[0..dsize])) {
1716 sqlite3_result_text(ctx, cbuf, cast(int)dsize, &free);
1717 } else {
1718 sqlite3_result_blob(ctx, cbuf, cast(int)dsize, &free);
1724 ** ChiroNormCRLF(content)
1726 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1727 ** Removes trailing blanks.
1729 private void sq3Fn_ChiroNormCRLF (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1730 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormCRLF()`", -1); return; }
1732 int sz = sqlite3_value_bytes(argv[0]);
1733 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1735 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1737 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1738 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormCRLF()`", -1); return; }
1740 // check if we have something to do, and calculate new string size
1741 bool needwork = false;
1742 if (vs[cast(uint)sz-1] <= 32) {
1743 needwork = true;
1744 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1745 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1747 uint newsz = cast(uint)sz;
1748 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1749 if (ch == 13) {
1750 needwork = true;
1751 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1752 } else if (!needwork) {
1753 needwork = ((ch < 32 && ch != 9 && ch != 10) || ch == 127);
1757 if (!needwork) {
1758 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1759 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1760 return;
1763 assert(newsz && newsz <= cast(uint)sz);
1765 // need a new string
1766 import core.stdc.stdlib : malloc, free;
1767 char* newstr = cast(char*)malloc(newsz);
1768 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1769 char* dest = newstr;
1770 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1771 if (ch == 13) {
1772 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1773 } else {
1774 if (ch == 127) *dest++ = '~';
1775 else if (ch == 11 || ch == 12) *dest++ = '\n';
1776 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1777 else *dest++ = ch;
1780 assert(dest == newstr+newsz);
1782 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1787 ** ChiroNormHeaders(content)
1789 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1790 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1791 ** Removes trailing blanks.
1793 private void sq3Fn_ChiroNormHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1794 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormHeaders()`", -1); return; }
1796 int sz = sqlite3_value_bytes(argv[0]);
1797 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1799 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1801 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1802 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormHeaders()`", -1); return; }
1804 // check if we have something to do, and calculate new string size
1805 bool needwork = false;
1806 if (vs[cast(uint)sz-1] <= 32) {
1807 needwork = true;
1808 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1809 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1811 uint newsz = cast(uint)sz;
1812 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1813 if (ch == 13) {
1814 needwork = true;
1815 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1816 } else if (ch == 10) {
1817 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) { needwork = true; --newsz; }
1818 } else if (!needwork) {
1819 needwork = ((ch < 32 && ch != 10) || ch == 127);
1823 if (!needwork) {
1824 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1825 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1826 return;
1829 assert(newsz && newsz <= cast(uint)sz);
1831 // need a new string
1832 import core.stdc.stdlib : malloc, free;
1833 char* newstr = cast(char*)malloc(newsz);
1834 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1835 char* dest = newstr;
1836 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1837 if (ch == 13) {
1838 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1839 } else if (ch == 10) {
1840 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1841 } else {
1842 if (ch == 127) *dest++ = '~';
1843 else if (ch < 32 && ch != 10) *dest++ = ' ';
1844 else *dest++ = ch;
1847 assert(dest == newstr+newsz);
1849 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1854 ** ChiroExtractHeaders(content)
1856 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1857 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1858 ** Removes trailing blanks.
1860 private void sq3Fn_ChiroExtractHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1861 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1863 int sz = sqlite3_value_bytes(argv[0]);
1864 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1866 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1868 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1869 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1871 // slice headers
1872 sz = sq3Supp_FindHeadersEnd(vs, sz);
1874 // strip trailing blanks
1875 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1876 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1878 // allocate new string (it can be smaller, but will never be bigger)
1879 import core.stdc.stdlib : malloc, free;
1880 char* newstr = cast(char*)malloc(cast(uint)sz);
1881 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1882 char* dest = newstr;
1883 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1884 if (ch == 13) {
1885 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1886 } else if (ch == 10) {
1887 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1888 } else {
1889 if (ch == 127) *dest++ = '~';
1890 else if (ch < 32 && ch != 10) *dest++ = ' ';
1891 else *dest++ = ch;
1894 assert(dest <= newstr+cast(uint)sz);
1895 sz = cast(int)cast(usize)(dest-newstr);
1896 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1897 sqlite3_result_text(ctx, newstr, sz, &free);
1902 ** ChiroExtractBody(content)
1904 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1905 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1906 ** Removes trailing blanks and final dot.
1908 private void sq3Fn_ChiroExtractBody (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1909 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1911 int sz = sqlite3_value_bytes(argv[0]);
1912 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1914 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1916 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1917 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1919 // slice body
1920 immutable int bstart = sq3Supp_FindHeadersEnd(vs, sz);
1921 if (bstart >= sz) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1922 vs += bstart;
1923 sz -= bstart;
1925 // strip trailing dot
1926 if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '\n') sz -= 2;
1927 else if (sz >= 1 && vs[cast(uint)sz-1U] == '\n') --sz;
1928 if (sz == 1 && vs[0] == '.') sz = 0;
1929 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\n' && vs[cast(uint)sz-1U] == '.') --sz;
1930 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '.') --sz;
1932 // strip trailing blanks
1933 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1934 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1936 // allocate new string (it can be smaller, but will never be bigger)
1937 import core.stdc.stdlib : malloc, free;
1938 char* newstr = cast(char*)malloc(cast(uint)sz);
1939 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1940 char* dest = newstr;
1941 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1942 if (ch == 13) {
1943 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1944 } else {
1945 if (ch == 127) *dest++ = '~';
1946 else if (ch == 11 || ch == 12) *dest++ = '\n';
1947 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1948 else *dest++ = ch;
1951 assert(dest <= newstr+cast(uint)sz);
1952 sz = cast(int)cast(usize)(dest-newstr);
1953 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1954 sqlite3_result_text(ctx, newstr, sz, &free);
1959 ** ChiroRIPEMD160(content)
1961 ** Calculates RIPEMD160 hash over the given content.
1963 ** Returns BINARY BLOB! You can use `tolower(hex(ChiroRIPEMD160(contents)))`
1964 ** to get lowercased hex hash string.
1966 private void sq3Fn_ChiroRIPEMD160 (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1967 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroRIPEMD160()`", -1); return; }
1969 immutable int sz = sqlite3_value_bytes(argv[0]);
1970 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1972 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1973 if (!vs && sz == 0) vs = "";
1974 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroRIPEMD160()`", -1); return; }
1976 ubyte[20] hash = ripemd160Of(vs[0..cast(uint)sz]);
1977 sqlite3_result_blob(ctx, cast(const(char)*)hash.ptr, cast(int)hash.length, SQLITE_TRANSIENT);
1981 enum HeaderProcStartTpl(string fnname) = `
1982 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"`~fnname~`()\"", -1); return; }
1984 immutable int sz = sqlite3_value_bytes(argv[0]);
1985 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1987 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1988 if (!vs && sz == 0) vs = "";
1989 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"`~fnname~`()\"", -1); return; }
1991 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
1996 ** ChiroHdr_NNTPIndex(headers)
1998 ** The content must be email with headers (or headers only).
1999 ** Returns "NNTP-Index" field or zero (int).
2001 private void sq3Fn_ChiroHdr_NNTPIndex (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2002 mixin(HeaderProcStartTpl!"ChiroHdr_NNTPIndex");
2004 uint nntpidx = 0;
2006 auto nntpidxfld = findHeaderField(hdrs, "NNTP-Index");
2007 if (nntpidxfld.length) {
2008 auto id = nntpidxfld.getFieldValue;
2009 if (id.length) {
2010 foreach (immutable ch; id) {
2011 if (ch < '0' || ch > '9') { nntpidx = 0; break; }
2012 if (nntpidx == 0 && ch == '0') continue;
2013 immutable uint nn = nntpidx*10u+(ch-'0');
2014 if (nn <= nntpidx) nntpidx = 0x7fffffff; else nntpidx = nn;
2019 // it is safe, it can't overflow
2020 sqlite3_result_int(ctx, cast(int)nntpidx);
2025 ** ChiroHdr_RecvTime(headers)
2027 ** The content must be email with headers (or headers only).
2028 ** Returns unixtime (can be zero).
2030 private void sq3Fn_ChiroHdr_RecvTime (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2031 mixin(HeaderProcStartTpl!"ChiroHdr_RecvTime");
2033 uint msgtime = 0; // message receiving time
2035 auto datefld = findHeaderField(hdrs, "Injection-Date");
2036 if (datefld.length != 0) {
2037 auto v = datefld.getFieldValue;
2038 try {
2039 msgtime = parseMailDate(v);
2040 } catch (Exception) {
2041 //writeln("UID=", uid, ": FUCKED INJECTION-DATE: |", v, "|");
2042 msgtime = 0; // just in case
2046 if (!msgtime) {
2047 // obsolete NNTP date field, because why not?
2048 datefld = findHeaderField(hdrs, "NNTP-Posting-Date");
2049 if (datefld.length != 0) {
2050 auto v = datefld.getFieldValue;
2051 try {
2052 msgtime = parseMailDate(v);
2053 } catch (Exception) {
2054 //writeln("UID=", uid, ": FUCKED NNTP-POSTING-DATE: |", v, "|");
2055 msgtime = 0; // just in case
2060 if (!msgtime) {
2061 datefld = findHeaderField(hdrs, "Date");
2062 if (datefld.length != 0) {
2063 auto v = datefld.getFieldValue;
2064 try {
2065 msgtime = parseMailDate(v);
2066 } catch (Exception) {
2067 //writeln("UID=", uid, ": FUCKED DATE: |", v, "|");
2068 msgtime = 0; // just in case
2073 // finally, try to get time from "Received:"
2074 //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
2075 if (!msgtime) {
2076 //writeln("!!! --- !!!");
2077 uint lowesttime = uint.max;
2078 foreach (uint fidx; 0..uint.max) {
2079 auto recvfld = findHeaderField(hdrs, "Received", fidx);
2080 if (recvfld.length == 0) break;
2081 auto lsemi = recvfld.lastIndexOf(';');
2082 if (lsemi >= 0) recvfld = recvfld[lsemi+1..$].xstrip;
2083 if (recvfld.length != 0) {
2084 auto v = recvfld.getFieldValue;
2085 uint tm = 0;
2086 try {
2087 tm = parseMailDate(v);
2088 } catch (Exception) {
2089 //writeln("UID=", uid, ": FUCKED RECV DATE: |", v, "|");
2090 tm = 0; // just in case
2092 //writeln(tm, " : ", lowesttime);
2093 if (tm && tm < lowesttime) lowesttime = tm;
2096 if (lowesttime != uint.max) msgtime = lowesttime;
2099 sqlite3_result_int64(ctx, msgtime);
2104 ** ChiroHdr_FromEmail(headers)
2106 ** The content must be email with headers (or headers only).
2107 ** Returns email "From" field.
2109 private void sq3Fn_ChiroHdr_FromEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2110 mixin(HeaderProcStartTpl!"ChiroHdr_FromEmail");
2111 auto from = findHeaderField(hdrs, "From").extractMail;
2112 if (from.length == 0) {
2113 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
2114 } else {
2115 sqlite3_result_text(ctx, from.ptr, cast(int)from.length, SQLITE_TRANSIENT);
2121 ** ChiroHdr_ToEmail(headers)
2123 ** The content must be email with headers (or headers only).
2124 ** Returns email "From" field.
2126 private void sq3Fn_ChiroHdr_ToEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2127 mixin(HeaderProcStartTpl!"ChiroHdr_ToEmail");
2128 auto to = findHeaderField(hdrs, "To").extractMail;
2129 if (to.length == 0) {
2130 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
2131 } else {
2132 sqlite3_result_text(ctx, to.ptr, cast(int)to.length, SQLITE_TRANSIENT);
2138 ** ChiroHdr_Subj(headers)
2140 ** The content must be email with headers (or headers only).
2141 ** Returns email "From" field.
2143 private void sq3Fn_ChiroHdr_Subj (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2144 mixin(HeaderProcStartTpl!"sq3Fn_ChiroHdr_Subj");
2145 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
2146 if (subj.length == 0) {
2147 sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
2148 } else {
2149 sqlite3_result_text(ctx, subj.ptr, cast(int)subj.length, SQLITE_TRANSIENT);
2155 ** ChiroHdr_Field(headers, fieldname)
2157 ** The content must be email with headers (or headers only).
2158 ** Returns field value as text, or NULL if there is no such field.
2160 private void sq3Fn_ChiroHdr_Field (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2161 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroHdr_Field()\"", -1); return; }
2163 immutable int sz = sqlite3_value_bytes(argv[0]);
2164 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
2166 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2167 if (!vs && sz == 0) vs = "";
2168 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
2170 immutable int fldsz = sqlite3_value_bytes(argv[1]);
2171 if (fldsz < 0) { sqlite3_result_error_toobig(ctx); return; }
2173 const(char)* fldname = cast(const(char) *)sqlite3_value_blob(argv[1]);
2174 if (!fldname && fldsz == 0) fldname = "";
2175 if (!fldname) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
2177 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
2178 auto value = findHeaderField(hdrs, fldname[0..fldsz]);
2179 if (value is null) {
2180 sqlite3_result_null(ctx);
2181 } else if (value.length == 0) {
2182 sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
2183 } else {
2184 sqlite3_result_text(ctx, value.ptr, cast(int)value.length, SQLITE_TRANSIENT);
2190 ** ChiroTimerStart([msg])
2192 ** The content must be email with headers (or headers only).
2193 ** Returns email "From" field.
2195 private void sq3Fn_ChiroTimerStart (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2196 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStart()\"", -1); return; }
2198 delete chiTimerMsg;
2200 if (argc == 1) {
2201 immutable int sz = sqlite3_value_bytes(argv[0]);
2202 if (sz > 0) {
2203 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2204 if (vs) {
2205 chiTimerMsg = new char[cast(usize)sz];
2206 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
2207 writeln("started ", chiTimerMsg, "...");
2212 sqlite3_result_int(ctx, 1);
2213 chiTimer.restart();
2218 ** ChiroTimerStop([msg])
2220 ** The content must be email with headers (or headers only).
2221 ** Returns email "From" field.
2223 private void sq3Fn_ChiroTimerStop (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2224 chiTimer.stop;
2225 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStop()\"", -1); return; }
2227 if (ChiroTimerEnabled) {
2228 if (argc == 1) {
2229 delete chiTimerMsg;
2230 immutable int sz = sqlite3_value_bytes(argv[0]);
2231 if (sz > 0) {
2232 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2233 if (vs) {
2234 chiTimerMsg = new char[cast(usize)sz];
2235 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
2240 char[128] buf;
2241 auto tstr = chiTimer.toBuffer(buf[]);
2242 if (chiTimerMsg.length) {
2243 writeln("done ", chiTimerMsg, ": ", tstr);
2244 } else {
2245 writeln("time: ", tstr);
2249 delete chiTimerMsg;
2251 sqlite3_result_int(ctx, 1);
2256 ** ChiroGlob(pat, str)
2258 ** GLOB replacement, with extended word matching.
2260 private void sq3Fn_ChiroGlob_common (sqlite3_context *ctx, int argc, sqlite3_value **argv, int casesens,
2261 uint stridx=1, uint patidx=0)
2263 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroGlob()\"", -1); return; }
2265 immutable int patsz = sqlite3_value_bytes(argv[patidx]);
2266 if (patsz < 0) { sqlite3_result_error_toobig(ctx); return; }
2268 const(char)* pat = cast(const(char) *)sqlite3_value_blob(argv[patidx]);
2269 if (!pat && patsz == 0) pat = "";
2270 if (!pat) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2272 immutable int strsz = sqlite3_value_bytes(argv[stridx]);
2273 if (strsz < 0) { sqlite3_result_error_toobig(ctx); return; }
2275 const(char)* str = cast(const(char) *)sqlite3_value_blob(argv[stridx]);
2276 if (!str && strsz == 0) str = "";
2277 if (!str) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2279 immutable bool res =
2280 casesens ?
2281 globmatch(str[0..cast(usize)strsz], pat[0..cast(usize)patsz]) :
2282 globmatchCI(str[0..cast(usize)strsz], pat[0..cast(usize)patsz]);
2284 sqlite3_result_int(ctx, (res ? 1 : 0));
2289 ** ChiroGlobSQL(pat, str)
2291 ** GLOB replacement, with extended word matching.
2293 private void sq3Fn_ChiroGlobSQL (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2294 sq3Fn_ChiroGlob_common(ctx, argc, argv, casesens:1);
2298 ** ChiroGlob(str, pat)
2300 ** GLOB replacement, with extended word matching.
2302 private void sq3Fn_ChiroGlob (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2303 sq3Fn_ChiroGlob_common(ctx, argc, argv, casesens:1, stridx:0, patidx:1);
2307 ** ChiroGlobCI(str, pat)
2309 ** GLOB replacement, with extended word matching.
2311 private void sq3Fn_ChiroGlobCI (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2312 sq3Fn_ChiroGlob_common(ctx, argc, argv, casesens:0, stridx:0, patidx:1);
2316 // ////////////////////////////////////////////////////////////////////////// //
2320 // ////////////////////////////////////////////////////////////////////////// //
2321 public void chiroRegisterSQLite3Functions (ref Database db) {
2322 sqlite3_busy_timeout(db.getHandle, 20000); // busy timeout: 20 seconds
2324 immutable int rc = sqlite3_extended_result_codes(db.getHandle, 1);
2325 if (rc != SQLITE_OK) {
2326 import core.stdc.stdio : stderr, fprintf;
2327 fprintf(stderr, "SQLITE WARNING: cannot enable extended result codes (this is harmless).\n");
2329 db.createFunction("glob", 2, &sq3Fn_ChiroGlobSQL, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2330 db.createFunction("ChiroGlob", 2, &sq3Fn_ChiroGlob, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2331 db.createFunction("ChiroGlobCI", 2, &sq3Fn_ChiroGlobCI, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2333 db.createFunction("ChiroPack", 1, &sq3Fn_ChiroPack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2334 db.createFunction("ChiroPack", 2, &sq3Fn_ChiroPackDPArg, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2335 db.createFunction("ChiroUnpack", 1, &sq3Fn_ChiroUnpack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2337 db.createFunction("ChiroPackLZMA", 1, &sq3Fn_ChiroPackLZMA, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2338 db.createFunction("ChiroPackLZMA", 2, &sq3Fn_ChiroPackLZMA, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2340 db.createFunction("ChiroGetPackType", 1, &sq3Fn_ChiroGetPackType, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2342 db.createFunction("ChiroNormCRLF", 1, &sq3Fn_ChiroNormCRLF, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2343 db.createFunction("ChiroNormHeaders", 1, &sq3Fn_ChiroNormHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2344 db.createFunction("ChiroExtractHeaders", 1, &sq3Fn_ChiroExtractHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2345 db.createFunction("ChiroExtractBody", 1, &sq3Fn_ChiroExtractBody, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2346 db.createFunction("ChiroRIPEMD160", 1, &sq3Fn_ChiroRIPEMD160, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2348 db.createFunction("ChiroHdr_NNTPIndex", 1, &sq3Fn_ChiroHdr_NNTPIndex, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2349 db.createFunction("ChiroHdr_RecvTime", 1, &sq3Fn_ChiroHdr_RecvTime, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2350 db.createFunction("ChiroHdr_FromEmail", 1, &sq3Fn_ChiroHdr_FromEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2351 db.createFunction("ChiroHdr_ToEmail", 1, &sq3Fn_ChiroHdr_ToEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2352 db.createFunction("ChiroHdr_Subj", 1, &sq3Fn_ChiroHdr_Subj, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2353 db.createFunction("ChiroHdr_Field", 2, &sq3Fn_ChiroHdr_Field, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2355 db.createFunction("ChiroTimerStart", 0, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2356 db.createFunction("ChiroTimerStart", 1, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2357 db.createFunction("ChiroTimerStop", 0, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2358 db.createFunction("ChiroTimerStop", 1, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2362 // ////////////////////////////////////////////////////////////////////////// //
2363 public void chiroRecreateStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName) {
2364 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2365 dbStore = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWStorageRecreate, schemaStorage);
2366 chiroRegisterSQLite3Functions(dbStore);
2367 dbStore.setOnClose(schemaStorageIndex~dbpragmasRWStorage~"ANALYZE;");
2371 // ////////////////////////////////////////////////////////////////////////// //
2372 public void chiroRecreateViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName) {
2373 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2374 dbView = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWSupportRecreate, schemaSupportTable);
2375 chiroRegisterSQLite3Functions(dbView);
2376 dbView.setOnClose(schemaSupportIndex~dbpragmasRWSupport~"ANALYZE;");
2380 public void chiroCreateViewIndiciesDB () {
2381 dbView.setOnClose(dbpragmasRWSupport~"ANALYZE;");
2382 dbView.execute(schemaSupportIndex);
2386 // ////////////////////////////////////////////////////////////////////////// //
2387 public void chiroRecreateConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName) {
2388 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2389 dbConf = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWOptionsRecreate, schemaOptions);
2390 chiroRegisterSQLite3Functions(dbConf);
2391 dbConf.setOnClose(schemaOptionsIndex~dbpragmasRWOptions~"ANALYZE;");
2395 // ////////////////////////////////////////////////////////////////////////// //
2396 public void chiroOpenStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName, bool readonly=false) {
2397 dbStore = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWStorage), schemaStorage);
2398 chiroRegisterSQLite3Functions(dbStore);
2399 if (!readonly) dbStore.setOnClose("PRAGMA optimize;");
2403 // ////////////////////////////////////////////////////////////////////////// //
2404 public void chiroOpenViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName, bool readonly=false) {
2405 dbView = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWSupport), schemaSupport);
2406 chiroRegisterSQLite3Functions(dbView);
2407 if (!readonly) {
2408 dbView.execute(schemaSupportTempTables);
2409 dbView.setOnClose("PRAGMA optimize;");
2414 // ////////////////////////////////////////////////////////////////////////// //
2415 public void chiroOpenConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName, bool readonly=false) {
2416 dbConf = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWOptions), schemaOptions);
2417 chiroRegisterSQLite3Functions(dbConf);
2418 if (!readonly) dbConf.setOnClose("PRAGMA optimize;");
2422 // ////////////////////////////////////////////////////////////////////////// //
2424 recreates FTS5 (full-text search) info.
2426 public void chiroRecreateFTS5 (bool repopulate=true) {
2427 dbView.execute(recreateFTS5);
2428 if (repopulate) dbView.execute(repopulateFTS5);
2429 dbView.execute(recreateFTS5Triggers);
2433 // ////////////////////////////////////////////////////////////////////////// //
2434 extern(C) {
2435 static void errorLogCallback (void *pArg, int rc, const char *zMsg) {
2436 if (ChiroSQLiteSilent) return;
2437 import core.stdc.stdio : stderr, fprintf;
2438 switch (rc) {
2439 case SQLITE_NOTICE: fprintf(stderr, "***SQLITE NOTICE: %s\n", zMsg); break;
2440 case SQLITE_NOTICE_RECOVER_WAL: fprintf(stderr, "***SQLITE NOTICE (WAL RECOVER): %s\n", zMsg); break;
2441 case SQLITE_NOTICE_RECOVER_ROLLBACK: fprintf(stderr, "***SQLITE NOTICE (ROLLBACK RECOVER): %s\n", zMsg); break;
2442 /* */
2443 case SQLITE_WARNING: fprintf(stderr, "***SQLITE WARNING: %s\n", zMsg); break;
2444 case SQLITE_WARNING_AUTOINDEX: fprintf(stderr, "***SQLITE AUTOINDEX WARNING: %s\n", zMsg); break;
2445 /* */
2446 case SQLITE_CANTOPEN:
2447 case SQLITE_SCHEMA:
2448 break; // ignore those
2449 /* */
2450 default: fprintf(stderr, "***SQLITE LOG(%d) [%s]: %s\n", rc, sqlite3_errstr(rc), zMsg); break;
2456 static string sqerrstr (immutable int rc) nothrow @trusted {
2457 const(char)* msg = sqlite3_errstr(rc);
2458 if (!msg || !msg[0]) return null;
2459 import core.stdc.string : strlen;
2460 return msg[0..strlen(msg)].idup;
2464 static void sqconfigcheck (immutable int rc, string msg, bool fatal) {
2465 if (rc == SQLITE_OK) return;
2466 if (fatal) {
2467 string errmsg = sqerrstr(rc);
2468 throw new Exception("FATAL: "~msg~": "~errmsg);
2469 } else {
2470 if (msg is null) msg = "";
2471 import core.stdc.stdio : stderr, fprintf;
2472 fprintf(stderr, "SQLITE WARNING: %.*s (this is harmless): %s\n", cast(uint)msg.length, msg.ptr, sqlite3_errstr(rc));
2477 // call this BEFORE opening any SQLite database connection!
2478 public void chiroSwitchToSingleThread () {
2479 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SINGLETHREAD), "cannot set single-threaded mode", fatal:false);
2483 public string MailDBPath () nothrow @trusted @nogc { return ExpandedMailDBPath; }
2486 public void MailDBPath(T:const(char)[]) (T mailpath) nothrow @trusted {
2487 while (mailpath.length > 1 && mailpath[$-1] == '/') mailpath = mailpath[0..$-1];
2489 if (mailpath.length == 0 || mailpath == ".") {
2490 ExpandedMailDBPath = "";
2491 return;
2494 if (mailpath[0] == '~') {
2495 char[] dpath = new char[mailpath.length+4096];
2496 dpath = expandTilde(dpath, mailpath);
2498 while (dpath.length > 1 && dpath[$-1] == '/') dpath = dpath[0..$-1];
2499 dpath ~= '/';
2500 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
2501 } else {
2502 char[] dpath = new char[mailpath.length+1];
2503 dpath[0..$-1] = mailpath[];
2504 dpath[$-1] = '/';
2505 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
2510 shared static this () {
2511 enum {
2512 SQLITE_CONFIG_STMTJRNL_SPILL = 26, /* int nByte */
2513 SQLITE_CONFIG_SMALL_MALLOC = 27, /* boolean */
2516 if (!sqlite3_threadsafe()) {
2517 throw new Exception("FATAL: SQLite must be compiled with threading support!");
2520 // we are interested in all errors
2521 sqlite3_config(SQLITE_CONFIG_LOG, &errorLogCallback, null);
2523 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SERIALIZED), "cannot set SQLite serialized threading mode", fatal:true);
2524 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SMALL_MALLOC, 0), "cannot enable SQLite unrestriced malloc mode", fatal:false);
2525 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_URI, 1), "cannot enable SQLite URI handling", fatal:false);
2526 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_COVERING_INDEX_SCAN, 1), "cannot enable SQLite covering index scan", fatal:false);
2527 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_STMTJRNL_SPILL, 512*1024), "cannot set SQLite statement journal spill threshold", fatal:false);
2529 MailDBPath = "~/Mail";
2533 shared static ~this () {
2534 dbConf.close();
2535 dbView.close();
2536 dbStore.close();
2540 // ////////////////////////////////////////////////////////////////////////// //
2541 public void transacted(string dbname) (void delegate () dg) {
2542 if (dg is null) return;
2543 static if (dbname == "View" || dbname == "view") alias db = dbView;
2544 else static if (dbname == "Store" || dbname == "store") alias db = dbStore;
2545 else static if (dbname == "Conf" || dbname == "conf") alias db = dbConf;
2546 else static assert(0, "invalid db name: '"~dbname~"'");
2547 db.transacted(dg);
2551 // ////////////////////////////////////////////////////////////////////////// //
2552 public DynStr chiroGetTagMonthLimitEx(T) (T tagnameid, out int val, int defval=6)
2553 if (is(T:const(char)[]) || is(T:uint))
2555 static if (is(T:const(char)[])) {
2556 alias tagname = tagnameid;
2557 } else {
2558 DynStr tagnameStr;
2559 static auto stGetTagName = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
2560 foreach (auto row; stGetTagName .st.bind(":tagid", tagnameid).range) {
2561 tagnameStr = row.tagname!SQ3Text;
2563 const(char)[] tagname = tagnameStr.getData;
2566 static auto stGetMLimit = LazyStatement!"Conf"(`
2567 WITH RECURSIVE pth(path) AS (
2568 VALUES('/mainpane/msgview/monthlimit'||:tagslash||:tagname)
2569 UNION ALL
2570 SELECT
2571 SUBSTR(path, 1, LENGTH(path)-LENGTH(REPLACE(path, RTRIM(path, REPLACE(path, '/', '')), ''))-1)
2572 FROM pth
2573 WHERE path LIKE '/mainpane/msgview/monthlimit%'
2575 SELECT
2576 -- pth.path AS path
2577 opt.name AS name
2578 , opt.value AS value
2579 FROM pth
2580 INNER JOIN options AS opt ON opt.name=pth.path
2581 WHERE pth.path LIKE '/mainpane/msgview/monthlimit%'
2582 LIMIT 1
2583 ;`);
2585 stGetMLimit.st
2586 .bindConstText(":tagslash", (tagname.length && tagname[0] != '/' ? "/" : ""))
2587 .bindConstText(":tagname", tagname);
2589 foreach (auto row; stGetMLimit.st.range) {
2590 //conwriteln("TAGNAME=<", tagname, ">; val=", row.value!int, "; sres=<", row.name!SQ3Text, ">");
2591 val = row.value!int;
2592 DynStr sres = row.name!SQ3Text;
2593 return sres;
2596 val = defval;
2597 return DynStr();
2601 public int chiroGetTagMonthLimit(T) (T tagnameid, int defval=6)
2602 if (is(T:const(char)[]) || is(T:uint))
2604 static if (is(T:const(char)[])) {
2605 alias tagname = tagnameid;
2606 } else {
2607 DynStr tagnameStr;
2608 static auto stGetTagName = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
2609 foreach (auto row; stGetTagName .st.bind(":tagid", tagnameid).range) {
2610 tagnameStr = row.tagname!SQ3Text;
2612 const(char)[] tagname = tagnameStr.getData;
2615 static auto stGetMLimit = LazyStatement!"Conf"(`
2616 WITH RECURSIVE pth(path) AS (
2617 VALUES('/mainpane/msgview/monthlimit'||:tagslash||:tagname)
2618 UNION ALL
2619 SELECT
2620 SUBSTR(path, 1, LENGTH(path)-LENGTH(REPLACE(path, RTRIM(path, REPLACE(path, '/', '')), ''))-1)
2621 FROM pth
2622 WHERE path LIKE '/mainpane/msgview/monthlimit%'
2624 SELECT
2625 -- pth.path AS path
2626 -- opt.name AS name
2627 opt.value AS value
2628 FROM pth
2629 INNER JOIN options AS opt ON opt.name=pth.path
2630 WHERE pth.path LIKE '/mainpane/msgview/monthlimit%'
2631 LIMIT 1
2632 ;`);
2634 stGetMLimit.st
2635 .bindConstText(":tagslash", (tagname.length && tagname[0] != '/' ? "/" : ""))
2636 .bindConstText(":tagname", tagname);
2638 foreach (auto row; stGetMLimit.st.range) return row.value!int;
2640 return defval;
2644 public void chiroDeleteOption (const(char)[] name) {
2645 assert(name.length != 0);
2646 static auto stat = LazyStatement!"Conf"(`DELETE FROM options WHERE name=:name;`);
2647 stat.st.bindConstText(":name", name).doAll();
2650 public void chiroSetOption(T) (const(char)[] name, T value)
2651 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2653 assert(name.length != 0);
2654 static auto stat = LazyStatement!"Conf"(`
2655 INSERT INTO options
2656 ( name, value)
2657 VALUES(:name,:value)
2658 ON CONFLICT(name)
2659 DO UPDATE SET value=:value
2660 ;`);
2661 stat.st.bindConstText(":name", name);
2662 static if (is(T == typeof(null))) {
2663 stat.st.bindConstText(":value", "");
2664 } else static if (__traits(isIntegral, T)) {
2665 stat.st.bind(":value", value);
2666 } else static if (is(T:const(char)[])) {
2667 stat.st.bindConstText(":value", value);
2668 } else {
2669 static assert(0, "oops");
2671 stat.st.doAll();
2674 public void chiroSetOption (const(char)[] name, DynStr value) {
2675 assert(name.length != 0);
2676 //{ import std.stdio; writeln("SETOPTION(", name, "): <", value.getData, ">"); }
2677 static auto stat = LazyStatement!"Conf"(`
2678 INSERT INTO options
2679 ( name, value)
2680 VALUES(:name,:value)
2681 ON CONFLICT(name)
2682 DO UPDATE SET value=:value
2683 ;`);
2684 stat.st
2685 .bindConstText(":name", name)
2686 .bindConstText(":value", value.getData)
2687 .doAll();
2691 public void chiroSetOptionUInts (const(char)[] name, uint v0, uint v1) {
2692 assert(name.length != 0);
2693 static auto stat = LazyStatement!"Conf"(`
2694 INSERT INTO options
2695 ( name, value)
2696 VALUES(:name,:value)
2697 ON CONFLICT(name)
2698 DO UPDATE SET value=:value
2699 ;`);
2700 import core.stdc.stdio : snprintf;
2701 char[64] value = void;
2702 auto vlen = snprintf(value.ptr, value.sizeof, "%u,%u", v0, v1);
2703 stat.st
2704 .bindConstText(":name", name)
2705 .bindConstText(":value", value[0..vlen])
2706 .doAll();
2710 public T chiroGetOptionEx(T) (const(char)[] name, out bool exists, T defval=T.init)
2711 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2713 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2714 assert(name.length != 0);
2715 exists = false;
2716 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2717 exists = true;
2718 return row.value!T;
2720 return defval;
2723 public T chiroGetOption(T) (const(char)[] name, T defval=T.init)
2724 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2726 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2727 assert(name.length != 0);
2728 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2729 return row.value!T;
2731 return defval;
2734 public void chiroGetOption (ref DynStr s, const(char)[] name, const(char)[] defval=null) {
2735 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2736 assert(name.length != 0);
2737 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2738 s = row.value!SQ3Text;
2739 return;
2741 s = defval;
2745 private uint parseUInt (ref SQ3Text s) {
2746 s = s.xstrip;
2747 if (s.length == 0 || !isdigit(s[0])) return uint.max;
2748 uint res = 0;
2749 while (s.length) {
2750 immutable int dg = s[0].digitInBase(10);
2751 if (dg < 0) break;
2752 immutable uint nr = res*10U+cast(uint)dg;
2753 if (nr < res) return uint.max;
2754 res = nr;
2755 s = s[1..$];
2757 if (s.length && s[0] == ',') s = s[1..$];
2758 s = s.xstrip;
2759 return res;
2763 public void chiroGetOptionUInts (ref uint v0, ref uint v1, const(char)[] name) {
2764 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2765 assert(name.length != 0);
2766 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2767 auto s = row.value!SQ3Text;
2768 immutable uint rv0 = parseUInt(s);
2769 immutable uint rv1 = parseUInt(s);
2770 if (rv0 != uint.max && rv1 != uint.max && s.length == 0) {
2771 v0 = rv0;
2772 v1 = rv1;
2774 return;
2779 // ////////////////////////////////////////////////////////////////////////// //
2780 // append tag if necessary, return tagid
2781 // tag name must be valid: not empty, and not end with a '/'
2782 // returns 0 on invalid tag name
2783 public uint chiroAppendTag (const(char)[] tagname, int hidden=0) {
2784 tagname = tagname.xstrip;
2785 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2786 tagname = tagname.xstrip;
2787 if (tagname.length == 0) return 0;
2788 if (tagname.indexOf('|') >= 0) return 0;
2790 static auto stAppendTag = LazyStatement!"View"(`
2791 INSERT INTO tagnames(tag, hidden, threading) VALUES(:tagname,:hidden,:threading)
2792 ON CONFLICT(tag)
2793 DO UPDATE SET hidden=hidden -- this is for "returning"
2794 RETURNING tagid AS tagid
2795 ;`);
2797 // alphanum tags must start with '/'
2798 DynStr tn;
2799 if (tagname[0].isalnum && tagname.indexOf(':') < 0) {
2800 tn = "/";
2801 tn ~= tagname;
2802 stAppendTag.st.bindConstText(":tagname", tn);
2803 } else {
2804 stAppendTag.st.bindConstText(":tagname", tagname);
2806 stAppendTag.st
2807 .bind(":hidden", hidden)
2808 .bind(":threading", (hidden ? 0 : 1));
2809 foreach (auto row; stAppendTag.st.range) return row.tagid!uint;
2811 return 0;
2815 // ////////////////////////////////////////////////////////////////////////// //
2816 /// returns `true` if we need to update pane
2817 /// if message is left without any tags, it will be tagged with "#hobo"
2818 public bool chiroMessageRemoveTag (uint uid, const(char)[] tagname) {
2819 if (uid == 0) return false;
2820 tagname = tagname.xstrip;
2821 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2822 tagname = tagname.xstrip;
2823 if (tagname.length == 0) return false;
2824 if (tagname.indexOf('|') >= 0) return false;
2826 immutable tagid = chiroGetTagUid(tagname);
2827 if (tagid == 0) return false;
2829 static auto stUpdateStorageTags = LazyStatement!"Store"(`
2830 UPDATE messages SET tags=:tags WHERE uid=:uid
2831 ;`);
2833 static auto stUidHasTag = LazyStatement!"View"(`
2834 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2835 ;`);
2837 static auto stInsertIntoThreads = LazyStatement!"View"(`
2838 INSERT INTO threads(uid, tagid,appearance,time)
2839 VALUES(:uid, :tagid, :appr, (SELECT time FROM info WHERE uid=:uid LIMIT 1))
2840 ;`);
2842 // delete message from threads
2843 static auto stClearThreads = LazyStatement!"View"(`
2844 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2845 ;`);
2847 static auto stGetMsgTags = LazyStatement!"View"(`
2848 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
2849 FROM threads
2850 INNER JOIN tagnames AS tt USING(tagid)
2851 WHERE uid=:uid
2852 ;`);
2855 immutable bool updatePane = (chiroGetTreePaneTableTagId() == tagid);
2856 bool wasChanges = false;
2858 transacted!"View"{
2859 // get tagid (possibly appending the tag)
2860 bool hasit = false;
2861 foreach (auto row; stUidHasTag.st.bind(":uid", uid).bind(":tagid", tagid).range) hasit = true;
2862 if (!hasit) return;
2864 stClearThreads.st.bind(":uid", uid).bind(":tagid", tagid).doAll((stmt) { wasChanges = true; });
2866 // if there were any changes, rebuild message tags
2867 if (!wasChanges) return;
2869 DynStr newtags;
2870 foreach (auto trow; stGetMsgTags.st.bind(":uid", uid).range) {
2871 auto tname = trow.name!SQ3Text;
2872 if (tname.length == 0) continue;
2873 if (newtags.length) newtags ~= "|";
2874 newtags ~= tname;
2877 // if there is no tags, assign "#hobo"
2878 // this should not happen, but...
2879 if (newtags.length == 0) {
2880 newtags = "#hobo";
2881 auto hobo = chiroAppendTag(newtags, hidden:1);
2882 assert(hobo != 0);
2883 // append record for this tag to threads
2884 // note that there is no need to relink hobos, they should not be threaded
2885 //FIXME: this clears message appearance
2886 stInsertIntoThreads.st
2887 .bind(":uid", uid)
2888 .bind(":tagid", hobo)
2889 .bind(":appr", Appearance.Read)
2890 .doAll();
2893 // update storage with new tag names
2894 assert(newtags.length);
2895 stUpdateStorageTags.st.bindConstText(":tags", newtags).doAll();
2897 // and relink threads for this tagid
2898 chiroSupportRelinkTagThreads(tagid);
2901 return (wasChanges && updatePane);
2905 // ////////////////////////////////////////////////////////////////////////// //
2906 /// returns `true` if we need to update pane
2907 public bool chiroMessageAddTag (uint uid, const(char)[] tagname) {
2908 if (uid == 0) return false;
2909 tagname = tagname.xstrip;
2910 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2911 tagname = tagname.xstrip;
2912 if (tagname.length == 0) return false;
2913 if (tagname.indexOf('|') >= 0) return false;
2915 static auto stUpdateStorageTags = LazyStatement!"Store"(`
2916 UPDATE messages SET tags=tags||'|'||:tagname WHERE uid=:uid
2917 ;`);
2919 static auto stUidExists = LazyStatement!"View"(`
2920 SELECT 1 FROM threads WHERE uid=:uid LIMIT 1
2921 ;`);
2923 static auto stUidHasTag = LazyStatement!"View"(`
2924 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2925 ;`);
2927 static auto stInsertIntoThreads = LazyStatement!"View"(`
2928 INSERT INTO threads(uid, tagid, appearance, time)
2929 VALUES(:uid, :tagid, :appr, (SELECT time FROM threads WHERE uid=:uid LIMIT 1))
2930 ;`);
2932 static auto stUnHobo = LazyStatement!"View"(`
2933 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2934 ;`);
2936 bool hasuid = false;
2937 foreach (auto row; stUidExists.st.bind(":uid", uid).range) hasuid = true;
2938 if (!hasuid) return false; // nothing to do
2940 immutable paneTagId = chiroGetTreePaneTableTagId();
2941 bool updatePane = false;
2943 transacted!"View"{
2944 // get tagid (possibly appending the tag)
2945 uint tagid = chiroAppendTag(tagname);
2946 if (tagid == 0) {
2947 conwriteln("ERROR: cannot append tag name '", tagname, "'!");
2948 return;
2951 bool hasit = false;
2952 foreach (auto row; stUidHasTag.st.bind(":uid", uid).bind(":tagid", tagid).range) hasit = true;
2953 if (hasit) return;
2955 // append this tag to the message in the storage
2956 stUpdateStorageTags.st.bind(":uid", uid).bindConstText(":tagname", tagname).doAll();
2958 // append record for this tag to threads
2959 stInsertIntoThreads.st
2960 .bind(":uid", uid)
2961 .bind(":tagid", tagid)
2962 .bind(":appr", Appearance.Read)
2963 .doAll();
2965 // and relink threads for this tagid
2966 chiroSupportRelinkTagThreads(tagid);
2968 // remove this message from "#hobo", if there is any
2969 auto hobo = chiroGetTagUid("#hobo");
2970 if (hobo && hobo != tagid) {
2971 stUnHobo.st.bind(":tagid", hobo).bind(":uid", uid).doAll();
2972 // there's no need to relink hobos, because they should have no links
2975 updatePane = (tagid == paneTagId);
2978 return updatePane;
2983 inserts the one message from the message storage with the given id into view storage.
2984 parses it and such, and optionally updates threads.
2986 doesn't updates NNTP indicies and such, never relinks anything.
2988 invalid (unknown) tags will be ignored.
2990 returns number of processed messages.
2992 doesn't start/end any transactions, so wrap it yourself.
2994 public bool chiroParseAndInsertOneMessage (uint uid, uint msgtime, int appearance,
2995 const(char)[] hdrs, const(char)[] body, const(char)[] tags)
2997 auto stInsThreads = dbView.statement(`
2998 INSERT INTO threads
2999 ( uid, tagid, time, appearance)
3000 VALUES(:uid,:tagid,:time,:appearance)
3001 ;`);
3003 auto stInsInfo = dbView.statement(`
3004 INSERT INTO info
3005 ( uid, from_name, from_mail, subj, to_name, to_mail)
3006 VALUES(:uid,:from_name,:from_mail,:subj,:to_name,:to_mail)
3007 ;`);
3009 auto stInsMsgId = dbView.statement(`
3010 INSERT INTO msgids
3011 ( uid, msgid, time)
3012 VALUES(:uid,:msgid,:time)
3013 ;`);
3015 auto stInsMsgRefId = dbView.statement(`
3016 INSERT INTO refids
3017 ( uid, idx, msgid)
3018 VALUES(:uid,:idx,:msgid)
3019 ;`);
3021 auto stInsContentText = dbView.statement(`
3022 INSERT INTO content_text
3023 ( uid, format, content)
3024 VALUES(:uid,:format, ChiroPack(:content))
3025 ;`);
3027 auto stInsContentHtml = dbView.statement(`
3028 INSERT INTO content_html
3029 ( uid, format, content)
3030 VALUES(:uid,:format, ChiroPack(:content))
3031 ;`);
3033 auto stInsAttach = dbView.statement(`
3034 INSERT INTO attaches
3035 ( uid, idx, mime, name, format, content)
3036 VALUES(:uid,:idx,:mime,:name,:format, ChiroPack(:content))
3037 ;`);
3039 bool noattaches = false; // do not store attaches?
3041 // create thread record for each tag (and update max nntp index)
3042 int tagCount = 0;
3043 int noAttachCount = 0;
3044 while (tags.length) {
3045 auto eep = tags.indexOf('|');
3046 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
3047 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
3048 if (tagname.length == 0) continue;
3050 //immutable uint tuid = chiroGetTagUid(tagname);
3051 immutable uint tuid = chiroAppendTag(tagname, (tagname == "#hobo" ? 1 : 0));
3052 if (tuid == 0) continue;
3054 /* nope
3055 if (nntpidx > 0 && tagname.startsWith("account:")) {
3056 auto accname = tagname[8..$];
3057 stInsNNTPIdx
3058 .bindConstText(":accname", accname)
3059 .bind(":nntpidx", nntpidx)
3060 .doAll();
3064 if (!chiroIsTagAllowAttaches(tuid)) ++noAttachCount;
3065 ++tagCount;
3067 int app = appearance;
3068 if (app == Appearance.Unread) {
3069 if (tagname.startsWith("account:") ||
3070 tagname.startsWith("#spam") ||
3071 tagname.startsWith("#hobo"))
3073 app = Appearance.Read;
3077 stInsThreads
3078 .bind(":uid", uid)
3079 .bind(":tagid", tuid)
3080 .bind(":time", msgtime)
3081 .bind(":appearance", app)
3082 .doAll();
3084 if (!tagCount) return false;
3085 noattaches = (noAttachCount && noAttachCount == tagCount);
3087 // insert msgid
3089 bool hasmsgid = false;
3090 auto msgidfield = findHeaderField(hdrs, "Message-Id");
3091 if (msgidfield.length) {
3092 auto id = msgidfield.getFieldValue;
3093 if (id.length) {
3094 hasmsgid = true;
3095 stInsMsgId
3096 .bind(":uid", uid)
3097 .bind("time", msgtime)
3098 .bindConstText(":msgid", id)
3099 .doAll();
3102 // if there is no msgid, create one
3103 if (!hasmsgid) {
3104 RIPEMD160 hash;
3105 hash.start();
3106 hash.put(cast(const(ubyte)[])hdrs);
3107 hash.put(cast(const(ubyte)[])body);
3108 ubyte[20] digest = hash.finish();
3109 char[20*2+2+16] buf;
3110 import core.stdc.stdio : snprintf;
3111 import core.stdc.string : strcat;
3112 foreach (immutable idx, ubyte b; digest[]) snprintf(buf.ptr+idx*2, 3, "%02x", b);
3113 strcat(buf.ptr, "@artificial"); // it is safe, there is enough room for it
3114 stInsMsgId
3115 .bind(":uid", uid)
3116 .bind("time", msgtime)
3117 .bindConstText(":msgid", buf[0..20*2])
3118 .doAll();
3122 // insert references
3124 uint refidx = 0;
3125 auto inreplyfld = findHeaderField(hdrs, "In-Reply-To");
3126 while (inreplyfld.length) {
3127 auto id = getNextFieldValue(inreplyfld);
3128 if (id.length) {
3129 stInsMsgRefId
3130 .bind(":uid", uid)
3131 .bind(":idx", refidx++)
3132 .bind(":msgid", id)
3133 .doAll();
3137 inreplyfld = findHeaderField(hdrs, "References");
3138 while (inreplyfld.length) {
3139 auto id = getNextFieldValue(inreplyfld);
3140 if (id.length) {
3141 stInsMsgRefId
3142 .bind(":uid", uid)
3143 .bind(":idx", refidx++)
3144 .bind(":msgid", id)
3145 .doAll();
3150 // insert base content and attaches
3152 Content[] content;
3153 parseContent(ref content, hdrs, body, noattaches);
3154 // insert text and html
3155 bool wasText = false, wasHtml = false;
3156 foreach (const ref Content cc; content) {
3157 if (cc.name.length) continue;
3158 if (noattaches && !cc.mime.startsWith("text/")) continue;
3159 if (!wasText && cc.mime == "text/plain") {
3160 wasText = true;
3161 stInsContentText
3162 .bind(":uid", uid)
3163 .bindConstText(":format", cc.format)
3164 .bindConstBlob(":content", cc.data)
3165 .doAll();
3166 } else if (!wasHtml && cc.mime == "text/html") {
3167 wasHtml = true;
3168 stInsContentHtml
3169 .bind(":uid", uid)
3170 .bindConstText(":format", cc.format)
3171 .bindConstBlob(":content", cc.data)
3172 .doAll();
3175 if (!wasText) {
3176 stInsContentText
3177 .bind(":uid", uid)
3178 .bindConstText(":format", "")
3179 .bindConstBlob(":content", "")
3180 .doAll();
3182 if (!wasHtml) {
3183 stInsContentHtml
3184 .bind(":uid", uid)
3185 .bindConstText(":format", "")
3186 .bindConstBlob(":content", "")
3187 .doAll();
3189 // insert everything
3190 uint cidx = 0;
3191 foreach (const ref Content cc; content) {
3192 if (cc.name.length == 0 && cc.mime.startsWith("text/")) continue;
3193 // for "no attaches" mode, still record the attach, but ignore its contents
3194 stInsAttach
3195 .bind(":uid", uid)
3196 .bind(":idx", cidx++)
3197 .bindConstText(":mime", cc.mime)
3198 .bindConstText(":name", cc.name)
3199 .bindConstText(":format", cc.name)
3200 .bindConstBlob(":content", (noattaches ? null : cc.data), allowNull:true)
3201 .doAll();
3205 // insert from/to/subj info
3206 // this must be done last to keep FTS5 in sync
3208 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
3209 auto from = findHeaderField(hdrs, "From");
3210 auto to = findHeaderField(hdrs, "To");
3211 stInsInfo
3212 .bind(":uid", uid)
3213 .bind(":from_name", from.extractName)
3214 .bind(":from_mail", from.extractMail)
3215 .bind(":subj", subj)
3216 .bind(":to_name", to.extractName)
3217 .bind(":to_mail", to.extractMail)
3218 .doAll();
3221 return true;
3226 inserts the messages from the message storage with the given id into view storage.
3227 parses it and such, and optionally updates threads.
3229 WARNING! DOESN'T UPDATE NNTP INDICIES! this should be done by the downloader.
3231 invalid (unknown) tags will be ignored.
3233 returns number of processed messages.
3235 public uint chiroParseAndInsertMessages (uint stmsgid,
3236 void delegate (uint count, uint total, uint nntpidx, const(char)[] tags) progresscb=null,
3237 uint emsgid=uint.max, bool relink=true, bool asread=false)
3239 if (emsgid < stmsgid) return 0; // nothing to do
3241 uint count = 0;
3242 uint total = 0;
3243 if (progresscb !is null) {
3244 // find total number of messages to process
3245 foreach (auto row; dbStore.statement(`
3246 SELECT count(uid) AS total FROM messages WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
3247 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
3249 total = row.total!uint;
3250 break;
3252 if (total == 0) return 0; // why not?
3255 transacted!"View"{
3256 uint[] uptagids;
3257 if (relink) uptagids.reserve(128);
3258 scope(exit) delete uptagids;
3260 foreach (auto mrow; dbStore.statement(`
3261 -- this should cache unpack results
3262 WITH msgunpacked(msguid, msgdata, msgtags) AS (
3263 SELECT uid AS msguid, ChiroUnpack(data) AS msgdata, tags AS msgtags
3264 FROM messages
3265 WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
3266 ORDER BY uid
3268 SELECT
3269 msguid AS uid
3270 , msgtags AS tags
3271 , ChiroExtractHeaders(msgdata) AS headers
3272 , ChiroExtractBody(msgdata) AS body
3273 , ChiroHdr_NNTPIndex(msgdata) AS nntpidx
3274 , ChiroHdr_RecvTime(msgdata) AS msgtime
3275 FROM msgunpacked
3276 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
3278 ++count;
3279 auto hdrs = mrow.headers!SQ3Text;
3280 auto body = mrow.body!SQ3Text;
3281 auto tags = mrow.tags!SQ3Text;
3282 uint uid = mrow.uid!uint;
3283 uint nntpidx = mrow.nntpidx!uint;
3284 uint msgtime = mrow.msgtime!uint;
3285 assert(tags.length);
3287 chiroParseAndInsertOneMessage(uid, msgtime, (asread ? 1 : 0), hdrs, body, tags);
3289 if (progresscb !is null) progresscb(count, total, nntpidx, tags);
3291 if (relink) {
3292 while (tags.length) {
3293 auto eep = tags.indexOf('|');
3294 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
3295 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
3296 if (tagname.length == 0) continue;
3298 immutable uint tuid = chiroGetTagUid(tagname);
3299 if (tuid == 0) continue;
3301 bool found = false;
3302 foreach (immutable n; uptagids) if (n == tuid) { found = true; break; }
3303 if (!found) uptagids ~= tuid;
3308 if (relink && uptagids.length) {
3309 foreach (immutable tagid; uptagids) chiroSupportRelinkTagThreads(tagid);
3313 return count;
3318 returns accouint uid (accid) or 0.
3320 public uint chiroGetAccountUid (const(char)[] accname) {
3321 static auto stat = LazyStatement!"Conf"(`SELECT accid AS accid FROM accounts WHERE name=:accname LIMIT 1;`);
3322 foreach (auto row; stat.st.bindConstText(":accname", accname).range) return row.accid!uint;
3323 return 0;
3328 returns accouint name, or empty string.
3330 public DynStr chiroGetAccountName (uint accid) {
3331 static auto stat = LazyStatement!"Conf"(`SELECT name AS name FROM accounts WHERE accid=:accid LIMIT 1;`);
3332 DynStr res;
3333 if (accid == 0) return res;
3334 foreach (auto row; stat.st.bind(":accid", accid).range) {
3335 res = row.name!SQ3Text;
3336 break;
3338 return res;
3343 returns list of known tags, sorted by name.
3345 public string[] chiroGetTagList () {
3346 static auto stat = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE hidden=0 ORDER BY tag;`);
3347 string[] res;
3348 foreach (auto row; stat.st.range) res ~= row.tagname!string;
3349 return res;
3354 returns tag uid (tagid) or 0.
3356 public uint chiroGetTagUid (const(char)[] tagname) {
3357 static auto stat = LazyStatement!"View"(`SELECT tagid AS tagid FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3358 foreach (auto row; stat.st.bindConstText(":tagname", tagname).range) {
3359 return row.tagid!uint;
3361 return 0;
3366 returns tag name or empty string.
3368 public DynStr chiroGetTagName (uint tagid) {
3369 static auto stat = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3370 DynStr s;
3371 foreach (auto row; stat.st.bind(":tagid", tagid).range) {
3372 s = row.tagname!SQ3Text;
3373 break;
3375 return s;
3380 returns `true` if the given tag supports threads.
3382 this is used only when adding new messages, to set all parents to 0.
3384 public bool chiroIsTagThreaded(T) (T tagnameid)
3385 if (is(T:const(char)[]) || is(T:uint))
3387 static if (is(T:const(char)[])) {
3388 static auto stat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3389 foreach (auto row; stat.st.bindConstText(":tagname", tagnameid).range) {
3390 return (row.threading!uint == 1);
3392 } else {
3393 static auto xstat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3394 foreach (auto row; xstat.st.bind(":tagid", tagnameid).range) {
3395 return (row.threading!uint == 1);
3398 return false;
3403 returns `true` if the given tag allows attaches.
3405 this is used only when adding new messages, to set all parents to 0.
3407 public bool chiroIsTagAllowAttaches(T) (T tagnameid)
3408 if (is(T:const(char)[]) || is(T:uint))
3410 static if (is(T:const(char)[])) {
3411 static auto stat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3412 foreach (auto row; stat.st.bindConstText(":tagname", tagnameid).range) {
3413 return (row.threading!uint == 1);
3415 } else {
3416 static auto xstat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3417 foreach (auto row; xstat.st.bind(":tagid", tagnameid).range) {
3418 return (row.threading!uint == 1);
3421 return false;
3426 relinks all messages in all threads suitable for relinking, and
3427 sets parents to zero otherwise.
3429 public void chiroSupportRelinkAllThreads () {
3430 // yeah, that's it: a single SQL statement
3431 dbView.execute(`
3432 -- clear parents where threading is disabled
3433 SELECT ChiroTimerStart('clearing parents');
3434 UPDATE threads
3436 parent = 0
3437 WHERE
3438 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=0)
3439 AND parent <> 0
3441 SELECT ChiroTimerStop();
3443 SELECT ChiroTimerStart('relinking threads');
3444 UPDATE threads
3446 parent=ifnull(
3448 SELECT uid FROM msgids
3449 WHERE
3450 -- find MSGID for any of our current references
3451 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3452 -- check if UID for that MSGID has the valid tag
3453 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=threads.tagid)
3454 ORDER BY time DESC
3455 LIMIT 1
3457 , 0)
3458 WHERE
3459 -- do not process messages with non-threading tags
3460 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=1)
3462 SELECT ChiroTimerStop();
3468 relinks all messages for the given tag, or sets parents to zero if
3469 threading for that tag is disabled.
3471 public void chiroSupportRelinkTagThreads(T) (T tagnameid)
3472 if (is(T:const(char)[]) || is(T:uint))
3474 static if (is(T:const(char)[])) {
3475 immutable uint tid = chiroGetTagUid(tagnameid);
3476 if (!tid) return;
3477 } else {
3478 alias tid = tagnameid;
3481 static auto statNoTrd = LazyStatement!"View"(`
3482 UPDATE threads
3484 parent = 0
3485 WHERE
3486 tagid = :tagid AND parent <> 0
3487 ;`);
3489 static auto statTrd = LazyStatement!"View"(`
3490 UPDATE threads
3492 parent=ifnull(
3494 SELECT uid FROM msgids
3495 WHERE
3496 -- find MSGID for any of our current references
3497 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3498 -- check if UID for that MSGID has the valid tag
3499 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=:tagid)
3500 ORDER BY time DESC
3501 LIMIT 1
3503 , 0)
3504 WHERE
3505 threads.tagid = :tagid
3506 ;`);
3508 if (!chiroIsTagThreaded(tid)) {
3509 // clear parents (just in case)
3510 statNoTrd.st.bind(":tagid", tid).doAll();
3511 } else {
3512 // yeah, that's it: a single SQL statement
3513 statTrd.st.bind(":tagid", tid).doAll();
3519 * get "from info" for the given message.
3521 * returns `false` if there is no such message.
3523 public bool chiroGetMessageFrom (uint uid, ref DynStr fromMail, ref DynStr fromName) {
3524 static auto statGetFrom = LazyStatement!"View"(`
3525 SELECT
3526 from_name AS fromName
3527 , from_mail AS fromMail
3528 FROM info
3529 WHERE uid=:uid
3530 LIMIT 1
3531 ;`);
3532 fromMail.clear();
3533 fromName.clear();
3534 foreach (auto row; statGetFrom.st.bind(":uid", uid).range) {
3535 fromMail = row.fromMail!SQ3Text;
3536 fromName = row.fromName!SQ3Text;
3537 return true;
3539 return false;
3544 gets twit title and state for the given (tagid, uid) message.
3546 returns -666 if there is no such message.
3548 public DynStr chiroGetMessageTwit(T) (T tagidname, uint uid, out bool twited)
3549 if (is(T:const(char)[]) || is(T:uint))
3551 twited = false;
3552 DynStr res;
3553 if (!uid) return res;
3555 static if (is(T:const(char)[])) {
3556 immutable uint tid = chiroGetTagUid(tagidname);
3557 if (!tid) return 0;
3558 enum selHdr = ``;
3559 } else {
3560 alias tid = tagidname;
3563 if (!tid) return res;
3565 static auto statGetTwit = LazyStatement!"View"(`
3566 SELECT title AS title
3567 FROM threads
3568 WHERE uid=:uid AND tagid=:tagid AND mute>0
3569 LIMIT 1
3570 ;`);
3572 statGetTwit.st
3573 .bind(":uid", uid)
3574 .bind(":tagid", tid);
3575 foreach (auto row; statGetTwit.st.range) {
3576 twited = true;
3577 res = row.title!SQ3Text;
3580 return res;
3585 gets mute state for the given (tagid, uid) message.
3587 returns -666 if there is no such message.
3589 public int chiroGetMessageMute(T) (T tagidname, uint uid)
3590 if (is(T:const(char)[]) || is(T:uint))
3592 if (!uid) return -666;
3594 static if (is(T:const(char)[])) {
3595 immutable uint tid = chiroGetTagUid(tagidname);
3596 if (!tid) return 0;
3597 enum selHdr = ``;
3598 } else {
3599 alias tid = tagidname;
3602 if (!tid) return -666;
3604 static auto statGetApp = LazyStatement!"View"(`
3605 SELECT mute AS mute
3606 FROM threads
3607 WHERE uid=:uid AND tagid=:tagid
3608 LIMIT 1
3609 ;`);
3611 statGetApp.st
3612 .bind(":uid", uid)
3613 .bind(":tagid", tid);
3614 foreach (auto row; statGetApp.st.range) return row.mute!int;
3615 return -666;
3620 sets mute state the given (tagid, uid) message.
3622 doesn't change children states.
3624 public void chiroSetMessageMute(T) (T tagidname, uint uid, Mute mute)
3625 if (is(T:const(char)[]) || is(T:uint))
3627 if (!uid) return;
3629 static if (is(T:const(char)[])) {
3630 immutable uint tid = chiroGetTagUid(tagidname);
3631 if (!tid) return 0;
3632 enum selHdr = ``;
3633 } else {
3634 alias tid = tagidname;
3637 if (!tid) return;
3639 static auto statSetApp = LazyStatement!"View"(`
3640 UPDATE threads
3642 mute=:mute
3643 WHERE
3644 uid=:uid AND tagid=:tagid
3645 ;`);
3647 static auto statSetAppRead = LazyStatement!"View"(`
3648 UPDATE threads
3650 mute=:mute
3651 , appearance=iif(appearance=0,1,appearance)
3652 WHERE
3653 uid=:uid AND tagid=:tagid
3654 ;`);
3656 if (mute > Mute.Normal) {
3657 statSetAppRead.st
3658 .bind(":mute", cast(int)mute)
3659 .bind(":uid", uid)
3660 .bind(":tagid", tid)
3661 .doAll();
3662 } else {
3663 statSetApp.st
3664 .bind(":mute", cast(int)mute)
3665 .bind(":uid", uid)
3666 .bind(":tagid", tid)
3667 .doAll();
3673 gets appearance for the given (tagid, uid) message.
3675 returns -666 if there is no such message.
3677 public int chiroGetMessageAppearance(T) (T tagidname, uint uid)
3678 if (is(T:const(char)[]) || is(T:uint))
3680 if (!uid) return -666;
3682 static if (is(T:const(char)[])) {
3683 immutable uint tid = chiroGetTagUid(tagidname);
3684 if (!tid) return 0;
3685 enum selHdr = ``;
3686 } else {
3687 alias tid = tagidname;
3690 if (!tid) return -666;
3692 static auto statGetApp = LazyStatement!"View"(`
3693 SELECT appearance AS appearance
3694 FROM threads
3695 WHERE uid=:uid AND tagid=:tagid
3696 LIMIT 1
3697 ;`);
3699 statGetApp.st
3700 .bind(":uid", uid)
3701 .bind(":tagid", tid);
3702 foreach (auto row; statGetApp.st.range) return row.appearance!int;
3703 return -666;
3708 gets appearance for the given (tagid, uid) message.
3710 public bool chiroGetMessageUnread(T) (T tagidname, uint uid)
3711 if (is(T:const(char)[]) || is(T:uint))
3713 return (chiroGetMessageAppearance(tagidname, uid) == Appearance.Unread);
3718 gets appearance for the given (tagid, uid) message.
3720 public bool chiroGetMessageExactRead(T) (T tagidname, uint uid)
3721 if (is(T:const(char)[]) || is(T:uint))
3723 return (chiroGetMessageAppearance(tagidname, uid) == Appearance.Read);
3728 sets appearance for the given (tagid, uid) message.
3730 public void chiroSetMessageAppearance(T) (T tagidname, uint uid, Appearance appearance)
3731 if (is(T:const(char)[]) || is(T:uint))
3733 if (!uid) return;
3735 static if (is(T:const(char)[])) {
3736 immutable uint tid = chiroGetTagUid(tagidname);
3737 if (!tid) return 0;
3738 enum selHdr = ``;
3739 } else {
3740 alias tid = tagidname;
3743 if (!tid) return;
3745 static auto statSetApp = LazyStatement!"View"(`
3746 UPDATE threads
3748 appearance=:appearance
3749 WHERE
3750 uid=:uid AND tagid=:tagid
3751 ;`);
3753 statSetApp.st
3754 .bind(":appearance", cast(int)appearance)
3755 .bind(":uid", uid)
3756 .bind(":tagid", tid)
3757 .doAll();
3762 mark (tagid, uid) message as read.
3764 public void chiroSetReadOrUnreadMessageAppearance(T) (T tagidname, uint uid, Appearance appearance)
3765 if (is(T:const(char)[]) || is(T:uint))
3767 if (!uid) return;
3769 static if (is(T:const(char)[])) {
3770 immutable uint tid = chiroGetTagUid(tagidname);
3771 if (!tid) return 0;
3772 enum selHdr = ``;
3773 } else {
3774 alias tid = tagidname;
3777 if (!tid) return;
3779 static auto statSetApp = LazyStatement!"View"(`
3780 UPDATE threads
3782 appearance=:setapp
3783 WHERE
3784 uid=:uid AND tagid=:tagid AND (appearance=:checkapp0 OR appearance=:checkapp1)
3785 ;`);
3787 statSetApp.st
3788 .bind(":uid", uid)
3789 .bind(":tagid", tid)
3790 .bind(":setapp", cast(int)appearance)
3791 .bind(":checkapp0", Appearance.Read)
3792 .bind(":checkapp1", Appearance.Unread)
3793 .doAll();
3798 mark (tagid, uid) message as read.
3800 public void chiroSetMessageRead(T) (T tagidname, uint uid)
3801 if (is(T:const(char)[]) || is(T:uint))
3803 chiroSetReadOrUnreadMessageAppearance(tagidname, uid, Appearance.Read);
3807 public void chiroSetMessageUnread(T) (T tagidname, uint uid)
3808 if (is(T:const(char)[]) || is(T:uint))
3810 chiroSetReadOrUnreadMessageAppearance(tagidname, uid, Appearance.Unread);
3815 purge all messages with the given tag.
3817 this removes messages from all view tables, removes content from
3818 the "messages" table, and sets "messages" table tags to NULL.
3820 public void chiroDeletePurgedWithTag(T) (T tagidname)
3821 if (is(T:const(char)[]) || is(T:uint))
3823 static if (is(T:const(char)[])) {
3824 immutable uint tid = chiroGetTagUid(tagidname);
3825 if (!tid) return 0;
3826 enum selHdr = ``;
3827 } else {
3828 alias tid = tagidname;
3831 if (!tid) return;
3833 static auto statCountPurged = LazyStatement!"View"(`
3834 SELECT COUNT(uid) AS pcount FROM threads
3835 WHERE tagid=:tagid AND appearance=:appr
3838 uint purgedCount = 0;
3839 foreach (auto row; statCountPurged.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).range) {
3840 purgedCount = row.pcount!uint;
3842 if (!purgedCount) return;
3844 // we will need this to clear storage
3845 uint[] plist;
3846 scope(exit) delete plist;
3847 plist.reserve(purgedCount);
3849 static auto statListPurged = LazyStatement!"View"(`
3850 SELECT uid AS uid FROM threads
3851 WHERE tagid=:tagid AND appearance=:appr
3852 ORDER BY uid
3855 foreach (auto row; statListPurged.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).range) {
3856 plist ~= row.uid!uint;
3858 if (plist.length == 0) return; // just in case
3860 static auto statClearStorage = LazyStatement!"Store"(`
3861 UPDATE messages
3862 SET tags=NULL, data=NULL
3863 WHERE uid=:uid
3864 ;`);
3866 enum BulkClearSQL(string table) = `
3867 DELETE FROM `~table~`
3868 WHERE
3869 uid IN (SELECT uid FROM threads WHERE tagid=:tagid AND appearance=:appr)
3872 // bulk clearing of info
3873 static auto statClearInfo = LazyStatement!"View"(BulkClearSQL!"info");
3874 // bulk clearing of msgids
3875 static auto statClearMsgids = LazyStatement!"View"(BulkClearSQL!"msgids");
3876 // bulk clearing of refids
3877 static auto statClearRefids = LazyStatement!"View"(BulkClearSQL!"refids");
3878 // bulk clearing of text
3879 static auto statClearText = LazyStatement!"View"(BulkClearSQL!"content_text");
3880 // bulk clearing of html
3881 static auto statClearHtml = LazyStatement!"View"(BulkClearSQL!"content_html");
3882 // bulk clearing of attaches
3883 static auto statClearAttach = LazyStatement!"View"(BulkClearSQL!"attaches");
3884 // bulk clearing of threads
3885 static auto statClearThreads = LazyStatement!"View"(`
3886 DELETE FROM threads
3887 WHERE tagid=:tagid AND appearance=:appr
3888 ;`);
3890 static if (is(T:const(char)[])) {
3891 conwriteln("removing ", plist.length, " message", (plist.length != 1 ? "s" : ""), " from '", tagidname, "'...");
3892 } else {
3893 DynStr tname = chiroGetTagName(tid);
3894 conwriteln("removing ", plist.length, " message", (plist.length != 1 ? "s" : ""), " from '", tname.getData, "'...");
3897 // WARNING! "info" must be cleared FIRST, and "threads" LAST
3898 transacted!"View"{
3899 statClearInfo.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3900 statClearMsgids.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3901 statClearRefids.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3902 statClearText.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3903 statClearHtml.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3904 statClearAttach.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3905 statClearThreads.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3906 // relink tag threads
3907 chiroSupportRelinkTagThreads(tid);
3910 // now clear the storage
3911 conwriteln("clearing the storage...");
3912 transacted!"Store"{
3913 foreach (immutable uint uid; plist) {
3914 statClearStorage.st.bind(":uid", uid).doAll();
3918 conwriteln("done purging.");
3923 creates "treepane" table for the given tag. that table can be used to
3924 render threaded listview.
3926 returns max id of the existing item. can be used for pagination.
3927 item ids are guaranteed to be sequential, and without any holes.
3928 the first id is `1`.
3930 returned table has "rowid", and two integer fields: "uid" (message uid), and
3931 "level" (message depth, starting from 0).
3933 public uint chiroCreateTreePaneTable(T) (T tagidname, int lastmonthes=12, bool allowThreading=true)
3934 if (is(T:const(char)[]) || is(T:uint))
3936 auto ctm = Timer(true);
3938 // shrink temp table to the bare minimum, because each field costs several msecs
3939 // we don't need parent and time here, because we can easily select them with inner joins
3941 dbView.execute(`
3942 DROP TABLE IF EXISTS treepane;
3943 CREATE TEMP TABLE IF NOT EXISTS treepane (
3944 iid INTEGER PRIMARY KEY
3945 , uid INTEGER
3946 , level INTEGER
3947 -- to make joins easier
3948 , tagid INTEGER
3953 // this need to add answers to some ancient crap
3954 static auto statFirstUnreadTime = LazyStatement!"View"(`
3955 SELECT MIN(time) AS time, parent AS parent
3956 FROM threads
3957 WHERE tagid=:tagidname AND appearance=:app
3958 ;`);
3960 static auto statFindParentFor = LazyStatement!"View"(`
3961 SELECT time AS time, parent AS parent
3962 FROM threads
3963 WHERE tagid=:tagidname AND uid=:uid
3964 LIMIT 1
3965 ;`);
3967 // clear it (should be faster than dropping and recreating)
3968 dbView.execute(`DELETE FROM treepane;`);
3970 // this "%08X" will do up to 2038; i'm fine with it
3971 static auto statTrd = LazyStatement!"View"(`
3972 INSERT INTO treepane
3973 (uid, level, tagid)
3974 WITH tree(uid, parent, level, time, path) AS (
3975 WITH RECURSIVE fulltree(uid, parent, level, time, path) AS (
3976 SELECT t.uid AS uid, t.parent AS parent, 1 AS level, t.time AS time, printf('%08X', t.time) AS path
3977 FROM threads t
3978 WHERE t.time>=:starttime AND parent=0 AND t.tagid=:tagidname AND t.appearance <> -1
3979 UNION ALL
3980 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
3981 FROM threads t, fulltree ft
3982 WHERE t.time>=:starttime AND t.parent=ft.uid AND t.tagid=:tagidname AND t.appearance <> -1
3984 SELECT * FROM fulltree
3986 SELECT
3987 tree.uid AS uid
3988 , tree.level-1 AS level
3989 , :tagidname AS tagid
3990 FROM tree
3991 ORDER BY path
3992 ;`);
3994 static auto statNoTrd = LazyStatement!"View"(`
3995 INSERT INTO treepane
3996 (uid, level, tagid)
3997 SELECT
3998 threads.uid AS uid
3999 , 0 AS level
4000 , :tagidname AS tagid
4001 FROM threads
4002 WHERE
4003 threads.time>=:starttime AND threads.tagid=:tagidname AND threads.appearance <> -1
4004 ORDER BY
4005 threads.time
4006 ;`);
4008 static if (is(T:const(char)[])) {
4009 immutable uint tid = chiroGetTagUid(tagidname);
4010 if (!tid) return 0;
4011 enum selHdr = ``;
4012 } else {
4013 alias tid = tagidname;
4016 uint startTime = 0;
4018 if (lastmonthes > 0) {
4019 if (lastmonthes > 12*100) {
4020 startTime = 0;
4021 } else {
4022 // show last `lastmonthes` (full monthes)
4023 import std.datetime;
4024 import core.time : Duration;
4026 SysTime now = Clock.currTime().toUTC();
4027 int year = now.year;
4028 int month = now.month; // from 1
4029 --lastmonthes;
4030 // yes, i am THAT lazy
4031 while (lastmonthes > 0) {
4032 if (month > lastmonthes) { month -= lastmonthes; break; }
4033 lastmonthes -= month;
4034 month = 12;
4035 --year;
4037 // construct unix time
4038 now.fracSecs = Duration.zero;
4039 now.second = 0;
4040 now.hour = 0;
4041 now.minute = 0;
4042 now.day = 1;
4043 now.month = cast(Month)month;
4044 now.year = year;
4045 startTime = cast(uint)now.toUnixTime();
4049 // check if we need to fix unread time
4050 // required to show the whole ancient thread if somebody answered
4051 if (startTime > 0) {
4052 uint unTime = 0;
4053 uint unParent = 0;
4054 foreach (auto row; statFirstUnreadTime.st.bind(":tagidname", tid).bind(":app", Appearance.Unread).range) {
4055 unTime = row.time!uint;
4056 unParent = row.parent!uint;
4058 if (unTime > 0 && unTime < startTime) {
4059 // find root message, and start from it
4060 startTime = unTime;
4061 while (unParent && allowThreading) {
4062 statFindParentFor.st
4063 .bind(":tagidname", tid)
4064 .bind(":uid", unParent);
4065 unParent = 0;
4066 unTime = 0;
4067 foreach (auto row; statFindParentFor.st.range) {
4068 unTime = row.time!uint;
4069 unParent = row.parent!uint;
4071 if (unTime > 0 && unTime < startTime) startTime = unTime;
4076 if (allowThreading) {
4077 statTrd.st.bind(":tagidname", tid).bind(":starttime", startTime).doAll();
4078 } else {
4079 statNoTrd.st.bind(":tagidname", tid).bind(":starttime", startTime).doAll();
4081 ctm.stop;
4082 if (ChiroTimerEnabled) writeln("creating treepane time: ", ctm);
4084 immutable uint res = cast(uint)dbView.lastRowId;
4086 version(chidb_drop_pane_table) {
4087 dbView.execute(`CREATE INDEX treepane_uid ON treepane(uid);`);
4090 return res;
4095 returns current treepane tagid.
4097 public uint chiroGetTreePaneTableTagId () {
4098 static auto stat = LazyStatement!"View"(`SELECT tagid AS tagid FROM treepane WHERE iid=1 LIMIT 1;`);
4099 foreach (auto row; stat.st.range) return row.tagid!uint;
4100 return 0;
4105 returns current treepane max uid.
4107 public uint chiroGetTreePaneTableMaxUId () {
4108 static auto stat = LazyStatement!"View"(`SELECT MAX(uid) AS uid FROM treepane LIMIT 1;`);
4109 foreach (auto row; stat.st.range) return row.uid!uint;
4110 return 0;
4115 returns number of items in the current treepane.
4117 public uint chiroGetTreePaneTableCount () {
4118 static auto stat = LazyStatement!"View"(`SELECT COUNT(*) AS total FROM treepane;`);
4119 foreach (auto row; stat.st.range) return row.total!uint;
4120 return 0;
4125 returns index of the given uid in the treepane.
4127 public bool chiroIsTreePaneTableUidValid (uint uid) {
4128 static auto stat = LazyStatement!"View"(`SELECT iid AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
4129 if (uid == 0) return false;
4130 foreach (auto row; stat.st.bind(":uid", uid).range) return true;
4131 return false;
4136 returns first treepane uid.
4138 public uint chiroGetTreePaneTableFirstUid () {
4139 static auto stmt = LazyStatement!"View"(`SELECT uid AS uid FROM treepane WHERE iid=1 LIMIT 1;`);
4140 foreach (auto row; stmt.st.range) return row.uid!uint;
4141 return 0;
4146 returns last treepane uid.
4148 public uint chiroGetTreePaneTableLastUid () {
4149 static auto stmt = LazyStatement!"View"(`SELECT MAX(iid), uid AS uid FROM treepane LIMIT 1;`);
4150 foreach (auto row; stmt.st.range) return row.uid!uint;
4151 return 0;
4156 returns index of the given uid in the treepane.
4158 public int chiroGetTreePaneTableUid2Index (uint uid) {
4159 static auto stmt = LazyStatement!"View"(`SELECT iid-1 AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
4160 if (uid == 0) return -1;
4161 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.idx!int;
4162 return -1;
4167 returns uid of the given index in the treepane.
4169 public uint chiroGetTreePaneTableIndex2Uid (int index) {
4170 static auto stmt = LazyStatement!"View"(`SELECT uid AS uid FROM treepane WHERE iid=:idx+1 LIMIT 1;`);
4171 if (index < 0 || index == int.max) return 0;
4172 foreach (auto row; stmt.st.bind(":idx", index).range) return row.uid!uint;
4173 return 0;
4178 returns previous uid in the treepane.
4180 public uint chiroGetTreePaneTablePrevUid (uint uid) {
4181 static auto stmt = LazyStatement!"View"(`
4182 SELECT uid AS uid FROM treepane
4183 WHERE iid IN (SELECT iid-1 FROM treepane WHERE uid=:uid LIMIT 1)
4184 LIMIT 1
4185 ;`);
4186 if (uid == 0) return chiroGetTreePaneTableFirstUid();
4187 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.uid!uint;
4188 return 0;
4193 returns uid of the given index in the treepane.
4195 public uint chiroGetTreePaneTableNextUid (uint uid) {
4196 static auto stmt = LazyStatement!"View"(`
4197 SELECT uid AS uid FROM treepane
4198 WHERE iid IN (SELECT iid+1 FROM treepane WHERE uid=:uid LIMIT 1)
4199 LIMIT 1
4200 ;`);
4201 if (uid == 0) return chiroGetTreePaneTableFirstUid();
4202 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.uid!uint;
4203 return 0;
4208 releases (drops) "treepane" table.
4210 can be called several times, but usually you don't need to call this at all.
4212 public void chiroClearTreePaneTable () {
4213 //dbView.execute(`DROP TABLE IF EXISTS treepane;`);
4214 dbView.execute(`DELETE FROM treepane;`);
4219 return next unread message uid in treepane, or 0.
4221 public uint chiroGetPaneNextUnread (uint curruid) {
4222 static auto stmtNext = LazyStatement!"View"(`
4223 SELECT treepane.uid AS uid FROM treepane
4224 INNER JOIN threads USING(uid, tagid)
4225 WHERE treepane.iid-1 > :cidx AND threads.appearance=:appr
4226 ORDER BY iid
4227 LIMIT 1
4228 ;`);
4229 immutable int cidx = chiroGetTreePaneTableUid2Index(curruid);
4230 foreach (auto row; stmtNext.st.bind(":cidx", cidx).bind(":appr", Appearance.Unread).range) return row.uid!uint;
4231 if (curruid) {
4232 // try from the beginning
4233 foreach (auto row; stmtNext.st.bind(":cidx", -1).bind(":appr", Appearance.Unread).range) return row.uid!uint;
4235 return 0;
4240 selects given number of items starting with the given item id.
4242 returns numer of selected items.
4244 `stiid` counts from zero
4246 WARNING! "treepane" table must be prepared with `chiroCreateTreePaneTable()`!
4248 WARNING! [i]dup `SQ3Text` arguments if necessary, they won't survive the `cb` return!
4250 public int chiroGetPaneTablePage (int stiid, int limit,
4251 void delegate (int pgofs, /* offset from the page start, from zero and up to `limit` */
4252 int iid, /* item id, counts from zero*/
4253 uint uid, /* msguid, never zero */
4254 uint parentuid, /* parent msguid, may be zero */
4255 uint level, /* threading level, from zero */
4256 Appearance appearance, /* see above */
4257 Mute mute, /* see above */
4258 SQ3Text date, /* string representation of receiving date and time */
4259 SQ3Text subj, /* message subject, can be empty string */
4260 SQ3Text fromName, /* message sender name, can be empty string */
4261 SQ3Text fromMail, /* message sender email, can be empty string */
4262 SQ3Text title) cb /* title from twiting */
4264 static auto stat = LazyStatement!"View"(`
4265 SELECT
4266 treepane.iid AS iid
4267 , treepane.uid AS uid
4268 , treepane.level AS level
4269 , threads.parent AS parent
4270 , threads.appearance AS appearance
4271 , threads.mute AS mute
4272 , datetime(threads.time, 'unixepoch') AS time
4273 , info.subj AS subj
4274 , info.from_name AS from_name
4275 , info.from_mail AS from_mail
4276 , threads.title AS title
4277 FROM treepane
4278 INNER JOIN info USING(uid)
4279 INNER JOIN threads USING(uid, tagid)
4280 WHERE treepane.iid >= :stiid
4281 ORDER BY treepane.iid
4282 LIMIT :limit
4285 if (limit <= 0) return 0;
4286 if (stiid < 0) {
4287 if (stiid == int.min) return 0;
4288 limit += stiid;
4289 if (limit <= 0) return 0;
4290 stiid = 0;
4292 int total = 0;
4293 foreach (auto row; stat.st.bind(":stiid", stiid+1).bind(":limit", limit).range)
4295 if (cb !is null) {
4296 cb(total, row.iid!int, row.uid!uint, row.parent!uint, row.level!uint,
4297 cast(Appearance)row.appearance!int, cast(Mute)row.mute!int,
4298 row.time!SQ3Text, row.subj!SQ3Text, row.from_name!SQ3Text, row.from_mail!SQ3Text, row.title!SQ3Text);
4300 ++total;
4302 return total;
4306 // ////////////////////////////////////////////////////////////////////////// //
4307 /** returns full content of the messare or `null` if no message found (or it was deleted).
4309 public DynStr chiroGetFullMessageContent (uint uid) {
4310 DynStr res;
4311 if (uid == 0) return res;
4312 foreach (auto row; dbStore.statement(`SELECT ChiroUnpack(data) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid).range) {
4313 res = row.result!SQ3Text;
4314 return res;
4316 return res;
4320 /** returns full content of the messare or `null` if no message found (or it was deleted).
4322 public DynStr chiroMessageHeaders (uint uid) {
4323 DynStr res;
4324 if (uid == 0) return res;
4325 foreach (auto row; dbStore.statement(`SELECT ChiroExtractHeaders(ChiroUnpack(data)) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid).range) {
4326 res = row.result!SQ3Text;
4327 return res;
4329 return res;
4333 /** returns full content of the messare or `null` if no message found (or it was deleted).
4335 public DynStr chiroMessageBody (uint uid) {
4336 DynStr res;
4337 if (uid == 0) return res;
4338 foreach (auto row; dbStore.statement(`SELECT ChiroExtractBody(ChiroUnpack(data)) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid).range) {
4339 res = row.result!SQ3Text;
4340 return res;
4342 return res;
4346 // ////////////////////////////////////////////////////////////////////////// //
4347 public enum Bogo {
4348 Error, // some error occured
4349 Ham,
4350 Unsure,
4351 Spam,
4354 public Bogo messageBogoCheck (uint uid) {
4355 if (uid == 0) return Bogo.Error;
4356 DynStr content = chiroGetFullMessageContent(uid);
4357 if (content.length == 0) return Bogo.Error;
4359 try {
4360 import std.process;
4361 //{ auto fo = VFile("/tmp/zzzz", "w"); fo.rawWriteExact(art.data); }
4362 auto pipes = pipeProcess(["/usr/bin/bogofilter", "-T"]);
4363 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4364 //pipes.stdin.writeln();
4365 //foreach (string s; art.text) pipes.stdin.writeln(s);
4366 pipes.stdin.writeln(content.getData.xstripright);
4367 pipes.stdin.flush();
4368 pipes.stdin.close();
4369 auto res = pipes.stdout.readln();
4370 wait(pipes.pid);
4371 //conwriteln("RESULT: [", res, "]");
4372 if (res.length == 0) {
4373 //conwriteln("ERROR: bogofilter returned nothing");
4374 return Bogo.Error;
4376 if (res[0] == 'H') return Bogo.Ham;
4377 if (res[0] == 'U') return Bogo.Unsure;
4378 if (res[0] == 'S') return Bogo.Spam;
4379 //while (res.length && res[$-1] <= ' ') res = res[0..$-1];
4380 //conwriteln("ERROR: bogofilter returned some shit: [", res, "]");
4381 } catch (Exception e) { // sorry
4382 //conwriteln("ERROR bogofiltering: ", e.msg);
4385 return Bogo.Error;
4389 // ////////////////////////////////////////////////////////////////////////// //
4390 private void messageBogoMarkSpamHam(bool spam) (uint uid) {
4391 if (uid == 0) return;
4392 DynStr content = chiroGetFullMessageContent(uid);
4393 if (content.length == 0) return;
4395 static if (spam) enum arg = "-s"; else enum arg = "-n";
4396 try {
4397 import std.process;
4398 auto pipes = pipeProcess(["/usr/bin/bogofilter", arg]);
4399 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4400 //pipes.stdin.writeln();
4401 //foreach (string s; art.text) pipes.stdin.writeln(s);
4402 pipes.stdin.writeln(content.getData.xstripright);
4403 pipes.stdin.flush();
4404 pipes.stdin.close();
4405 wait(pipes.pid);
4406 } catch (Exception e) { // sorry
4407 //conwriteln("ERROR bogofiltering: ", e.msg);
4412 public void messageBogoMarkHam (uint uid) { messageBogoMarkSpamHam!false(uid); }
4413 public void messageBogoMarkSpam (uint uid) { messageBogoMarkSpamHam!true(uid); }
4416 // ////////////////////////////////////////////////////////////////////////// //
4417 public alias TwitProcessCallback = void delegate (const(char)[] msg, uint curr, uint total);
4419 void processEmailTwits (TwitProcessCallback cb) {
4420 enum Message = "processing email twits";
4422 auto stFindTwitNameEmail = LazyStatement!"View"(`
4423 SELECT
4424 threads.uid AS uid
4425 , threads.tagid AS tagid
4426 FROM threads
4427 INNER JOIN info AS ii ON
4428 ii.uid=threads.uid AND
4429 ii.from_mail=:email AND
4430 ii.from_name=:name
4431 WHERE mute=0
4432 ;`);
4434 auto stFindTwitEmail = LazyStatement!"View"(`
4435 SELECT
4436 threads.uid AS uid
4437 , threads.tagid AS tagid
4438 FROM threads
4439 INNER JOIN info AS ii ON
4440 ii.uid=threads.uid AND
4441 ii.from_mail=:email
4442 WHERE mute=0
4443 ;`);
4445 auto stFindTwitName = LazyStatement!"View"(`
4446 SELECT
4447 threads.uid AS uid
4448 , threads.tagid AS tagid
4449 FROM threads
4450 INNER JOIN info AS ii ON
4451 ii.uid=threads.uid AND
4452 ii.from_name=:name
4453 WHERE mute=0
4454 ;`);
4457 auto stFindTwitNameEmailMasked = LazyStatement!"View"(`
4458 SELECT
4459 threads.uid AS uid
4460 , threads.tagid AS tagid
4461 FROM threads
4462 INNER JOIN info AS ii ON
4463 ii.uid=threads.uid AND
4464 ii.from_name=:name AND
4465 ii.from_mail GLOB :email
4466 WHERE mute=0
4467 ;`);
4469 auto stFindTwitEmailMasked = LazyStatement!"View"(`
4470 SELECT
4471 threads.uid AS uid
4472 , threads.tagid AS tagid
4473 FROM threads
4474 INNER JOIN info AS ii ON
4475 ii.uid=threads.uid AND
4476 ii.from_mail GLOB :email
4477 WHERE mute=0
4478 ;`);
4481 auto stUpdateMute = LazyStatement!"View"(`
4482 UPDATE threads
4483 SET mute=:mute, title=:title
4484 WHERE uid=:uid AND tagid=:tagid AND mute=0
4485 ;`);
4487 static struct UidTag {
4488 uint uid;
4489 uint tagid;
4492 uint twitcount = 0;
4493 foreach (auto trow; dbConf.statement(`SELECT COUNT(*) AS twitcount FROM emailtwits;`).range) twitcount = trow.twitcount!uint;
4495 if (cb !is null) cb(Message, 0, twitcount);
4497 dbView.execute(`
4498 CREATE TEMP TABLE IF NOT EXISTS disemails(
4499 email TEXT NOT NULL UNIQUE
4502 scope(exit) {
4503 if (cb !is null) cb("dropping temp tables", twitcount, twitcount);
4504 dbView.execute(`DROP TABLE IF EXISTS disemails;`);
4507 transacted!"View"{
4508 uint twitdone = 0;
4509 foreach (auto trow; dbConf.statement(`
4510 SELECT
4511 tagglob AS tagglob
4512 , email AS email
4513 , name AS name
4514 , title AS title
4515 FROM emailtwits
4516 WHERE email NOT LIKE '%*%'
4517 ;`).range)
4519 ++twitdone;
4520 auto title = trow.title!SQ3Text;
4521 if (title.length == 0) continue;
4522 auto email = trow.email!SQ3Text;
4523 auto name = trow.name!SQ3Text;
4524 assert(email.indexOf('*') < 0);
4525 DBStatement st;
4526 if (email.length && name.length) {
4527 st = stFindTwitNameEmail.st;
4528 st.bindConstText(":email", email).bindConstText(":name", name);
4529 } else if (email.length) {
4530 st = stFindTwitEmail.st;
4531 st.bindConstText(":email", email);
4532 } else if (name.length) {
4533 st = stFindTwitName.st;
4534 st.bindConstText(":name", name);
4535 } else {
4536 continue;
4538 UidTag[] msguids;
4539 msguids.reserve(128);
4540 scope(exit) delete msguids;
4541 //writeln("::: ", email, " : ", name);
4542 foreach (auto mrow; st.range) {
4543 auto tname = chiroGetTagName(mrow.tagid!uint);
4544 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4545 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4546 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4548 if (msguids.length == 0) continue;
4549 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4550 immutable bool muteAllow = title.startsWith("!"); // allow this
4551 //transacted!"View"{
4552 foreach (immutable pair; msguids) {
4553 stUpdateMute.st
4554 .bind(":uid", pair.uid)
4555 .bind(":tagid", pair.tagid)
4556 .bind(":mute", (muteAllow ? Mute.Never : Mute.ThreadStart))
4557 .bindConstText(":title", title)
4558 .doAll();
4560 //};
4561 if (cb !is null) cb(Message, twitdone, twitcount);
4564 if (cb !is null) cb("selecting distinct emails", twitdone, twitcount);
4565 dbView.execute(`
4566 DELETE FROM disemails;
4567 INSERT INTO disemails
4568 SELECT DISTINCT(from_mail) FROM info;
4570 if (cb !is null) cb(Message, twitdone, twitcount);
4572 version(all) {
4573 foreach (auto trow; dbConf.statement(`
4574 SELECT
4575 tagglob AS tagglob
4576 , email AS email
4577 , name AS name
4578 , title AS title
4579 FROM emailtwits
4580 WHERE email LIKE '%*%'
4581 ;`).range)
4583 ++twitdone;
4584 auto title = trow.title!SQ3Text;
4585 if (title.length == 0) continue;
4586 auto email = trow.email!SQ3Text;
4587 auto name = trow.name!SQ3Text;
4588 assert(email.indexOf('*') >= 0);
4589 assert(email.length);
4591 foreach (auto drow; dbView.statement(`SELECT email AS demail FROM disemails WHERE email GLOB :email;`)
4592 .bindConstText(":email", email).range)
4594 DBStatement st;
4595 if (name.length) {
4596 st = stFindTwitNameEmail.st;
4597 st.bindConstText(":email", drow.demail!SQ3Text).bindConstText(":name", name);
4598 } else {
4599 st = stFindTwitEmail.st;
4600 st.bindConstText(":email", drow.demail!SQ3Text);
4602 UidTag[] msguids;
4603 msguids.reserve(128);
4604 scope(exit) delete msguids;
4605 //writeln("::: ", email, " : ", name);
4606 foreach (auto mrow; st.range) {
4607 auto tname = chiroGetTagName(mrow.tagid!uint);
4608 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4609 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4610 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4612 if (msguids.length == 0) continue;
4613 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4614 immutable bool muteAllow = title.startsWith("!"); // allow this
4615 //transacted!"View"{
4616 foreach (immutable pair; msguids) {
4617 stUpdateMute.st
4618 .bind(":uid", pair.uid)
4619 .bind(":tagid", pair.tagid)
4620 .bind(":mute", (muteAllow ? Mute.Never : Mute.ThreadStart))
4621 .bindConstText(":title", title)
4622 .doAll();
4624 //};
4627 if (cb !is null) cb(Message, twitdone, twitcount);
4629 } else {
4630 foreach (auto trow; dbConf.statement(`
4631 SELECT
4632 tagglob AS tagglob
4633 , email AS email
4634 , name AS name
4635 , title AS title
4636 FROM emailtwits
4637 WHERE email LIKE '%*%'
4638 ;`).range)
4640 ++twitdone;
4641 auto title = trow.title!SQ3Text;
4642 if (title.length == 0) continue;
4643 auto email = trow.email!SQ3Text;
4644 auto name = trow.name!SQ3Text;
4645 assert(email.indexOf('*') >= 0);
4646 assert(email.length);
4647 DBStatement st;
4648 if (email.length && name.length) {
4649 st = stFindTwitNameEmailMasked.st;
4650 st.bindConstText(":email", email).bindConstText(":name", name);
4651 } else {
4652 st = stFindTwitEmailMasked.st;
4653 st.bindConstText(":email", email);
4655 UidTag[] msguids;
4656 msguids.reserve(128);
4657 scope(exit) delete msguids;
4658 //writeln("::: ", email, " : ", name);
4659 foreach (auto mrow; st.range) {
4660 auto tname = chiroGetTagName(mrow.tagid!uint);
4661 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4662 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4663 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4665 if (msguids.length == 0) continue;
4666 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4667 immutable bool muteAllow = title.startsWith("!"); // allow this
4668 //transacted!"View"{
4669 foreach (immutable pair; msguids) {
4670 stUpdateMute.st
4671 .bind(":uid", pair.uid)
4672 .bind(":tagid", pair.tagid)
4673 .bind(":mute", (muteAllow ? Mute.Never : Mute.ThreadStart))
4674 .bindConstText(":title", title)
4675 .doAll();
4677 //};
4678 if (cb !is null) cb(Message, twitdone, twitcount);
4683 //if (cb !is null) cb(Message, twitcount, twitcount);
4687 void processMsgidTwits (TwitProcessCallback cb) {
4688 enum Message = "processing msgid twits";
4690 auto stUpdateMute = LazyStatement!"View"(`
4691 UPDATE threads
4692 SET mute=:mute, title=NULL
4693 WHERE uid=:uid AND tagid=:tagid AND mute=0
4694 ;`);
4696 static struct UidTag {
4697 uint uid;
4698 uint tagid;
4701 uint twitcount = 0;
4702 foreach (auto trow; dbConf.statement(`SELECT COUNT(*) AS twitcount FROM msgidtwits;`).range) twitcount = trow.twitcount!uint;
4704 if (cb !is null) cb(Message, 0, twitcount);
4706 transacted!"View"{
4707 uint twitdone = 0;
4708 foreach (auto trow; dbConf.statement(`SELECT msgid AS msgid, tagglob AS tagglob FROM msgidtwits;`).range) {
4709 ++twitdone;
4710 UidTag[] msguids;
4711 msguids.reserve(128);
4712 scope(exit) delete msguids;
4714 foreach (auto mrow; dbView.statement(`
4715 SELECT threads.uid AS uid, threads.tagid AS tagid
4716 FROM threads
4717 INNER JOIN msgids AS mm
4718 ON mm.msgid=:msgid AND mm.uid=threads.uid
4719 WHERE mute=0
4720 ;`).bindConstText(":msgid", trow.msgid!SQ3Text).range)
4722 auto tname = chiroGetTagName(mrow.tagid!uint);
4723 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4724 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4725 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4727 if (msguids.length == 0) continue;
4728 //conwriteln("updating ", msguids.length, " messages for msgid <", trow.msgid!SQ3Text, ">");
4729 //transacted!"View"{
4730 foreach (immutable pair; msguids) {
4731 stUpdateMute.st
4732 .bind(":uid", pair.uid)
4733 .bind(":tagid", pair.tagid)
4734 .bind(":mute", Mute.ThreadStart)
4735 .doAll();
4737 //};
4738 if (cb !is null) cb(Message, twitdone, twitcount);
4742 if (cb !is null) cb(Message, twitcount, twitcount);
4746 void processThreadMutes (TwitProcessCallback cb) {
4747 enum Message = "processing thread mutes";
4749 if (cb !is null) cb(Message, 0, 0);
4751 dbConf.execute(`
4752 ATTACH DATABASE '`~MailDBPath~`chiview.db' AS chiview;
4754 BEGIN TRANSACTION;
4756 --------------------------------------------------------------------------------
4757 -- create temp table with mute pairs
4758 SELECT ChiroTimerStart('creating mute pairs');
4759 CREATE TEMP TABLE mutepairs AS
4760 WITH RECURSIVE children(muid, paruid, mtagid) AS (
4761 SELECT 0, chiview.threads.uid, chiview.threads.tagid
4762 FROM chiview.threads
4763 WHERE chiview.threads.parent=0 AND chiview.threads.mute=2
4764 AND EXISTS (SELECT uid FROM chiview.threads AS tx WHERE tx.tagid=chiview.threads.tagid AND tx.parent=chiview.threads.uid)
4765 UNION ALL
4766 SELECT
4767 tt.uid, tt.uid, mtagid
4768 FROM children AS cc
4769 INNER JOIN chiview.threads AS tt
4771 tt.tagid=cc.mtagid AND
4772 tt.parent=cc.paruid AND
4773 tt.uid<>cc.muid AND
4774 tt.uid<>cc.paruid
4776 SELECT
4777 muid AS muid
4778 , mtagid AS mtagid
4779 FROM children
4780 WHERE muid<>0
4782 SELECT ChiroTimerStop();
4785 SELECT 'nested mute pairs to skip:', COUNT(uid)
4786 FROM chiview.threads
4787 INNER JOIN mutepairs AS tt
4789 tagid=tt.mtagid AND
4790 uid=tt.muid
4791 WHERE mute<>0
4795 SELECT ChiroTimerStart('updating thread mutes');
4796 UPDATE chiview.threads
4798 mute=3 -- child
4799 , appearance=(SELECT CASE WHEN appearance=0 THEN 1 ELSE appearance END)
4800 FROM (SELECT muid, mtagid FROM mutepairs) AS cc
4801 WHERE uid=cc.muid AND tagid=cc.mtagid AND mute=0
4803 SELECT ChiroTimerStop();
4805 DROP TABLE mutepairs;
4808 --SELECT 'secondary mutes:', COUNT(mute) FROM threads WHERE mute=3;
4811 COMMIT TRANSACTION;
4813 DETACH DATABASE chiview;
4818 public void chiroRecalcAllTwits (TwitProcessCallback cb) {
4819 // clear all twits
4820 conwriteln("clearing all mutes...");
4821 if (cb !is null) cb("clearing mutes", 0, 0);
4822 dbView.execute(`
4823 UPDATE threads
4824 SET mute=0, title=NULL
4825 ;`);
4826 conwriteln("processing email twits...");
4827 processEmailTwits(cb);
4828 conwriteln("processing msgid twits...");
4829 processMsgidTwits(cb);
4830 conwriteln("propagating thread twits...");
4831 processThreadMutes(cb);
4832 conwriteln("twit recalculation complete.");