it is now possible to compose new messages and replies, and send them
[chiroptera.git] / chibackend / sqbase.d
blobfd5206226712ad0e154d4f3881903acaf7d762ed
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 iv.encoding;
70 private import iv.cmdcon;
71 private import iv.ripemd160;
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 a content
526 -- (this is because we will receive them back from NNTP server later)
527 -- succesfully sent messages will be marked with non-zero sendtime
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 */
534 , sendtime INTEGER DEFAULT 0 /* 0: not yet; unixtime */
535 , lastsendtime INTEGER DEFAULT 0 /* when we last tried to send it? 0 means "not yet" */
539 static immutable string schemaSupportTempTables = `
540 --DROP TABLE IF EXISTS treepane;
541 CREATE TEMP TABLE IF NOT EXISTS treepane (
542 iid INTEGER PRIMARY KEY
543 , uid INTEGER
544 , level INTEGER
545 -- to make joins easier
546 , tagid INTEGER
549 CREATE INDEX IF NOT EXISTS treepane_uid ON treepane(uid);
552 enum schemaSupportIndex = `
553 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_tag_uid ON threads(tagid, uid);
554 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_uid_tag ON threads(uid, tagid);
556 -- this is for views where threading is disabled
557 CREATE INDEX IF NOT EXISTS trd_by_tag_time ON threads(tagid, time);
558 --CREATE INDEX IF NOT EXISTS trd_by_tag_time_parent ON threads(tagid, time, parent);
559 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent_time ON threads(tagid, parent, time);
560 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent ON threads(tagid, parent);
561 CREATE INDEX IF NOT EXISTS trd_by_parent_tag ON threads(parent, tagid);
563 -- this is for test if we have any unread articles (we don't mind the exact numbers, tho)
564 CREATE INDEX IF NOT EXISTS trd_by_appearance ON threads(appearance);
565 -- this is for removing purged messages
566 CREATE INDEX IF NOT EXISTS trd_by_tag_appearance ON threads(tagid, appearance);
567 -- was used in table view creation, not used anymore
568 --CREATE INDEX IF NOT EXISTS trd_by_parent_tag_appearance ON threads(parent, tagid, appearance);
570 -- for theadmsgview
571 -- was used in table view creation, not used anymore
572 --CREATE INDEX IF NOT EXISTS trd_by_tag_appearance_time ON threads(tagid, appearance, time);
574 CREATE INDEX IF NOT EXISTS msgid_by_msgid_time ON msgids(msgid, time DESC);
576 CREATE INDEX IF NOT EXISTS refid_by_refids_idx ON refids(msgid, idx);
577 CREATE INDEX IF NOT EXISTS refid_by_uid_idx ON refids(uid, idx);
579 CREATE INDEX IF NOT EXISTS content_text_by_uid ON content_text(uid);
580 CREATE INDEX IF NOT EXISTS content_html_by_uid ON content_html(uid);
582 CREATE INDEX IF NOT EXISTS attaches_by_uid_name ON attaches(uid, name);
583 CREATE INDEX IF NOT EXISTS attaches_by_uid_idx ON attaches(uid, idx);
585 -- "info" indicies for twits
586 CREATE INDEX IF NOT EXISTS info_by_from_mail_name ON info(from_mail, from_name);
587 --CREATE INDEX IF NOT EXISTS info_by_from_mail ON info(from_mail);
588 CREATE INDEX IF NOT EXISTS info_by_from_name ON info(from_name);
591 CREATE INDEX IF NOT EXISTS unsent_by_accid ON unsent(accid);
594 static immutable string schemaSupport = schemaSupportTable~schemaSupportIndex;
597 version(fts5_use_porter) {
598 enum FTS5_Tokenizer = "porter unicode61 remove_diacritics 2";
599 } else {
600 enum FTS5_Tokenizer = "unicode61 remove_diacritics 2";
603 static immutable string recreateFTS5 = `
604 DROP TABLE IF EXISTS fts5_messages;
605 CREATE VIRTUAL TABLE fts5_messages USING fts5(
606 sender /* sender name and email, separated by " \x1a " (dec 26) (substitute char) */
607 , subj /* email subject */
608 , text /* email body, text/plain */
609 , html /* email body, text/html */
610 --, uid UNINDEXED /* message uid this comes from (not needed, use "rowid" instead */
611 , tokenize = '`~FTS5_Tokenizer~`'
612 , content = 'fts5_msgview'
613 , content_rowid = 'uid'
615 /* sender, subj, text, html */
616 INSERT INTO fts5_messages(fts5_messages, rank) VALUES('rank', 'bm25(1.0, 3.0, 10.0, 6.0)');
619 static immutable string repopulateFTS5 = `
620 SELECT ChiroTimerStart('updating FTS5');
621 BEGIN TRANSACTION;
623 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
624 SELECT uid, sender, subj, text, html
625 FROM fts5_msgview
626 WHERE
627 EXISTS (
628 SELECT threads.tagid FROM threads
629 INNER JOIN tagnames USING(tagid)
630 WHERE
631 threads.uid=fts5_msgview.uid AND
632 tagnames.hidden=0 AND SUBSTR(tagnames.tag, 1, 1)='/'
635 COMMIT TRANSACTION;
636 SELECT ChiroTimerStop();
640 static immutable string recreateFTS5Triggers = `
641 -- triggers to keep the FTS index up to date
643 -- this rely on the proper "info" table update order
644 -- info must be inserted LAST
645 DROP TRIGGER IF EXISTS fts5xtrig_insert;
646 CREATE TRIGGER fts5xtrig_insert
647 AFTER INSERT ON info
648 BEGIN
649 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
650 SELECT uid, sender, subj, text, html FROM fts5_msgview WHERE uid=NEW.uid LIMIT 1;
651 END;
653 -- not AFTER, because we still need a valid view!
654 -- this rely on the proper "info" table update order
655 -- info must be deleted FIRST
656 DROP TRIGGER IF EXISTS fts5xtrig_delete;
657 CREATE TRIGGER fts5xtrig_delete
658 BEFORE DELETE ON info
659 BEGIN
660 INSERT INTO fts5_messages(fts5_messages, rowid, sender, subj, text, html)
661 SELECT 'delete', uid, sender, subj, text, html FROM fts5_msgview WHERE uid=OLD.uid LIMIT 1;
662 END;
664 -- message texts should NEVER be updated, so no ON UPDATE trigger
668 // ////////////////////////////////////////////////////////////////////////// //
669 // not properly implemented yet
670 //version = lazy_mt_safe;
672 version(lazy_mt_safe) {
673 enum lazy_mt_safe_flag = true;
674 } else {
675 enum lazy_mt_safe_flag = false;
678 public struct LazyStatement(string dbname) {
679 public:
680 enum DB {
681 Store,
682 View,
683 Conf,
686 private:
687 static struct Data {
688 DBStatement st = void;
689 version(lazy_mt_safe) {
690 sqlite3_mutex* mutex = void;
692 char* sql = void;
693 usize sqlsize = void;
694 uint compiled = void;
697 private:
698 usize udata = 0;
699 DB dbtype;
700 string delayInit = null;
702 private:
703 inout(Data)* datap () inout pure nothrow @trusted @nogc { pragma(inline, true); return cast(Data*)udata; }
704 void datap (Data *v) pure nothrow @trusted @nogc { pragma(inline, true); udata = cast(usize)v; }
706 public:
707 //@disable this ();
708 @disable this (this);
710 this (string sql) {
711 delayInit = sql;
713 assert(sql.length);
714 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
715 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
716 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
717 else static assert(0, "invalid db name: '"~dbname~"'");
718 import core.stdc.stdlib : calloc;
719 Data* dp = cast(Data*)calloc(1, Data.sizeof);
720 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
721 datap = dp;
722 dp.sql = cast(char*)calloc(1, sql.length);
723 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
724 dp.sql[0..sql.length] = sql[];
725 dp.sqlsize = sql.length;
726 version(lazy_mt_safe) {
727 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
728 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
731 //dbtype = adb;
732 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
735 ~this () {
736 import core.stdc.stdlib : free;
737 if (!udata) return;
738 Data* dp = datap;
739 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===DEINIT===\n%s\n============\n", dp.sql); }
740 dp.st = DBStatement.init;
741 free(dp.sql);
742 version(lazy_mt_safe) {
743 sqlite3_mutex_free(dp.mutex);
745 free(dp);
746 udata = 0;
749 bool valid () pure nothrow @safe @nogc { pragma(inline, true); return (udata != 0 || delayInit.length); }
751 private void setupWith (const(char)[] sql) {
752 if (udata) throw new Exception("statement already inited");
753 assert(sql.length);
754 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
755 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
756 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
757 else static assert(0, "invalid db name: '"~dbname~"'");
758 import core.stdc.stdlib : calloc;
759 Data* dp = cast(Data*)calloc(1, Data.sizeof);
760 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
761 datap = dp;
762 dp.sql = cast(char*)calloc(1, sql.length);
763 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
764 dp.sql[0..sql.length] = sql[];
765 dp.sqlsize = sql.length;
766 version(lazy_mt_safe) {
767 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
768 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
770 //dbtype = adb;
771 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
774 ref DBStatement st () {
775 if (!udata) {
776 //throw new Exception("no statement set");
777 setupWith(delayInit);
779 Data* dp = datap;
780 if (!dp.compiled) {
781 version(lazy_mt_safe) {
782 sqlite3_mutex_enter(dp.mutex);
784 scope(exit) {
785 version(lazy_mt_safe) {
786 sqlite3_mutex_leave(dp.mutex);
789 //{ import core.stdc.stdio : printf; printf("***compiling:\n%s\n=====\n", dp.sql); }
790 final switch (dbtype) {
791 case DB.Store: dp.st = dbStore.persistentStatement(dp.sql[0..dp.sqlsize]); break;
792 case DB.View: dp.st = dbView.persistentStatement(dp.sql[0..dp.sqlsize]); break;
793 case DB.Conf: dp.st = dbConf.persistentStatement(dp.sql[0..dp.sqlsize]); break;
795 dp.compiled = 1;
796 //assert(dp.st.valid);
798 //assert(dp.st.valid);
799 return dp.st;
804 // ////////////////////////////////////////////////////////////////////////// //
805 private bool isGoodText (const(void)[] buf) pure nothrow @safe @nogc {
806 foreach (immutable ubyte ch; cast(const(ubyte)[])buf) {
807 if (ch < 32) {
808 if (ch != 9 && ch != 10 && ch != 13 && ch != 27) return false;
809 } else {
810 if (ch == 127) return false;
813 return true;
814 //return utf8ValidText(buf);
818 // ////////////////////////////////////////////////////////////////////////// //
819 private bool isBadPrefix (const(char)[] buf) pure nothrow @trusted @nogc {
820 if (buf.length < 5) return false;
821 return
822 buf.ptr[0] == '\x1b' &&
823 buf.ptr[1] >= 'A' && buf.ptr[1] <= 'Z' &&
824 buf.ptr[2] >= 'A' && buf.ptr[2] <= 'Z' &&
825 buf.ptr[3] >= 'A' && buf.ptr[3] <= 'Z' &&
826 buf.ptr[4] >= 'A' && buf.ptr[4] <= 'Z';
830 /* two high bits of the first byte holds the size:
831 00: fit into 6 bits: [0.. 0x3f] (1 byte)
832 01: fit into 14 bits: [0.. 0x3fff] (2 bytes)
833 10: fit into 22 bits: [0.. 0x3f_ffff] (3 bytes)
834 11: fit into 30 bits: [0..0x3fff_ffff] (4 bytes)
836 number is stored as big-endian.
837 will not write anything to `dest` if there is not enough room.
839 returns number of bytes, or 0 if the number is too big.
841 private uint encodeUInt (void[] dest, uint v) nothrow @trusted @nogc {
842 if (v > 0x3fff_ffffU) return 0;
843 ubyte[] d = cast(ubyte[])dest;
844 // 4 bytes?
845 if (v > 0x3f_ffffU) {
846 v |= 0xc000_0000U;
847 if (d.length >= 4) {
848 d.ptr[0] = cast(ubyte)(v>>24);
849 d.ptr[1] = cast(ubyte)(v>>16);
850 d.ptr[2] = cast(ubyte)(v>>8);
851 d.ptr[3] = cast(ubyte)v;
853 return 4;
855 // 3 bytes?
856 if (v > 0x3fffU) {
857 v |= 0x80_0000U;
858 if (d.length >= 3) {
859 d.ptr[0] = cast(ubyte)(v>>16);
860 d.ptr[1] = cast(ubyte)(v>>8);
861 d.ptr[2] = cast(ubyte)v;
863 return 3;
865 // 2 bytes?
866 if (v > 0x3fU) {
867 v |= 0x4000U;
868 if (d.length >= 2) {
869 d.ptr[0] = cast(ubyte)(v>>8);
870 d.ptr[1] = cast(ubyte)v;
872 return 2;
874 // 1 byte
875 if (d.length >= 1) d.ptr[0] = cast(ubyte)v;
876 return 1;
880 private uint decodeUIntLength (const(void)[] dest) pure nothrow @trusted @nogc {
881 const(ubyte)[] d = cast(const(ubyte)[])dest;
882 if (d.length == 0) return 0;
883 switch (d.ptr[0]&0xc0) {
884 case 0x00: return 1;
885 case 0x40: return (d.length >= 2 ? 2 : 0);
886 case 0x80: return (d.length >= 3 ? 3 : 0);
887 default:
889 return (d.length >= 4 ? 4 : 0);
893 // returns uint.max on error (impossible value)
894 private uint decodeUInt (const(void)[] dest) pure nothrow @trusted @nogc {
895 const(ubyte)[] d = cast(const(ubyte)[])dest;
896 if (d.length == 0) return uint.max;
897 uint res = void;
898 switch (d.ptr[0]&0xc0) {
899 case 0x00:
900 res = d.ptr[0];
901 break;
902 case 0x40:
903 if (d.length < 2) return uint.max;
904 res = ((d.ptr[0]&0x3fU)<<8)|d.ptr[1];
905 break;
906 case 0x80:
907 if (d.length < 3) return uint.max;
908 res = ((d.ptr[0]&0x3fU)<<16)|(d.ptr[1]<<8)|d.ptr[2];
909 break;
910 default:
911 if (d.length < 4) return uint.max;
912 res = ((d.ptr[0]&0x3fU)<<24)|(d.ptr[1]<<16)|(d.ptr[2]<<8)|d.ptr[3];
913 break;
915 return res;
919 // returns position AFTER the headers (empty line is skipped too)
920 // returned value is safe for slicing
921 private int sq3Supp_FindHeadersEnd (const(char)* vs, const int sz) {
922 import core.stdc.string : memchr;
923 if (sz <= 0) return 0;
924 const(char)* eptr = cast(const(char)*)memchr(vs, '\n', cast(uint)sz);
925 while (eptr !is null) {
926 ++eptr;
927 int epos = cast(int)cast(usize)(eptr-vs);
928 if (sz-epos < 1) break;
929 if (*eptr == '\r') {
930 if (sz-epos < 2) break;
931 ++epos;
932 ++eptr;
934 if (*eptr == '\n') return epos+1;
935 assert(epos < sz);
936 eptr = cast(const(char)*)memchr(eptr, '\n', cast(uint)(sz-epos));
938 return sz;
942 // hack for some invalid dates
943 uint parseMailDate (const(char)[] s) nothrow {
944 import std.datetime;
945 if (s.length == 0) return 0;
946 try {
947 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
948 } catch (Exception) {}
949 // sometimes this helps
950 usize dcount = 0;
951 foreach_reverse (immutable char ch; s) {
952 if (ch < '0' || ch > '9') break;
953 ++dcount;
955 if (dcount > 4) return 0;
956 s ~= "0000"[0..4-dcount];
957 try {
958 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
959 } catch (Exception) {}
960 return 0;
964 // ////////////////////////////////////////////////////////////////////////// //
965 extern(C) {
968 ** ChiroPackLZMA(content)
969 ** ChiroPackLZMA(content, packflag)
971 ** second form accepts int flag; 0 means "don't pack"
973 private void sq3Fn_ChiroPackLZMA (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
974 if (argc < 1 || argc > 2) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPackLZMA()`", -1); return; }
975 int packlevel = (argc > 1 ? sqlite3_value_int(argv[1]) : ChiroDefaultPackLevel);
976 if (packlevel < 0) packlevel = 5/*lzma default*/; else if (packlevel > 9) packlevel = 9;
978 sqlite3_value *val = argv[0];
980 immutable int sz = sqlite3_value_bytes(val);
981 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
983 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
985 const(char)* vs = cast(const(char) *)sqlite3_value_blob(val);
986 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroPackLZMA()`", -1); return; }
988 if (sz >= 0x3fffffff-8) {
989 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_toobig(ctx); return; }
990 sqlite3_result_value(ctx, val);
991 return;
994 import core.stdc.stdlib : malloc, free;
995 import core.stdc.string : memcpy;
997 if (packlevel > 0 && sz > 8) {
998 import core.stdc.stdio : snprintf;
999 char[16] xsz = void;
1000 xsz[0..5] = "\x1bLZMA";
1001 uint xszlen = encodeUInt(xsz[5..$], cast(uint)sz);
1002 if (xszlen) {
1003 xszlen += 5;
1004 immutable uint bsz = cast(uint)sz;
1005 char* cbuf = cast(char*)malloc(bsz+xszlen+LZMA_PROPS_SIZE+1+16);
1006 if (cbuf is null) {
1007 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_nomem(ctx); return; }
1008 } else {
1009 cbuf[0..xszlen] = xsz[0..xszlen];
1010 usize destLen = bsz;
1011 ubyte[LZMA_PROPS_SIZE+8] hdr = void;
1012 uint hdrSize = cast(uint)hdr.sizeof;
1014 CLzmaEncProps props;
1015 props.level = packlevel;
1016 props.dictSize = 1<<22; //4MB
1017 props.reduceSize = bsz;
1019 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);
1020 assert(hdrSize == LZMA_PROPS_SIZE);
1021 if (res == SZ_OK && destLen+xszlen+LZMA_PROPS_SIZE+1 < cast(usize)sz) {
1022 import core.stdc.string : memcpy;
1023 cbuf[xszlen] = LZMA_PROPS_SIZE;
1024 memcpy(cbuf+xszlen+1, hdr.ptr, LZMA_PROPS_SIZE);
1025 sqlite3_result_blob(ctx, cbuf, destLen+xszlen+LZMA_PROPS_SIZE+1, &free);
1026 return;
1028 free(cbuf);
1033 if (isBadPrefix(vs[0..cast(uint)sz])) {
1034 char *res = cast(char *)malloc(sz+5);
1035 if (res is null) { sqlite3_result_error_nomem(ctx); return; }
1036 res[0..5] = "\x1bRAWB";
1037 res[5..sz+5] = vs[0..sz];
1038 if (isGoodText(vs[0..cast(usize)sz])) {
1039 sqlite3_result_text(ctx, res, sz+5, &free);
1040 } else {
1041 sqlite3_result_blob(ctx, res, sz+5, &free);
1043 } else {
1044 immutable bool wantBlob = !isGoodText(vs[0..cast(usize)sz]);
1045 immutable int tp = sqlite3_value_type(val);
1046 if ((wantBlob && tp == SQLITE_BLOB) || (!wantBlob && tp == SQLITE3_TEXT)) {
1047 sqlite3_result_value(ctx, val);
1048 } else if (wantBlob) {
1049 sqlite3_result_blob(ctx, vs, sz, SQLITE_TRANSIENT);
1050 } else {
1051 sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1058 ** ChiroPack(content)
1059 ** ChiroPack(content, packflag)
1061 ** second form accepts int flag; 0 means "don't pack"
1063 private void sq3Fn_ChiroPackCommon (sqlite3_context *ctx, sqlite3_value *val, int packlevel) nothrow @trusted {
1064 immutable int sz = sqlite3_value_bytes(val);
1065 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
1067 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1069 const(char)* vs = cast(const(char) *)sqlite3_value_blob(val);
1070 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroPack()`", -1); return; }
1072 if (sz >= 0x3fffffff-8) {
1073 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_toobig(ctx); return; }
1074 sqlite3_result_value(ctx, val);
1075 return;
1078 import core.stdc.stdlib : malloc, free;
1079 import core.stdc.string : memcpy;
1081 if (packlevel > 0 && sz > 8) {
1082 import core.stdc.stdio : snprintf;
1083 char[16] xsz = void;
1084 version(use_balz) {
1085 xsz[0..5] = "\x1bBALZ";
1086 } else version(use_libxpack) {
1087 xsz[0..5] = "\x1bXPAK";
1088 } else version(use_libbrieflz) {
1089 xsz[0..5] = "\x1bBRLZ";
1090 } else version(use_liblzfse) {
1091 xsz[0..5] = "\x1bLZFS";
1092 } else version(use_lzjb) {
1093 xsz[0..5] = "\x1bLZJB";
1094 } else version(use_libwim_lzms) {
1095 xsz[0..5] = "\x1bLZMS";
1096 } else version(use_libwim_lzx) {
1097 xsz[0..5] = "\x1bLZMX";
1098 } else version(use_libwim_xpress) {
1099 xsz[0..5] = "\x1bXPRS";
1100 } else version(use_lz4) {
1101 xsz[0..5] = "\x1bLZ4D";
1102 } else version(use_zstd) {
1103 xsz[0..5] = "\x1bZSTD";
1104 } else {
1105 xsz[0..5] = "\x1bZLIB";
1107 immutable uint xszlenNum = encodeUInt(xsz[5..$], cast(uint)sz);
1108 if (xszlenNum) {
1109 immutable uint xszlen = xszlenNum+5;
1110 //xsz[xszlen++] = ':';
1111 version(use_libbrieflz) {
1112 immutable usize bsz = blz_max_packed_size(cast(usize)sz);
1113 } else version(use_lzjb) {
1114 immutable uint bsz = cast(uint)sz+1024;
1115 } else version(use_lz4) {
1116 immutable uint bsz = cast(uint)LZ4_compressBound(sz)+1024;
1117 } else {
1118 immutable uint bsz = cast(uint)sz;
1120 char* cbuf = cast(char*)malloc(bsz+xszlen+64);
1121 if (cbuf is null) {
1122 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_nomem(ctx); return; }
1123 } else {
1124 cbuf[0..xszlen] = xsz[0..xszlen];
1125 version(use_balz) {
1126 Balz bz;
1127 usize spos = 0;
1128 usize dpos = xszlen;
1129 try {
1130 bz.compress(
1131 // reader
1132 (buf) {
1133 if (spos >= cast(usize)sz) return 0;
1134 usize left = cast(usize)sz-spos;
1135 if (left > buf.length) left = buf.length;
1136 if (left) memcpy(buf.ptr, vs+spos, left);
1137 spos += left;
1138 return left;
1140 // writer
1141 (buf) {
1142 if (dpos+buf.length >= cast(usize)sz) throw new Exception("uncompressible");
1143 memcpy(cbuf+dpos, buf.ptr, buf.length);
1144 dpos += buf.length;
1146 // maximum compression?
1147 true
1149 } catch(Exception) {
1150 dpos = usize.max;
1152 if (dpos < cast(usize)sz) {
1153 sqlite3_result_blob(ctx, cbuf, dpos, &free);
1154 return;
1156 } else version(use_libdeflate) {
1157 if (packlevel > 12) packlevel = 12;
1158 libdeflate_compressor *cpr = libdeflate_alloc_compressor(packlevel);
1159 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1160 usize dsize = libdeflate_zlib_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
1161 libdeflate_free_compressor(cpr);
1162 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1163 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1164 return;
1166 } else version(use_libxpack) {
1167 // 2^19 (524288) bytes. This is definitely a big problem and I am planning to address it.
1168 // https://github.com/ebiggers/xpack/issues/1
1169 if (sz < 524288-64) {
1170 if (packlevel > 9) packlevel = 9;
1171 xpack_compressor *cpr = xpack_alloc_compressor(cast(usize)sz, packlevel);
1172 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1173 usize dsize = xpack_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
1174 xpack_free_compressor(cpr);
1175 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1176 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1177 return;
1180 } else version(use_libbrieflz) {
1181 if (packlevel > 10) packlevel = 10;
1182 immutable usize wbsize = blz_workmem_size_level(cast(usize)sz, packlevel);
1183 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1184 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1185 uint dsize = blz_pack_level(vs, cbuf+xszlen, cast(uint)sz, wbuf, packlevel);
1186 free(wbuf);
1187 if (dsize+xszlen < cast(usize)sz) {
1188 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1189 return;
1191 } else version(use_liblzfse) {
1192 immutable usize wbsize = lzfse_encode_scratch_size();
1193 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1194 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1195 usize dsize = lzfse_encode_buffer(cbuf+xszlen, bsz, vs, cast(uint)sz, wbuf);
1196 free(wbuf);
1197 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1198 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1199 return;
1201 } else version(use_lzjb) {
1202 usize dsize = lzjb_compress(vs, cast(usize)sz, cbuf+xszlen, bsz);
1203 if (dsize == usize.max) dsize = 0;
1204 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1205 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1206 return;
1208 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "LZJB FAILED!\n"); }
1209 } else version(use_libwim_lzms) {
1210 wimlib_compressor* cpr;
1211 uint clevel = (packlevel < 10 ? 50 : 1000);
1212 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZMS, cast(usize)sz, clevel, &cpr);
1213 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1214 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1215 wimlib_free_compressor(cpr);
1216 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1217 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1218 return;
1220 } else version(use_libwim_lzx) {
1221 if (sz <= WIMLIB_LZX_MAX_CHUNK) {
1222 wimlib_compressor* cpr;
1223 uint clevel = (packlevel < 10 ? 50 : 1000);
1224 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZX, cast(usize)sz, clevel, &cpr);
1225 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1226 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1227 wimlib_free_compressor(cpr);
1228 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1229 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1230 return;
1233 } else version(use_libwim_xpress) {
1234 if (sz <= WIMLIB_XPRESS_MAX_CHUNK) {
1235 wimlib_compressor* cpr;
1236 uint clevel = (packlevel < 10 ? 50 : 1000);
1237 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
1238 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < cast(uint)sz) csz *= 2U;
1239 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, clevel, &cpr);
1240 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1241 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1242 wimlib_free_compressor(cpr);
1243 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1244 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1245 return;
1248 } else version(use_lz4) {
1249 int dsize = LZ4_compress_default(vs, cbuf+xszlen, sz, cast(int)bsz);
1250 if (dsize > 0 && dsize+xszlen < sz) {
1251 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1252 return;
1254 } else version(use_zstd) {
1255 immutable int clev =
1256 packlevel <= 3 ? ZSTD_minCLevel() :
1257 packlevel <= 6 ? ZSTD_defaultCLevel() :
1258 packlevel < 10 ? 19 :
1259 ZSTD_maxCLevel();
1260 usize dsize = ZSTD_compress(cbuf+xszlen, cast(int)bsz, vs, sz, clev);
1261 if (!ZSTD_isError(dsize) && dsize > 0 && dsize+xszlen < sz) {
1262 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1263 return;
1265 } else {
1266 import etc.c.zlib : /*compressBound,*/ compress2, Z_OK;
1267 //uint bsz = cast(uint)compressBound(cast(uint)sz);
1268 if (packlevel > 9) packlevel = 9;
1269 usize dsize = bsz;
1270 immutable int zres = compress2(cast(ubyte *)(cbuf+xszlen), &dsize, cast(const(ubyte) *)vs, sz, packlevel);
1271 if (zres == Z_OK && dsize+xszlen < cast(usize)sz) {
1272 if (!ChiroPackTryHard) {
1273 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1274 return;
1276 } else {
1277 free(cbuf);
1278 cbuf = null;
1280 // try LZMA?
1281 if (ChiroPackTryHard) {
1282 char* lzmabuf = cast(char*)malloc(bsz+xszlen+LZMA_PROPS_SIZE+1+64);
1283 if (lzmabuf !is null) {
1284 lzmabuf[0..xszlen] = xsz[0..xszlen];
1285 lzmabuf[1..5] = "LZMA";
1286 usize destLen = (cbuf is null ? bsz : dsize); // do not take more than zlib
1287 if (destLen > bsz) destLen = bsz; // just in case
1288 ubyte[LZMA_PROPS_SIZE+8] hdr = void;
1289 uint hdrSize = cast(uint)hdr.sizeof;
1291 CLzmaEncProps props;
1292 props.level = packlevel;
1293 props.dictSize = 1<<22; //4MB
1294 props.reduceSize = bsz;
1296 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);
1297 assert(hdrSize == LZMA_PROPS_SIZE);
1298 if (nres == SZ_OK && destLen+xszlen+LZMA_PROPS_SIZE+1 < cast(usize)sz) {
1299 if (cbuf is null || destLen+xszlen+LZMA_PROPS_SIZE+1 < dsize+xszlen) {
1300 if (cbuf !is null) free(cbuf); // free zlib result
1301 import core.stdc.string : memcpy;
1302 lzmabuf[xszlen] = LZMA_PROPS_SIZE;
1303 memcpy(lzmabuf+xszlen+1, hdr.ptr, LZMA_PROPS_SIZE);
1304 sqlite3_result_blob(ctx, lzmabuf, destLen+xszlen+LZMA_PROPS_SIZE+1, &free);
1305 return;
1308 free(lzmabuf);
1311 // return zlib result?
1312 if (cbuf !is null) {
1313 assert(dsize < bsz);
1314 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1315 return;
1318 if (cbuf !is null) free(cbuf);
1323 if (isBadPrefix(vs[0..cast(uint)sz])) {
1324 char *res = cast(char *)malloc(sz+5);
1325 if (res is null) { sqlite3_result_error_nomem(ctx); return; }
1326 res[0..5] = "\x1bRAWB";
1327 res[5..sz+5] = vs[0..sz];
1328 if (isGoodText(vs[0..cast(usize)sz])) {
1329 sqlite3_result_text(ctx, res, sz+5, &free);
1330 } else {
1331 sqlite3_result_blob(ctx, res, sz+5, &free);
1333 } else {
1334 immutable bool wantBlob = !isGoodText(vs[0..cast(usize)sz]);
1335 immutable int tp = sqlite3_value_type(val);
1336 if ((wantBlob && tp == SQLITE_BLOB) || (!wantBlob && tp == SQLITE3_TEXT)) {
1337 sqlite3_result_value(ctx, val);
1338 } else if (wantBlob) {
1339 sqlite3_result_blob(ctx, vs, sz, SQLITE_TRANSIENT);
1340 } else {
1341 sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1348 ** ChiroPack(content)
1350 private void sq3Fn_ChiroPack (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
1351 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
1352 return sq3Fn_ChiroPackCommon(ctx, argv[0], ChiroCompressionLevel);
1357 ** ChiroPack(content, packlevel)
1359 ** `packlevel` == 0 means "don't pack"
1360 ** `packlevel` == 9 means "maximum compression"
1362 private void sq3Fn_ChiroPackDPArg (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
1363 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
1364 return sq3Fn_ChiroPackCommon(ctx, argv[0], sqlite3_value_int(argv[1]));
1369 ** ChiroGetPackType(content)
1371 private void sq3Fn_ChiroGetPackType (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1372 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroGetPackType()`", -1); return; }
1374 int sz = sqlite3_value_bytes(argv[0]);
1375 if (sz < 5 || sz > 0x3fffffff-4) { sqlite3_result_text(ctx, "RAWB", 4, SQLITE_STATIC); return; }
1377 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1378 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1380 if (!isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_text(ctx, "RAWB", 4, SQLITE_STATIC); return; }
1382 sqlite3_result_text(ctx, vs+1, 4, SQLITE_TRANSIENT);
1387 ** ChiroUnpack(content)
1389 ** it is (almost) safe to pass non-packed content here
1391 private void sq3Fn_ChiroUnpack (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1392 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!000\n"); }
1393 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroUnpack()`", -1); return; }
1395 int sz = sqlite3_value_bytes(argv[0]);
1396 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
1398 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1400 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1401 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1403 if (!isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_value(ctx, argv[0]); return; }
1404 if (vs[0..5] == "\x1bRAWB") { sqlite3_result_blob(ctx, vs+5, sz-5, SQLITE_TRANSIENT); return; }
1405 if (sz < 6) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1407 enum {
1408 Codec_ZLIB,
1409 Codec_LZMA,
1410 Codec_BALZ,
1411 Codec_XPAK,
1412 Codec_BRLZ,
1413 Codec_LZFS,
1414 Codec_LZJB,
1415 Codec_LZMS,
1416 Codec_LZMX,
1417 Codec_XPRS,
1418 Codec_LZ4D,
1419 Codec_ZSTD,
1422 int codec = Codec_ZLIB;
1423 if (vs[0..5] != "\x1bZLIB") {
1424 if (vs[0..5] == "\x1bLZMA") codec = Codec_LZMA;
1425 version(use_balz) {
1426 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBALZ") codec = Codec_BALZ;
1428 version(use_libxpack) {
1429 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1431 version(use_libxpack) {
1432 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1434 version(use_libbrieflz) {
1435 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBRLZ") codec = Codec_BRLZ;
1437 version(use_liblzfse) {
1438 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZFS") codec = Codec_LZFS;
1440 version(use_lzjb) {
1441 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZJB") codec = Codec_LZJB;
1443 version(use_libwim_lzms) {
1444 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMS") codec = Codec_LZMS;
1446 version(use_libwim_lzx) {
1447 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMX") codec = Codec_LZMX;
1449 version(use_libwim_xpress) {
1450 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPRS") codec = Codec_XPRS;
1452 version(use_lz4) {
1453 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZ4D") codec = Codec_LZ4D;
1455 version(use_zstd) {
1456 if (codec == Codec_ZLIB && vs[0..5] == "\x1bZSTD") codec = Codec_ZSTD;
1458 if (codec == Codec_ZLIB) { sqlite3_result_error(ctx, "invalid codec in `ChiroUnpack()`", -1); return; }
1461 // skip codec id
1462 // size is guaranteed to be at least 6 here
1463 vs += 5;
1464 sz -= 5;
1466 immutable uint numsz = decodeUIntLength(vs[0..cast(uint)sz]);
1467 //{ 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]); }
1468 //writeln("sq3Fn_ChiroUnpack: nsz=", sz-5);
1469 if (numsz == 0 || numsz > cast(uint)sz) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1470 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!100\n"); }
1471 immutable uint rsize = decodeUInt(vs[0..cast(uint)sz]);
1472 if (rsize == uint.max) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1473 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!101:rsize=%u\n", rsize); }
1474 if (rsize == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1475 // skip number
1476 vs += numsz;
1477 sz -= cast(int)numsz;
1478 //{ import core.stdc.stdio : printf; printf("sz=%d; rsize=%u\n", sz, rsize, dpos); }
1480 import core.stdc.stdlib : malloc, free;
1481 import core.stdc.string : memcpy;
1483 char* cbuf = cast(char*)malloc(rsize);
1484 if (cbuf is null) { sqlite3_result_error_nomem(ctx); return; }
1485 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz-dpos);
1487 usize dsize = rsize;
1488 final switch (codec) {
1489 case Codec_ZLIB:
1490 version(use_libdeflate) {
1491 libdeflate_decompressor *dcp = libdeflate_alloc_decompressor();
1492 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1493 auto rc = libdeflate_zlib_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1494 if (rc != LIBDEFLATE_SUCCESS) {
1495 free(cbuf);
1496 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1497 return;
1499 } else {
1500 import etc.c.zlib : uncompress, Z_OK;
1501 int zres = uncompress(cast(ubyte *)cbuf, &dsize, cast(const(ubyte) *)vs, sz);
1502 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz, "; dsize=", dsize, "; zres=", zres);
1503 if (zres != Z_OK || dsize != rsize) {
1504 free(cbuf);
1505 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1506 return;
1509 break;
1510 case Codec_LZMA:
1512 if (sz < LZMA_PROPS_SIZE+1 || vs[0] != LZMA_PROPS_SIZE) {
1513 free(cbuf);
1514 sqlite3_result_error(ctx, "broken LZMA data in `ChiroUnpack()`", -1);
1515 return;
1518 usize srcSize = sz-vs[0]-1;
1519 ELzmaStatus status;
1520 SRes zres = LzmaDecode(cast(ubyte *)cbuf, &dsize, cast(const(ubyte) *)vs+vs[0]+1, &srcSize,
1521 cast(const(ubyte)*)(vs+1)/*propData*/, vs[0]/*propSize*/, LZMA_FINISH_ANY, &status, &lzmaDefAllocator);
1522 if (zres != SZ_OK || dsize != rsize || status == LZMA_STATUS_FINISHED_WITH_MARK || status == LZMA_STATUS_NEEDS_MORE_INPUT) {
1523 free(cbuf);
1524 sqlite3_result_error(ctx, "broken LZMA data in `ChiroUnpack()`", -1);
1525 return;
1528 break;
1529 case Codec_BALZ:
1530 version(use_balz) {
1531 uint spos = 0;
1532 uint outpos = 0;
1533 try {
1534 Unbalz bz;
1535 auto dc = bz.decompress(
1536 // reader
1537 (buf) {
1538 uint left = cast(uint)sz-spos;
1539 if (left > buf.length) left = cast(uint)buf.length;
1540 if (left != 0) memcpy(buf.ptr, vs, left);
1541 spos += left;
1542 return left;
1544 // writer
1545 (buf) {
1546 uint left = rsize-outpos;
1547 if (left == 0) throw new Exception("broken data");
1548 if (left > buf.length) left = cast(uint)buf.length;
1549 if (left) memcpy(cbuf+outpos, buf.ptr, left);
1550 outpos += left;
1553 if (dc != rsize) throw new Exception("broken data");
1554 } catch (Exception) {
1555 outpos = uint.max;
1557 if (outpos == uint.max) {
1558 free(cbuf);
1559 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1560 return;
1562 dsize = outpos;
1563 } else {
1564 free(cbuf);
1565 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1566 return;
1568 break;
1569 case Codec_XPAK:
1570 version(use_libxpack) {
1571 xpack_decompressor *dcp = xpack_alloc_decompressor();
1572 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1573 auto rc = xpack_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1574 if (rc != DECOMPRESS_SUCCESS) {
1575 free(cbuf);
1576 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1577 return;
1579 } else {
1580 free(cbuf);
1581 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1582 return;
1584 break;
1585 case Codec_BRLZ:
1586 version(use_libbrieflz) {
1587 dsize = blz_depack_safe(vs, cast(uint)sz, cbuf, rsize);
1588 if (dsize != rsize) {
1589 free(cbuf);
1590 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1591 return;
1593 } else {
1594 free(cbuf);
1595 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1596 return;
1598 break;
1599 case Codec_LZFS:
1600 version(use_liblzfse) {
1601 immutable usize wbsize = lzfse_decode_scratch_size();
1602 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1603 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1604 dsize = lzfse_decode_buffer(cbuf, cast(usize)rsize, vs, cast(usize)sz, wbuf);
1605 free(wbuf);
1606 if (dsize == 0 || dsize != rsize) {
1607 free(cbuf);
1608 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1609 return;
1611 } else {
1612 free(cbuf);
1613 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1614 return;
1616 break;
1617 case Codec_LZJB:
1618 version(use_lzjb) {
1619 dsize = lzjb_decompress(vs, cast(usize)sz, cbuf, rsize);
1620 if (dsize != rsize) {
1621 free(cbuf);
1622 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1623 return;
1625 } else {
1626 free(cbuf);
1627 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1628 return;
1630 break;
1631 case Codec_LZMS:
1632 version(use_libwim_lzms) {
1633 wimlib_decompressor* dpr;
1634 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZMS, rsize, &dpr);
1635 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1636 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1637 wimlib_free_decompressor(dpr);
1638 if (rc != 0) {
1639 free(cbuf);
1640 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1641 return;
1643 } else {
1644 free(cbuf);
1645 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1646 return;
1648 break;
1649 case Codec_LZMX:
1650 version(use_libwim_lzx) {
1651 wimlib_decompressor* dpr;
1652 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZX, rsize, &dpr);
1653 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1654 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1655 wimlib_free_decompressor(dpr);
1656 if (rc != 0) {
1657 free(cbuf);
1658 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1659 return;
1661 } else {
1662 free(cbuf);
1663 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1664 return;
1666 break;
1667 case Codec_XPRS:
1668 version(use_libwim_xpress) {
1669 wimlib_decompressor* dpr;
1670 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
1671 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < rsize) csz *= 2U;
1672 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, &dpr);
1673 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1674 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1675 wimlib_free_decompressor(dpr);
1676 if (rc != 0) {
1677 free(cbuf);
1678 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1679 return;
1681 } else {
1682 free(cbuf);
1683 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1684 return;
1686 break;
1687 case Codec_LZ4D:
1688 version(use_lz4) {
1689 dsize = LZ4_decompress_safe(vs, cbuf, sz, rsize);
1690 if (dsize != rsize) {
1691 free(cbuf);
1692 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1693 return;
1695 } else {
1696 free(cbuf);
1697 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1698 return;
1700 break;
1701 case Codec_ZSTD:
1702 version(use_zstd) {
1703 dsize = ZSTD_decompress(cbuf, rsize, vs, sz);
1704 if (ZSTD_isError(dsize) || dsize != rsize) {
1705 free(cbuf);
1706 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1707 return;
1709 } else {
1710 free(cbuf);
1711 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1712 return;
1714 break;
1717 if (isGoodText(cbuf[0..dsize])) {
1718 sqlite3_result_text(ctx, cbuf, cast(int)dsize, &free);
1719 } else {
1720 sqlite3_result_blob(ctx, cbuf, cast(int)dsize, &free);
1726 ** ChiroNormCRLF(content)
1728 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1729 ** Removes trailing blanks.
1731 private void sq3Fn_ChiroNormCRLF (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1732 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormCRLF()`", -1); return; }
1734 int sz = sqlite3_value_bytes(argv[0]);
1735 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1737 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1739 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1740 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormCRLF()`", -1); return; }
1742 // check if we have something to do, and calculate new string size
1743 bool needwork = false;
1744 if (vs[cast(uint)sz-1] <= 32) {
1745 needwork = true;
1746 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1747 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1749 uint newsz = cast(uint)sz;
1750 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1751 if (ch == 13) {
1752 needwork = true;
1753 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1754 } else if (!needwork) {
1755 needwork = ((ch < 32 && ch != 9 && ch != 10) || ch == 127);
1759 if (!needwork) {
1760 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1761 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1762 return;
1765 assert(newsz && newsz <= cast(uint)sz);
1767 // need a new string
1768 import core.stdc.stdlib : malloc, free;
1769 char* newstr = cast(char*)malloc(newsz);
1770 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1771 char* dest = newstr;
1772 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1773 if (ch == 13) {
1774 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1775 } else {
1776 if (ch == 127) *dest++ = '~';
1777 else if (ch == 11 || ch == 12) *dest++ = '\n';
1778 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1779 else *dest++ = ch;
1782 assert(dest == newstr+newsz);
1784 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1789 ** ChiroNormHeaders(content)
1791 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1792 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1793 ** Removes trailing blanks.
1795 private void sq3Fn_ChiroNormHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1796 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormHeaders()`", -1); return; }
1798 int sz = sqlite3_value_bytes(argv[0]);
1799 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1801 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1803 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1804 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormHeaders()`", -1); return; }
1806 // check if we have something to do, and calculate new string size
1807 bool needwork = false;
1808 if (vs[cast(uint)sz-1] <= 32) {
1809 needwork = true;
1810 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1811 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1813 uint newsz = cast(uint)sz;
1814 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1815 if (ch == 13) {
1816 needwork = true;
1817 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1818 } else if (ch == 10) {
1819 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) { needwork = true; --newsz; }
1820 } else if (!needwork) {
1821 needwork = ((ch < 32 && ch != 10) || ch == 127);
1825 if (!needwork) {
1826 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1827 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1828 return;
1831 assert(newsz && newsz <= cast(uint)sz);
1833 // need a new string
1834 import core.stdc.stdlib : malloc, free;
1835 char* newstr = cast(char*)malloc(newsz);
1836 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1837 char* dest = newstr;
1838 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1839 if (ch == 13) {
1840 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1841 } else if (ch == 10) {
1842 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1843 } else {
1844 if (ch == 127) *dest++ = '~';
1845 else if (ch < 32 && ch != 10) *dest++ = ' ';
1846 else *dest++ = ch;
1849 assert(dest == newstr+newsz);
1851 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1856 ** ChiroExtractHeaders(content)
1858 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1859 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1860 ** Removes trailing blanks.
1862 private void sq3Fn_ChiroExtractHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1863 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1865 int sz = sqlite3_value_bytes(argv[0]);
1866 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1868 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1870 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1871 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1873 // slice headers
1874 sz = sq3Supp_FindHeadersEnd(vs, sz);
1876 // strip trailing blanks
1877 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1878 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1880 // allocate new string (it can be smaller, but will never be bigger)
1881 import core.stdc.stdlib : malloc, free;
1882 char* newstr = cast(char*)malloc(cast(uint)sz);
1883 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1884 char* dest = newstr;
1885 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1886 if (ch == 13) {
1887 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1888 } else if (ch == 10) {
1889 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1890 } else {
1891 if (ch == 127) *dest++ = '~';
1892 else if (ch < 32 && ch != 10) *dest++ = ' ';
1893 else *dest++ = ch;
1896 assert(dest <= newstr+cast(uint)sz);
1897 sz = cast(int)cast(usize)(dest-newstr);
1898 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1899 sqlite3_result_text(ctx, newstr, sz, &free);
1904 ** ChiroExtractBody(content)
1906 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1907 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1908 ** Removes trailing blanks and final dot.
1910 private void sq3Fn_ChiroExtractBody (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1911 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1913 int sz = sqlite3_value_bytes(argv[0]);
1914 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1916 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1918 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1919 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1921 // slice body
1922 immutable int bstart = sq3Supp_FindHeadersEnd(vs, sz);
1923 if (bstart >= sz) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1924 vs += bstart;
1925 sz -= bstart;
1927 // strip trailing dot
1928 if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '\n') sz -= 2;
1929 else if (sz >= 1 && vs[cast(uint)sz-1U] == '\n') --sz;
1930 if (sz == 1 && vs[0] == '.') sz = 0;
1931 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\n' && vs[cast(uint)sz-1U] == '.') --sz;
1932 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '.') --sz;
1934 // strip trailing blanks
1935 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1936 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1938 // allocate new string (it can be smaller, but will never be bigger)
1939 import core.stdc.stdlib : malloc, free;
1940 char* newstr = cast(char*)malloc(cast(uint)sz);
1941 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1942 char* dest = newstr;
1943 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1944 if (ch == 13) {
1945 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1946 } else {
1947 if (ch == 127) *dest++ = '~';
1948 else if (ch == 11 || ch == 12) *dest++ = '\n';
1949 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1950 else *dest++ = ch;
1953 assert(dest <= newstr+cast(uint)sz);
1954 sz = cast(int)cast(usize)(dest-newstr);
1955 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1956 sqlite3_result_text(ctx, newstr, sz, &free);
1961 ** ChiroRIPEMD160(content)
1963 ** Calculates RIPEMD160 hash over the given content.
1965 ** Returns BINARY BLOB! You can use `tolower(hex(ChiroRIPEMD160(contents)))`
1966 ** to get lowercased hex hash string.
1968 private void sq3Fn_ChiroRIPEMD160 (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1969 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroRIPEMD160()`", -1); return; }
1971 immutable int sz = sqlite3_value_bytes(argv[0]);
1972 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1974 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1975 if (!vs && sz == 0) vs = "";
1976 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroRIPEMD160()`", -1); return; }
1978 RIPEMD160_Ctx rmd;
1979 ripemd160_put(ref rmd, vs[0..cast(uint)sz]);
1980 ubyte[RIPEMD160_BYTES] hash = ripemd160_finish(ref rmd);
1981 sqlite3_result_blob(ctx, cast(const(char)*)hash.ptr, cast(int)hash.length, SQLITE_TRANSIENT);
1985 enum HeaderProcStartTpl(string fnname) = `
1986 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"`~fnname~`()\"", -1); return; }
1988 immutable int sz = sqlite3_value_bytes(argv[0]);
1989 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1991 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1992 if (!vs && sz == 0) vs = "";
1993 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"`~fnname~`()\"", -1); return; }
1995 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
2000 ** ChiroHdr_NNTPIndex(headers)
2002 ** The content must be email with headers (or headers only).
2003 ** Returns "NNTP-Index" field or zero (int).
2005 private void sq3Fn_ChiroHdr_NNTPIndex (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2006 mixin(HeaderProcStartTpl!"ChiroHdr_NNTPIndex");
2008 uint nntpidx = 0;
2010 auto nntpidxfld = findHeaderField(hdrs, "NNTP-Index");
2011 if (nntpidxfld.length) {
2012 auto id = nntpidxfld.getFieldValue;
2013 if (id.length) {
2014 foreach (immutable ch; id) {
2015 if (ch < '0' || ch > '9') { nntpidx = 0; break; }
2016 if (nntpidx == 0 && ch == '0') continue;
2017 immutable uint nn = nntpidx*10u+(ch-'0');
2018 if (nn <= nntpidx) nntpidx = 0x7fffffff; else nntpidx = nn;
2023 // it is safe, it can't overflow
2024 sqlite3_result_int(ctx, cast(int)nntpidx);
2029 ** ChiroHdr_RecvTime(headers)
2031 ** The content must be email with headers (or headers only).
2032 ** Returns unixtime (can be zero).
2034 private void sq3Fn_ChiroHdr_RecvTime (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2035 mixin(HeaderProcStartTpl!"ChiroHdr_RecvTime");
2037 uint msgtime = 0; // message receiving time
2039 auto datefld = findHeaderField(hdrs, "Injection-Date");
2040 if (datefld.length != 0) {
2041 auto v = datefld.getFieldValue;
2042 try {
2043 msgtime = parseMailDate(v);
2044 } catch (Exception) {
2045 //writeln("UID=", uid, ": FUCKED INJECTION-DATE: |", v, "|");
2046 msgtime = 0; // just in case
2050 if (!msgtime) {
2051 // obsolete NNTP date field, because why not?
2052 datefld = findHeaderField(hdrs, "NNTP-Posting-Date");
2053 if (datefld.length != 0) {
2054 auto v = datefld.getFieldValue;
2055 try {
2056 msgtime = parseMailDate(v);
2057 } catch (Exception) {
2058 //writeln("UID=", uid, ": FUCKED NNTP-POSTING-DATE: |", v, "|");
2059 msgtime = 0; // just in case
2064 if (!msgtime) {
2065 datefld = findHeaderField(hdrs, "Date");
2066 if (datefld.length != 0) {
2067 auto v = datefld.getFieldValue;
2068 try {
2069 msgtime = parseMailDate(v);
2070 } catch (Exception) {
2071 //writeln("UID=", uid, ": FUCKED DATE: |", v, "|");
2072 msgtime = 0; // just in case
2077 // finally, try to get time from "Received:"
2078 //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
2079 if (!msgtime) {
2080 //writeln("!!! --- !!!");
2081 uint lowesttime = uint.max;
2082 foreach (uint fidx; 0..uint.max) {
2083 auto recvfld = findHeaderField(hdrs, "Received", fidx);
2084 if (recvfld.length == 0) break;
2085 auto lsemi = recvfld.lastIndexOf(';');
2086 if (lsemi >= 0) recvfld = recvfld[lsemi+1..$].xstrip;
2087 if (recvfld.length != 0) {
2088 auto v = recvfld.getFieldValue;
2089 uint tm = 0;
2090 try {
2091 tm = parseMailDate(v);
2092 } catch (Exception) {
2093 //writeln("UID=", uid, ": FUCKED RECV DATE: |", v, "|");
2094 tm = 0; // just in case
2096 //writeln(tm, " : ", lowesttime);
2097 if (tm && tm < lowesttime) lowesttime = tm;
2100 if (lowesttime != uint.max) msgtime = lowesttime;
2103 sqlite3_result_int64(ctx, msgtime);
2108 ** ChiroHdr_FromEmail(headers)
2110 ** The content must be email with headers (or headers only).
2111 ** Returns email "From" field.
2113 private void sq3Fn_ChiroHdr_FromEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2114 mixin(HeaderProcStartTpl!"ChiroHdr_FromEmail");
2115 auto from = findHeaderField(hdrs, "From").extractMail;
2116 if (from.length == 0) {
2117 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
2118 } else {
2119 sqlite3_result_text(ctx, from.ptr, cast(int)from.length, SQLITE_TRANSIENT);
2125 ** ChiroHdr_ToEmail(headers)
2127 ** The content must be email with headers (or headers only).
2128 ** Returns email "From" field.
2130 private void sq3Fn_ChiroHdr_ToEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2131 mixin(HeaderProcStartTpl!"ChiroHdr_ToEmail");
2132 auto to = findHeaderField(hdrs, "To").extractMail;
2133 if (to.length == 0) {
2134 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
2135 } else {
2136 sqlite3_result_text(ctx, to.ptr, cast(int)to.length, SQLITE_TRANSIENT);
2142 ** ChiroHdr_Subj(headers)
2144 ** The content must be email with headers (or headers only).
2145 ** Returns email "From" field.
2147 private void sq3Fn_ChiroHdr_Subj (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2148 mixin(HeaderProcStartTpl!"sq3Fn_ChiroHdr_Subj");
2149 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
2150 if (subj.length == 0) {
2151 sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
2152 } else {
2153 sqlite3_result_text(ctx, subj.ptr, cast(int)subj.length, SQLITE_TRANSIENT);
2159 ** ChiroHdr_Field(headers, fieldname)
2161 ** The content must be email with headers (or headers only).
2162 ** Returns field value as text, or NULL if there is no such field.
2164 private void sq3Fn_ChiroHdr_Field (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2165 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroHdr_Field()\"", -1); return; }
2167 immutable int sz = sqlite3_value_bytes(argv[0]);
2168 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
2170 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2171 if (!vs && sz == 0) vs = "";
2172 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
2174 immutable int fldsz = sqlite3_value_bytes(argv[1]);
2175 if (fldsz < 0) { sqlite3_result_error_toobig(ctx); return; }
2177 const(char)* fldname = cast(const(char) *)sqlite3_value_blob(argv[1]);
2178 if (!fldname && fldsz == 0) fldname = "";
2179 if (!fldname) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
2181 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
2182 auto value = findHeaderField(hdrs, fldname[0..fldsz]);
2183 if (value is null) {
2184 sqlite3_result_null(ctx);
2185 } else if (value.length == 0) {
2186 sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
2187 } else {
2188 sqlite3_result_text(ctx, value.ptr, cast(int)value.length, SQLITE_TRANSIENT);
2194 ** ChiroTimerStart([msg])
2196 ** The content must be email with headers (or headers only).
2197 ** Returns email "From" field.
2199 private void sq3Fn_ChiroTimerStart (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2200 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStart()\"", -1); return; }
2202 delete chiTimerMsg;
2204 if (argc == 1) {
2205 immutable int sz = sqlite3_value_bytes(argv[0]);
2206 if (sz > 0) {
2207 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2208 if (vs) {
2209 chiTimerMsg = new char[cast(usize)sz];
2210 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
2211 writeln("started ", chiTimerMsg, "...");
2216 sqlite3_result_int(ctx, 1);
2217 chiTimer.restart();
2222 ** ChiroTimerStop([msg])
2224 ** The content must be email with headers (or headers only).
2225 ** Returns email "From" field.
2227 private void sq3Fn_ChiroTimerStop (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2228 chiTimer.stop;
2229 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStop()\"", -1); return; }
2231 if (ChiroTimerEnabled) {
2232 if (argc == 1) {
2233 delete chiTimerMsg;
2234 immutable int sz = sqlite3_value_bytes(argv[0]);
2235 if (sz > 0) {
2236 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2237 if (vs) {
2238 chiTimerMsg = new char[cast(usize)sz];
2239 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
2244 char[128] buf;
2245 auto tstr = chiTimer.toBuffer(buf[]);
2246 if (chiTimerMsg.length) {
2247 writeln("done ", chiTimerMsg, ": ", tstr);
2248 } else {
2249 writeln("time: ", tstr);
2253 delete chiTimerMsg;
2255 sqlite3_result_int(ctx, 1);
2260 ** ChiroGlob(pat, str)
2262 ** GLOB replacement, with extended word matching.
2264 private void sq3Fn_ChiroGlob_common (sqlite3_context *ctx, int argc, sqlite3_value **argv, int casesens,
2265 uint stridx=1, uint patidx=0)
2267 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroGlob()\"", -1); return; }
2269 immutable int patsz = sqlite3_value_bytes(argv[patidx]);
2270 if (patsz < 0) { sqlite3_result_error_toobig(ctx); return; }
2272 const(char)* pat = cast(const(char) *)sqlite3_value_blob(argv[patidx]);
2273 if (!pat && patsz == 0) pat = "";
2274 if (!pat) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2276 immutable int strsz = sqlite3_value_bytes(argv[stridx]);
2277 if (strsz < 0) { sqlite3_result_error_toobig(ctx); return; }
2279 const(char)* str = cast(const(char) *)sqlite3_value_blob(argv[stridx]);
2280 if (!str && strsz == 0) str = "";
2281 if (!str) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2283 immutable bool res =
2284 casesens ?
2285 globmatch(str[0..cast(usize)strsz], pat[0..cast(usize)patsz]) :
2286 globmatchCI(str[0..cast(usize)strsz], pat[0..cast(usize)patsz]);
2288 sqlite3_result_int(ctx, (res ? 1 : 0));
2293 ** ChiroGlobSQL(pat, str)
2295 ** GLOB replacement, with extended word matching.
2297 private void sq3Fn_ChiroGlobSQL (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2298 sq3Fn_ChiroGlob_common(ctx, argc, argv, casesens:1);
2302 ** ChiroGlob(str, pat)
2304 ** GLOB replacement, with extended word matching.
2306 private void sq3Fn_ChiroGlob (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2307 sq3Fn_ChiroGlob_common(ctx, argc, argv, casesens:1, stridx:0, patidx:1);
2311 ** ChiroGlobCI(str, pat)
2313 ** GLOB replacement, with extended word matching.
2315 private void sq3Fn_ChiroGlobCI (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2316 sq3Fn_ChiroGlob_common(ctx, argc, argv, casesens:0, stridx:0, patidx:1);
2320 // ////////////////////////////////////////////////////////////////////////// //
2324 // ////////////////////////////////////////////////////////////////////////// //
2325 public void chiroRegisterSQLite3Functions (ref Database db) {
2326 sqlite3_busy_timeout(db.getHandle, 20000); // busy timeout: 20 seconds
2328 immutable int rc = sqlite3_extended_result_codes(db.getHandle, 1);
2329 if (rc != SQLITE_OK) {
2330 import core.stdc.stdio : stderr, fprintf;
2331 fprintf(stderr, "SQLITE WARNING: cannot enable extended result codes (this is harmless).\n");
2333 db.createFunction("glob", 2, &sq3Fn_ChiroGlobSQL, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2334 db.createFunction("ChiroGlob", 2, &sq3Fn_ChiroGlob, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2335 db.createFunction("ChiroGlobCI", 2, &sq3Fn_ChiroGlobCI, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2337 db.createFunction("ChiroPack", 1, &sq3Fn_ChiroPack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2338 db.createFunction("ChiroPack", 2, &sq3Fn_ChiroPackDPArg, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2339 db.createFunction("ChiroUnpack", 1, &sq3Fn_ChiroUnpack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2341 db.createFunction("ChiroPackLZMA", 1, &sq3Fn_ChiroPackLZMA, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2342 db.createFunction("ChiroPackLZMA", 2, &sq3Fn_ChiroPackLZMA, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2344 db.createFunction("ChiroGetPackType", 1, &sq3Fn_ChiroGetPackType, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2346 db.createFunction("ChiroNormCRLF", 1, &sq3Fn_ChiroNormCRLF, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2347 db.createFunction("ChiroNormHeaders", 1, &sq3Fn_ChiroNormHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2348 db.createFunction("ChiroExtractHeaders", 1, &sq3Fn_ChiroExtractHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2349 db.createFunction("ChiroExtractBody", 1, &sq3Fn_ChiroExtractBody, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2350 db.createFunction("ChiroRIPEMD160", 1, &sq3Fn_ChiroRIPEMD160, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2352 db.createFunction("ChiroHdr_NNTPIndex", 1, &sq3Fn_ChiroHdr_NNTPIndex, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2353 db.createFunction("ChiroHdr_RecvTime", 1, &sq3Fn_ChiroHdr_RecvTime, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2354 db.createFunction("ChiroHdr_FromEmail", 1, &sq3Fn_ChiroHdr_FromEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2355 db.createFunction("ChiroHdr_ToEmail", 1, &sq3Fn_ChiroHdr_ToEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2356 db.createFunction("ChiroHdr_Subj", 1, &sq3Fn_ChiroHdr_Subj, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2357 db.createFunction("ChiroHdr_Field", 2, &sq3Fn_ChiroHdr_Field, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2359 db.createFunction("ChiroTimerStart", 0, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2360 db.createFunction("ChiroTimerStart", 1, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2361 db.createFunction("ChiroTimerStop", 0, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2362 db.createFunction("ChiroTimerStop", 1, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2366 // ////////////////////////////////////////////////////////////////////////// //
2367 public void chiroRecreateStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName) {
2368 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2369 dbStore = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWStorageRecreate, schemaStorage);
2370 chiroRegisterSQLite3Functions(dbStore);
2371 dbStore.setOnClose(schemaStorageIndex~dbpragmasRWStorage~"ANALYZE;");
2375 // ////////////////////////////////////////////////////////////////////////// //
2376 public void chiroRecreateViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName) {
2377 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2378 dbView = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWSupportRecreate, schemaSupportTable);
2379 chiroRegisterSQLite3Functions(dbView);
2380 dbView.setOnClose(schemaSupportIndex~dbpragmasRWSupport~"ANALYZE;");
2384 public void chiroCreateViewIndiciesDB () {
2385 dbView.setOnClose(dbpragmasRWSupport~"ANALYZE;");
2386 dbView.execute(schemaSupportIndex);
2390 // ////////////////////////////////////////////////////////////////////////// //
2391 public void chiroRecreateConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName) {
2392 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2393 dbConf = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWOptionsRecreate, schemaOptions);
2394 chiroRegisterSQLite3Functions(dbConf);
2395 dbConf.setOnClose(schemaOptionsIndex~dbpragmasRWOptions~"ANALYZE;");
2399 // ////////////////////////////////////////////////////////////////////////// //
2400 public void chiroOpenStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName, bool readonly=false) {
2401 dbStore = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWStorage), schemaStorage);
2402 chiroRegisterSQLite3Functions(dbStore);
2403 if (!readonly) dbStore.setOnClose("PRAGMA optimize;");
2407 // ////////////////////////////////////////////////////////////////////////// //
2408 public void chiroOpenViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName, bool readonly=false) {
2409 dbView = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWSupport), schemaSupport);
2410 chiroRegisterSQLite3Functions(dbView);
2411 if (!readonly) {
2412 dbView.execute(schemaSupportTempTables);
2413 dbView.setOnClose("PRAGMA optimize;");
2418 // ////////////////////////////////////////////////////////////////////////// //
2419 public void chiroOpenConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName, bool readonly=false) {
2420 dbConf = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWOptions), schemaOptions);
2421 chiroRegisterSQLite3Functions(dbConf);
2422 if (!readonly) dbConf.setOnClose("PRAGMA optimize;");
2426 // ////////////////////////////////////////////////////////////////////////// //
2428 recreates FTS5 (full-text search) info.
2430 public void chiroRecreateFTS5 (bool repopulate=true) {
2431 dbView.execute(recreateFTS5);
2432 if (repopulate) dbView.execute(repopulateFTS5);
2433 dbView.execute(recreateFTS5Triggers);
2437 // ////////////////////////////////////////////////////////////////////////// //
2438 extern(C) {
2439 static void errorLogCallback (void *pArg, int rc, const char *zMsg) {
2440 if (ChiroSQLiteSilent) return;
2441 import core.stdc.stdio : stderr, fprintf;
2442 switch (rc) {
2443 case SQLITE_NOTICE: fprintf(stderr, "***SQLITE NOTICE: %s\n", zMsg); break;
2444 case SQLITE_NOTICE_RECOVER_WAL: fprintf(stderr, "***SQLITE NOTICE (WAL RECOVER): %s\n", zMsg); break;
2445 case SQLITE_NOTICE_RECOVER_ROLLBACK: fprintf(stderr, "***SQLITE NOTICE (ROLLBACK RECOVER): %s\n", zMsg); break;
2446 /* */
2447 case SQLITE_WARNING: fprintf(stderr, "***SQLITE WARNING: %s\n", zMsg); break;
2448 case SQLITE_WARNING_AUTOINDEX: fprintf(stderr, "***SQLITE AUTOINDEX WARNING: %s\n", zMsg); break;
2449 /* */
2450 case SQLITE_CANTOPEN:
2451 case SQLITE_SCHEMA:
2452 break; // ignore those
2453 /* */
2454 default: fprintf(stderr, "***SQLITE LOG(%d) [%s]: %s\n", rc, sqlite3_errstr(rc), zMsg); break;
2460 static string sqerrstr (immutable int rc) nothrow @trusted {
2461 const(char)* msg = sqlite3_errstr(rc);
2462 if (!msg || !msg[0]) return null;
2463 import core.stdc.string : strlen;
2464 return msg[0..strlen(msg)].idup;
2468 static void sqconfigcheck (immutable int rc, string msg, bool fatal) {
2469 if (rc == SQLITE_OK) return;
2470 if (fatal) {
2471 string errmsg = sqerrstr(rc);
2472 throw new Exception("FATAL: "~msg~": "~errmsg);
2473 } else {
2474 if (msg is null) msg = "";
2475 import core.stdc.stdio : stderr, fprintf;
2476 fprintf(stderr, "SQLITE WARNING: %.*s (this is harmless): %s\n", cast(uint)msg.length, msg.ptr, sqlite3_errstr(rc));
2481 // call this BEFORE opening any SQLite database connection!
2482 public void chiroSwitchToSingleThread () {
2483 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SINGLETHREAD), "cannot set single-threaded mode", fatal:false);
2487 public string MailDBPath () nothrow @trusted @nogc { return ExpandedMailDBPath; }
2490 public void MailDBPath(T:const(char)[]) (T mailpath) nothrow @trusted {
2491 while (mailpath.length > 1 && mailpath[$-1] == '/') mailpath = mailpath[0..$-1];
2493 if (mailpath.length == 0 || mailpath == ".") {
2494 ExpandedMailDBPath = "";
2495 return;
2498 if (mailpath[0] == '~') {
2499 char[] dpath = new char[mailpath.length+4096];
2500 dpath = expandTilde(dpath, mailpath);
2502 while (dpath.length > 1 && dpath[$-1] == '/') dpath = dpath[0..$-1];
2503 dpath ~= '/';
2504 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
2505 } else {
2506 char[] dpath = new char[mailpath.length+1];
2507 dpath[0..$-1] = mailpath[];
2508 dpath[$-1] = '/';
2509 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
2514 shared static this () {
2515 enum {
2516 SQLITE_CONFIG_STMTJRNL_SPILL = 26, /* int nByte */
2517 SQLITE_CONFIG_SMALL_MALLOC = 27, /* boolean */
2520 if (!sqlite3_threadsafe()) {
2521 throw new Exception("FATAL: SQLite must be compiled with threading support!");
2524 // we are interested in all errors
2525 sqlite3_config(SQLITE_CONFIG_LOG, &errorLogCallback, null);
2527 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SERIALIZED), "cannot set SQLite serialized threading mode", fatal:true);
2528 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SMALL_MALLOC, 0), "cannot enable SQLite unrestriced malloc mode", fatal:false);
2529 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_URI, 1), "cannot enable SQLite URI handling", fatal:false);
2530 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_COVERING_INDEX_SCAN, 1), "cannot enable SQLite covering index scan", fatal:false);
2531 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_STMTJRNL_SPILL, 512*1024), "cannot set SQLite statement journal spill threshold", fatal:false);
2533 MailDBPath = "~/Mail";
2537 shared static ~this () {
2538 dbConf.close();
2539 dbView.close();
2540 dbStore.close();
2544 // ////////////////////////////////////////////////////////////////////////// //
2545 public void transacted(string dbname) (void delegate () dg) {
2546 if (dg is null) return;
2547 static if (dbname == "View" || dbname == "view") alias db = dbView;
2548 else static if (dbname == "Store" || dbname == "store") alias db = dbStore;
2549 else static if (dbname == "Conf" || dbname == "conf") alias db = dbConf;
2550 else static assert(0, "invalid db name: '"~dbname~"'");
2551 db.transacted(dg);
2555 // ////////////////////////////////////////////////////////////////////////// //
2556 public DynStr chiroGetTagMonthLimitEx(T) (T tagnameid, out int val, int defval=6)
2557 if (is(T:const(char)[]) || is(T:uint))
2559 static if (is(T:const(char)[])) {
2560 alias tagname = tagnameid;
2561 } else {
2562 DynStr tagnameStr;
2563 static auto stGetTagName = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
2564 foreach (auto row; stGetTagName .st.bind(":tagid", tagnameid).range) {
2565 tagnameStr = row.tagname!SQ3Text;
2567 const(char)[] tagname = tagnameStr.getData;
2570 static auto stGetMLimit = LazyStatement!"Conf"(`
2571 WITH RECURSIVE pth(path) AS (
2572 VALUES('/mainpane/msgview/monthlimit'||:tagslash||:tagname)
2573 UNION ALL
2574 SELECT
2575 SUBSTR(path, 1, LENGTH(path)-LENGTH(REPLACE(path, RTRIM(path, REPLACE(path, '/', '')), ''))-1)
2576 FROM pth
2577 WHERE path LIKE '/mainpane/msgview/monthlimit%'
2579 SELECT
2580 -- pth.path AS path
2581 opt.name AS name
2582 , opt.value AS value
2583 FROM pth
2584 INNER JOIN options AS opt ON opt.name=pth.path
2585 WHERE pth.path LIKE '/mainpane/msgview/monthlimit%'
2586 LIMIT 1
2587 ;`);
2589 stGetMLimit.st
2590 .bindConstText(":tagslash", (tagname.length && tagname[0] != '/' ? "/" : ""))
2591 .bindConstText(":tagname", tagname);
2593 foreach (auto row; stGetMLimit.st.range) {
2594 //conwriteln("TAGNAME=<", tagname, ">; val=", row.value!int, "; sres=<", row.name!SQ3Text, ">");
2595 val = row.value!int;
2596 DynStr sres = row.name!SQ3Text;
2597 return sres;
2600 val = defval;
2601 return DynStr();
2605 public int chiroGetTagMonthLimit(T) (T tagnameid, int defval=6)
2606 if (is(T:const(char)[]) || is(T:uint))
2608 static if (is(T:const(char)[])) {
2609 alias tagname = tagnameid;
2610 } else {
2611 DynStr tagnameStr;
2612 static auto stGetTagName = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
2613 foreach (auto row; stGetTagName .st.bind(":tagid", tagnameid).range) {
2614 tagnameStr = row.tagname!SQ3Text;
2616 const(char)[] tagname = tagnameStr.getData;
2619 static auto stGetMLimit = LazyStatement!"Conf"(`
2620 WITH RECURSIVE pth(path) AS (
2621 VALUES('/mainpane/msgview/monthlimit'||:tagslash||:tagname)
2622 UNION ALL
2623 SELECT
2624 SUBSTR(path, 1, LENGTH(path)-LENGTH(REPLACE(path, RTRIM(path, REPLACE(path, '/', '')), ''))-1)
2625 FROM pth
2626 WHERE path LIKE '/mainpane/msgview/monthlimit%'
2628 SELECT
2629 -- pth.path AS path
2630 -- opt.name AS name
2631 opt.value AS value
2632 FROM pth
2633 INNER JOIN options AS opt ON opt.name=pth.path
2634 WHERE pth.path LIKE '/mainpane/msgview/monthlimit%'
2635 LIMIT 1
2636 ;`);
2638 stGetMLimit.st
2639 .bindConstText(":tagslash", (tagname.length && tagname[0] != '/' ? "/" : ""))
2640 .bindConstText(":tagname", tagname);
2642 foreach (auto row; stGetMLimit.st.range) return row.value!int;
2644 return defval;
2648 public void chiroDeleteOption (const(char)[] name) {
2649 assert(name.length != 0);
2650 static auto stat = LazyStatement!"Conf"(`DELETE FROM options WHERE name=:name;`);
2651 stat.st.bindConstText(":name", name).doAll();
2654 public void chiroSetOption(T) (const(char)[] name, T value)
2655 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2657 assert(name.length != 0);
2658 static auto stat = LazyStatement!"Conf"(`
2659 INSERT INTO options
2660 ( name, value)
2661 VALUES(:name,:value)
2662 ON CONFLICT(name)
2663 DO UPDATE SET value=:value
2664 ;`);
2665 stat.st.bindConstText(":name", name);
2666 static if (is(T == typeof(null))) {
2667 stat.st.bindConstText(":value", "");
2668 } else static if (__traits(isIntegral, T)) {
2669 stat.st.bind(":value", value);
2670 } else static if (is(T:const(char)[])) {
2671 stat.st.bindConstText(":value", value);
2672 } else {
2673 static assert(0, "oops");
2675 stat.st.doAll();
2678 public void chiroSetOption (const(char)[] name, DynStr value) {
2679 assert(name.length != 0);
2680 //{ import std.stdio; writeln("SETOPTION(", name, "): <", value.getData, ">"); }
2681 static auto stat = LazyStatement!"Conf"(`
2682 INSERT INTO options
2683 ( name, value)
2684 VALUES(:name,:value)
2685 ON CONFLICT(name)
2686 DO UPDATE SET value=:value
2687 ;`);
2688 stat.st
2689 .bindConstText(":name", name)
2690 .bindConstText(":value", value.getData)
2691 .doAll();
2695 public void chiroSetOptionUInts (const(char)[] name, uint v0, uint v1) {
2696 assert(name.length != 0);
2697 static auto stat = LazyStatement!"Conf"(`
2698 INSERT INTO options
2699 ( name, value)
2700 VALUES(:name,:value)
2701 ON CONFLICT(name)
2702 DO UPDATE SET value=:value
2703 ;`);
2704 import core.stdc.stdio : snprintf;
2705 char[64] value = void;
2706 auto vlen = snprintf(value.ptr, value.sizeof, "%u,%u", v0, v1);
2707 stat.st
2708 .bindConstText(":name", name)
2709 .bindConstText(":value", value[0..vlen])
2710 .doAll();
2714 public T chiroGetOptionEx(T) (const(char)[] name, out bool exists, T defval=T.init)
2715 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2717 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2718 assert(name.length != 0);
2719 exists = false;
2720 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2721 exists = true;
2722 return row.value!T;
2724 return defval;
2727 public T chiroGetOption(T) (const(char)[] name, T defval=T.init)
2728 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2730 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2731 assert(name.length != 0);
2732 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2733 return row.value!T;
2735 return defval;
2738 public void chiroGetOption (ref DynStr s, const(char)[] name, const(char)[] defval=null) {
2739 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2740 assert(name.length != 0);
2741 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2742 s = row.value!SQ3Text;
2743 return;
2745 s = defval;
2749 private uint parseUInt (ref SQ3Text s) {
2750 s = s.xstrip;
2751 if (s.length == 0 || !isdigit(s[0])) return uint.max;
2752 uint res = 0;
2753 while (s.length) {
2754 immutable int dg = s[0].digitInBase(10);
2755 if (dg < 0) break;
2756 immutable uint nr = res*10U+cast(uint)dg;
2757 if (nr < res) return uint.max;
2758 res = nr;
2759 s = s[1..$];
2761 if (s.length && s[0] == ',') s = s[1..$];
2762 s = s.xstrip;
2763 return res;
2767 public void chiroGetOptionUInts (ref uint v0, ref uint v1, const(char)[] name) {
2768 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2769 assert(name.length != 0);
2770 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2771 auto s = row.value!SQ3Text;
2772 immutable uint rv0 = parseUInt(s);
2773 immutable uint rv1 = parseUInt(s);
2774 if (rv0 != uint.max && rv1 != uint.max && s.length == 0) {
2775 v0 = rv0;
2776 v1 = rv1;
2778 return;
2783 // ////////////////////////////////////////////////////////////////////////// //
2784 // append tag if necessary, return tagid
2785 // tag name must be valid: not empty, and not end with a '/'
2786 // returns 0 on invalid tag name
2787 public uint chiroAppendTag (const(char)[] tagname, int hidden=0) {
2788 tagname = tagname.xstrip;
2789 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2790 tagname = tagname.xstrip;
2791 if (tagname.length == 0) return 0;
2792 if (tagname.indexOf('|') >= 0) return 0;
2794 static auto stAppendTag = LazyStatement!"View"(`
2795 INSERT INTO tagnames(tag, hidden, threading) VALUES(:tagname,:hidden,:threading)
2796 ON CONFLICT(tag)
2797 DO UPDATE SET hidden=hidden -- this is for "returning"
2798 RETURNING tagid AS tagid
2799 ;`);
2801 // alphanum tags must start with '/'
2802 DynStr tn;
2803 if (tagname[0].isalnum && tagname.indexOf(':') < 0) {
2804 tn = "/";
2805 tn ~= tagname;
2806 stAppendTag.st.bindConstText(":tagname", tn);
2807 } else {
2808 stAppendTag.st.bindConstText(":tagname", tagname);
2810 stAppendTag.st
2811 .bind(":hidden", hidden)
2812 .bind(":threading", (hidden ? 0 : 1));
2813 foreach (auto row; stAppendTag.st.range) return row.tagid!uint;
2815 return 0;
2819 // ////////////////////////////////////////////////////////////////////////// //
2820 /// returns `true` if we need to update pane
2821 /// if message is left without any tags, it will be tagged with "#hobo"
2822 public bool chiroMessageRemoveTag (uint uid, const(char)[] tagname) {
2823 if (uid == 0) return false;
2824 tagname = tagname.xstrip;
2825 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2826 tagname = tagname.xstrip;
2827 if (tagname.length == 0) return false;
2828 if (tagname.indexOf('|') >= 0) return false;
2830 immutable tagid = chiroGetTagUid(tagname);
2831 if (tagid == 0) return false;
2833 static auto stUpdateStorageTags = LazyStatement!"Store"(`
2834 UPDATE messages SET tags=:tags WHERE uid=:uid
2835 ;`);
2837 static auto stUidHasTag = LazyStatement!"View"(`
2838 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2839 ;`);
2841 static auto stInsertIntoThreads = LazyStatement!"View"(`
2842 INSERT INTO threads(uid, tagid,appearance,time)
2843 VALUES(:uid, :tagid, :appr, (SELECT time FROM info WHERE uid=:uid LIMIT 1))
2844 ;`);
2846 // delete message from threads
2847 static auto stClearThreads = LazyStatement!"View"(`
2848 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2849 ;`);
2851 static auto stGetMsgTags = LazyStatement!"View"(`
2852 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
2853 FROM threads
2854 INNER JOIN tagnames AS tt USING(tagid)
2855 WHERE uid=:uid
2856 ;`);
2859 immutable bool updatePane = (chiroGetTreePaneTableTagId() == tagid);
2860 bool wasChanges = false;
2862 transacted!"View"{
2863 // get tagid (possibly appending the tag)
2864 bool hasit = false;
2865 foreach (auto row; stUidHasTag.st.bind(":uid", uid).bind(":tagid", tagid).range) hasit = true;
2866 if (!hasit) return;
2868 stClearThreads.st.bind(":uid", uid).bind(":tagid", tagid).doAll((stmt) { wasChanges = true; });
2870 // if there were any changes, rebuild message tags
2871 if (!wasChanges) return;
2873 DynStr newtags;
2874 foreach (auto trow; stGetMsgTags.st.bind(":uid", uid).range) {
2875 auto tname = trow.name!SQ3Text;
2876 if (tname.length == 0) continue;
2877 if (newtags.length) newtags ~= "|";
2878 newtags ~= tname;
2881 // if there is no tags, assign "#hobo"
2882 // this should not happen, but...
2883 if (newtags.length == 0) {
2884 newtags = "#hobo";
2885 auto hobo = chiroAppendTag(newtags, hidden:1);
2886 assert(hobo != 0);
2887 // append record for this tag to threads
2888 // note that there is no need to relink hobos, they should not be threaded
2889 //FIXME: this clears message appearance
2890 stInsertIntoThreads.st
2891 .bind(":uid", uid)
2892 .bind(":tagid", hobo)
2893 .bind(":appr", Appearance.Read)
2894 .doAll();
2897 // update storage with new tag names
2898 assert(newtags.length);
2899 stUpdateStorageTags.st.bindConstText(":tags", newtags).doAll();
2901 // and relink threads for this tagid
2902 chiroSupportRelinkTagThreads(tagid);
2905 return (wasChanges && updatePane);
2909 // ////////////////////////////////////////////////////////////////////////// //
2910 /// returns `true` if we need to update pane
2911 public bool chiroMessageAddTag (uint uid, const(char)[] tagname) {
2912 if (uid == 0) return false;
2913 tagname = tagname.xstrip;
2914 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2915 tagname = tagname.xstrip;
2916 if (tagname.length == 0) return false;
2917 if (tagname.indexOf('|') >= 0) return false;
2919 static auto stUpdateStorageTags = LazyStatement!"Store"(`
2920 UPDATE messages SET tags=tags||'|'||:tagname WHERE uid=:uid
2921 ;`);
2923 static auto stUidExists = LazyStatement!"View"(`
2924 SELECT 1 FROM threads WHERE uid=:uid LIMIT 1
2925 ;`);
2927 static auto stUidHasTag = LazyStatement!"View"(`
2928 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2929 ;`);
2931 static auto stInsertIntoThreads = LazyStatement!"View"(`
2932 INSERT INTO threads(uid, tagid, appearance, time)
2933 VALUES(:uid, :tagid, :appr, (SELECT time FROM threads WHERE uid=:uid LIMIT 1))
2934 ;`);
2936 static auto stUnHobo = LazyStatement!"View"(`
2937 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2938 ;`);
2940 bool hasuid = false;
2941 foreach (auto row; stUidExists.st.bind(":uid", uid).range) hasuid = true;
2942 if (!hasuid) return false; // nothing to do
2944 immutable paneTagId = chiroGetTreePaneTableTagId();
2945 bool updatePane = false;
2947 transacted!"View"{
2948 // get tagid (possibly appending the tag)
2949 uint tagid = chiroAppendTag(tagname);
2950 if (tagid == 0) {
2951 conwriteln("ERROR: cannot append tag name '", tagname, "'!");
2952 return;
2955 bool hasit = false;
2956 foreach (auto row; stUidHasTag.st.bind(":uid", uid).bind(":tagid", tagid).range) hasit = true;
2957 if (hasit) return;
2959 // append this tag to the message in the storage
2960 stUpdateStorageTags.st.bind(":uid", uid).bindConstText(":tagname", tagname).doAll();
2962 // append record for this tag to threads
2963 stInsertIntoThreads.st
2964 .bind(":uid", uid)
2965 .bind(":tagid", tagid)
2966 .bind(":appr", Appearance.Read)
2967 .doAll();
2969 // and relink threads for this tagid
2970 chiroSupportRelinkTagThreads(tagid);
2972 // remove this message from "#hobo", if there is any
2973 auto hobo = chiroGetTagUid("#hobo");
2974 if (hobo && hobo != tagid) {
2975 stUnHobo.st.bind(":tagid", hobo).bind(":uid", uid).doAll();
2976 // there's no need to relink hobos, because they should have no links
2979 updatePane = (tagid == paneTagId);
2982 return updatePane;
2987 inserts the one message from the message storage with the given id into view storage.
2988 parses it and such, and optionally updates threads.
2990 doesn't updates NNTP indicies and such, never relinks anything.
2992 invalid (unknown) tags will be ignored.
2994 returns number of processed messages.
2996 doesn't start/end any transactions, so wrap it yourself.
2998 public bool chiroParseAndInsertOneMessage (uint uid, uint msgtime, int appearance,
2999 const(char)[] hdrs, const(char)[] body, const(char)[] tags)
3001 auto stInsThreads = dbView.statement(`
3002 INSERT INTO threads
3003 ( uid, tagid, time, appearance)
3004 VALUES(:uid,:tagid,:time,:appearance)
3005 ;`);
3007 auto stInsInfo = dbView.statement(`
3008 INSERT INTO info
3009 ( uid, from_name, from_mail, subj, to_name, to_mail)
3010 VALUES(:uid,:from_name,:from_mail,:subj,:to_name,:to_mail)
3011 ;`);
3013 auto stInsMsgId = dbView.statement(`
3014 INSERT INTO msgids
3015 ( uid, msgid, time)
3016 VALUES(:uid,:msgid,:time)
3017 ;`);
3019 auto stInsMsgRefId = dbView.statement(`
3020 INSERT INTO refids
3021 ( uid, idx, msgid)
3022 VALUES(:uid,:idx,:msgid)
3023 ;`);
3025 auto stInsContentText = dbView.statement(`
3026 INSERT INTO content_text
3027 ( uid, format, content)
3028 VALUES(:uid,:format, ChiroPack(:content))
3029 ;`);
3031 auto stInsContentHtml = dbView.statement(`
3032 INSERT INTO content_html
3033 ( uid, format, content)
3034 VALUES(:uid,:format, ChiroPack(:content))
3035 ;`);
3037 auto stInsAttach = dbView.statement(`
3038 INSERT INTO attaches
3039 ( uid, idx, mime, name, format, content)
3040 VALUES(:uid,:idx,:mime,:name,:format, ChiroPack(:content))
3041 ;`);
3043 bool noattaches = false; // do not store attaches?
3045 // create thread record for each tag (and update max nntp index)
3046 int tagCount = 0;
3047 int noAttachCount = 0;
3048 while (tags.length) {
3049 auto eep = tags.indexOf('|');
3050 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
3051 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
3052 if (tagname.length == 0) continue;
3054 //immutable uint tuid = chiroGetTagUid(tagname);
3055 immutable uint tuid = chiroAppendTag(tagname, (tagname == "#hobo" ? 1 : 0));
3056 if (tuid == 0) continue;
3058 /* nope
3059 if (nntpidx > 0 && tagname.startsWith("account:")) {
3060 auto accname = tagname[8..$];
3061 stInsNNTPIdx
3062 .bindConstText(":accname", accname)
3063 .bind(":nntpidx", nntpidx)
3064 .doAll();
3068 if (!chiroIsTagAllowAttaches(tuid)) ++noAttachCount;
3069 ++tagCount;
3071 int app = appearance;
3072 if (app == Appearance.Unread) {
3073 if (tagname.startsWith("account:") ||
3074 tagname.startsWith("#spam") ||
3075 tagname.startsWith("#hobo"))
3077 app = Appearance.Read;
3081 stInsThreads
3082 .bind(":uid", uid)
3083 .bind(":tagid", tuid)
3084 .bind(":time", msgtime)
3085 .bind(":appearance", app)
3086 .doAll();
3088 if (!tagCount) return false;
3089 noattaches = (noAttachCount && noAttachCount == tagCount);
3091 // insert msgid
3093 bool hasmsgid = false;
3094 auto msgidfield = findHeaderField(hdrs, "Message-Id");
3095 if (msgidfield.length) {
3096 auto id = msgidfield.getFieldValue;
3097 if (id.length) {
3098 hasmsgid = true;
3099 stInsMsgId
3100 .bind(":uid", uid)
3101 .bind("time", msgtime)
3102 .bindConstText(":msgid", id)
3103 .doAll();
3106 // if there is no msgid, create one
3107 if (!hasmsgid) {
3108 RIPEMD160_Ctx rmd;
3109 ripemd160_put(ref rmd, hdrs[]);
3110 ripemd160_put(ref rmd, body[]);
3111 ubyte[RIPEMD160_BYTES] digest = ripemd160_finish(ref rmd);
3112 char[20*2+2+16] buf = 0;
3113 import core.stdc.stdio : snprintf;
3114 import core.stdc.string : strcat;
3115 foreach (immutable idx, ubyte b; digest[]) snprintf(buf.ptr+idx*2, 3, "%02x", b);
3116 strcat(buf.ptr, "@artificial"); // it is safe, there is enough room for it
3117 stInsMsgId
3118 .bind(":uid", uid)
3119 .bind("time", msgtime)
3120 .bindConstText(":msgid", buf[0..20*2])
3121 .doAll();
3125 // insert references
3127 uint refidx = 0;
3128 auto inreplyfld = findHeaderField(hdrs, "In-Reply-To");
3129 while (inreplyfld.length) {
3130 auto id = getNextFieldValue(inreplyfld);
3131 if (id.length) {
3132 stInsMsgRefId
3133 .bind(":uid", uid)
3134 .bind(":idx", refidx++)
3135 .bind(":msgid", id)
3136 .doAll();
3140 inreplyfld = findHeaderField(hdrs, "References");
3141 while (inreplyfld.length) {
3142 auto id = getNextFieldValue(inreplyfld);
3143 if (id.length) {
3144 stInsMsgRefId
3145 .bind(":uid", uid)
3146 .bind(":idx", refidx++)
3147 .bind(":msgid", id)
3148 .doAll();
3153 // insert base content and attaches
3155 Content[] content;
3156 parseContent(ref content, hdrs, body, noattaches);
3157 // insert text and html
3158 bool wasText = false, wasHtml = false;
3159 foreach (const ref Content cc; content) {
3160 if (cc.name.length) continue;
3161 if (noattaches && !cc.mime.startsWith("text/")) continue;
3162 if (!wasText && cc.mime == "text/plain") {
3163 wasText = true;
3164 stInsContentText
3165 .bind(":uid", uid)
3166 .bindConstText(":format", cc.format)
3167 .bindConstBlob(":content", cc.data)
3168 .doAll();
3169 } else if (!wasHtml && cc.mime == "text/html") {
3170 wasHtml = true;
3171 stInsContentHtml
3172 .bind(":uid", uid)
3173 .bindConstText(":format", cc.format)
3174 .bindConstBlob(":content", cc.data)
3175 .doAll();
3178 if (!wasText) {
3179 stInsContentText
3180 .bind(":uid", uid)
3181 .bindConstText(":format", "")
3182 .bindConstBlob(":content", "")
3183 .doAll();
3185 if (!wasHtml) {
3186 stInsContentHtml
3187 .bind(":uid", uid)
3188 .bindConstText(":format", "")
3189 .bindConstBlob(":content", "")
3190 .doAll();
3192 // insert everything
3193 uint cidx = 0;
3194 foreach (const ref Content cc; content) {
3195 if (cc.name.length == 0 && cc.mime.startsWith("text/")) continue;
3196 // for "no attaches" mode, still record the attach, but ignore its contents
3197 stInsAttach
3198 .bind(":uid", uid)
3199 .bind(":idx", cidx++)
3200 .bindConstText(":mime", cc.mime)
3201 .bindConstText(":name", cc.name)
3202 .bindConstText(":format", cc.name)
3203 .bindConstBlob(":content", (noattaches ? null : cc.data), allowNull:true)
3204 .doAll();
3208 // insert from/to/subj info
3209 // this must be done last to keep FTS5 in sync
3211 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
3212 auto from = findHeaderField(hdrs, "From");
3213 auto to = findHeaderField(hdrs, "To");
3214 stInsInfo
3215 .bind(":uid", uid)
3216 .bind(":from_name", from.extractName)
3217 .bind(":from_mail", from.extractMail)
3218 .bind(":subj", subj)
3219 .bind(":to_name", to.extractName)
3220 .bind(":to_mail", to.extractMail)
3221 .doAll();
3224 return true;
3229 inserts the messages from the message storage with the given id into view storage.
3230 parses it and such, and optionally updates threads.
3232 WARNING! DOESN'T UPDATE NNTP INDICIES! this should be done by the downloader.
3234 invalid (unknown) tags will be ignored.
3236 returns number of processed messages.
3238 public uint chiroParseAndInsertMessages (uint stmsgid,
3239 void delegate (uint count, uint total, uint nntpidx, const(char)[] tags) progresscb=null,
3240 uint emsgid=uint.max, bool relink=true, bool asread=false)
3242 if (emsgid < stmsgid) return 0; // nothing to do
3244 uint count = 0;
3245 uint total = 0;
3246 if (progresscb !is null) {
3247 // find total number of messages to process
3248 foreach (auto row; dbStore.statement(`
3249 SELECT count(uid) AS total FROM messages WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
3250 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
3252 total = row.total!uint;
3253 break;
3255 if (total == 0) return 0; // why not?
3258 transacted!"View"{
3259 uint[] uptagids;
3260 if (relink) uptagids.reserve(128);
3261 scope(exit) delete uptagids;
3263 foreach (auto mrow; dbStore.statement(`
3264 -- this should cache unpack results
3265 WITH msgunpacked(msguid, msgdata, msgtags) AS (
3266 SELECT uid AS msguid, ChiroUnpack(data) AS msgdata, tags AS msgtags
3267 FROM messages
3268 WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
3269 ORDER BY uid
3271 SELECT
3272 msguid AS uid
3273 , msgtags AS tags
3274 , ChiroExtractHeaders(msgdata) AS headers
3275 , ChiroExtractBody(msgdata) AS body
3276 , ChiroHdr_NNTPIndex(msgdata) AS nntpidx
3277 , ChiroHdr_RecvTime(msgdata) AS msgtime
3278 FROM msgunpacked
3279 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
3281 ++count;
3282 auto hdrs = mrow.headers!SQ3Text;
3283 auto body = mrow.body!SQ3Text;
3284 auto tags = mrow.tags!SQ3Text;
3285 uint uid = mrow.uid!uint;
3286 uint nntpidx = mrow.nntpidx!uint;
3287 uint msgtime = mrow.msgtime!uint;
3288 assert(tags.length);
3290 chiroParseAndInsertOneMessage(uid, msgtime, (asread ? 1 : 0), hdrs, body, tags);
3292 if (progresscb !is null) progresscb(count, total, nntpidx, tags);
3294 if (relink) {
3295 while (tags.length) {
3296 auto eep = tags.indexOf('|');
3297 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
3298 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
3299 if (tagname.length == 0) continue;
3301 immutable uint tuid = chiroGetTagUid(tagname);
3302 if (tuid == 0) continue;
3304 bool found = false;
3305 foreach (immutable n; uptagids) if (n == tuid) { found = true; break; }
3306 if (!found) uptagids ~= tuid;
3311 if (relink && uptagids.length) {
3312 foreach (immutable tagid; uptagids) chiroSupportRelinkTagThreads(tagid);
3316 return count;
3321 returns accouint uid (accid) or 0.
3323 public uint chiroGetAccountUid (const(char)[] accname) {
3324 static auto stat = LazyStatement!"Conf"(`SELECT accid AS accid FROM accounts WHERE name=:accname LIMIT 1;`);
3325 foreach (auto row; stat.st.bindConstText(":accname", accname).range) return row.accid!uint;
3326 return 0;
3331 returns accouint name, or empty string.
3333 public DynStr chiroGetAccountName (uint accid) {
3334 static auto stat = LazyStatement!"Conf"(`SELECT name AS name FROM accounts WHERE accid=:accid LIMIT 1;`);
3335 DynStr res;
3336 if (accid == 0) return res;
3337 foreach (auto row; stat.st.bind(":accid", accid).range) {
3338 res = row.name!SQ3Text;
3339 break;
3341 return res;
3345 public struct AccountInfo {
3346 uint accid;
3347 DynStr name;
3348 DynStr realname;
3349 DynStr email;
3350 DynStr nntpgroup;
3351 DynStr inbox;
3353 @property bool isValid () const pure nothrow @safe @nogc { return (accid != 0); }
3356 public bool chiroGetAccountInfo (uint accid, out AccountInfo nfo) {
3357 static auto stat = LazyStatement!"Conf"(`
3358 SELECT name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup, inbox AS inbox
3359 FROM accounts
3360 WHERE accid=:accid LIMIT 1
3361 ;`);
3362 if (accid == 0) return false;
3363 foreach (auto row; stat.st.bind(":accid", accid).range) {
3364 nfo.accid = accid;
3365 nfo.name = row.name!SQ3Text;
3366 nfo.realname = row.realname!SQ3Text;
3367 nfo.email = row.email!SQ3Text;
3368 nfo.nntpgroup = row.nntpgroup!SQ3Text;
3369 nfo.inbox = row.inbox!SQ3Text;
3370 return true;
3372 return false;
3375 public bool chiroGetAccountInfo (const(char)[] accname, out AccountInfo nfo) {
3376 static auto stat = LazyStatement!"Conf"(`
3377 SELECT accid AS accid, name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup, inbox AS inbox
3378 FROM accounts
3379 WHERE name=:name LIMIT 1
3380 ;`);
3381 if (accname.length == 0) return false;
3382 foreach (auto row; stat.st.bindConstText(":name", accname).range) {
3383 nfo.accid = row.accid!uint;
3384 nfo.name = row.name!SQ3Text;
3385 nfo.realname = row.realname!SQ3Text;
3386 nfo.email = row.email!SQ3Text;
3387 nfo.nntpgroup = row.nntpgroup!SQ3Text;
3388 nfo.inbox = row.inbox!SQ3Text;
3389 return true;
3391 return false;
3396 returns list of known tags, sorted by name.
3398 public DynStr[] chiroGetTagList () {
3399 static auto stat = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE hidden=0 ORDER BY tag;`);
3400 DynStr[] res;
3401 foreach (auto row; stat.st.range) res ~= DynStr(row.tagname!SQ3Text);
3402 return res;
3407 returns tag uid (tagid) or 0.
3409 public uint chiroGetTagUid (const(char)[] tagname) {
3410 static auto stat = LazyStatement!"View"(`SELECT tagid AS tagid FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3411 foreach (auto row; stat.st.bindConstText(":tagname", tagname).range) {
3412 return row.tagid!uint;
3414 return 0;
3419 returns tag name or empty string.
3421 public DynStr chiroGetTagName (uint tagid) {
3422 static auto stat = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3423 DynStr s;
3424 foreach (auto row; stat.st.bind(":tagid", tagid).range) {
3425 s = row.tagname!SQ3Text;
3426 break;
3428 return s;
3433 returns `true` if the given tag supports threads.
3435 this is used only when adding new messages, to set all parents to 0.
3437 public bool chiroIsTagThreaded(T) (T tagnameid)
3438 if (is(T:const(char)[]) || is(T:uint))
3440 static if (is(T:const(char)[])) {
3441 static auto stat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3442 foreach (auto row; stat.st.bindConstText(":tagname", tagnameid).range) {
3443 return (row.threading!uint == 1);
3445 } else {
3446 static auto xstat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3447 foreach (auto row; xstat.st.bind(":tagid", tagnameid).range) {
3448 return (row.threading!uint == 1);
3451 return false;
3456 returns `true` if the given tag allows attaches.
3458 this is used only when adding new messages, to set all parents to 0.
3460 public bool chiroIsTagAllowAttaches(T) (T tagnameid)
3461 if (is(T:const(char)[]) || is(T:uint))
3463 static if (is(T:const(char)[])) {
3464 static auto stat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3465 foreach (auto row; stat.st.bindConstText(":tagname", tagnameid).range) {
3466 return (row.threading!uint == 1);
3468 } else {
3469 static auto xstat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3470 foreach (auto row; xstat.st.bind(":tagid", tagnameid).range) {
3471 return (row.threading!uint == 1);
3474 return false;
3479 relinks all messages in all threads suitable for relinking, and
3480 sets parents to zero otherwise.
3482 public void chiroSupportRelinkAllThreads () {
3483 // yeah, that's it: a single SQL statement
3484 dbView.execute(`
3485 -- clear parents where threading is disabled
3486 SELECT ChiroTimerStart('clearing parents');
3487 UPDATE threads
3489 parent = 0
3490 WHERE
3491 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=0)
3492 AND parent <> 0
3494 SELECT ChiroTimerStop();
3496 SELECT ChiroTimerStart('relinking threads');
3497 UPDATE threads
3499 parent=ifnull(
3501 SELECT uid FROM msgids
3502 WHERE
3503 -- find MSGID for any of our current references
3504 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3505 -- check if UID for that MSGID has the valid tag
3506 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=threads.tagid)
3507 ORDER BY time DESC
3508 LIMIT 1
3510 , 0)
3511 WHERE
3512 -- do not process messages with non-threading tags
3513 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=1)
3515 SELECT ChiroTimerStop();
3521 relinks all messages for the given tag, or sets parents to zero if
3522 threading for that tag is disabled.
3524 public void chiroSupportRelinkTagThreads(T) (T tagnameid)
3525 if (is(T:const(char)[]) || is(T:uint))
3527 static if (is(T:const(char)[])) {
3528 immutable uint tid = chiroGetTagUid(tagnameid);
3529 if (!tid) return;
3530 } else {
3531 alias tid = tagnameid;
3534 static auto statNoTrd = LazyStatement!"View"(`
3535 UPDATE threads
3537 parent = 0
3538 WHERE
3539 tagid = :tagid AND parent <> 0
3540 ;`);
3542 static auto statTrd = LazyStatement!"View"(`
3543 UPDATE threads
3545 parent=ifnull(
3547 SELECT uid FROM msgids
3548 WHERE
3549 -- find MSGID for any of our current references
3550 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3551 -- check if UID for that MSGID has the valid tag
3552 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=:tagid)
3553 ORDER BY time DESC
3554 LIMIT 1
3556 , 0)
3557 WHERE
3558 threads.tagid = :tagid
3559 ;`);
3561 if (!chiroIsTagThreaded(tid)) {
3562 // clear parents (just in case)
3563 statNoTrd.st.bind(":tagid", tid).doAll();
3564 } else {
3565 // yeah, that's it: a single SQL statement
3566 statTrd.st.bind(":tagid", tid).doAll();
3572 * get "from info" for the given message.
3574 * returns `false` if there is no such message.
3576 public bool chiroGetMessageFrom (uint uid, ref DynStr fromMail, ref DynStr fromName) {
3577 static auto statGetFrom = LazyStatement!"View"(`
3578 SELECT
3579 from_name AS fromName
3580 , from_mail AS fromMail
3581 FROM info
3582 WHERE uid=:uid
3583 LIMIT 1
3584 ;`);
3585 fromMail.clear();
3586 fromName.clear();
3587 foreach (auto row; statGetFrom.st.bind(":uid", uid).range) {
3588 fromMail = row.fromMail!SQ3Text;
3589 fromName = row.fromName!SQ3Text;
3590 return true;
3592 return false;
3597 gets twit title and state for the given (tagid, uid) message.
3599 returns -666 if there is no such message.
3601 public DynStr chiroGetMessageTwit(T) (T tagidname, uint uid, out bool twited)
3602 if (is(T:const(char)[]) || is(T:uint))
3604 twited = false;
3605 DynStr res;
3606 if (!uid) return res;
3608 static if (is(T:const(char)[])) {
3609 immutable uint tid = chiroGetTagUid(tagidname);
3610 if (!tid) return 0;
3611 enum selHdr = ``;
3612 } else {
3613 alias tid = tagidname;
3616 if (!tid) return res;
3618 static auto statGetTwit = LazyStatement!"View"(`
3619 SELECT title AS title
3620 FROM threads
3621 WHERE uid=:uid AND tagid=:tagid AND mute>0
3622 LIMIT 1
3623 ;`);
3625 statGetTwit.st
3626 .bind(":uid", uid)
3627 .bind(":tagid", tid);
3628 foreach (auto row; statGetTwit.st.range) {
3629 twited = true;
3630 res = row.title!SQ3Text;
3633 return res;
3638 gets mute state for the given (tagid, uid) message.
3640 returns -666 if there is no such message.
3642 public int chiroGetMessageMute(T) (T tagidname, uint uid)
3643 if (is(T:const(char)[]) || is(T:uint))
3645 if (!uid) return -666;
3647 static if (is(T:const(char)[])) {
3648 immutable uint tid = chiroGetTagUid(tagidname);
3649 if (!tid) return 0;
3650 enum selHdr = ``;
3651 } else {
3652 alias tid = tagidname;
3655 if (!tid) return -666;
3657 static auto statGetApp = LazyStatement!"View"(`
3658 SELECT mute AS mute
3659 FROM threads
3660 WHERE uid=:uid AND tagid=:tagid
3661 LIMIT 1
3662 ;`);
3664 statGetApp.st
3665 .bind(":uid", uid)
3666 .bind(":tagid", tid);
3667 foreach (auto row; statGetApp.st.range) return row.mute!int;
3668 return -666;
3673 sets mute state the given (tagid, uid) message.
3675 doesn't change children states.
3677 public void chiroSetMessageMute(T) (T tagidname, uint uid, Mute mute)
3678 if (is(T:const(char)[]) || is(T:uint))
3680 if (!uid) return;
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;
3692 static auto statSetApp = LazyStatement!"View"(`
3693 UPDATE threads
3695 mute=:mute
3696 WHERE
3697 uid=:uid AND tagid=:tagid
3698 ;`);
3700 static auto statSetAppRead = LazyStatement!"View"(`
3701 UPDATE threads
3703 mute=:mute
3704 , appearance=iif(appearance=0,1,appearance)
3705 WHERE
3706 uid=:uid AND tagid=:tagid
3707 ;`);
3709 if (mute > Mute.Normal) {
3710 statSetAppRead.st
3711 .bind(":mute", cast(int)mute)
3712 .bind(":uid", uid)
3713 .bind(":tagid", tid)
3714 .doAll();
3715 } else {
3716 statSetApp.st
3717 .bind(":mute", cast(int)mute)
3718 .bind(":uid", uid)
3719 .bind(":tagid", tid)
3720 .doAll();
3726 gets appearance for the given (tagid, uid) message.
3728 returns -666 if there is no such message.
3730 public int chiroGetMessageAppearance(T) (T tagidname, uint uid)
3731 if (is(T:const(char)[]) || is(T:uint))
3733 if (!uid) return -666;
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 -666;
3745 static auto statGetApp = LazyStatement!"View"(`
3746 SELECT appearance AS appearance
3747 FROM threads
3748 WHERE uid=:uid AND tagid=:tagid
3749 LIMIT 1
3750 ;`);
3752 statGetApp.st
3753 .bind(":uid", uid)
3754 .bind(":tagid", tid);
3755 foreach (auto row; statGetApp.st.range) return row.appearance!int;
3756 return -666;
3761 gets appearance for the given (tagid, uid) message.
3763 public bool chiroGetMessageUnread(T) (T tagidname, uint uid)
3764 if (is(T:const(char)[]) || is(T:uint))
3766 return (chiroGetMessageAppearance(tagidname, uid) == Appearance.Unread);
3771 gets appearance for the given (tagid, uid) message.
3773 public bool chiroGetMessageExactRead(T) (T tagidname, uint uid)
3774 if (is(T:const(char)[]) || is(T:uint))
3776 return (chiroGetMessageAppearance(tagidname, uid) == Appearance.Read);
3781 sets appearance for the given (tagid, uid) message.
3783 public void chiroSetMessageAppearance(T) (T tagidname, uint uid, Appearance appearance)
3784 if (is(T:const(char)[]) || is(T:uint))
3786 if (!uid) return;
3788 static if (is(T:const(char)[])) {
3789 immutable uint tid = chiroGetTagUid(tagidname);
3790 if (!tid) return 0;
3791 enum selHdr = ``;
3792 } else {
3793 alias tid = tagidname;
3796 if (!tid) return;
3798 static auto statSetApp = LazyStatement!"View"(`
3799 UPDATE threads
3801 appearance=:appearance
3802 WHERE
3803 uid=:uid AND tagid=:tagid
3804 ;`);
3806 statSetApp.st
3807 .bind(":appearance", cast(int)appearance)
3808 .bind(":uid", uid)
3809 .bind(":tagid", tid)
3810 .doAll();
3815 mark (tagid, uid) message as read.
3817 public void chiroSetReadOrUnreadMessageAppearance(T) (T tagidname, uint uid, Appearance appearance)
3818 if (is(T:const(char)[]) || is(T:uint))
3820 if (!uid) return;
3822 static if (is(T:const(char)[])) {
3823 immutable uint tid = chiroGetTagUid(tagidname);
3824 if (!tid) return 0;
3825 enum selHdr = ``;
3826 } else {
3827 alias tid = tagidname;
3830 if (!tid) return;
3832 static auto statSetApp = LazyStatement!"View"(`
3833 UPDATE threads
3835 appearance=:setapp
3836 WHERE
3837 uid=:uid AND tagid=:tagid AND (appearance=:checkapp0 OR appearance=:checkapp1)
3838 ;`);
3840 statSetApp.st
3841 .bind(":uid", uid)
3842 .bind(":tagid", tid)
3843 .bind(":setapp", cast(int)appearance)
3844 .bind(":checkapp0", Appearance.Read)
3845 .bind(":checkapp1", Appearance.Unread)
3846 .doAll();
3851 mark (tagid, uid) message as read.
3853 public void chiroSetMessageRead(T) (T tagidname, uint uid)
3854 if (is(T:const(char)[]) || is(T:uint))
3856 chiroSetReadOrUnreadMessageAppearance(tagidname, uid, Appearance.Read);
3860 public void chiroSetMessageUnread(T) (T tagidname, uint uid)
3861 if (is(T:const(char)[]) || is(T:uint))
3863 chiroSetReadOrUnreadMessageAppearance(tagidname, uid, Appearance.Unread);
3868 purge all messages with the given tag.
3870 this removes messages from all view tables, removes content from
3871 the "messages" table, and sets "messages" table tags to NULL.
3873 public void chiroDeletePurgedWithTag(T) (T tagidname)
3874 if (is(T:const(char)[]) || is(T:uint))
3876 static if (is(T:const(char)[])) {
3877 immutable uint tid = chiroGetTagUid(tagidname);
3878 if (!tid) return 0;
3879 enum selHdr = ``;
3880 } else {
3881 alias tid = tagidname;
3884 if (!tid) return;
3886 static auto statCountPurged = LazyStatement!"View"(`
3887 SELECT COUNT(uid) AS pcount FROM threads
3888 WHERE tagid=:tagid AND appearance=:appr
3891 uint purgedCount = 0;
3892 foreach (auto row; statCountPurged.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).range) {
3893 purgedCount = row.pcount!uint;
3895 if (!purgedCount) return;
3897 // we will need this to clear storage
3898 uint[] plist;
3899 scope(exit) delete plist;
3900 plist.reserve(purgedCount);
3902 static auto statListPurged = LazyStatement!"View"(`
3903 SELECT uid AS uid FROM threads
3904 WHERE tagid=:tagid AND appearance=:appr
3905 ORDER BY uid
3908 foreach (auto row; statListPurged.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).range) {
3909 plist ~= row.uid!uint;
3911 if (plist.length == 0) return; // just in case
3913 static auto statClearStorage = LazyStatement!"Store"(`
3914 UPDATE messages
3915 SET tags=NULL, data=NULL
3916 WHERE uid=:uid
3917 ;`);
3919 enum BulkClearSQL(string table) = `
3920 DELETE FROM `~table~`
3921 WHERE
3922 uid IN (SELECT uid FROM threads WHERE tagid=:tagid AND appearance=:appr)
3925 // bulk clearing of info
3926 static auto statClearInfo = LazyStatement!"View"(BulkClearSQL!"info");
3927 // bulk clearing of msgids
3928 static auto statClearMsgids = LazyStatement!"View"(BulkClearSQL!"msgids");
3929 // bulk clearing of refids
3930 static auto statClearRefids = LazyStatement!"View"(BulkClearSQL!"refids");
3931 // bulk clearing of text
3932 static auto statClearText = LazyStatement!"View"(BulkClearSQL!"content_text");
3933 // bulk clearing of html
3934 static auto statClearHtml = LazyStatement!"View"(BulkClearSQL!"content_html");
3935 // bulk clearing of attaches
3936 static auto statClearAttach = LazyStatement!"View"(BulkClearSQL!"attaches");
3937 // bulk clearing of threads
3938 static auto statClearThreads = LazyStatement!"View"(`
3939 DELETE FROM threads
3940 WHERE tagid=:tagid AND appearance=:appr
3941 ;`);
3943 static if (is(T:const(char)[])) {
3944 conwriteln("removing ", plist.length, " message", (plist.length != 1 ? "s" : ""), " from '", tagidname, "'...");
3945 } else {
3946 DynStr tname = chiroGetTagName(tid);
3947 conwriteln("removing ", plist.length, " message", (plist.length != 1 ? "s" : ""), " from '", tname.getData, "'...");
3950 // WARNING! "info" must be cleared FIRST, and "threads" LAST
3951 transacted!"View"{
3952 statClearInfo.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3953 statClearMsgids.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3954 statClearRefids.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3955 statClearText.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3956 statClearHtml.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3957 statClearAttach.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3958 statClearThreads.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3959 // relink tag threads
3960 chiroSupportRelinkTagThreads(tid);
3963 // now clear the storage
3964 conwriteln("clearing the storage...");
3965 transacted!"Store"{
3966 foreach (immutable uint uid; plist) {
3967 statClearStorage.st.bind(":uid", uid).doAll();
3971 conwriteln("done purging.");
3976 creates "treepane" table for the given tag. that table can be used to
3977 render threaded listview.
3979 returns max id of the existing item. can be used for pagination.
3980 item ids are guaranteed to be sequential, and without any holes.
3981 the first id is `1`.
3983 returned table has "rowid", and two integer fields: "uid" (message uid), and
3984 "level" (message depth, starting from 0).
3986 public uint chiroCreateTreePaneTable(T) (T tagidname, int lastmonthes=12, bool allowThreading=true)
3987 if (is(T:const(char)[]) || is(T:uint))
3989 auto ctm = Timer(true);
3991 // shrink temp table to the bare minimum, because each field costs several msecs
3992 // we don't need parent and time here, because we can easily select them with inner joins
3994 dbView.execute(`
3995 DROP TABLE IF EXISTS treepane;
3996 CREATE TEMP TABLE IF NOT EXISTS treepane (
3997 iid INTEGER PRIMARY KEY
3998 , uid INTEGER
3999 , level INTEGER
4000 -- to make joins easier
4001 , tagid INTEGER
4006 // this need to add answers to some ancient crap
4007 static auto statFirstUnreadTime = LazyStatement!"View"(`
4008 SELECT MIN(time) AS time, parent AS parent
4009 FROM threads
4010 WHERE tagid=:tagidname AND appearance=:app
4011 ;`);
4013 static auto statFindParentFor = LazyStatement!"View"(`
4014 SELECT time AS time, parent AS parent
4015 FROM threads
4016 WHERE tagid=:tagidname AND uid=:uid
4017 LIMIT 1
4018 ;`);
4020 // clear it (should be faster than dropping and recreating)
4021 dbView.execute(`DELETE FROM treepane;`);
4023 // this "%08X" will do up to 2038; i'm fine with it
4024 static auto statTrd = LazyStatement!"View"(`
4025 INSERT INTO treepane
4026 (uid, level, tagid)
4027 WITH tree(uid, parent, level, time, path) AS (
4028 WITH RECURSIVE fulltree(uid, parent, level, time, path) AS (
4029 SELECT t.uid AS uid, t.parent AS parent, 1 AS level, t.time AS time, printf('%08X', t.time) AS path
4030 FROM threads t
4031 WHERE t.time>=:starttime AND parent=0 AND t.tagid=:tagidname AND t.appearance <> -1
4032 UNION ALL
4033 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
4034 FROM threads t, fulltree ft
4035 WHERE t.time>=:starttime AND t.parent=ft.uid AND t.tagid=:tagidname AND t.appearance <> -1
4037 SELECT * FROM fulltree
4039 SELECT
4040 tree.uid AS uid
4041 , tree.level-1 AS level
4042 , :tagidname AS tagid
4043 FROM tree
4044 ORDER BY path
4045 ;`);
4047 static auto statNoTrd = LazyStatement!"View"(`
4048 INSERT INTO treepane
4049 (uid, level, tagid)
4050 SELECT
4051 threads.uid AS uid
4052 , 0 AS level
4053 , :tagidname AS tagid
4054 FROM threads
4055 WHERE
4056 threads.time>=:starttime AND threads.tagid=:tagidname AND threads.appearance <> -1
4057 ORDER BY
4058 threads.time
4059 ;`);
4061 static if (is(T:const(char)[])) {
4062 immutable uint tid = chiroGetTagUid(tagidname);
4063 if (!tid) return 0;
4064 enum selHdr = ``;
4065 } else {
4066 alias tid = tagidname;
4069 uint startTime = 0;
4071 if (lastmonthes > 0) {
4072 if (lastmonthes > 12*100) {
4073 startTime = 0;
4074 } else {
4075 // show last `lastmonthes` (full monthes)
4076 import std.datetime;
4077 import core.time : Duration;
4079 SysTime now = Clock.currTime().toUTC();
4080 int year = now.year;
4081 int month = now.month; // from 1
4082 --lastmonthes;
4083 // yes, i am THAT lazy
4084 while (lastmonthes > 0) {
4085 if (month > lastmonthes) { month -= lastmonthes; break; }
4086 lastmonthes -= month;
4087 month = 12;
4088 --year;
4090 // construct unix time
4091 now.fracSecs = Duration.zero;
4092 now.second = 0;
4093 now.hour = 0;
4094 now.minute = 0;
4095 now.day = 1;
4096 now.month = cast(Month)month;
4097 now.year = year;
4098 startTime = cast(uint)now.toUnixTime();
4102 // check if we need to fix unread time
4103 // required to show the whole ancient thread if somebody answered
4104 if (startTime > 0) {
4105 uint unTime = 0;
4106 uint unParent = 0;
4107 foreach (auto row; statFirstUnreadTime.st.bind(":tagidname", tid).bind(":app", Appearance.Unread).range) {
4108 unTime = row.time!uint;
4109 unParent = row.parent!uint;
4111 if (unTime > 0 && unTime < startTime) {
4112 // find root message, and start from it
4113 startTime = unTime;
4114 while (unParent && allowThreading) {
4115 statFindParentFor.st
4116 .bind(":tagidname", tid)
4117 .bind(":uid", unParent);
4118 unParent = 0;
4119 unTime = 0;
4120 foreach (auto row; statFindParentFor.st.range) {
4121 unTime = row.time!uint;
4122 unParent = row.parent!uint;
4124 if (unTime > 0 && unTime < startTime) startTime = unTime;
4129 if (allowThreading) {
4130 statTrd.st.bind(":tagidname", tid).bind(":starttime", startTime).doAll();
4131 } else {
4132 statNoTrd.st.bind(":tagidname", tid).bind(":starttime", startTime).doAll();
4134 ctm.stop;
4135 if (ChiroTimerEnabled) writeln("creating treepane time: ", ctm);
4137 immutable uint res = cast(uint)dbView.lastRowId;
4139 version(chidb_drop_pane_table) {
4140 dbView.execute(`CREATE INDEX treepane_uid ON treepane(uid);`);
4143 return res;
4148 returns current treepane tagid.
4150 public uint chiroGetTreePaneTableTagId () {
4151 static auto stat = LazyStatement!"View"(`SELECT tagid AS tagid FROM treepane WHERE iid=1 LIMIT 1;`);
4152 foreach (auto row; stat.st.range) return row.tagid!uint;
4153 return 0;
4158 returns current treepane max uid.
4160 public uint chiroGetTreePaneTableMaxUId () {
4161 static auto stat = LazyStatement!"View"(`SELECT MAX(uid) AS uid FROM treepane LIMIT 1;`);
4162 foreach (auto row; stat.st.range) return row.uid!uint;
4163 return 0;
4168 returns number of items in the current treepane.
4170 public uint chiroGetTreePaneTableCount () {
4171 static auto stat = LazyStatement!"View"(`SELECT COUNT(*) AS total FROM treepane;`);
4172 foreach (auto row; stat.st.range) return row.total!uint;
4173 return 0;
4178 returns index of the given uid in the treepane.
4180 public bool chiroIsTreePaneTableUidValid (uint uid) {
4181 static auto stat = LazyStatement!"View"(`SELECT iid AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
4182 if (uid == 0) return false;
4183 foreach (auto row; stat.st.bind(":uid", uid).range) return true;
4184 return false;
4189 returns first treepane uid.
4191 public uint chiroGetTreePaneTableFirstUid () {
4192 static auto stmt = LazyStatement!"View"(`SELECT uid AS uid FROM treepane WHERE iid=1 LIMIT 1;`);
4193 foreach (auto row; stmt.st.range) return row.uid!uint;
4194 return 0;
4199 returns last treepane uid.
4201 public uint chiroGetTreePaneTableLastUid () {
4202 static auto stmt = LazyStatement!"View"(`SELECT MAX(iid), uid AS uid FROM treepane LIMIT 1;`);
4203 foreach (auto row; stmt.st.range) return row.uid!uint;
4204 return 0;
4209 returns index of the given uid in the treepane.
4211 public int chiroGetTreePaneTableUid2Index (uint uid) {
4212 static auto stmt = LazyStatement!"View"(`SELECT iid-1 AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
4213 if (uid == 0) return -1;
4214 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.idx!int;
4215 return -1;
4220 returns uid of the given index in the treepane.
4222 public uint chiroGetTreePaneTableIndex2Uid (int index) {
4223 static auto stmt = LazyStatement!"View"(`SELECT uid AS uid FROM treepane WHERE iid=:idx+1 LIMIT 1;`);
4224 if (index < 0 || index == int.max) return 0;
4225 foreach (auto row; stmt.st.bind(":idx", index).range) return row.uid!uint;
4226 return 0;
4231 returns previous uid in the treepane.
4233 public uint chiroGetTreePaneTablePrevUid (uint uid) {
4234 static auto stmt = LazyStatement!"View"(`
4235 SELECT uid AS uid FROM treepane
4236 WHERE iid IN (SELECT iid-1 FROM treepane WHERE uid=:uid LIMIT 1)
4237 LIMIT 1
4238 ;`);
4239 if (uid == 0) return chiroGetTreePaneTableFirstUid();
4240 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.uid!uint;
4241 return 0;
4246 returns uid of the given index in the treepane.
4248 public uint chiroGetTreePaneTableNextUid (uint uid) {
4249 static auto stmt = LazyStatement!"View"(`
4250 SELECT uid AS uid FROM treepane
4251 WHERE iid IN (SELECT iid+1 FROM treepane WHERE uid=:uid LIMIT 1)
4252 LIMIT 1
4253 ;`);
4254 if (uid == 0) return chiroGetTreePaneTableFirstUid();
4255 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.uid!uint;
4256 return 0;
4261 releases (drops) "treepane" table.
4263 can be called several times, but usually you don't need to call this at all.
4265 public void chiroClearTreePaneTable () {
4266 //dbView.execute(`DROP TABLE IF EXISTS treepane;`);
4267 dbView.execute(`DELETE FROM treepane;`);
4272 return next unread message uid in treepane, or 0.
4274 public uint chiroGetPaneNextUnread (uint curruid) {
4275 static auto stmtNext = LazyStatement!"View"(`
4276 SELECT treepane.uid AS uid FROM treepane
4277 INNER JOIN threads USING(uid, tagid)
4278 WHERE treepane.iid-1 > :cidx AND threads.appearance=:appr
4279 ORDER BY iid
4280 LIMIT 1
4281 ;`);
4282 immutable int cidx = chiroGetTreePaneTableUid2Index(curruid);
4283 foreach (auto row; stmtNext.st.bind(":cidx", cidx).bind(":appr", Appearance.Unread).range) return row.uid!uint;
4284 if (curruid) {
4285 // try from the beginning
4286 foreach (auto row; stmtNext.st.bind(":cidx", -1).bind(":appr", Appearance.Unread).range) return row.uid!uint;
4288 return 0;
4293 selects given number of items starting with the given item id.
4295 returns numer of selected items.
4297 `stiid` counts from zero
4299 WARNING! "treepane" table must be prepared with `chiroCreateTreePaneTable()`!
4301 WARNING! [i]dup `SQ3Text` arguments if necessary, they won't survive the `cb` return!
4303 public int chiroGetPaneTablePage (int stiid, int limit,
4304 void delegate (int pgofs, /* offset from the page start, from zero and up to `limit` */
4305 int iid, /* item id, counts from zero*/
4306 uint uid, /* msguid, never zero */
4307 uint parentuid, /* parent msguid, may be zero */
4308 uint level, /* threading level, from zero */
4309 Appearance appearance, /* see above */
4310 Mute mute, /* see above */
4311 SQ3Text date, /* string representation of receiving date and time */
4312 SQ3Text subj, /* message subject, can be empty string */
4313 SQ3Text fromName, /* message sender name, can be empty string */
4314 SQ3Text fromMail, /* message sender email, can be empty string */
4315 SQ3Text title) cb /* title from twiting */
4317 static auto stat = LazyStatement!"View"(`
4318 SELECT
4319 treepane.iid AS iid
4320 , treepane.uid AS uid
4321 , treepane.level AS level
4322 , threads.parent AS parent
4323 , threads.appearance AS appearance
4324 , threads.mute AS mute
4325 , datetime(threads.time, 'unixepoch') AS time
4326 , info.subj AS subj
4327 , info.from_name AS from_name
4328 , info.from_mail AS from_mail
4329 , threads.title AS title
4330 FROM treepane
4331 INNER JOIN info USING(uid)
4332 INNER JOIN threads USING(uid, tagid)
4333 WHERE treepane.iid >= :stiid
4334 ORDER BY treepane.iid
4335 LIMIT :limit
4338 if (limit <= 0) return 0;
4339 if (stiid < 0) {
4340 if (stiid == int.min) return 0;
4341 limit += stiid;
4342 if (limit <= 0) return 0;
4343 stiid = 0;
4345 int total = 0;
4346 foreach (auto row; stat.st.bind(":stiid", stiid+1).bind(":limit", limit).range)
4348 if (cb !is null) {
4349 cb(total, row.iid!int, row.uid!uint, row.parent!uint, row.level!uint,
4350 cast(Appearance)row.appearance!int, cast(Mute)row.mute!int,
4351 row.time!SQ3Text, row.subj!SQ3Text, row.from_name!SQ3Text, row.from_mail!SQ3Text, row.title!SQ3Text);
4353 ++total;
4355 return total;
4359 // ////////////////////////////////////////////////////////////////////////// //
4360 /** returns full content of the messare or `null` if no message found (or it was deleted).
4362 public DynStr chiroGetFullMessageContent (uint uid) {
4363 DynStr res;
4364 if (uid == 0) return res;
4365 foreach (auto row; dbStore.statement(`SELECT ChiroUnpack(data) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid).range) {
4366 res = row.result!SQ3Text;
4367 return res;
4369 return res;
4373 /** returns full content of the messare or `null` if no message found (or it was deleted).
4375 public DynStr chiroMessageHeaders (uint uid) {
4376 DynStr res;
4377 if (uid == 0) return res;
4378 foreach (auto row; dbStore.statement(`SELECT ChiroExtractHeaders(ChiroUnpack(data)) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid).range) {
4379 res = row.result!SQ3Text;
4380 return res;
4382 return res;
4386 /** returns full content of the messare or `null` if no message found (or it was deleted).
4388 public DynStr chiroMessageBody (uint uid) {
4389 DynStr res;
4390 if (uid == 0) return res;
4391 foreach (auto row; dbStore.statement(`SELECT ChiroExtractBody(ChiroUnpack(data)) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid).range) {
4392 res = row.result!SQ3Text;
4393 return res;
4395 return res;
4399 // ////////////////////////////////////////////////////////////////////////// //
4400 public enum Bogo {
4401 Error, // some error occured
4402 Ham,
4403 Unsure,
4404 Spam,
4407 public Bogo messageBogoCheck (uint uid) {
4408 if (uid == 0) return Bogo.Error;
4409 DynStr content = chiroGetFullMessageContent(uid);
4410 if (content.length == 0) return Bogo.Error;
4412 try {
4413 import std.process;
4414 //{ auto fo = VFile("/tmp/zzzz", "w"); fo.rawWriteExact(art.data); }
4415 auto pipes = pipeProcess(["/usr/bin/bogofilter", "-T"]);
4416 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4417 //pipes.stdin.writeln();
4418 //foreach (string s; art.text) pipes.stdin.writeln(s);
4419 pipes.stdin.writeln(content.getData.xstripright);
4420 pipes.stdin.flush();
4421 pipes.stdin.close();
4422 auto res = pipes.stdout.readln();
4423 wait(pipes.pid);
4424 //conwriteln("RESULT: [", res, "]");
4425 if (res.length == 0) {
4426 //conwriteln("ERROR: bogofilter returned nothing");
4427 return Bogo.Error;
4429 if (res[0] == 'H') return Bogo.Ham;
4430 if (res[0] == 'U') return Bogo.Unsure;
4431 if (res[0] == 'S') return Bogo.Spam;
4432 //while (res.length && res[$-1] <= ' ') res = res[0..$-1];
4433 //conwriteln("ERROR: bogofilter returned some shit: [", res, "]");
4434 } catch (Exception e) { // sorry
4435 //conwriteln("ERROR bogofiltering: ", e.msg);
4438 return Bogo.Error;
4442 // ////////////////////////////////////////////////////////////////////////// //
4443 private void messageBogoMarkSpamHam(bool spam) (uint uid) {
4444 if (uid == 0) return;
4445 DynStr content = chiroGetFullMessageContent(uid);
4446 if (content.length == 0) return;
4448 static if (spam) enum arg = "-s"; else enum arg = "-n";
4449 try {
4450 import std.process;
4451 auto pipes = pipeProcess(["/usr/bin/bogofilter", arg]);
4452 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4453 //pipes.stdin.writeln();
4454 //foreach (string s; art.text) pipes.stdin.writeln(s);
4455 pipes.stdin.writeln(content.getData.xstripright);
4456 pipes.stdin.flush();
4457 pipes.stdin.close();
4458 wait(pipes.pid);
4459 } catch (Exception e) { // sorry
4460 //conwriteln("ERROR bogofiltering: ", e.msg);
4465 public void messageBogoMarkHam (uint uid) { messageBogoMarkSpamHam!false(uid); }
4466 public void messageBogoMarkSpam (uint uid) { messageBogoMarkSpamHam!true(uid); }
4469 // ////////////////////////////////////////////////////////////////////////// //
4470 public alias TwitProcessCallback = void delegate (const(char)[] msg, uint curr, uint total);
4472 void processEmailTwits (TwitProcessCallback cb) {
4473 enum Message = "processing email twits";
4475 auto stFindTwitNameEmail = LazyStatement!"View"(`
4476 SELECT
4477 threads.uid AS uid
4478 , threads.tagid AS tagid
4479 FROM threads
4480 INNER JOIN info AS ii ON
4481 ii.uid=threads.uid AND
4482 ii.from_mail=:email AND
4483 ii.from_name=:name
4484 WHERE mute=0
4485 ;`);
4487 auto stFindTwitEmail = LazyStatement!"View"(`
4488 SELECT
4489 threads.uid AS uid
4490 , threads.tagid AS tagid
4491 FROM threads
4492 INNER JOIN info AS ii ON
4493 ii.uid=threads.uid AND
4494 ii.from_mail=:email
4495 WHERE mute=0
4496 ;`);
4498 auto stFindTwitName = LazyStatement!"View"(`
4499 SELECT
4500 threads.uid AS uid
4501 , threads.tagid AS tagid
4502 FROM threads
4503 INNER JOIN info AS ii ON
4504 ii.uid=threads.uid AND
4505 ii.from_name=:name
4506 WHERE mute=0
4507 ;`);
4510 auto stFindTwitNameEmailMasked = LazyStatement!"View"(`
4511 SELECT
4512 threads.uid AS uid
4513 , threads.tagid AS tagid
4514 FROM threads
4515 INNER JOIN info AS ii ON
4516 ii.uid=threads.uid AND
4517 ii.from_name=:name AND
4518 ii.from_mail GLOB :email
4519 WHERE mute=0
4520 ;`);
4522 auto stFindTwitEmailMasked = LazyStatement!"View"(`
4523 SELECT
4524 threads.uid AS uid
4525 , threads.tagid AS tagid
4526 FROM threads
4527 INNER JOIN info AS ii ON
4528 ii.uid=threads.uid AND
4529 ii.from_mail GLOB :email
4530 WHERE mute=0
4531 ;`);
4534 auto stUpdateMute = LazyStatement!"View"(`
4535 UPDATE threads
4536 SET mute=:mute, title=:title
4537 WHERE uid=:uid AND tagid=:tagid AND mute=0
4538 ;`);
4540 static struct UidTag {
4541 uint uid;
4542 uint tagid;
4545 uint twitcount = 0;
4546 foreach (auto trow; dbConf.statement(`SELECT COUNT(*) AS twitcount FROM emailtwits;`).range) twitcount = trow.twitcount!uint;
4548 if (cb !is null) cb(Message, 0, twitcount);
4550 dbView.execute(`
4551 CREATE TEMP TABLE IF NOT EXISTS disemails(
4552 email TEXT NOT NULL UNIQUE
4555 scope(exit) {
4556 if (cb !is null) cb("dropping temp tables", twitcount, twitcount);
4557 dbView.execute(`DROP TABLE IF EXISTS disemails;`);
4560 transacted!"View"{
4561 uint twitdone = 0;
4562 foreach (auto trow; dbConf.statement(`
4563 SELECT
4564 tagglob AS tagglob
4565 , email AS email
4566 , name AS name
4567 , title AS title
4568 FROM emailtwits
4569 WHERE email NOT LIKE '%*%'
4570 ;`).range)
4572 ++twitdone;
4573 auto title = trow.title!SQ3Text;
4574 if (title.length == 0) continue;
4575 auto email = trow.email!SQ3Text;
4576 auto name = trow.name!SQ3Text;
4577 assert(email.indexOf('*') < 0);
4578 DBStatement st;
4579 if (email.length && name.length) {
4580 st = stFindTwitNameEmail.st;
4581 st.bindConstText(":email", email).bindConstText(":name", name);
4582 } else if (email.length) {
4583 st = stFindTwitEmail.st;
4584 st.bindConstText(":email", email);
4585 } else if (name.length) {
4586 st = stFindTwitName.st;
4587 st.bindConstText(":name", name);
4588 } else {
4589 continue;
4591 UidTag[] msguids;
4592 msguids.reserve(128);
4593 scope(exit) delete msguids;
4594 //writeln("::: ", email, " : ", name);
4595 foreach (auto mrow; st.range) {
4596 auto tname = chiroGetTagName(mrow.tagid!uint);
4597 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4598 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4599 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4601 if (msguids.length == 0) continue;
4602 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4603 immutable bool muteAllow = title.startsWith("!"); // allow this
4604 //transacted!"View"{
4605 foreach (immutable pair; msguids) {
4606 stUpdateMute.st
4607 .bind(":uid", pair.uid)
4608 .bind(":tagid", pair.tagid)
4609 .bind(":mute", (muteAllow ? Mute.Never : Mute.ThreadStart))
4610 .bindConstText(":title", title)
4611 .doAll();
4613 //};
4614 if (cb !is null) cb(Message, twitdone, twitcount);
4617 if (cb !is null) cb("selecting distinct emails", twitdone, twitcount);
4618 dbView.execute(`
4619 DELETE FROM disemails;
4620 INSERT INTO disemails
4621 SELECT DISTINCT(from_mail) FROM info;
4623 if (cb !is null) cb(Message, twitdone, twitcount);
4625 version(all) {
4626 foreach (auto trow; dbConf.statement(`
4627 SELECT
4628 tagglob AS tagglob
4629 , email AS email
4630 , name AS name
4631 , title AS title
4632 FROM emailtwits
4633 WHERE email LIKE '%*%'
4634 ;`).range)
4636 ++twitdone;
4637 auto title = trow.title!SQ3Text;
4638 if (title.length == 0) continue;
4639 auto email = trow.email!SQ3Text;
4640 auto name = trow.name!SQ3Text;
4641 assert(email.indexOf('*') >= 0);
4642 assert(email.length);
4644 foreach (auto drow; dbView.statement(`SELECT email AS demail FROM disemails WHERE email GLOB :email;`)
4645 .bindConstText(":email", email).range)
4647 DBStatement st;
4648 if (name.length) {
4649 st = stFindTwitNameEmail.st;
4650 st.bindConstText(":email", drow.demail!SQ3Text).bindConstText(":name", name);
4651 } else {
4652 st = stFindTwitEmail.st;
4653 st.bindConstText(":email", drow.demail!SQ3Text);
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 //};
4680 if (cb !is null) cb(Message, twitdone, twitcount);
4682 } else {
4683 foreach (auto trow; dbConf.statement(`
4684 SELECT
4685 tagglob AS tagglob
4686 , email AS email
4687 , name AS name
4688 , title AS title
4689 FROM emailtwits
4690 WHERE email LIKE '%*%'
4691 ;`).range)
4693 ++twitdone;
4694 auto title = trow.title!SQ3Text;
4695 if (title.length == 0) continue;
4696 auto email = trow.email!SQ3Text;
4697 auto name = trow.name!SQ3Text;
4698 assert(email.indexOf('*') >= 0);
4699 assert(email.length);
4700 DBStatement st;
4701 if (email.length && name.length) {
4702 st = stFindTwitNameEmailMasked.st;
4703 st.bindConstText(":email", email).bindConstText(":name", name);
4704 } else {
4705 st = stFindTwitEmailMasked.st;
4706 st.bindConstText(":email", email);
4708 UidTag[] msguids;
4709 msguids.reserve(128);
4710 scope(exit) delete msguids;
4711 //writeln("::: ", email, " : ", name);
4712 foreach (auto mrow; st.range) {
4713 auto tname = chiroGetTagName(mrow.tagid!uint);
4714 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4715 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4716 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4718 if (msguids.length == 0) continue;
4719 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4720 immutable bool muteAllow = title.startsWith("!"); // allow this
4721 //transacted!"View"{
4722 foreach (immutable pair; msguids) {
4723 stUpdateMute.st
4724 .bind(":uid", pair.uid)
4725 .bind(":tagid", pair.tagid)
4726 .bind(":mute", (muteAllow ? Mute.Never : Mute.ThreadStart))
4727 .bindConstText(":title", title)
4728 .doAll();
4730 //};
4731 if (cb !is null) cb(Message, twitdone, twitcount);
4736 //if (cb !is null) cb(Message, twitcount, twitcount);
4740 void processMsgidTwits (TwitProcessCallback cb) {
4741 enum Message = "processing msgid twits";
4743 auto stUpdateMute = LazyStatement!"View"(`
4744 UPDATE threads
4745 SET mute=:mute, title=NULL
4746 WHERE uid=:uid AND tagid=:tagid AND mute=0
4747 ;`);
4749 static struct UidTag {
4750 uint uid;
4751 uint tagid;
4754 uint twitcount = 0;
4755 foreach (auto trow; dbConf.statement(`SELECT COUNT(*) AS twitcount FROM msgidtwits;`).range) twitcount = trow.twitcount!uint;
4757 if (cb !is null) cb(Message, 0, twitcount);
4759 transacted!"View"{
4760 uint twitdone = 0;
4761 foreach (auto trow; dbConf.statement(`SELECT msgid AS msgid, tagglob AS tagglob FROM msgidtwits;`).range) {
4762 ++twitdone;
4763 UidTag[] msguids;
4764 msguids.reserve(128);
4765 scope(exit) delete msguids;
4767 foreach (auto mrow; dbView.statement(`
4768 SELECT threads.uid AS uid, threads.tagid AS tagid
4769 FROM threads
4770 INNER JOIN msgids AS mm
4771 ON mm.msgid=:msgid AND mm.uid=threads.uid
4772 WHERE mute=0
4773 ;`).bindConstText(":msgid", trow.msgid!SQ3Text).range)
4775 auto tname = chiroGetTagName(mrow.tagid!uint);
4776 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4777 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4778 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4780 if (msguids.length == 0) continue;
4781 //conwriteln("updating ", msguids.length, " messages for msgid <", trow.msgid!SQ3Text, ">");
4782 //transacted!"View"{
4783 foreach (immutable pair; msguids) {
4784 stUpdateMute.st
4785 .bind(":uid", pair.uid)
4786 .bind(":tagid", pair.tagid)
4787 .bind(":mute", Mute.ThreadStart)
4788 .doAll();
4790 //};
4791 if (cb !is null) cb(Message, twitdone, twitcount);
4795 if (cb !is null) cb(Message, twitcount, twitcount);
4799 void processThreadMutes (TwitProcessCallback cb) {
4800 enum Message = "processing thread mutes";
4802 if (cb !is null) cb(Message, 0, 0);
4804 dbConf.execute(`
4805 ATTACH DATABASE '`~MailDBPath~`chiview.db' AS chiview;
4807 BEGIN TRANSACTION;
4809 --------------------------------------------------------------------------------
4810 -- create temp table with mute pairs
4811 SELECT ChiroTimerStart('creating mute pairs');
4812 CREATE TEMP TABLE mutepairs AS
4813 WITH RECURSIVE children(muid, paruid, mtagid) AS (
4814 SELECT 0, chiview.threads.uid, chiview.threads.tagid
4815 FROM chiview.threads
4816 WHERE chiview.threads.parent=0 AND chiview.threads.mute=2
4817 AND EXISTS (SELECT uid FROM chiview.threads AS tx WHERE tx.tagid=chiview.threads.tagid AND tx.parent=chiview.threads.uid)
4818 UNION ALL
4819 SELECT
4820 tt.uid, tt.uid, mtagid
4821 FROM children AS cc
4822 INNER JOIN chiview.threads AS tt
4824 tt.tagid=cc.mtagid AND
4825 tt.parent=cc.paruid AND
4826 tt.uid<>cc.muid AND
4827 tt.uid<>cc.paruid
4829 SELECT
4830 muid AS muid
4831 , mtagid AS mtagid
4832 FROM children
4833 WHERE muid<>0
4835 SELECT ChiroTimerStop();
4838 SELECT 'nested mute pairs to skip:', COUNT(uid)
4839 FROM chiview.threads
4840 INNER JOIN mutepairs AS tt
4842 tagid=tt.mtagid AND
4843 uid=tt.muid
4844 WHERE mute<>0
4848 SELECT ChiroTimerStart('updating thread mutes');
4849 UPDATE chiview.threads
4851 mute=3 -- child
4852 , appearance=(SELECT CASE WHEN appearance=0 THEN 1 ELSE appearance END)
4853 FROM (SELECT muid, mtagid FROM mutepairs) AS cc
4854 WHERE uid=cc.muid AND tagid=cc.mtagid AND mute=0
4856 SELECT ChiroTimerStop();
4858 DROP TABLE mutepairs;
4861 --SELECT 'secondary mutes:', COUNT(mute) FROM threads WHERE mute=3;
4864 COMMIT TRANSACTION;
4866 DETACH DATABASE chiview;
4871 public void chiroRecalcAllTwits (TwitProcessCallback cb) {
4872 // clear all twits
4873 conwriteln("clearing all mutes...");
4874 if (cb !is null) cb("clearing mutes", 0, 0);
4875 dbView.execute(`
4876 UPDATE threads
4877 SET mute=0, title=NULL
4878 ;`);
4879 conwriteln("processing email twits...");
4880 processEmailTwits(cb);
4881 conwriteln("processing msgid twits...");
4882 processMsgidTwits(cb);
4883 conwriteln("propagating thread twits...");
4884 processThreadMutes(cb);
4885 conwriteln("twit recalculation complete.");