faster twitting
[chiroptera.git] / chibackend / sqbase.d
blobcf005a5d889fd108de51223550e6add5d2482a39
1 /* E-Mail Client
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module chibackend.sqbase is aliced;
18 private:
20 //version = fts5_use_porter;
22 // do not use, for testing only!
23 // and it seems to generate bigger files, lol
24 //version = use_balz;
26 // use libdeflate instead of zlib
27 // see https://github.com/ebiggers/libdeflate
28 // around 2 times slower on level 9 than zlib, resulting size is 5MB less
29 // around 3 times slower on level 12, resulting size it 10MB less
30 // totally doesn't worth it
31 //version = use_libdeflate;
33 // use libxpack instead of zlib
34 // see https://github.com/ebiggers/xpack
35 // it supports buffers up to 2^19 (524288) bytes (see https://github.com/ebiggers/xpack/issues/1)
36 // therefore it is useless (the resulting file is bigger than with zlib)
37 //version = use_libxpack;
39 // just for fun
40 // see https://github.com/jibsen/brieflz
41 // it has spurious slowdowns, and so is 4 times slower than zlib, with worser compression
42 //version = use_libbrieflz;
44 // apple crap; i just wanted to see how bad it is ;-)
45 // speed is comparable with zlib, compression is shittier by 60MB; crap
46 //version = use_liblzfse;
48 // just for fun
49 //version = use_lzjb;
51 // just for fun, slightly better than lzjb
52 //version = use_lz4;
54 // some compressors from wimlib
55 // see https://wimlib.net/
56 // only one can be selected!
57 // 15 times slower than zlib, much worser compression (~100MB bigger)
58 //version = use_libwim_lzms; // this supports chunks up to our maximum blob size
59 // two times faster than lzms, compression is still awful
60 //version = use_libwim_lzx; // this supports chunks up to 2MB; more-or-less useful
61 // quite fast (because it refuses to compress anything bigger than 64KB); compression is most awful
62 //version = use_libwim_xpress; // this supports chunks up to 64KB; useless
64 // oh, because why not?
65 // surprisingly good (but not as good as zlib), and lightning fast on default compression level
66 // sadly, requires external lib
67 //version = use_zstd;
69 private import std.digest.ripemd;
70 private import iv.encoding;
71 private import iv.cmdcon;
72 private import iv.strex;
73 private import iv.sq3;
74 private import iv.timer;
75 private import iv.vfs.io;
76 private import iv.vfs.util;
78 private import chibackend.mbuilder : DynStr;
79 private import chibackend.parse;
80 private import chibackend.decode;
81 //private import iv.utfutil;
82 //private import iv.vfs.io;
84 version(use_libdeflate) private import chibackend.pack.libdeflate;
85 else version(use_balz) private import iv.balz;
86 else version(use_libxpack) private import chibackend.pack.libxpack;
87 else version(use_libbrieflz) private import chibackend.pack.libbrieflz;
88 else version(use_liblzfse) private import chibackend.pack.liblzfse;
89 else version(use_lzjb) private import chibackend.pack.lzjb;
90 else version(use_libwim_lzms) private import chibackend.pack.libwim;
91 else version(use_libwim_lzx) private import chibackend.pack.libwim;
92 else version(use_libwim_xpress) private import chibackend.pack.libwim;
93 else version(use_lz4) private import chibackend.pack.liblz4;
94 else version(use_zstd) private import chibackend.pack.libzstd;
97 version(use_zstd) {
98 public enum ChiroDefaultPackLevel = 6;
99 } else {
100 public enum ChiroDefaultPackLevel = 9;
104 // use `MailDBPath()` to get/set it
105 private __gshared string ExpandedMailDBPath = null;
106 public __gshared int ChiroCompressionLevel = ChiroDefaultPackLevel;
107 public __gshared bool ChiroSQLiteSilent = false;
109 public __gshared bool ChiroTimerEnabled = false;
110 private __gshared Timer chiTimer = Timer(false);
111 private __gshared char[] chiTimerMsg = null;
113 // opened databases
114 public __gshared Database dbStore; // message store db
115 public __gshared Database dbView; // message view db
116 public __gshared Database dbConf; // config/options db
119 public enum Appearance {
120 Ignore = -1, // can be used to ignore messages in thread view
121 Unread = 0,
122 Read = 1,
123 SoftDeleteFilter = 2, // soft-delete from filter
124 SoftDeleteUser = 3, // soft-delete by user
125 SoftDeletePurge = 4, // soft-delete by user (will be purged on folder change)
128 public enum Mute {
129 Never = -1,
130 Normal = 0,
131 Message = 1, /* single message */
132 ThreadStart = 2, /* all children starting from this */
133 ThreadOther = 3, /* muted by some parent */
136 public bool isSoftDeleted (const int appearance) pure nothrow @safe @nogc {
137 pragma(inline, true);
138 return
139 appearance >= Appearance.SoftDeleteFilter &&
140 appearance <= Appearance.SoftDeletePurge;
145 There are several added SQLite functions:
147 ChiroPack(data[, compratio])
148 ===============
150 This tries to compress the given data, and returns a compressed blob.
151 If `compratio` is negative or zero, do not compress anything.
154 ChiroUnpack(data)
155 =================
157 This decompresses the blob compressed with `ChiroPack()`. It is (usually) safe to pass
158 non-compressed data to this function.
161 ChiroNormCRLF(content)
162 ======================
164 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
165 Removes trailing blanks.
168 ChiroNormHeaders(content)
169 =========================
171 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
172 Then replaces 'space, LF' with a single space (joins multiline headers).
173 Removes trailing blanks.
176 ChiroExtractHeaders(content)
177 ============================
179 Can be used to extract headers from the message.
180 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
181 Then replaces 'space, LF' with a single space (joins multiline headers).
182 Removes trailing blanks.
185 ChiroExtractBody(content)
186 =========================
188 Can be used to extract body from the message.
189 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
190 Then replaces 'space, LF' with a single space (joins multiline headers).
191 Removes trailing blanks and final dot.
195 public enum OptionsDBName = "chiroptera.db";
196 public enum StorageDBName = "chistore.db";
197 public enum SupportDBName = "chiview.db";
200 // ////////////////////////////////////////////////////////////////////////// //
201 private enum CommonPragmas = `
202 PRAGMA case_sensitive_like = OFF;
203 PRAGMA foreign_keys = OFF;
204 PRAGMA locking_mode = NORMAL; /*EXCLUSIVE;*/
205 PRAGMA secure_delete = OFF;
206 PRAGMA threads = 3;
207 PRAGMA trusted_schema = OFF;
208 PRAGMA writable_schema = OFF;
211 enum CommonPragmasRO = CommonPragmas~`
212 PRAGMA temp_store = MEMORY; /*DEFAULT*/ /*FILE*/
215 enum CommonPragmasRW = CommonPragmas~`
216 PRAGMA application_id = 1128810834; /*CHIR*/
217 PRAGMA auto_vacuum = NONE;
218 PRAGMA encoding = "UTF-8";
219 PRAGMA temp_store = DEFAULT;
220 --PRAGMA journal_mode = WAL; /*OFF;*/
221 --PRAGMA journal_mode = DELETE; /*OFF;*/
222 PRAGMA synchronous = NORMAL; /*OFF;*/
225 enum CommonPragmasRecreate = `
226 PRAGMA locking_mode = EXCLUSIVE;
227 PRAGMA journal_mode = OFF;
228 PRAGMA synchronous = OFF;
231 static immutable dbpragmasRO = CommonPragmasRO;
233 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
234 // use the smallest page size, because we don't need to perform alot of selects here
235 static immutable dbpragmasRWStorage = "PRAGMA page_size = 512;"~CommonPragmasRW~"PRAGMA journal_mode = DELETE;";
236 static immutable dbpragmasRWStorageRecreate = dbpragmasRWStorage~CommonPragmasRecreate;
238 // use slightly bigger pages
239 // funny, smaller pages leads to bigger files
240 static immutable dbpragmasRWSupport = "PRAGMA page_size = 4096;"~CommonPragmasRW~"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;";
241 static immutable dbpragmasRWSupportRecreate = dbpragmasRWSupport~CommonPragmasRecreate;
243 // smaller page size is ok
244 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
245 static immutable dbpragmasRWOptions = "PRAGMA page_size = 512;"~CommonPragmasRW~"PRAGMA journal_mode = /*DELETE*/WAL; PRAGMA synchronous = NORMAL;";
246 static immutable dbpragmasRWOptionsRecreate = dbpragmasRWOptions~CommonPragmasRecreate;
249 enum msgTagNameCheckSQL = `
250 WITH RECURSIVE tagtable(tag, rest) AS (
251 VALUES('', NEW.tags||'|')
252 UNION ALL
253 SELECT
254 SUBSTR(rest, 0, INSTR(rest, '|')),
255 SUBSTR(rest, INSTR(rest, '|')+1)
256 FROM tagtable
257 WHERE rest <> '')
258 SELECT
259 (CASE
260 WHEN tag = '/' THEN RAISE(FAIL, 'tag name violation (root tags are not allowed)')
261 WHEN LENGTH(tag) = 1 THEN RAISE(FAIL, 'tag name violation (too short tag name)')
262 WHEN SUBSTR(tag, LENGTH(tag)) = '/' THEN RAISE(FAIL, 'tag name violation (tag should not end with a slash)')
263 END)
264 FROM tagtable
265 WHERE tag <> '';
268 // main storage and support databases will be in different files
269 static immutable string schemaStorage = `
270 -- deleted messages have empty headers and body
271 -- this is so uids will remain unique on inserting
272 -- tags are used to associate the message with various folders, and stored here for rebuild purposes
273 -- the frontend will use the separate "tags" table to select messages
274 -- deleted messages must not have any tags, and should contain no other data
275 -- (keeping the data is harmless, it simply sits there and takes space)
276 CREATE TABLE IF NOT EXISTS messages (
277 uid INTEGER PRIMARY KEY /* rowid, never zero */
278 , tags TEXT DEFAULT NULL /* associated message tags, '|'-separated; case-sensitive, no extra whitespaces or '||'! */
279 -- article data; MUST contain the ending dot, and be properly dot-stuffed
280 -- basically, this is "what we had received, as is" (*WITH* the ending dot!)
281 -- there is no need to normalize it in any way (and you *SHOULD NOT* do it!)
282 -- it should be compressed with "ChiroPack()", and extracted with "ChiroUnpack()"
283 , data BLOB
286 -- check tag constraints
287 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_insert
288 BEFORE INSERT ON messages
289 FOR EACH ROW
290 BEGIN`~msgTagNameCheckSQL~`
291 END;
293 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_update_tags
294 BEFORE UPDATE OF tags ON messages
295 FOR EACH ROW
296 BEGIN`~msgTagNameCheckSQL~`
297 END;
300 static immutable string schemaStorageIndex = `
304 static immutable string schemaOptions = `
305 -- use "autoincrement" to allow account deletion
306 CREATE TABLE IF NOT EXISTS accounts (
307 accid INTEGER PRIMARY KEY AUTOINCREMENT /* unique, never zero */
308 , checktime INTEGER NOT NULL DEFAULT 15 /* check time, in minutes */
309 , nosendauth INTEGER NOT NULL DEFAULT 0 /* turn off authentication on sending? */
310 , debuglog INTEGER NOT NULL DEFAULT 0 /* do debug logging? */
311 , nocheck INTEGER NOT NULL DEFAULT 0 /* disable checking? */
312 , nntplastindex INTEGER NOT NULL DEFAULT 0 /* last seen article index for NNTP groups */
313 , name TEXT NOT NULL UNIQUE /* account name; lowercase alphanum, '_', '-', '.' */
314 , recvserver TEXT NOT NULL /* server for receiving messages */
315 , sendserver TEXT NOT NULL /* server for sending messages */
316 , user TEXT NOT NULL /* pop3 user name */
317 , pass TEXT NOT NULL /* pop3 password, empty for no authorisation */
318 , realname TEXT NOT NULL /* user name for e-mail headers */
319 , email TEXT NOT NULL /* account e-mail address (full, name@host) */
320 , inbox TEXT NOT NULL /* inbox tag, usually "/accname/inbox", or folder for nntp */
321 , nntpgroup TEXT NOT NULL DEFAULT '' /* nntp group name for NNTP accounts; if empty, this is POP3 account */
325 CREATE TABLE IF NOT EXISTS options (
326 name TEXT NOT NULL UNIQUE
327 , value TEXT
331 CREATE TABLE IF NOT EXISTS addressbook (
332 nick TEXT NOT NULL UNIQUE /* short nick for this address book entry */
333 , name TEXT NOT NULL DEFAULT ''
334 , email TEXT NOT NULL
335 , notes TEXT DEFAULT NULL
339 -- twits by email/name
340 CREATE TABLE IF NOT EXISTS emailtwits (
341 etwitid INTEGER PRIMARY KEY
342 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
343 , email TEXT /* if both name and email present, use only email */
344 , name TEXT /* name to twit by */
345 , title TEXT /* optional title */
346 , notes TEXT /* notes; often URL */
349 -- twits by msgids
350 CREATE TABLE IF NOT EXISTS msgidtwits (
351 mtwitid INTEGER PRIMARY KEY
352 , etwitid INTEGER /* parent mail twit, if any */
353 , automatic INTEGER DEFAULT 1 /* added by message filtering, not from .rc? */
354 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
355 , msgid TEXT /* message used to set twit */
359 -- message filters
360 CREATE TABLE IF NOT EXISTS filters (
361 filterid INTEGER PRIMARY KEY
362 , valid INTEGER NOT NULL DEFAULT 1 /* is this filter valid? used to skip bad filters */
363 , idx INTEGER NOT NULL DEFAULT 0 /* used for ordering */
364 , post INTEGER NOT NULL DEFAULT 0 /* post-spamcheck filter? */
365 , hitcount INTEGER NOT NULL DEFAULT 0 /* for statistics */
366 , name TEXT NOT NULL UNIQUE /* filter name */
367 , body TEXT /* filter text */
370 CREATE TRIGGER IF NOT EXISTS filters_new_index
371 AFTER INSERT ON filters
372 FOR EACH ROW
373 BEGIN
374 UPDATE filters SET idx=(SELECT MAX(idx)+10 FROM filters)
375 WHERE NEW.idx=0 AND filterid=NEW.filterid;
376 END;
379 static immutable string schemaOptionsIndex = `
380 -- no need to, "UNIQUE" automaptically creates it
381 --CREATE INDEX IF NOT EXISTS accounts_name ON accounts(name);
383 -- this index in implicit
384 --CREATE INDEX IF NOT EXISTS options_name ON options(name);
386 CREATE INDEX IF NOT EXISTS emailtwits_email ON emailtwits(email);
387 CREATE INDEX IF NOT EXISTS emailtwits_name ON emailtwits(name);
388 CREATE UNIQUE INDEX IF NOT EXISTS emailtwits_email_name ON emailtwits(email, name);
390 CREATE INDEX IF NOT EXISTS msgidtwits_msgid ON msgidtwits(msgid);
392 CREATE INDEX IF NOT EXISTS filters_idx_post_valid ON filters(idx, post, valid);
396 enum schemaSupportTable = `
397 -- tag <-> messageid correspondence
398 -- note that one message can be tagged with more than one tag
399 -- there is always tag with "uid=0", to keep all tags alive
400 -- special tags:
401 -- account:name -- received via this account
402 -- #spam -- this is spam message
403 -- #hobo -- will be autoassigned to messages without any tags (created on demand)
404 CREATE TABLE IF NOT EXISTS tagnames (
405 tagid INTEGER PRIMARY KEY
406 , hidden INTEGER NOT NULL DEFAULT 0 /* deleting tags may cause 'tagid' reuse, so it's better to hide them instead */
407 , threading INTEGER NOT NULL DEFAULT 1 /* enable threaded view? */
408 , noattaches INTEGER NOT NULL DEFAULT 0 /* ignore non-text attachments? */
409 , tag TEXT NOT NULL UNIQUE
412 -- it is here, because we don't have a lot of tags, and inserts are slightly faster this way
413 -- it's not required, because "UNIQUE" constraint will create automatic index
414 --CREATE INDEX IF NOT EXISTS tagname_tag ON tagnames(tag);
416 --CREATE INDEX IF NOT EXISTS tagname_tag_uid ON tagnames(tag, tagid);
419 -- each tag has its own unique threads (so uids can be duplicated, but (uid,tagid) paris cannot
420 -- see above for "apearance" and "mute" values
421 CREATE TABLE IF NOT EXISTS threads (
422 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
423 , tagid INTEGER /* we need separate threads for each tag */
424 , time INTEGER DEFAULT 0 /* unixtime -- creation/send/receive */
425 /* threading info */
426 , parent INTEGER DEFAULT 0 /* uid: parent message in thread, or 0 */
427 /* flags */
428 , appearance INTEGER DEFAULT 0 /* how the message should look */
429 , mute INTEGER DEFAULT 0 /* 1: only this message, 2: the whole thread */
430 , title TEXT DEFAULT NULL /* title from the filter */
434 -- WARNING!
435 -- for FTS5 to work, this table must be:
436 -- updated LAST on INSERT
437 -- updated FIRST on DELETE
438 -- this is due to FTS5 triggers
439 -- message texts should NEVER be updated!
440 -- if you want to do update a message:
441 -- first, DELETE the old one from this table
442 -- then, update textx
443 -- then, INSERT here again
444 -- doing it like that will keep FTS5 in sync
445 CREATE TABLE IF NOT EXISTS info (
446 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
447 , from_name TEXT /* can be empty */
448 , from_mail TEXT /* can be empty */
449 , subj TEXT /* can be empty */
450 , to_name TEXT /* can be empty */
451 , to_mail TEXT /* can be empty */
455 -- this holds msgid
456 -- moved to separate table, because this info is used only when inserting new messages
457 CREATE TABLE IF NOT EXISTS msgids (
458 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
459 , time INTEGER /* so we can select the most recent message */
460 , msgid TEXT /* message id */
464 -- this holds in-reply-to, and references
465 -- moved to separate table, because this info is used only when inserting new messages
466 CREATE TABLE IF NOT EXISTS refids (
467 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
468 , idx INTEGER /* internal index in headers, cannot have gaps, starts from 0 */
469 , msgid TEXT /* message id */
473 -- this ALWAYS contain an entry (yet content may be empty string)
474 CREATE TABLE IF NOT EXISTS content_text (
475 uid INTEGER PRIMARY KEY /* owner message uid */
476 , format TEXT NOT NULL /* optional format, like 'flowed' */
477 , content TEXT NOT NULL /* properly decoded */
481 -- this ALWAYS contain an entry (yet content may be empty string)
482 CREATE TABLE IF NOT EXISTS content_html (
483 uid INTEGER PRIMARY KEY /* owner message uid */
484 , format TEXT NOT NULL /* optional format, like 'flowed' */
485 , content TEXT NOT NULL /* properly decoded */
489 -- this DOES NOT include text and html contents (and may exclude others)
490 CREATE TABLE IF NOT EXISTS attaches (
491 uid INTEGER /* owner message uid */
492 , idx INTEGER /* code should take care of proper autoincrementing this */
493 , mime TEXT NOT NULL /* always lowercased */
494 , name TEXT NOT NULL /* attachment name; always empty for inline content, never empty for non-inline content */
495 , format TEXT NOT NULL /* optional format, like 'flowed' */
496 , content BLOB /* properly decoded; NULL if the attach was dropped */
500 -- this view is used for FTS5 content queries
501 -- it is harmless to keep it here even if FTS5 is not used
502 --DROP VIEW IF EXISTS fts5_msgview;
503 CREATE VIEW IF NOT EXISTS fts5_msgview (uid, sender, subj, text, html)
505 SELECT
506 info.uid AS uid
507 , info.from_name||' '||CHAR(26)||' '||info.from_mail AS sender
508 , info.subj AS subj
509 , ChiroUnpack(content_text.content) AS text
510 , ChiroUnpack(content_html.content) AS html
511 FROM info
512 INNER JOIN content_text USING(uid)
513 INNER JOIN content_html USING(uid)
517 -- this table holds all unsent messages
518 -- they are put in the storage and properly inserted,
519 -- but also put in this table, for the receiver to send them
520 -- also note that NNTP messages will be put in the storage without any tags (but with the contents)
521 -- (this is because we will receive them back from NNTP server later)
522 -- succesfully sent messages will be simply DELETEd
523 CREATE TABLE IF NOT EXISTS unsent(
524 uid INTEGER PRIMARY KEY /* the same as in the storage, not automatic */
525 , accid INTEGER /* account from which this message should be sent */
526 , from_pop3 TEXT /* "from" for POP3 */
527 , to_pop3 TEXT /* "to" for POP3 */
528 , data TEXT /* PACKED data to send */
532 static immutable string schemaSupportTempTables = `
533 --DROP TABLE IF EXISTS treepane;
534 CREATE TEMP TABLE IF NOT EXISTS treepane (
535 iid INTEGER PRIMARY KEY
536 , uid INTEGER
537 , level INTEGER
538 -- to make joins easier
539 , tagid INTEGER
542 CREATE INDEX IF NOT EXISTS treepane_uid ON treepane(uid);
545 enum schemaSupportIndex = `
546 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_tag_uid ON threads(tagid, uid);
547 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_uid_tag ON threads(uid, tagid);
549 -- this is for views where threading is disabled
550 CREATE INDEX IF NOT EXISTS trd_by_tag_time ON threads(tagid, time);
551 --CREATE INDEX IF NOT EXISTS trd_by_tag_time_parent ON threads(tagid, time, parent);
552 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent_time ON threads(tagid, parent, time);
553 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent ON threads(tagid, parent);
554 CREATE INDEX IF NOT EXISTS trd_by_parent_tag ON threads(parent, tagid);
556 -- this is for test if we have any unread articles (we don't mind the exact numbers, tho)
557 CREATE INDEX IF NOT EXISTS trd_by_appearance ON threads(appearance);
558 -- this is for removing purged messages
559 CREATE INDEX IF NOT EXISTS trd_by_tag_appearance ON threads(tagid, appearance);
560 -- was used in table view creation, not used anymore
561 --CREATE INDEX IF NOT EXISTS trd_by_parent_tag_appearance ON threads(parent, tagid, appearance);
563 -- for theadmsgview
564 -- was used in table view creation, not used anymore
565 --CREATE INDEX IF NOT EXISTS trd_by_tag_appearance_time ON threads(tagid, appearance, time);
567 CREATE INDEX IF NOT EXISTS msgid_by_msgid_time ON msgids(msgid, time DESC);
569 CREATE INDEX IF NOT EXISTS refid_by_refids_idx ON refids(msgid, idx);
570 CREATE INDEX IF NOT EXISTS refid_by_uid_idx ON refids(uid, idx);
572 CREATE INDEX IF NOT EXISTS content_text_by_uid ON content_text(uid);
573 CREATE INDEX IF NOT EXISTS content_html_by_uid ON content_html(uid);
575 CREATE INDEX IF NOT EXISTS attaches_by_uid_name ON attaches(uid, name);
576 CREATE INDEX IF NOT EXISTS attaches_by_uid_idx ON attaches(uid, idx);
578 -- "info" indicies for twits
579 CREATE INDEX IF NOT EXISTS info_by_from_mail_name ON info(from_mail, from_name);
580 --CREATE INDEX IF NOT EXISTS info_by_from_mail ON info(from_mail);
581 CREATE INDEX IF NOT EXISTS info_by_from_name ON info(from_name);
584 CREATE INDEX IF NOT EXISTS unsent_by_accid ON unsent(accid);
587 static immutable string schemaSupport = schemaSupportTable~schemaSupportIndex;
590 version(fts5_use_porter) {
591 enum FTS5_Tokenizer = "porter unicode61 remove_diacritics 2";
592 } else {
593 enum FTS5_Tokenizer = "unicode61 remove_diacritics 2";
596 static immutable string recreateFTS5 = `
597 DROP TABLE IF EXISTS fts5_messages;
598 CREATE VIRTUAL TABLE fts5_messages USING fts5(
599 sender /* sender name and email, separated by " \x1a " (dec 26) (substitute char) */
600 , subj /* email subject */
601 , text /* email body, text/plain */
602 , html /* email body, text/html */
603 --, uid UNINDEXED /* message uid this comes from (not needed, use "rowid" instead */
604 , tokenize = '`~FTS5_Tokenizer~`'
605 , content = 'fts5_msgview'
606 , content_rowid = 'uid'
608 /* sender, subj, text, html */
609 INSERT INTO fts5_messages(fts5_messages, rank) VALUES('rank', 'bm25(1.0, 3.0, 10.0, 6.0)');
612 static immutable string repopulateFTS5 = `
613 SELECT ChiroTimerStart('updating FTS5');
614 BEGIN TRANSACTION;
616 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
617 SELECT uid, sender, subj, text, html
618 FROM fts5_msgview
619 WHERE
620 EXISTS (
621 SELECT threads.tagid FROM threads
622 INNER JOIN tagnames USING(tagid)
623 WHERE
624 threads.uid=fts5_msgview.uid AND
625 tagnames.hidden=0 AND SUBSTR(tagnames.tag, 1, 1)='/'
628 COMMIT TRANSACTION;
629 SELECT ChiroTimerStop();
633 static immutable string recreateFTS5Triggers = `
634 -- triggers to keep the FTS index up to date
636 -- this rely on the proper "info" table update order
637 -- info must be inserted LAST
638 DROP TRIGGER IF EXISTS fts5xtrig_insert;
639 CREATE TRIGGER fts5xtrig_insert
640 AFTER INSERT ON info
641 BEGIN
642 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
643 SELECT uid, sender, subj, text, html FROM fts5_msgview WHERE uid=NEW.uid LIMIT 1;
644 END;
646 -- not AFTER, because we still need a valid view!
647 -- this rely on the proper "info" table update order
648 -- info must be deleted FIRST
649 DROP TRIGGER IF EXISTS fts5xtrig_delete;
650 CREATE TRIGGER fts5xtrig_delete
651 BEFORE DELETE ON info
652 BEGIN
653 INSERT INTO fts5_messages(fts5_messages, rowid, sender, subj, text, html)
654 SELECT 'delete', uid, sender, subj, text, html FROM fts5_msgview WHERE uid=OLD.uid LIMIT 1;
655 END;
657 -- message texts should NEVER be updated, so no ON UPDATE trigger
661 // ////////////////////////////////////////////////////////////////////////// //
662 // not properly implemented yet
663 //version = lazy_mt_safe;
665 version(lazy_mt_safe) {
666 enum lazy_mt_safe_flag = true;
667 } else {
668 enum lazy_mt_safe_flag = false;
671 public struct LazyStatement(string dbname) {
672 public:
673 enum DB {
674 Store,
675 View,
676 Conf,
679 private:
680 static struct Data {
681 DBStatement st = void;
682 version(lazy_mt_safe) {
683 sqlite3_mutex* mutex = void;
685 char* sql = void;
686 usize sqlsize = void;
687 uint compiled = void;
690 private:
691 usize udata = 0;
692 DB dbtype;
693 string delayInit = null;
695 private:
696 inout(Data)* datap () inout pure nothrow @trusted @nogc { pragma(inline, true); return cast(Data*)udata; }
697 void datap (Data *v) pure nothrow @trusted @nogc { pragma(inline, true); udata = cast(usize)v; }
699 public:
700 //@disable this ();
701 @disable this (this);
703 this (string sql) {
704 delayInit = sql;
706 assert(sql.length);
707 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
708 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
709 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
710 else static assert(0, "invalid db name: '"~dbname~"'");
711 import core.stdc.stdlib : calloc;
712 Data* dp = cast(Data*)calloc(1, Data.sizeof);
713 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
714 datap = dp;
715 dp.sql = cast(char*)calloc(1, sql.length);
716 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
717 dp.sql[0..sql.length] = sql[];
718 dp.sqlsize = sql.length;
719 version(lazy_mt_safe) {
720 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
721 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
724 //dbtype = adb;
725 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
728 ~this () {
729 import core.stdc.stdlib : free;
730 if (!udata) return;
731 Data* dp = datap;
732 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===DEINIT===\n%s\n============\n", dp.sql); }
733 dp.st = DBStatement.init;
734 free(dp.sql);
735 version(lazy_mt_safe) {
736 sqlite3_mutex_free(dp.mutex);
738 free(dp);
739 udata = 0;
742 bool valid () pure nothrow @safe @nogc { pragma(inline, true); return (udata != 0 || delayInit.length); }
744 private void setupWith (const(char)[] sql) {
745 if (udata) throw new Exception("statement already inited");
746 assert(sql.length);
747 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
748 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
749 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
750 else static assert(0, "invalid db name: '"~dbname~"'");
751 import core.stdc.stdlib : calloc;
752 Data* dp = cast(Data*)calloc(1, Data.sizeof);
753 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
754 datap = dp;
755 dp.sql = cast(char*)calloc(1, sql.length);
756 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
757 dp.sql[0..sql.length] = sql[];
758 dp.sqlsize = sql.length;
759 version(lazy_mt_safe) {
760 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
761 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
763 //dbtype = adb;
764 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
767 ref DBStatement st () {
768 if (!udata) {
769 //throw new Exception("no statement set");
770 setupWith(delayInit);
772 Data* dp = datap;
773 if (!dp.compiled) {
774 version(lazy_mt_safe) {
775 sqlite3_mutex_enter(dp.mutex);
777 scope(exit) {
778 version(lazy_mt_safe) {
779 sqlite3_mutex_leave(dp.mutex);
782 //{ import core.stdc.stdio : printf; printf("***compiling:\n%s\n=====\n", dp.sql); }
783 final switch (dbtype) {
784 case DB.Store: dp.st = dbStore.persistentStatement(dp.sql[0..dp.sqlsize]); break;
785 case DB.View: dp.st = dbView.persistentStatement(dp.sql[0..dp.sqlsize]); break;
786 case DB.Conf: dp.st = dbConf.persistentStatement(dp.sql[0..dp.sqlsize]); break;
788 dp.compiled = 1;
789 //assert(dp.st.valid);
791 //assert(dp.st.valid);
792 return dp.st;
797 // ////////////////////////////////////////////////////////////////////////// //
798 private bool isGoodText (const(void)[] buf) pure nothrow @safe @nogc {
799 foreach (immutable ubyte ch; cast(const(ubyte)[])buf) {
800 if (ch < 32) {
801 if (ch != 9 && ch != 10 && ch != 13 && ch != 27) return false;
802 } else {
803 if (ch == 127) return false;
806 return true;
807 //return utf8ValidText(buf);
811 // ////////////////////////////////////////////////////////////////////////// //
812 private bool isBadPrefix (const(char)[] buf) pure nothrow @trusted @nogc {
813 if (buf.length < 5) return false;
814 return
815 buf.ptr[0] == '\x1b' &&
816 buf.ptr[1] >= 'A' && buf.ptr[1] <= 'Z' &&
817 buf.ptr[2] >= 'A' && buf.ptr[2] <= 'Z' &&
818 buf.ptr[3] >= 'A' && buf.ptr[3] <= 'Z' &&
819 buf.ptr[4] >= 'A' && buf.ptr[4] <= 'Z';
823 /* two high bits of the first byte holds the size:
824 00: fit into 6 bits: [0.. 0x3f] (1 byte)
825 01: fit into 14 bits: [0.. 0x3fff] (2 bytes)
826 10: fit into 22 bits: [0.. 0x3f_ffff] (3 bytes)
827 11: fit into 30 bits: [0..0x3fff_ffff] (4 bytes)
829 number is stored as big-endian.
830 will not write anything to `dest` if there is not enough room.
832 returns number of bytes, or 0 if the number is too big.
834 private uint encodeUInt (void[] dest, uint v) nothrow @trusted @nogc {
835 if (v > 0x3fff_ffffU) return 0;
836 ubyte[] d = cast(ubyte[])dest;
837 // 4 bytes?
838 if (v > 0x3f_ffffU) {
839 v |= 0xc000_0000U;
840 if (d.length >= 4) {
841 d.ptr[0] = cast(ubyte)(v>>24);
842 d.ptr[1] = cast(ubyte)(v>>16);
843 d.ptr[2] = cast(ubyte)(v>>8);
844 d.ptr[3] = cast(ubyte)v;
846 return 4;
848 // 3 bytes?
849 if (v > 0x3fffU) {
850 v |= 0x80_0000U;
851 if (d.length >= 3) {
852 d.ptr[0] = cast(ubyte)(v>>16);
853 d.ptr[1] = cast(ubyte)(v>>8);
854 d.ptr[2] = cast(ubyte)v;
856 return 3;
858 // 2 bytes?
859 if (v > 0x3fU) {
860 v |= 0x4000U;
861 if (d.length >= 2) {
862 d.ptr[0] = cast(ubyte)(v>>8);
863 d.ptr[1] = cast(ubyte)v;
865 return 2;
867 // 1 byte
868 if (d.length >= 1) d.ptr[0] = cast(ubyte)v;
869 return 1;
873 private uint decodeUIntLength (const(void)[] dest) pure nothrow @trusted @nogc {
874 const(ubyte)[] d = cast(const(ubyte)[])dest;
875 if (d.length == 0) return 0;
876 switch (d.ptr[0]&0xc0) {
877 case 0x00: return 1;
878 case 0x40: return (d.length >= 2 ? 2 : 0);
879 case 0x80: return (d.length >= 3 ? 3 : 0);
880 default:
882 return (d.length >= 4 ? 4 : 0);
886 // returns uint.max on error (impossible value)
887 private uint decodeUInt (const(void)[] dest) pure nothrow @trusted @nogc {
888 const(ubyte)[] d = cast(const(ubyte)[])dest;
889 if (d.length == 0) return uint.max;
890 uint res = void;
891 switch (d.ptr[0]&0xc0) {
892 case 0x00:
893 res = d.ptr[0];
894 break;
895 case 0x40:
896 if (d.length < 2) return uint.max;
897 res = ((d.ptr[0]&0x3fU)<<8)|d.ptr[1];
898 break;
899 case 0x80:
900 if (d.length < 3) return uint.max;
901 res = ((d.ptr[0]&0x3fU)<<16)|(d.ptr[1]<<8)|d.ptr[2];
902 break;
903 default:
904 if (d.length < 4) return uint.max;
905 res = ((d.ptr[0]&0x3fU)<<24)|(d.ptr[1]<<16)|(d.ptr[2]<<8)|d.ptr[3];
906 break;
908 return res;
912 // returns position AFTER the headers (empty line is skipped too)
913 // returned value is safe for slicing
914 private int sq3Supp_FindHeadersEnd (const(char)* vs, const int sz) {
915 import core.stdc.string : memchr;
916 if (sz <= 0) return 0;
917 const(char)* eptr = cast(const(char)*)memchr(vs, '\n', cast(uint)sz);
918 while (eptr !is null) {
919 ++eptr;
920 int epos = cast(int)cast(usize)(eptr-vs);
921 if (sz-epos < 1) break;
922 if (*eptr == '\r') {
923 if (sz-epos < 2) break;
924 ++epos;
925 ++eptr;
927 if (*eptr == '\n') return epos+1;
928 assert(epos < sz);
929 eptr = cast(const(char)*)memchr(eptr, '\n', cast(uint)(sz-epos));
931 return sz;
935 // hack for some invalid dates
936 uint parseMailDate (const(char)[] s) nothrow {
937 import std.datetime;
938 if (s.length == 0) return 0;
939 try {
940 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
941 } catch (Exception) {}
942 // sometimes this helps
943 usize dcount = 0;
944 foreach_reverse (immutable char ch; s) {
945 if (ch < '0' || ch > '9') break;
946 ++dcount;
948 if (dcount > 4) return 0;
949 s ~= "0000"[0..4-dcount];
950 try {
951 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
952 } catch (Exception) {}
953 return 0;
957 // ////////////////////////////////////////////////////////////////////////// //
958 extern(C) {
961 ** ChiroPack(content)
962 ** ChiroPack(content, packflag)
964 ** second form accepts int flag; 0 means "don't pack"
966 private void sq3Fn_ChiroPackCommon (sqlite3_context *ctx, sqlite3_value *val, int packlevel) nothrow @trusted {
967 immutable int sz = sqlite3_value_bytes(val);
968 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
970 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
972 const(char)* vs = cast(const(char) *)sqlite3_value_blob(val);
973 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroPack()`", -1); return; }
975 if (sz >= 0x3fffffff-8) {
976 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_toobig(ctx); return; }
977 sqlite3_result_value(ctx, val);
978 return;
981 import core.stdc.stdlib : malloc, free;
982 import core.stdc.string : memcpy;
984 if (packlevel > 0 && sz > 8) {
985 import core.stdc.stdio : snprintf;
986 char[16] xsz = void;
987 version(use_balz) {
988 xsz[0..5] = "\x1bBALZ";
989 } else version(use_libxpack) {
990 xsz[0..5] = "\x1bXPAK";
991 } else version(use_libbrieflz) {
992 xsz[0..5] = "\x1bBRLZ";
993 } else version(use_liblzfse) {
994 xsz[0..5] = "\x1bLZFS";
995 } else version(use_lzjb) {
996 xsz[0..5] = "\x1bLZJB";
997 } else version(use_libwim_lzms) {
998 xsz[0..5] = "\x1bLZMS";
999 } else version(use_libwim_lzx) {
1000 xsz[0..5] = "\x1bLZMX";
1001 } else version(use_libwim_xpress) {
1002 xsz[0..5] = "\x1bXPRS";
1003 } else version(use_lz4) {
1004 xsz[0..5] = "\x1bLZ4D";
1005 } else version(use_zstd) {
1006 xsz[0..5] = "\x1bZSTD";
1007 } else {
1008 xsz[0..5] = "\x1bZLIB";
1010 uint xszlen = encodeUInt(xsz[5..$], cast(uint)sz);
1011 if (xszlen) {
1012 xszlen += 5;
1013 //xsz[xszlen++] = ':';
1014 version(use_libbrieflz) {
1015 immutable usize bsz = blz_max_packed_size(cast(usize)sz);
1016 } else version(use_lzjb) {
1017 immutable uint bsz = cast(uint)sz+1024;
1018 } else version(use_lz4) {
1019 immutable uint bsz = cast(uint)LZ4_compressBound(sz)+1024;
1020 } else {
1021 immutable uint bsz = cast(uint)sz;
1023 char* cbuf = cast(char*)malloc(bsz+xszlen);
1024 if (cbuf is null) {
1025 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_nomem(ctx); return; }
1026 } else {
1027 cbuf[0..xszlen] = xsz[0..xszlen];
1028 version(use_balz) {
1029 Balz bz;
1030 usize spos = 0;
1031 usize dpos = xszlen;
1032 try {
1033 bz.compress(
1034 // reader
1035 (buf) {
1036 if (spos >= cast(usize)sz) return 0;
1037 usize left = cast(usize)sz-spos;
1038 if (left > buf.length) left = buf.length;
1039 if (left) memcpy(buf.ptr, vs+spos, left);
1040 spos += left;
1041 return left;
1043 // writer
1044 (buf) {
1045 if (dpos+buf.length >= cast(usize)sz) throw new Exception("uncompressible");
1046 memcpy(cbuf+dpos, buf.ptr, buf.length);
1047 dpos += buf.length;
1049 // maximum compression?
1050 true
1052 } catch(Exception) {
1053 dpos = usize.max;
1055 if (dpos < cast(usize)sz) {
1056 sqlite3_result_blob(ctx, cbuf, dpos, &free);
1057 return;
1059 } else version(use_libdeflate) {
1060 if (packlevel > 12) packlevel = 12;
1061 libdeflate_compressor *cpr = libdeflate_alloc_compressor(packlevel);
1062 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1063 usize dsize = libdeflate_zlib_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
1064 libdeflate_free_compressor(cpr);
1065 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1066 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1067 return;
1069 } else version(use_libxpack) {
1070 // 2^19 (524288) bytes. This is definitely a big problem and I am planning to address it.
1071 // https://github.com/ebiggers/xpack/issues/1
1072 if (sz < 524288-64) {
1073 if (packlevel > 9) packlevel = 9;
1074 xpack_compressor *cpr = xpack_alloc_compressor(cast(usize)sz, packlevel);
1075 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1076 usize dsize = xpack_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
1077 xpack_free_compressor(cpr);
1078 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1079 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1080 return;
1083 } else version(use_libbrieflz) {
1084 if (packlevel > 10) packlevel = 10;
1085 immutable usize wbsize = blz_workmem_size_level(cast(usize)sz, packlevel);
1086 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1087 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1088 uint dsize = blz_pack_level(vs, cbuf+xszlen, cast(uint)sz, wbuf, packlevel);
1089 free(wbuf);
1090 if (dsize+xszlen < cast(usize)sz) {
1091 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1092 return;
1094 } else version(use_liblzfse) {
1095 immutable usize wbsize = lzfse_encode_scratch_size();
1096 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1097 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1098 usize dsize = lzfse_encode_buffer(cbuf+xszlen, bsz, vs, cast(uint)sz, wbuf);
1099 free(wbuf);
1100 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1101 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1102 return;
1104 } else version(use_lzjb) {
1105 usize dsize = lzjb_compress(vs, cast(usize)sz, cbuf+xszlen, bsz);
1106 if (dsize == usize.max) dsize = 0;
1107 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1108 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1109 return;
1111 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "LZJB FAILED!\n"); }
1112 } else version(use_libwim_lzms) {
1113 wimlib_compressor* cpr;
1114 uint clevel = (packlevel < 10 ? 50 : 1000);
1115 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZMS, cast(usize)sz, clevel, &cpr);
1116 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1117 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1118 wimlib_free_compressor(cpr);
1119 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1120 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1121 return;
1123 } else version(use_libwim_lzx) {
1124 if (sz <= WIMLIB_LZX_MAX_CHUNK) {
1125 wimlib_compressor* cpr;
1126 uint clevel = (packlevel < 10 ? 50 : 1000);
1127 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZX, cast(usize)sz, clevel, &cpr);
1128 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1129 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1130 wimlib_free_compressor(cpr);
1131 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1132 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1133 return;
1136 } else version(use_libwim_xpress) {
1137 if (sz <= WIMLIB_XPRESS_MAX_CHUNK) {
1138 wimlib_compressor* cpr;
1139 uint clevel = (packlevel < 10 ? 50 : 1000);
1140 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
1141 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < cast(uint)sz) csz *= 2U;
1142 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, clevel, &cpr);
1143 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1144 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
1145 wimlib_free_compressor(cpr);
1146 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
1147 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1148 return;
1151 } else version(use_lz4) {
1152 int dsize = LZ4_compress_default(vs, cbuf+xszlen, sz, cast(int)bsz);
1153 if (dsize > 0 && dsize+xszlen < sz) {
1154 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1155 return;
1157 } else version(use_zstd) {
1158 immutable int clev =
1159 packlevel <= 3 ? ZSTD_minCLevel() :
1160 packlevel <= 6 ? ZSTD_defaultCLevel() :
1161 packlevel < 10 ? 19 :
1162 ZSTD_maxCLevel();
1163 usize dsize = ZSTD_compress(cbuf+xszlen, cast(int)bsz, vs, sz, clev);
1164 if (!ZSTD_isError(dsize) && dsize > 0 && dsize+xszlen < sz) {
1165 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1166 return;
1168 } else {
1169 import etc.c.zlib : /*compressBound,*/ compress2, Z_OK;
1170 //uint bsz = cast(uint)compressBound(cast(uint)sz);
1171 if (packlevel > 9) packlevel = 9;
1172 usize dsize = bsz;
1173 int zres = compress2(cast(ubyte *)(cbuf+xszlen), &dsize, cast(const(ubyte) *)vs, sz, packlevel);
1174 if (zres == Z_OK && dsize+xszlen < cast(usize)sz) {
1175 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
1176 return;
1179 free(cbuf);
1184 if (isBadPrefix(vs[0..cast(uint)sz])) {
1185 char *res = cast(char *)malloc(sz+4);
1186 if (res is null) { sqlite3_result_error_nomem(ctx); return; }
1187 res[0..5] = "\x1bRAWB";
1188 res[5..sz+5] = vs[0..sz];
1189 if (isGoodText(vs[0..cast(usize)sz])) {
1190 sqlite3_result_text(ctx, res, sz+5, &free);
1191 } else {
1192 sqlite3_result_blob(ctx, res, sz+5, &free);
1194 } else {
1195 immutable bool wantBlob = !isGoodText(vs[0..cast(usize)sz]);
1196 immutable int tp = sqlite3_value_type(val);
1197 if ((wantBlob && tp == SQLITE_BLOB) || (!wantBlob && tp == SQLITE3_TEXT)) {
1198 sqlite3_result_value(ctx, val);
1199 } else if (wantBlob) {
1200 sqlite3_result_blob(ctx, vs, sz, SQLITE_TRANSIENT);
1201 } else {
1202 sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1209 ** ChiroPack(content)
1211 private void sq3Fn_ChiroPack (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
1212 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
1213 return sq3Fn_ChiroPackCommon(ctx, argv[0], ChiroCompressionLevel);
1218 ** ChiroPack(content, packlevel)
1220 ** `packlevel` == 0 means "don't pack"
1221 ** `packlevel` == 9 means "maximum compression"
1223 private void sq3Fn_ChiroPackDPArg (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
1224 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
1225 return sq3Fn_ChiroPackCommon(ctx, argv[0], sqlite3_value_int(argv[1]));
1230 ** ChiroUnpack(content)
1232 ** it is (almost) safe to pass non-packed content here
1234 private void sq3Fn_ChiroUnpack (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1235 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!000\n"); }
1236 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroUnpack()`", -1); return; }
1238 int sz = sqlite3_value_bytes(argv[0]);
1239 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
1241 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1243 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1244 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1246 if (!isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_value(ctx, argv[0]); return; }
1247 if (vs[0..5] == "\x1bRAWB") { sqlite3_result_blob(ctx, vs+5, sz-5, SQLITE_TRANSIENT); return; }
1248 if (sz < 6) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1250 enum {
1251 Codec_ZLIB,
1252 Codec_BALZ,
1253 Codec_XPAK,
1254 Codec_BRLZ,
1255 Codec_LZFS,
1256 Codec_LZJB,
1257 Codec_LZMS,
1258 Codec_LZMX,
1259 Codec_XPRS,
1260 Codec_LZ4D,
1261 Codec_ZSTD,
1264 int codec = Codec_ZLIB;
1265 if (vs[0..5] != "\x1bZLIB") {
1266 version(use_balz) {
1267 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBALZ") codec = Codec_BALZ;
1269 version(use_libxpack) {
1270 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1272 version(use_libxpack) {
1273 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1275 version(use_libbrieflz) {
1276 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBRLZ") codec = Codec_BRLZ;
1278 version(use_liblzfse) {
1279 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZFS") codec = Codec_LZFS;
1281 version(use_lzjb) {
1282 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZJB") codec = Codec_LZJB;
1284 version(use_libwim_lzms) {
1285 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMS") codec = Codec_LZMS;
1287 version(use_libwim_lzx) {
1288 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMX") codec = Codec_LZMX;
1290 version(use_libwim_xpress) {
1291 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPRS") codec = Codec_XPRS;
1293 version(use_lz4) {
1294 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZ4D") codec = Codec_LZ4D;
1296 version(use_zstd) {
1297 if (codec == Codec_ZLIB && vs[0..5] == "\x1bZSTD") codec = Codec_ZSTD;
1299 if (codec == Codec_ZLIB) { sqlite3_result_error(ctx, "invalid codec in `ChiroUnpack()`", -1); return; }
1302 // skip codec id
1303 // size is guaranteed to be at least 6 here
1304 vs += 5;
1305 sz -= 5;
1307 immutable uint numsz = decodeUIntLength(vs[0..cast(uint)sz]);
1308 //{ 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]); }
1309 //writeln("sq3Fn_ChiroUnpack: nsz=", sz-5);
1310 if (numsz == 0 || numsz > cast(uint)sz) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1311 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!100\n"); }
1312 immutable uint rsize = decodeUInt(vs[0..cast(uint)sz]);
1313 if (rsize == uint.max) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1314 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!101:rsize=%u\n", rsize); }
1315 if (rsize == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1316 // skip number
1317 vs += numsz;
1318 sz -= cast(int)numsz;
1319 //{ import core.stdc.stdio : printf; printf("sz=%d; rsize=%u\n", sz, rsize, dpos); }
1321 import core.stdc.stdlib : malloc, free;
1322 import core.stdc.string : memcpy;
1324 char* cbuf = cast(char*)malloc(rsize);
1325 if (cbuf is null) { sqlite3_result_error_nomem(ctx); return; }
1326 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz-dpos);
1328 usize dsize = rsize;
1329 final switch (codec) {
1330 case Codec_ZLIB:
1331 version(use_libdeflate) {
1332 libdeflate_decompressor *dcp = libdeflate_alloc_decompressor();
1333 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1334 auto rc = libdeflate_zlib_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1335 if (rc != LIBDEFLATE_SUCCESS) {
1336 free(cbuf);
1337 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1338 return;
1340 } else {
1341 import etc.c.zlib : uncompress, Z_OK;
1342 int zres = uncompress(cast(ubyte *)cbuf, &dsize, cast(const(ubyte) *)vs, sz);
1343 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz, "; dsize=", dsize, "; zres=", zres);
1344 if (zres != Z_OK || dsize != rsize) {
1345 free(cbuf);
1346 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1347 return;
1350 break;
1351 case Codec_BALZ:
1352 version(use_balz) {
1353 uint spos = 0;
1354 uint outpos = 0;
1355 try {
1356 Unbalz bz;
1357 auto dc = bz.decompress(
1358 // reader
1359 (buf) {
1360 uint left = cast(uint)sz-spos;
1361 if (left > buf.length) left = cast(uint)buf.length;
1362 if (left != 0) memcpy(buf.ptr, vs, left);
1363 spos += left;
1364 return left;
1366 // writer
1367 (buf) {
1368 uint left = rsize-outpos;
1369 if (left == 0) throw new Exception("broken data");
1370 if (left > buf.length) left = cast(uint)buf.length;
1371 if (left) memcpy(cbuf+outpos, buf.ptr, left);
1372 outpos += left;
1375 if (dc != rsize) throw new Exception("broken data");
1376 } catch (Exception) {
1377 outpos = uint.max;
1379 if (outpos == uint.max) {
1380 free(cbuf);
1381 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1382 return;
1384 dsize = outpos;
1385 } else {
1386 free(cbuf);
1387 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1388 return;
1390 break;
1391 case Codec_XPAK:
1392 version(use_libxpack) {
1393 xpack_decompressor *dcp = xpack_alloc_decompressor();
1394 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1395 auto rc = xpack_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1396 if (rc != DECOMPRESS_SUCCESS) {
1397 free(cbuf);
1398 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1399 return;
1401 } else {
1402 free(cbuf);
1403 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1404 return;
1406 break;
1407 case Codec_BRLZ:
1408 version(use_libbrieflz) {
1409 dsize = blz_depack_safe(vs, cast(uint)sz, cbuf, rsize);
1410 if (dsize != rsize) {
1411 free(cbuf);
1412 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1413 return;
1415 } else {
1416 free(cbuf);
1417 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1418 return;
1420 break;
1421 case Codec_LZFS:
1422 version(use_liblzfse) {
1423 immutable usize wbsize = lzfse_decode_scratch_size();
1424 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1425 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1426 dsize = lzfse_decode_buffer(cbuf, cast(usize)rsize, vs, cast(usize)sz, wbuf);
1427 free(wbuf);
1428 if (dsize == 0 || dsize != rsize) {
1429 free(cbuf);
1430 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1431 return;
1433 } else {
1434 free(cbuf);
1435 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1436 return;
1438 break;
1439 case Codec_LZJB:
1440 version(use_lzjb) {
1441 dsize = lzjb_decompress(vs, cast(usize)sz, cbuf, rsize);
1442 if (dsize != rsize) {
1443 free(cbuf);
1444 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1445 return;
1447 } else {
1448 free(cbuf);
1449 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1450 return;
1452 break;
1453 case Codec_LZMS:
1454 version(use_libwim_lzms) {
1455 wimlib_decompressor* dpr;
1456 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZMS, rsize, &dpr);
1457 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1458 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1459 wimlib_free_decompressor(dpr);
1460 if (rc != 0) {
1461 free(cbuf);
1462 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1463 return;
1465 } else {
1466 free(cbuf);
1467 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1468 return;
1470 break;
1471 case Codec_LZMX:
1472 version(use_libwim_lzx) {
1473 wimlib_decompressor* dpr;
1474 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZX, rsize, &dpr);
1475 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1476 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1477 wimlib_free_decompressor(dpr);
1478 if (rc != 0) {
1479 free(cbuf);
1480 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1481 return;
1483 } else {
1484 free(cbuf);
1485 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1486 return;
1488 break;
1489 case Codec_XPRS:
1490 version(use_libwim_xpress) {
1491 wimlib_decompressor* dpr;
1492 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
1493 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < rsize) csz *= 2U;
1494 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, &dpr);
1495 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1496 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1497 wimlib_free_decompressor(dpr);
1498 if (rc != 0) {
1499 free(cbuf);
1500 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1501 return;
1503 } else {
1504 free(cbuf);
1505 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1506 return;
1508 break;
1509 case Codec_LZ4D:
1510 version(use_lz4) {
1511 dsize = LZ4_decompress_safe(vs, cbuf, sz, rsize);
1512 if (dsize != rsize) {
1513 free(cbuf);
1514 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1515 return;
1517 } else {
1518 free(cbuf);
1519 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1520 return;
1522 break;
1523 case Codec_ZSTD:
1524 version(use_zstd) {
1525 dsize = ZSTD_decompress(cbuf, rsize, vs, sz);
1526 if (ZSTD_isError(dsize) || dsize != rsize) {
1527 free(cbuf);
1528 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1529 return;
1531 } else {
1532 free(cbuf);
1533 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1534 return;
1536 break;
1539 if (isGoodText(cbuf[0..dsize])) {
1540 sqlite3_result_text(ctx, cbuf, cast(int)dsize, &free);
1541 } else {
1542 sqlite3_result_blob(ctx, cbuf, cast(int)dsize, &free);
1548 ** ChiroNormCRLF(content)
1550 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1551 ** Removes trailing blanks.
1553 private void sq3Fn_ChiroNormCRLF (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1554 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormCRLF()`", -1); return; }
1556 int sz = sqlite3_value_bytes(argv[0]);
1557 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1559 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1561 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1562 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormCRLF()`", -1); return; }
1564 // check if we have something to do, and calculate new string size
1565 bool needwork = false;
1566 if (vs[cast(uint)sz-1] <= 32) {
1567 needwork = true;
1568 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1569 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1571 uint newsz = cast(uint)sz;
1572 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1573 if (ch == 13) {
1574 needwork = true;
1575 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1576 } else if (!needwork) {
1577 needwork = ((ch < 32 && ch != 9 && ch != 10) || ch == 127);
1581 if (!needwork) {
1582 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1583 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1584 return;
1587 assert(newsz && newsz <= cast(uint)sz);
1589 // need a new string
1590 import core.stdc.stdlib : malloc, free;
1591 char* newstr = cast(char*)malloc(newsz);
1592 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1593 char* dest = newstr;
1594 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1595 if (ch == 13) {
1596 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1597 } else {
1598 if (ch == 127) *dest++ = '~';
1599 else if (ch == 11 || ch == 12) *dest++ = '\n';
1600 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1601 else *dest++ = ch;
1604 assert(dest == newstr+newsz);
1606 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1611 ** ChiroNormHeaders(content)
1613 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1614 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1615 ** Removes trailing blanks.
1617 private void sq3Fn_ChiroNormHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1618 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormHeaders()`", -1); return; }
1620 int sz = sqlite3_value_bytes(argv[0]);
1621 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1623 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1625 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1626 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormHeaders()`", -1); return; }
1628 // check if we have something to do, and calculate new string size
1629 bool needwork = false;
1630 if (vs[cast(uint)sz-1] <= 32) {
1631 needwork = true;
1632 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1633 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1635 uint newsz = cast(uint)sz;
1636 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1637 if (ch == 13) {
1638 needwork = true;
1639 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1640 } else if (ch == 10) {
1641 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) { needwork = true; --newsz; }
1642 } else if (!needwork) {
1643 needwork = ((ch < 32 && ch != 10) || ch == 127);
1647 if (!needwork) {
1648 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1649 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1650 return;
1653 assert(newsz && newsz <= cast(uint)sz);
1655 // need a new string
1656 import core.stdc.stdlib : malloc, free;
1657 char* newstr = cast(char*)malloc(newsz);
1658 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1659 char* dest = newstr;
1660 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1661 if (ch == 13) {
1662 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1663 } else if (ch == 10) {
1664 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1665 } else {
1666 if (ch == 127) *dest++ = '~';
1667 else if (ch < 32 && ch != 10) *dest++ = ' ';
1668 else *dest++ = ch;
1671 assert(dest == newstr+newsz);
1673 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1678 ** ChiroExtractHeaders(content)
1680 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1681 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1682 ** Removes trailing blanks.
1684 private void sq3Fn_ChiroExtractHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1685 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1687 int sz = sqlite3_value_bytes(argv[0]);
1688 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1690 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1692 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1693 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1695 // slice headers
1696 sz = sq3Supp_FindHeadersEnd(vs, sz);
1698 // strip trailing blanks
1699 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1700 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1702 // allocate new string (it can be smaller, but will never be bigger)
1703 import core.stdc.stdlib : malloc, free;
1704 char* newstr = cast(char*)malloc(cast(uint)sz);
1705 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1706 char* dest = newstr;
1707 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1708 if (ch == 13) {
1709 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1710 } else if (ch == 10) {
1711 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1712 } else {
1713 if (ch == 127) *dest++ = '~';
1714 else if (ch < 32 && ch != 10) *dest++ = ' ';
1715 else *dest++ = ch;
1718 assert(dest <= newstr+cast(uint)sz);
1719 sz = cast(int)cast(usize)(dest-newstr);
1720 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1721 sqlite3_result_text(ctx, newstr, sz, &free);
1726 ** ChiroExtractBody(content)
1728 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1729 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1730 ** Removes trailing blanks and final dot.
1732 private void sq3Fn_ChiroExtractBody (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1733 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1735 int sz = sqlite3_value_bytes(argv[0]);
1736 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1738 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1740 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1741 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1743 // slice body
1744 immutable int bstart = sq3Supp_FindHeadersEnd(vs, sz);
1745 if (bstart >= sz) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1746 vs += bstart;
1747 sz -= bstart;
1749 // strip trailing dot
1750 if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '\n') sz -= 2;
1751 else if (sz >= 1 && vs[cast(uint)sz-1U] == '\n') --sz;
1752 if (sz == 1 && vs[0] == '.') sz = 0;
1753 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\n' && vs[cast(uint)sz-1U] == '.') --sz;
1754 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '.') --sz;
1756 // strip trailing blanks
1757 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1758 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1760 // allocate new string (it can be smaller, but will never be bigger)
1761 import core.stdc.stdlib : malloc, free;
1762 char* newstr = cast(char*)malloc(cast(uint)sz);
1763 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1764 char* dest = newstr;
1765 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1766 if (ch == 13) {
1767 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1768 } else {
1769 if (ch == 127) *dest++ = '~';
1770 else if (ch == 11 || ch == 12) *dest++ = '\n';
1771 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1772 else *dest++ = ch;
1775 assert(dest <= newstr+cast(uint)sz);
1776 sz = cast(int)cast(usize)(dest-newstr);
1777 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1778 sqlite3_result_text(ctx, newstr, sz, &free);
1783 ** ChiroRIPEMD160(content)
1785 ** Calculates RIPEMD160 hash over the given content.
1787 ** Returns BINARY BLOB! You can use `tolower(hex(ChiroRIPEMD160(contents)))`
1788 ** to get lowercased hex hash string.
1790 private void sq3Fn_ChiroRIPEMD160 (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1791 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroRIPEMD160()`", -1); return; }
1793 immutable int sz = sqlite3_value_bytes(argv[0]);
1794 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1796 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1797 if (!vs && sz == 0) vs = "";
1798 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroRIPEMD160()`", -1); return; }
1800 ubyte[20] hash = ripemd160Of(vs[0..cast(uint)sz]);
1801 sqlite3_result_blob(ctx, cast(const(char)*)hash.ptr, cast(int)hash.length, SQLITE_TRANSIENT);
1805 enum HeaderProcStartTpl(string fnname) = `
1806 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"`~fnname~`()\"", -1); return; }
1808 immutable int sz = sqlite3_value_bytes(argv[0]);
1809 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1811 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1812 if (!vs && sz == 0) vs = "";
1813 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"`~fnname~`()\"", -1); return; }
1815 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
1820 ** ChiroHdr_NNTPIndex(headers)
1822 ** The content must be email with headers (or headers only).
1823 ** Returns "NNTP-Index" field or zero (int).
1825 private void sq3Fn_ChiroHdr_NNTPIndex (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1826 mixin(HeaderProcStartTpl!"ChiroHdr_NNTPIndex");
1828 uint nntpidx = 0;
1830 auto nntpidxfld = findHeaderField(hdrs, "NNTP-Index");
1831 if (nntpidxfld.length) {
1832 auto id = nntpidxfld.getFieldValue;
1833 if (id.length) {
1834 foreach (immutable ch; id) {
1835 if (ch < '0' || ch > '9') { nntpidx = 0; break; }
1836 if (nntpidx == 0 && ch == '0') continue;
1837 immutable uint nn = nntpidx*10u+(ch-'0');
1838 if (nn <= nntpidx) nntpidx = 0x7fffffff; else nntpidx = nn;
1843 // it is safe, it can't overflow
1844 sqlite3_result_int(ctx, cast(int)nntpidx);
1849 ** ChiroHdr_RecvTime(headers)
1851 ** The content must be email with headers (or headers only).
1852 ** Returns unixtime (can be zero).
1854 private void sq3Fn_ChiroHdr_RecvTime (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1855 mixin(HeaderProcStartTpl!"ChiroHdr_RecvTime");
1857 uint msgtime = 0; // message receiving time
1859 auto datefld = findHeaderField(hdrs, "Injection-Date");
1860 if (datefld.length != 0) {
1861 auto v = datefld.getFieldValue;
1862 try {
1863 msgtime = parseMailDate(v);
1864 } catch (Exception) {
1865 //writeln("UID=", uid, ": FUCKED INJECTION-DATE: |", v, "|");
1866 msgtime = 0; // just in case
1870 if (!msgtime) {
1871 // obsolete NNTP date field, because why not?
1872 datefld = findHeaderField(hdrs, "NNTP-Posting-Date");
1873 if (datefld.length != 0) {
1874 auto v = datefld.getFieldValue;
1875 try {
1876 msgtime = parseMailDate(v);
1877 } catch (Exception) {
1878 //writeln("UID=", uid, ": FUCKED NNTP-POSTING-DATE: |", v, "|");
1879 msgtime = 0; // just in case
1884 if (!msgtime) {
1885 datefld = findHeaderField(hdrs, "Date");
1886 if (datefld.length != 0) {
1887 auto v = datefld.getFieldValue;
1888 try {
1889 msgtime = parseMailDate(v);
1890 } catch (Exception) {
1891 //writeln("UID=", uid, ": FUCKED DATE: |", v, "|");
1892 msgtime = 0; // just in case
1897 // finally, try to get time from "Received:"
1898 //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
1899 if (!msgtime) {
1900 //writeln("!!! --- !!!");
1901 uint lowesttime = uint.max;
1902 foreach (uint fidx; 0..uint.max) {
1903 auto recvfld = findHeaderField(hdrs, "Received", fidx);
1904 if (recvfld.length == 0) break;
1905 auto lsemi = recvfld.lastIndexOf(';');
1906 if (lsemi >= 0) recvfld = recvfld[lsemi+1..$].xstrip;
1907 if (recvfld.length != 0) {
1908 auto v = recvfld.getFieldValue;
1909 uint tm = 0;
1910 try {
1911 tm = parseMailDate(v);
1912 } catch (Exception) {
1913 //writeln("UID=", uid, ": FUCKED RECV DATE: |", v, "|");
1914 tm = 0; // just in case
1916 //writeln(tm, " : ", lowesttime);
1917 if (tm && tm < lowesttime) lowesttime = tm;
1920 if (lowesttime != uint.max) msgtime = lowesttime;
1923 sqlite3_result_int64(ctx, msgtime);
1928 ** ChiroHdr_FromEmail(headers)
1930 ** The content must be email with headers (or headers only).
1931 ** Returns email "From" field.
1933 private void sq3Fn_ChiroHdr_FromEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1934 mixin(HeaderProcStartTpl!"ChiroHdr_FromEmail");
1935 auto from = findHeaderField(hdrs, "From").extractMail;
1936 if (from.length == 0) {
1937 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
1938 } else {
1939 sqlite3_result_text(ctx, from.ptr, cast(int)from.length, SQLITE_TRANSIENT);
1945 ** ChiroHdr_ToEmail(headers)
1947 ** The content must be email with headers (or headers only).
1948 ** Returns email "From" field.
1950 private void sq3Fn_ChiroHdr_ToEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1951 mixin(HeaderProcStartTpl!"ChiroHdr_ToEmail");
1952 auto to = findHeaderField(hdrs, "To").extractMail;
1953 if (to.length == 0) {
1954 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
1955 } else {
1956 sqlite3_result_text(ctx, to.ptr, cast(int)to.length, SQLITE_TRANSIENT);
1962 ** ChiroHdr_Subj(headers)
1964 ** The content must be email with headers (or headers only).
1965 ** Returns email "From" field.
1967 private void sq3Fn_ChiroHdr_Subj (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1968 mixin(HeaderProcStartTpl!"sq3Fn_ChiroHdr_Subj");
1969 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
1970 if (subj.length == 0) {
1971 sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
1972 } else {
1973 sqlite3_result_text(ctx, subj.ptr, cast(int)subj.length, SQLITE_TRANSIENT);
1979 ** ChiroHdr_Field(headers, fieldname)
1981 ** The content must be email with headers (or headers only).
1982 ** Returns field value as text, or NULL if there is no such field.
1984 private void sq3Fn_ChiroHdr_Field (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1985 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroHdr_Field()\"", -1); return; }
1987 immutable int sz = sqlite3_value_bytes(argv[0]);
1988 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1990 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1991 if (!vs && sz == 0) vs = "";
1992 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
1994 immutable int fldsz = sqlite3_value_bytes(argv[1]);
1995 if (fldsz < 0) { sqlite3_result_error_toobig(ctx); return; }
1997 const(char)* fldname = cast(const(char) *)sqlite3_value_blob(argv[1]);
1998 if (!fldname && fldsz == 0) fldname = "";
1999 if (!fldname) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
2001 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
2002 auto value = findHeaderField(hdrs, fldname[0..fldsz]);
2003 if (value is null) {
2004 sqlite3_result_null(ctx);
2005 } else if (value.length == 0) {
2006 sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
2007 } else {
2008 sqlite3_result_text(ctx, value.ptr, cast(int)value.length, SQLITE_TRANSIENT);
2014 ** ChiroTimerStart([msg])
2016 ** The content must be email with headers (or headers only).
2017 ** Returns email "From" field.
2019 private void sq3Fn_ChiroTimerStart (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2020 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStart()\"", -1); return; }
2022 delete chiTimerMsg;
2024 if (argc == 1) {
2025 immutable int sz = sqlite3_value_bytes(argv[0]);
2026 if (sz > 0) {
2027 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2028 if (vs) {
2029 chiTimerMsg = new char[cast(usize)sz];
2030 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
2031 writeln("started ", chiTimerMsg, "...");
2036 sqlite3_result_int(ctx, 1);
2037 chiTimer.restart();
2042 ** ChiroTimerStop([msg])
2044 ** The content must be email with headers (or headers only).
2045 ** Returns email "From" field.
2047 private void sq3Fn_ChiroTimerStop (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2048 chiTimer.stop;
2049 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStop()\"", -1); return; }
2051 if (ChiroTimerEnabled) {
2052 if (argc == 1) {
2053 delete chiTimerMsg;
2054 immutable int sz = sqlite3_value_bytes(argv[0]);
2055 if (sz > 0) {
2056 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
2057 if (vs) {
2058 chiTimerMsg = new char[cast(usize)sz];
2059 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
2064 char[128] buf;
2065 auto tstr = chiTimer.toBuffer(buf[]);
2066 if (chiTimerMsg.length) {
2067 writeln("done ", chiTimerMsg, ": ", tstr);
2068 } else {
2069 writeln("time: ", tstr);
2073 delete chiTimerMsg;
2075 sqlite3_result_int(ctx, 1);
2080 ** ChiroGlob(pat, str)
2082 ** GLOB replacement, with extended word matching.
2084 private void sq3Fn_ChiroGlob_common (sqlite3_context *ctx, int argc, sqlite3_value **argv, int casesens,
2085 uint stridx=1, uint patidx=0)
2087 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroGlob()\"", -1); return; }
2089 immutable int patsz = sqlite3_value_bytes(argv[patidx]);
2090 if (patsz < 0) { sqlite3_result_error_toobig(ctx); return; }
2092 const(char)* pat = cast(const(char) *)sqlite3_value_blob(argv[patidx]);
2093 if (!pat && patsz == 0) pat = "";
2094 if (!pat) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2096 immutable int strsz = sqlite3_value_bytes(argv[stridx]);
2097 if (strsz < 0) { sqlite3_result_error_toobig(ctx); return; }
2099 const(char)* str = cast(const(char) *)sqlite3_value_blob(argv[stridx]);
2100 if (!str && strsz == 0) str = "";
2101 if (!str) { sqlite3_result_error(ctx, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2103 immutable bool res =
2104 casesens ?
2105 globmatch(str[0..cast(usize)strsz], pat[0..cast(usize)patsz]) :
2106 globmatchCI(str[0..cast(usize)strsz], pat[0..cast(usize)patsz]);
2108 sqlite3_result_int(ctx, (res ? 1 : 0));
2113 ** ChiroGlobSQL(pat, str)
2115 ** GLOB replacement, with extended word matching.
2117 private void sq3Fn_ChiroGlobSQL (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2118 sq3Fn_ChiroGlob_common(ctx, argc, argv, casesens:1);
2122 ** ChiroGlob(str, pat)
2124 ** GLOB replacement, with extended word matching.
2126 private void sq3Fn_ChiroGlob (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2127 sq3Fn_ChiroGlob_common(ctx, argc, argv, casesens:1, stridx:0, patidx:1);
2131 ** ChiroGlobCI(str, pat)
2133 ** GLOB replacement, with extended word matching.
2135 private void sq3Fn_ChiroGlobCI (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
2136 sq3Fn_ChiroGlob_common(ctx, argc, argv, casesens:0, stridx:0, patidx:1);
2140 // ////////////////////////////////////////////////////////////////////////// //
2144 // ////////////////////////////////////////////////////////////////////////// //
2145 private void registerFunctions (ref Database db) {
2146 sqlite3_busy_timeout(db.getHandle, 20000); // busy timeout: 20 seconds
2148 immutable int rc = sqlite3_extended_result_codes(db.getHandle, 1);
2149 if (rc != SQLITE_OK) {
2150 import core.stdc.stdio : stderr, fprintf;
2151 fprintf(stderr, "SQLITE WARNING: cannot enable extended result codes (this is harmless).\n");
2153 db.createFunction("glob", 2, &sq3Fn_ChiroGlobSQL, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2154 db.createFunction("ChiroGlob", 2, &sq3Fn_ChiroGlob, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2155 db.createFunction("ChiroGlobCI", 2, &sq3Fn_ChiroGlobCI, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2157 db.createFunction("ChiroPack", 1, &sq3Fn_ChiroPack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2158 db.createFunction("ChiroPack", 2, &sq3Fn_ChiroPackDPArg, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2159 db.createFunction("ChiroUnpack", 1, &sq3Fn_ChiroUnpack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2160 db.createFunction("ChiroNormCRLF", 1, &sq3Fn_ChiroNormCRLF, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2161 db.createFunction("ChiroNormHeaders", 1, &sq3Fn_ChiroNormHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2162 db.createFunction("ChiroExtractHeaders", 1, &sq3Fn_ChiroExtractHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2163 db.createFunction("ChiroExtractBody", 1, &sq3Fn_ChiroExtractBody, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2164 db.createFunction("ChiroRIPEMD160", 1, &sq3Fn_ChiroRIPEMD160, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2166 db.createFunction("ChiroHdr_NNTPIndex", 1, &sq3Fn_ChiroHdr_NNTPIndex, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2167 db.createFunction("ChiroHdr_RecvTime", 1, &sq3Fn_ChiroHdr_RecvTime, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2168 db.createFunction("ChiroHdr_FromEmail", 1, &sq3Fn_ChiroHdr_FromEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2169 db.createFunction("ChiroHdr_ToEmail", 1, &sq3Fn_ChiroHdr_ToEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2170 db.createFunction("ChiroHdr_Subj", 1, &sq3Fn_ChiroHdr_Subj, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2171 db.createFunction("ChiroHdr_Field", 2, &sq3Fn_ChiroHdr_Field, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2173 db.createFunction("ChiroTimerStart", 0, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2174 db.createFunction("ChiroTimerStart", 1, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2175 db.createFunction("ChiroTimerStop", 0, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2176 db.createFunction("ChiroTimerStop", 1, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
2180 // ////////////////////////////////////////////////////////////////////////// //
2181 public void chiroRecreateStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName) {
2182 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2183 dbStore = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWStorageRecreate, schemaStorage);
2184 registerFunctions(dbStore);
2185 dbStore.setOnClose(schemaStorageIndex~dbpragmasRWStorage~"ANALYZE;");
2189 // ////////////////////////////////////////////////////////////////////////// //
2190 public void chiroRecreateViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName) {
2191 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2192 dbView = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWSupportRecreate, schemaSupportTable);
2193 registerFunctions(dbView);
2194 dbView.setOnClose(schemaSupportIndex~dbpragmasRWSupport~"ANALYZE;");
2198 public void chiroCreateViewIndiciesDB () {
2199 dbView.setOnClose(dbpragmasRWSupport~"ANALYZE;");
2200 dbView.execute(schemaSupportIndex);
2204 // ////////////////////////////////////////////////////////////////////////// //
2205 public void chiroRecreateConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName) {
2206 try { import std.file : remove; remove(dbname); } catch (Exception) {}
2207 dbConf = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWOptionsRecreate, schemaOptions);
2208 registerFunctions(dbConf);
2209 dbConf.setOnClose(schemaOptionsIndex~dbpragmasRWOptions~"ANALYZE;");
2213 // ////////////////////////////////////////////////////////////////////////// //
2214 public void chiroOpenStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName, bool readonly=false) {
2215 dbStore = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWStorage), schemaStorage);
2216 registerFunctions(dbStore);
2217 if (!readonly) dbStore.setOnClose("PRAGMA optimize;");
2221 // ////////////////////////////////////////////////////////////////////////// //
2222 public void chiroOpenViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName, bool readonly=false) {
2223 dbView = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWSupport), schemaSupport);
2224 registerFunctions(dbView);
2225 if (!readonly) {
2226 dbView.execute(schemaSupportTempTables);
2227 dbView.setOnClose("PRAGMA optimize;");
2232 // ////////////////////////////////////////////////////////////////////////// //
2233 public void chiroOpenConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName, bool readonly=false) {
2234 dbConf = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWOptions), schemaOptions);
2235 registerFunctions(dbConf);
2236 if (!readonly) dbConf.setOnClose("PRAGMA optimize;");
2240 // ////////////////////////////////////////////////////////////////////////// //
2242 recreates FTS5 (full-text search) info.
2244 public void chiroRecreateFTS5 (bool repopulate=true) {
2245 dbView.execute(recreateFTS5);
2246 if (repopulate) dbView.execute(repopulateFTS5);
2247 dbView.execute(recreateFTS5Triggers);
2251 // ////////////////////////////////////////////////////////////////////////// //
2252 extern(C) {
2253 static void errorLogCallback (void *pArg, int rc, const char *zMsg) {
2254 if (ChiroSQLiteSilent) return;
2255 import core.stdc.stdio : stderr, fprintf;
2256 switch (rc) {
2257 case SQLITE_NOTICE: fprintf(stderr, "***SQLITE NOTICE: %s\n", zMsg); break;
2258 case SQLITE_NOTICE_RECOVER_WAL: fprintf(stderr, "***SQLITE NOTICE (WAL RECOVER): %s\n", zMsg); break;
2259 case SQLITE_NOTICE_RECOVER_ROLLBACK: fprintf(stderr, "***SQLITE NOTICE (ROLLBACK RECOVER): %s\n", zMsg); break;
2260 /* */
2261 case SQLITE_WARNING: fprintf(stderr, "***SQLITE WARNING: %s\n", zMsg); break;
2262 case SQLITE_WARNING_AUTOINDEX: fprintf(stderr, "***SQLITE AUTOINDEX WARNING: %s\n", zMsg); break;
2263 /* */
2264 case SQLITE_CANTOPEN:
2265 case SQLITE_SCHEMA:
2266 break; // ignore those
2267 /* */
2268 default: fprintf(stderr, "***SQLITE LOG(%d) [%s]: %s\n", rc, sqlite3_errstr(rc), zMsg); break;
2274 static string sqerrstr (immutable int rc) nothrow @trusted {
2275 const(char)* msg = sqlite3_errstr(rc);
2276 if (!msg || !msg[0]) return null;
2277 import core.stdc.string : strlen;
2278 return msg[0..strlen(msg)].idup;
2282 static void sqconfigcheck (immutable int rc, string msg, bool fatal) {
2283 if (rc == SQLITE_OK) return;
2284 if (fatal) {
2285 string errmsg = sqerrstr(rc);
2286 throw new Exception("FATAL: "~msg~": "~errmsg);
2287 } else {
2288 if (msg is null) msg = "";
2289 import core.stdc.stdio : stderr, fprintf;
2290 fprintf(stderr, "SQLITE WARNING: %.*s (this is harmless): %s\n", cast(uint)msg.length, msg.ptr, sqlite3_errstr(rc));
2295 // call this BEFORE opening any SQLite database connection!
2296 public void chiroSwitchToSingleThread () {
2297 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SINGLETHREAD), "cannot set single-threaded mode", fatal:false);
2301 public string MailDBPath () nothrow @trusted @nogc { return ExpandedMailDBPath; }
2304 public void MailDBPath(T:const(char)[]) (T mailpath) nothrow @trusted {
2305 while (mailpath.length > 1 && mailpath[$-1] == '/') mailpath = mailpath[0..$-1];
2307 if (mailpath.length == 0 || mailpath == ".") {
2308 ExpandedMailDBPath = "";
2309 return;
2312 if (mailpath[0] == '~') {
2313 char[] dpath = new char[mailpath.length+4096];
2314 dpath = expandTilde(dpath, mailpath);
2316 while (dpath.length > 1 && dpath[$-1] == '/') dpath = dpath[0..$-1];
2317 dpath ~= '/';
2318 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
2319 } else {
2320 char[] dpath = new char[mailpath.length+1];
2321 dpath[0..$-1] = mailpath[];
2322 dpath[$-1] = '/';
2323 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
2328 shared static this () {
2329 enum {
2330 SQLITE_CONFIG_STMTJRNL_SPILL = 26, /* int nByte */
2331 SQLITE_CONFIG_SMALL_MALLOC = 27, /* boolean */
2334 if (!sqlite3_threadsafe()) {
2335 throw new Exception("FATAL: SQLite must be compiled with threading support!");
2338 // we are interested in all errors
2339 sqlite3_config(SQLITE_CONFIG_LOG, &errorLogCallback, null);
2341 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SERIALIZED), "cannot set SQLite serialized threading mode", fatal:true);
2342 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SMALL_MALLOC, 0), "cannot enable SQLite unrestriced malloc mode", fatal:false);
2343 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_URI, 1), "cannot enable SQLite URI handling", fatal:false);
2344 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_COVERING_INDEX_SCAN, 1), "cannot enable SQLite covering index scan", fatal:false);
2345 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_STMTJRNL_SPILL, 512*1024), "cannot set SQLite statement journal spill threshold", fatal:false);
2347 MailDBPath = "~/Mail";
2351 shared static ~this () {
2352 dbConf.close();
2353 dbView.close();
2354 dbStore.close();
2358 // ////////////////////////////////////////////////////////////////////////// //
2359 public void transacted(string dbname) (void delegate () dg) {
2360 if (dg is null) return;
2361 static if (dbname == "View" || dbname == "view") alias db = dbView;
2362 else static if (dbname == "Store" || dbname == "store") alias db = dbStore;
2363 else static if (dbname == "Conf" || dbname == "conf") alias db = dbConf;
2364 else static assert(0, "invalid db name: '"~dbname~"'");
2365 db.transacted(dg);
2369 // ////////////////////////////////////////////////////////////////////////// //
2370 public void chiroSetOption(T) (const(char)[] name, T value)
2371 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2373 assert(name.length != 0);
2374 static auto stat = LazyStatement!"Conf"(`
2375 INSERT INTO options
2376 ( name, value)
2377 VALUES(:name,:value)
2378 ON CONFLICT(name)
2379 DO UPDATE SET value=:value
2380 ;`);
2381 stat.st.bindConstText(":name", name);
2382 static if (is(T == typeof(null))) {
2383 stat.st.bindConstText(":value", "");
2384 } else static if (__traits(isIntegral, T)) {
2385 stat.st.bind(":value", value);
2386 } else static if (is(T:const(char)[])) {
2387 stat.st.bindConstText(":value", value);
2388 } else {
2389 static assert(0, "oops");
2391 stat.st.doAll();
2394 public void chiroSetOption (const(char)[] name, DynStr value) {
2395 assert(name.length != 0);
2396 //{ import std.stdio; writeln("SETOPTION(", name, "): <", value.getData, ">"); }
2397 static auto stat = LazyStatement!"Conf"(`
2398 INSERT INTO options
2399 ( name, value)
2400 VALUES(:name,:value)
2401 ON CONFLICT(name)
2402 DO UPDATE SET value=:value
2403 ;`);
2404 stat.st
2405 .bindConstText(":name", name)
2406 .bindConstText(":value", value.getData)
2407 .doAll();
2411 public void chiroSetOptionUInts (const(char)[] name, uint v0, uint v1) {
2412 assert(name.length != 0);
2413 static auto stat = LazyStatement!"Conf"(`
2414 INSERT INTO options
2415 ( name, value)
2416 VALUES(:name,:value)
2417 ON CONFLICT(name)
2418 DO UPDATE SET value=:value
2419 ;`);
2420 import core.stdc.stdio : snprintf;
2421 char[64] value = void;
2422 auto vlen = snprintf(value.ptr, value.sizeof, "%u,%u", v0, v1);
2423 stat.st
2424 .bindConstText(":name", name)
2425 .bindConstText(":value", value[0..vlen])
2426 .doAll();
2430 public T chiroGetOptionEx(T) (const(char)[] name, out bool exists, T defval=T.init)
2431 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2433 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2434 assert(name.length != 0);
2435 exists = false;
2436 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2437 exists = true;
2438 return row.value!T;
2440 return defval;
2443 public T chiroGetOption(T) (const(char)[] name, T defval=T.init)
2444 if (!is(T:const(DynStr)) && (__traits(isIntegral, T) || is(T:const(char)[])))
2446 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2447 assert(name.length != 0);
2448 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2449 return row.value!T;
2451 return defval;
2454 public void chiroGetOption (ref DynStr s, const(char)[] name, const(char)[] defval=null) {
2455 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2456 assert(name.length != 0);
2457 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2458 s = row.value!SQ3Text;
2459 return;
2461 s = defval;
2465 private uint parseUInt (ref SQ3Text s) {
2466 s = s.xstrip;
2467 if (s.length == 0 || !isdigit(s[0])) return uint.max;
2468 uint res = 0;
2469 while (s.length) {
2470 immutable int dg = s[0].digitInBase(10);
2471 if (dg < 0) break;
2472 immutable uint nr = res*10U+cast(uint)dg;
2473 if (nr < res) return uint.max;
2474 res = nr;
2475 s = s[1..$];
2477 if (s.length && s[0] == ',') s = s[1..$];
2478 s = s.xstrip;
2479 return res;
2483 public void chiroGetOptionUInts (ref uint v0, ref uint v1, const(char)[] name) {
2484 static auto stat = LazyStatement!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2485 assert(name.length != 0);
2486 foreach (auto row; stat.st.bindConstText(":name", name).range) {
2487 auto s = row.value!SQ3Text;
2488 immutable uint rv0 = parseUInt(s);
2489 immutable uint rv1 = parseUInt(s);
2490 if (rv0 != uint.max && rv1 != uint.max && s.length == 0) {
2491 v0 = rv0;
2492 v1 = rv1;
2494 return;
2499 // ////////////////////////////////////////////////////////////////////////// //
2500 // append tag if necessary, return tagid
2501 // tag name must be valid: not empty, and not end with a '/'
2502 // returns 0 on invalid tag name
2503 uint chiroAppendTag (const(char)[] tagname, int hidden=0) {
2504 tagname = tagname.xstrip;
2505 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2506 tagname = tagname.xstrip;
2507 if (tagname.length == 0) return 0;
2508 if (tagname.indexOf('|') >= 0) return 0;
2510 static auto stAppendTag = LazyStatement!"View"(`
2511 INSERT INTO tagnames(tag, hidden, threading) VALUES(:tagname,:hidden,:threading)
2512 ON CONFLICT(tag)
2513 DO UPDATE SET hidden=hidden -- this is for "returning"
2514 RETURNING tagid AS tagid
2515 ;`);
2517 // alphanum tags must start with '/'
2518 DynStr tn;
2519 if (tagname[0].isalnum && tagname.indexOf(':') < 0) {
2520 tn = "/";
2521 tn ~= tagname;
2522 stAppendTag.st.bindConstText(":tagname", tn);
2523 } else {
2524 stAppendTag.st.bindConstText(":tagname", tagname);
2526 stAppendTag.st
2527 .bind(":hidden", hidden)
2528 .bind(":threading", (hidden ? 0 : 1));
2529 foreach (auto row; stAppendTag.st.range) return row.tagid!uint;
2531 return 0;
2535 // ////////////////////////////////////////////////////////////////////////// //
2536 /// returns `true` if we need to update pane
2537 /// if message is left without any tags, it will be tagged with "#hobo"
2538 public bool chiroMessageRemoveTag (uint uid, const(char)[] tagname) {
2539 if (uid == 0) return false;
2540 tagname = tagname.xstrip;
2541 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2542 tagname = tagname.xstrip;
2543 if (tagname.length == 0) return false;
2544 if (tagname.indexOf('|') >= 0) return false;
2546 immutable tagid = chiroGetTagUid(tagname);
2547 if (tagid == 0) return false;
2549 static auto stUpdateStorageTags = LazyStatement!"Store"(`
2550 UPDATE SET tags=:tags WHERE uid=:uid
2551 ;`);
2553 static auto stUidHasTag = LazyStatement!"View"(`
2554 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2555 ;`);
2557 static auto stInsertIntoThreads = LazyStatement!"View"(`
2558 INSERT INTO threads(uid, tagid,appearance,time)
2559 VALUES(:uid, :tagid, :appr, (SELECT time FROM info WHERE uid=:uid LIMIT 1))
2560 ;`);
2562 // delete message from threads
2563 static auto stClearThreads = LazyStatement!"View"(`
2564 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2565 ;`);
2567 static auto stGetMsgTags = LazyStatement!"View"(`
2568 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
2569 FROM threads
2570 WHERE uid=:uid
2571 INNER JOIN tagnames AS tt USING(tagid)
2572 ;`);
2575 immutable bool updatePane = (chiroGetTreePaneTableTagId() == tagid);
2576 bool wasChanges = false;
2578 transacted!"View"{
2579 // get tagid (possibly appending the tag)
2580 bool hasit = false;
2581 foreach (auto row; stUidHasTag.st.bind(":uid", uid).bind(":tagid", tagid).range) hasit = true;
2582 if (!hasit) return;
2584 stClearThreads.st.bind(":uid", uid).bind(":tagid", tagid).doAll((stmt) { wasChanges = true; });
2586 // if there were any changes, rebuild message tags
2587 if (!wasChanges) return;
2589 DynStr newtags;
2590 foreach (auto trow; stGetMsgTags.st.bind(":uid", uid).range) {
2591 auto tname = trow.name!SQ3Text;
2592 if (tname.length == 0) continue;
2593 if (newtags.length) newtags ~= "|";
2594 newtags ~= tname;
2597 // if there is no tags, assign "#hobo"
2598 // this should not happen, but...
2599 if (newtags.length == 0) {
2600 newtags = "#hobo";
2601 auto hobo = chiroAppendTag(newtags, hidden:1);
2602 assert(hobo != 0);
2603 // append record for this tag to threads
2604 // note that there is no need to relink hobos, they should not be threaded
2605 //FIXME: this clears message appearance
2606 stInsertIntoThreads.st
2607 .bind(":uid", uid)
2608 .bind(":tagid", hobo)
2609 .bind(":appr", Appearance.Read)
2610 .doAll();
2613 // update storage with new tag names
2614 assert(newtags.length);
2615 stUpdateStorageTags.st.bindConstText(":tags", newtags).doAll();
2617 // and relink threads for this tagid
2618 chiroSupportRelinkTagThreads(tagid);
2621 return (wasChanges && updatePane);
2625 // ////////////////////////////////////////////////////////////////////////// //
2626 /// returns `true` if we need to update pane
2627 public bool chiroMessageAddTag (uint uid, const(char)[] tagname) {
2628 if (uid == 0) return false;
2629 tagname = tagname.xstrip;
2630 while (tagname.length && tagname[$-1] == '/') tagname = tagname[0..$-1];
2631 tagname = tagname.xstrip;
2632 if (tagname.length == 0) return false;
2633 if (tagname.indexOf('|') >= 0) return false;
2635 static auto stUpdateStorageTags = LazyStatement!"Store"(`
2636 UPDATE SET tags=tags||'|'||:tagname WHERE uid=:uid
2637 ;`);
2639 static auto stUidExists = LazyStatement!"View"(`
2640 SELECT uid AS uid FROM threads WHERE uid=:uid LIMIT 1
2641 ;`);
2643 static auto stUidHasTag = LazyStatement!"View"(`
2644 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2645 ;`);
2647 static auto stInsertIntoThreads = LazyStatement!"View"(`
2648 INSERT INTO threads(uid, tagid,appearance,time)
2649 VALUES(:uid, :tagid, :appr, (SELECT time FROM info WHERE uid=:uid LIMIT 1))
2650 ;`);
2652 static auto stUnHobo = LazyStatement!"View"(`
2653 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2654 ;`);
2656 bool hasuid = false;
2657 foreach (auto row; stUidExists.st.bind(":uid", uid).range) hasuid = true;
2658 if (!hasuid) return false; // nothing to do
2660 immutable paneTagId = chiroGetTreePaneTableTagId();
2661 bool updatePane = false;
2663 transacted!"View"{
2664 // get tagid (possibly appending the tag)
2665 uint tagid = chiroAppendTag(tagname);
2666 if (tagid == 0) {
2667 conwriteln("ERROR: cannot append tag name '", tagname, "'!");
2668 return;
2671 bool hasit = false;
2672 foreach (auto row; stUidHasTag.st.bind(":uid", uid).bind(":tagid", tagid).range) hasit = true;
2673 if (hasit) return;
2675 // append this tag to the message in the storage
2676 stUpdateStorageTags.st.bind(":uid", uid).bindConstText(":tagname", tagname).doAll();
2678 // append record for this tag to threads
2679 stInsertIntoThreads.st
2680 .bind(":uid", uid)
2681 .bind(":tagid", tagid)
2682 .bind(":appr", Appearance.Read)
2683 .doAll();
2685 // and relink threads for this tagid
2686 chiroSupportRelinkTagThreads(tagid);
2688 // remove this message from "#hobo", if there is any
2689 auto hobo = chiroGetTagUid("#hobo");
2690 if (hobo && hobo != tagid) {
2691 stUnHobo.st.bind(":tagid", hobo).bind(":uid", uid).doAll();
2692 // there's no need to relink hobos, because they should have no links
2695 updatePane = (tagid == paneTagId);
2698 return updatePane;
2703 inserts the one message from the message storage with the given id into view storage.
2704 parses it and such, and optionally updates threads.
2706 doesn't updates NNTP indicies and such, never relinks anything.
2708 invalid (unknown) tags will be ignored.
2710 returns number of processed messages.
2712 doesn't start/end any transactions, so wrap it yourself.
2714 public bool chiroParseAndInsertOneMessage (uint uid, uint msgtime, int appearance,
2715 const(char)[] hdrs, const(char)[] body, const(char)[] tags)
2717 auto stInsThreads = dbView.statement(`
2718 INSERT INTO threads
2719 ( uid, tagid, time, appearance)
2720 VALUES(:uid,:tagid,:time,:appearance)
2721 ;`);
2723 auto stInsInfo = dbView.statement(`
2724 INSERT INTO info
2725 ( uid, from_name, from_mail, subj, to_name, to_mail)
2726 VALUES(:uid,:from_name,:from_mail,:subj,:to_name,:to_mail)
2727 ;`);
2729 auto stInsMsgId = dbView.statement(`
2730 INSERT INTO msgids
2731 ( uid, msgid, time)
2732 VALUES(:uid,:msgid,:time)
2733 ;`);
2735 auto stInsMsgRefId = dbView.statement(`
2736 INSERT INTO refids
2737 ( uid, idx, msgid)
2738 VALUES(:uid,:idx,:msgid)
2739 ;`);
2741 auto stInsContentText = dbView.statement(`
2742 INSERT INTO content_text
2743 ( uid, format, content)
2744 VALUES(:uid,:format, ChiroPack(:content))
2745 ;`);
2747 auto stInsContentHtml = dbView.statement(`
2748 INSERT INTO content_html
2749 ( uid, format, content)
2750 VALUES(:uid,:format, ChiroPack(:content))
2751 ;`);
2753 auto stInsAttach = dbView.statement(`
2754 INSERT INTO attaches
2755 ( uid, idx, mime, name, format, content)
2756 VALUES(:uid,:idx,:mime,:name,:format, ChiroPack(:content))
2757 ;`);
2759 bool noattaches = false; // do not store attaches?
2761 // create thread record for each tag (and update max nntp index)
2762 int tagCount = 0;
2763 int noAttachCount = 0;
2764 while (tags.length) {
2765 auto eep = tags.indexOf('|');
2766 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
2767 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
2768 if (tagname.length == 0) continue;
2770 //immutable uint tuid = chiroGetTagUid(tagname);
2771 immutable uint tuid = chiroAppendTag(tagname, (tagname == "#hobo" ? 1 : 0));
2772 if (tuid == 0) continue;
2774 /* nope
2775 if (nntpidx > 0 && tagname.startsWith("account:")) {
2776 auto accname = tagname[8..$];
2777 stInsNNTPIdx
2778 .bindConstText(":accname", accname)
2779 .bind(":nntpidx", nntpidx)
2780 .doAll();
2784 if (!chiroIsTagAllowAttaches(tuid)) ++noAttachCount;
2785 ++tagCount;
2787 int app = appearance;
2788 if (app == Appearance.Unread) {
2789 if (tagname.startsWith("account:") ||
2790 tagname.startsWith("#spam") ||
2791 tagname.startsWith("#hobo"))
2793 app = Appearance.Read;
2797 stInsThreads
2798 .bind(":uid", uid)
2799 .bind(":tagid", tuid)
2800 .bind(":time", msgtime)
2801 .bind(":appearance", app)
2802 .doAll();
2804 if (!tagCount) return false;
2805 noattaches = (noAttachCount && noAttachCount == tagCount);
2807 // insert msgid
2809 bool hasmsgid = false;
2810 auto msgidfield = findHeaderField(hdrs, "Message-Id");
2811 if (msgidfield.length) {
2812 auto id = msgidfield.getFieldValue;
2813 if (id.length) {
2814 hasmsgid = true;
2815 stInsMsgId
2816 .bind(":uid", uid)
2817 .bind("time", msgtime)
2818 .bindConstText(":msgid", id)
2819 .doAll();
2822 // if there is no msgid, create one
2823 if (!hasmsgid) {
2824 RIPEMD160 hash;
2825 hash.start();
2826 hash.put(cast(const(ubyte)[])hdrs);
2827 hash.put(cast(const(ubyte)[])body);
2828 ubyte[20] digest = hash.finish();
2829 char[20*2+2+16] buf;
2830 import core.stdc.stdio : snprintf;
2831 import core.stdc.string : strcat;
2832 foreach (immutable idx, ubyte b; digest[]) snprintf(buf.ptr+idx*2, 3, "%02x", b);
2833 strcat(buf.ptr, "@artificial"); // it is safe, there is enough room for it
2834 stInsMsgId
2835 .bind(":uid", uid)
2836 .bind("time", msgtime)
2837 .bindConstText(":msgid", buf[0..20*2])
2838 .doAll();
2842 // insert references
2844 uint refidx = 0;
2845 auto inreplyfld = findHeaderField(hdrs, "In-Reply-To");
2846 while (inreplyfld.length) {
2847 auto id = getNextFieldValue(inreplyfld);
2848 if (id.length) {
2849 stInsMsgRefId
2850 .bind(":uid", uid)
2851 .bind(":idx", refidx++)
2852 .bind(":msgid", id)
2853 .doAll();
2857 inreplyfld = findHeaderField(hdrs, "References");
2858 while (inreplyfld.length) {
2859 auto id = getNextFieldValue(inreplyfld);
2860 if (id.length) {
2861 stInsMsgRefId
2862 .bind(":uid", uid)
2863 .bind(":idx", refidx++)
2864 .bind(":msgid", id)
2865 .doAll();
2870 // insert base content and attaches
2872 Content[] content;
2873 parseContent(ref content, hdrs, body, noattaches);
2874 // insert text and html
2875 bool wasText = false, wasHtml = false;
2876 foreach (const ref Content cc; content) {
2877 if (cc.name.length) continue;
2878 if (noattaches && !cc.mime.startsWith("text/")) continue;
2879 if (!wasText && cc.mime == "text/plain") {
2880 wasText = true;
2881 stInsContentText
2882 .bind(":uid", uid)
2883 .bindConstText(":format", cc.format)
2884 .bindConstBlob(":content", cc.data)
2885 .doAll();
2886 } else if (!wasHtml && cc.mime == "text/html") {
2887 wasHtml = true;
2888 stInsContentHtml
2889 .bind(":uid", uid)
2890 .bindConstText(":format", cc.format)
2891 .bindConstBlob(":content", cc.data)
2892 .doAll();
2895 if (!wasText) {
2896 stInsContentText
2897 .bind(":uid", uid)
2898 .bindConstText(":format", "")
2899 .bindConstBlob(":content", "")
2900 .doAll();
2902 if (!wasHtml) {
2903 stInsContentHtml
2904 .bind(":uid", uid)
2905 .bindConstText(":format", "")
2906 .bindConstBlob(":content", "")
2907 .doAll();
2909 // insert everything
2910 uint cidx = 0;
2911 foreach (const ref Content cc; content) {
2912 if (cc.name.length == 0 && cc.mime.startsWith("text/")) continue;
2913 // for "no attaches" mode, still record the attach, but ignore its contents
2914 stInsAttach
2915 .bind(":uid", uid)
2916 .bind(":idx", cidx++)
2917 .bindConstText(":mime", cc.mime)
2918 .bindConstText(":name", cc.name)
2919 .bindConstText(":format", cc.name)
2920 .bindConstBlob(":content", (noattaches ? null : cc.data), allowNull:true)
2921 .doAll();
2925 // insert from/to/subj info
2926 // this must be done last to keep FTS5 in sync
2928 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
2929 auto from = findHeaderField(hdrs, "From");
2930 auto to = findHeaderField(hdrs, "To");
2931 stInsInfo
2932 .bind(":uid", uid)
2933 .bind(":from_name", from.extractName)
2934 .bind(":from_mail", from.extractMail)
2935 .bind(":subj", subj)
2936 .bind(":to_name", to.extractName)
2937 .bind(":to_mail", to.extractMail)
2938 .doAll();
2941 return true;
2946 inserts the messages from the message storage with the given id into view storage.
2947 parses it and such, and optionally updates threads.
2949 WARNING! DOESN'T UPDATE NNTP INDICIES! this should be done by the downloader.
2951 invalid (unknown) tags will be ignored.
2953 returns number of processed messages.
2955 public uint chiroParseAndInsertMessages (uint stmsgid,
2956 void delegate (uint count, uint total, uint nntpidx, const(char)[] tags) progresscb=null,
2957 uint emsgid=uint.max, bool relink=true, bool asread=false)
2959 if (emsgid < stmsgid) return 0; // nothing to do
2961 uint count = 0;
2962 uint total = 0;
2963 if (progresscb !is null) {
2964 // find total number of messages to process
2965 foreach (auto row; dbStore.statement(`
2966 SELECT count(uid) AS total FROM messages WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
2967 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
2969 total = row.total!uint;
2970 break;
2972 if (total == 0) return 0; // why not?
2975 transacted!"View"{
2976 uint[] uptagids;
2977 if (relink) uptagids.reserve(128);
2978 scope(exit) delete uptagids;
2980 foreach (auto mrow; dbStore.statement(`
2981 -- this should cache unpack results
2982 WITH msgunpacked(msguid, msgdata, msgtags) AS (
2983 SELECT uid AS msguid, ChiroUnpack(data) AS msgdata, tags AS msgtags
2984 FROM messages
2985 WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
2986 ORDER BY uid
2988 SELECT
2989 msguid AS uid
2990 , msgtags AS tags
2991 , ChiroExtractHeaders(msgdata) AS headers
2992 , ChiroExtractBody(msgdata) AS body
2993 , ChiroHdr_NNTPIndex(msgdata) AS nntpidx
2994 , ChiroHdr_RecvTime(msgdata) AS msgtime
2995 FROM msgunpacked
2996 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
2998 ++count;
2999 auto hdrs = mrow.headers!SQ3Text;
3000 auto body = mrow.body!SQ3Text;
3001 auto tags = mrow.tags!SQ3Text;
3002 uint uid = mrow.uid!uint;
3003 uint nntpidx = mrow.nntpidx!uint;
3004 uint msgtime = mrow.msgtime!uint;
3005 assert(tags.length);
3007 chiroParseAndInsertOneMessage(uid, msgtime, (asread ? 1 : 0), hdrs, body, tags);
3009 if (progresscb !is null) progresscb(count, total, nntpidx, tags);
3011 if (relink) {
3012 while (tags.length) {
3013 auto eep = tags.indexOf('|');
3014 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
3015 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
3016 if (tagname.length == 0) continue;
3018 immutable uint tuid = chiroGetTagUid(tagname);
3019 if (tuid == 0) continue;
3021 bool found = false;
3022 foreach (immutable n; uptagids) if (n == tuid) { found = true; break; }
3023 if (!found) uptagids ~= tuid;
3028 if (relink && uptagids.length) {
3029 foreach (immutable tagid; uptagids) chiroSupportRelinkTagThreads(tagid);
3033 return count;
3038 returns accouint uid (accid) or 0.
3040 public uint chiroGetAccountUid (const(char)[] accname) {
3041 static auto stat = LazyStatement!"Conf"(`SELECT accid AS accid FROM accounts WHERE name=:accname LIMIT 1;`);
3042 foreach (auto row; stat.st.bindConstText(":accname", accname).range) return row.accid!uint;
3043 return 0;
3048 returns accouint name, or empty string.
3050 public DynStr chiroGetAccountName (uint accid) {
3051 static auto stat = LazyStatement!"Conf"(`SELECT name AS name FROM accounts WHERE accid=:accid LIMIT 1;`);
3052 DynStr res;
3053 if (accid == 0) return res;
3054 foreach (auto row; stat.st.bind(":accid", accid).range) {
3055 res = row.name!SQ3Text;
3056 break;
3058 return res;
3063 returns list of known tags, sorted by name.
3065 public string[] chiroGetTagList () {
3066 static auto stat = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE hidden=0 ORDER BY tag;`);
3067 string[] res;
3068 foreach (auto row; stat.st.range) res ~= row.tagname!string;
3069 return res;
3074 returns tag uid (tagid) or 0.
3076 public uint chiroGetTagUid (const(char)[] tagname) {
3077 static auto stat = LazyStatement!"View"(`SELECT tagid AS tagid FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3078 foreach (auto row; stat.st.bindConstText(":tagname", tagname).range) {
3079 return row.tagid!uint;
3081 return 0;
3086 returns tag name or empty string.
3088 public DynStr chiroGetTagName (uint tagid) {
3089 static auto stat = LazyStatement!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3090 DynStr s;
3091 foreach (auto row; stat.st.bind(":tagid", tagid).range) {
3092 s = row.tagname!SQ3Text;
3093 break;
3095 return s;
3100 returns `true` if the given tag supports threads.
3102 this is used only when adding new messages, to set all parents to 0.
3104 public bool chiroIsTagThreaded(T) (T tagnameid)
3105 if (is(T:const(char)[]) || is(T:uint))
3107 static if (is(T:const(char)[])) {
3108 static auto stat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3109 foreach (auto row; stat.st.bindConstText(":tagname", tagnameid).range) {
3110 return (row.threading!uint == 1);
3112 } else {
3113 static auto xstat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3114 foreach (auto row; xstat.st.bind(":tagid", tagnameid).range) {
3115 return (row.threading!uint == 1);
3118 return false;
3123 returns `true` if the given tag allows attaches.
3125 this is used only when adding new messages, to set all parents to 0.
3127 public bool chiroIsTagAllowAttaches(T) (T tagnameid)
3128 if (is(T:const(char)[]) || is(T:uint))
3130 static if (is(T:const(char)[])) {
3131 static auto stat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3132 foreach (auto row; stat.st.bindConstText(":tagname", tagnameid).range) {
3133 return (row.threading!uint == 1);
3135 } else {
3136 static auto xstat = LazyStatement!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3137 foreach (auto row; xstat.st.bind(":tagid", tagnameid).range) {
3138 return (row.threading!uint == 1);
3141 return false;
3146 relinks all messages in all threads suitable for relinking, and
3147 sets parents to zero otherwise.
3149 public void chiroSupportRelinkAllThreads () {
3150 // yeah, that's it: a single SQL statement
3151 dbView.execute(`
3152 -- clear parents where threading is disabled
3153 SELECT ChiroTimerStart('clearing parents');
3154 UPDATE threads
3156 parent = 0
3157 WHERE
3158 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=0)
3159 AND parent <> 0
3161 SELECT ChiroTimerStop();
3163 SELECT ChiroTimerStart('relinking threads');
3164 UPDATE threads
3166 parent=ifnull(
3168 SELECT uid FROM msgids
3169 WHERE
3170 -- find MSGID for any of our current references
3171 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3172 -- check if UID for that MSGID has the valid tag
3173 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=threads.tagid)
3174 ORDER BY time DESC
3175 LIMIT 1
3177 , 0)
3178 WHERE
3179 -- do not process messages with non-threading tags
3180 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=1)
3182 SELECT ChiroTimerStop();
3188 relinks all messages for the given tag, or sets parents to zero if
3189 threading for that tag is disabled.
3191 public void chiroSupportRelinkTagThreads(T) (T tagnameid)
3192 if (is(T:const(char)[]) || is(T:uint))
3194 static if (is(T:const(char)[])) {
3195 immutable uint tid = chiroGetTagUid(tagnameid);
3196 if (!tid) return;
3197 } else {
3198 alias tid = tagnameid;
3201 static auto statNoTrd = LazyStatement!"View"(`
3202 UPDATE threads
3204 parent = 0
3205 WHERE
3206 tagid = :tagid AND parent <> 0
3207 ;`);
3209 static auto statTrd = LazyStatement!"View"(`
3210 UPDATE threads
3212 parent=ifnull(
3214 SELECT uid FROM msgids
3215 WHERE
3216 -- find MSGID for any of our current references
3217 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3218 -- check if UID for that MSGID has the valid tag
3219 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=:tagid)
3220 ORDER BY time DESC
3221 LIMIT 1
3223 , 0)
3224 WHERE
3225 threads.tagid = :tagid
3226 ;`);
3228 if (!chiroIsTagThreaded(tid)) {
3229 // clear parents (just in case)
3230 statNoTrd.st.bind(":tagid", tid).doAll();
3231 } else {
3232 // yeah, that's it: a single SQL statement
3233 statTrd.st.bind(":tagid", tid).doAll();
3239 * get "from info" for the given message.
3241 * returns `false` if there is no such message.
3243 public bool chiroGetMessageFrom (uint uid, ref DynStr fromMail, ref DynStr fromName) {
3244 static auto statGetFrom = LazyStatement!"View"(`
3245 SELECT
3246 from_name AS fromName
3247 , from_mail AS fromMail
3248 FROM info
3249 WHERE uid=:uid
3250 LIMIT 1
3251 ;`);
3252 fromMail.clear();
3253 fromName.clear();
3254 foreach (auto row; statGetFrom.st.bind(":uid", uid).range) {
3255 fromMail = row.fromMail!SQ3Text;
3256 fromName = row.fromName!SQ3Text;
3257 return true;
3259 return false;
3264 gets twit title and state for the given (tagid, uid) message.
3266 returns -666 if there is no such message.
3268 public DynStr chiroGetMessageTwit(T) (T tagidname, uint uid, out bool twited)
3269 if (is(T:const(char)[]) || is(T:uint))
3271 twited = false;
3272 DynStr res;
3273 if (!uid) return res;
3275 static if (is(T:const(char)[])) {
3276 immutable uint tid = chiroGetTagUid(tagidname);
3277 if (!tid) return 0;
3278 enum selHdr = ``;
3279 } else {
3280 alias tid = tagidname;
3283 if (!tid) return res;
3285 static auto statGetTwit = LazyStatement!"View"(`
3286 SELECT title AS title
3287 FROM threads
3288 WHERE uid=:uid AND tagid=:tagid AND mute>0
3289 LIMIT 1
3290 ;`);
3292 statGetTwit.st
3293 .bind(":uid", uid)
3294 .bind(":tagid", tid);
3295 foreach (auto row; statGetTwit.st.range) {
3296 twited = true;
3297 res = row.title!SQ3Text;
3300 return res;
3305 gets mute state for the given (tagid, uid) message.
3307 returns -666 if there is no such message.
3309 public int chiroGetMessageMute(T) (T tagidname, uint uid)
3310 if (is(T:const(char)[]) || is(T:uint))
3312 if (!uid) return -666;
3314 static if (is(T:const(char)[])) {
3315 immutable uint tid = chiroGetTagUid(tagidname);
3316 if (!tid) return 0;
3317 enum selHdr = ``;
3318 } else {
3319 alias tid = tagidname;
3322 if (!tid) return -666;
3324 static auto statGetApp = LazyStatement!"View"(`
3325 SELECT mute AS mute
3326 FROM threads
3327 WHERE uid=:uid AND tagid=:tagid
3328 LIMIT 1
3329 ;`);
3331 statGetApp.st
3332 .bind(":uid", uid)
3333 .bind(":tagid", tid);
3334 foreach (auto row; statGetApp.st.range) return row.mute!int;
3335 return -666;
3340 sets mute state the given (tagid, uid) message.
3342 doesn't change children states.
3344 public void chiroSetMessageMute(T) (T tagidname, uint uid, Mute mute)
3345 if (is(T:const(char)[]) || is(T:uint))
3347 if (!uid) return;
3349 static if (is(T:const(char)[])) {
3350 immutable uint tid = chiroGetTagUid(tagidname);
3351 if (!tid) return 0;
3352 enum selHdr = ``;
3353 } else {
3354 alias tid = tagidname;
3357 if (!tid) return;
3359 static auto statSetApp = LazyStatement!"View"(`
3360 UPDATE threads
3362 mute=:mute
3363 WHERE
3364 uid=:uid AND tagid=:tagid
3365 ;`);
3367 statSetApp.st
3368 .bind(":mute", cast(int)mute)
3369 .bind(":uid", uid)
3370 .bind(":tagid", tid)
3371 .doAll();
3376 gets appearance for the given (tagid, uid) message.
3378 returns -666 if there is no such message.
3380 public int chiroGetMessageAppearance(T) (T tagidname, uint uid)
3381 if (is(T:const(char)[]) || is(T:uint))
3383 if (!uid) return -666;
3385 static if (is(T:const(char)[])) {
3386 immutable uint tid = chiroGetTagUid(tagidname);
3387 if (!tid) return 0;
3388 enum selHdr = ``;
3389 } else {
3390 alias tid = tagidname;
3393 if (!tid) return -666;
3395 static auto statGetApp = LazyStatement!"View"(`
3396 SELECT appearance AS appearance
3397 FROM threads
3398 WHERE uid=:uid AND tagid=:tagid
3399 LIMIT 1
3400 ;`);
3402 statGetApp.st
3403 .bind(":uid", uid)
3404 .bind(":tagid", tid);
3405 foreach (auto row; statGetApp.st.range) return row.appearance!int;
3406 return -666;
3411 gets appearance for the given (tagid, uid) message.
3413 public bool chiroGetMessageUnread(T) (T tagidname, uint uid)
3414 if (is(T:const(char)[]) || is(T:uint))
3416 return (chiroGetMessageAppearance(tagidname, uid) == Appearance.Unread);
3421 gets appearance for the given (tagid, uid) message.
3423 public bool chiroGetMessageExactRead(T) (T tagidname, uint uid)
3424 if (is(T:const(char)[]) || is(T:uint))
3426 return (chiroGetMessageAppearance(tagidname, uid) == Appearance.Read);
3431 sets appearance for the given (tagid, uid) message.
3433 public void chiroSetMessageAppearance(T) (T tagidname, uint uid, Appearance appearance)
3434 if (is(T:const(char)[]) || is(T:uint))
3436 if (!uid) return;
3438 static if (is(T:const(char)[])) {
3439 immutable uint tid = chiroGetTagUid(tagidname);
3440 if (!tid) return 0;
3441 enum selHdr = ``;
3442 } else {
3443 alias tid = tagidname;
3446 if (!tid) return;
3448 static auto statSetApp = LazyStatement!"View"(`
3449 UPDATE threads
3451 appearance=:appearance
3452 WHERE
3453 uid=:uid AND tagid=:tagid
3454 ;`);
3456 statSetApp.st
3457 .bind(":appearance", cast(int)appearance)
3458 .bind(":uid", uid)
3459 .bind(":tagid", tid)
3460 .doAll();
3465 mark (tagid, uid) message as read.
3467 public void chiroSetReadOrUnreadMessageAppearance(T) (T tagidname, uint uid, Appearance appearance)
3468 if (is(T:const(char)[]) || is(T:uint))
3470 if (!uid) return;
3472 static if (is(T:const(char)[])) {
3473 immutable uint tid = chiroGetTagUid(tagidname);
3474 if (!tid) return 0;
3475 enum selHdr = ``;
3476 } else {
3477 alias tid = tagidname;
3480 if (!tid) return;
3482 static auto statSetApp = LazyStatement!"View"(`
3483 UPDATE threads
3485 appearance=:setapp
3486 WHERE
3487 uid=:uid AND tagid=:tagid AND (appearance=:checkapp0 OR appearance=:checkapp1)
3488 ;`);
3490 statSetApp.st
3491 .bind(":uid", uid)
3492 .bind(":tagid", tid)
3493 .bind(":setapp", cast(int)appearance)
3494 .bind(":checkapp0", Appearance.Read)
3495 .bind(":checkapp1", Appearance.Unread)
3496 .doAll();
3501 mark (tagid, uid) message as read.
3503 public void chiroSetMessageRead(T) (T tagidname, uint uid)
3504 if (is(T:const(char)[]) || is(T:uint))
3506 chiroSetReadOrUnreadMessageAppearance(tagidname, uid, Appearance.Read);
3510 public void chiroSetMessageUnread(T) (T tagidname, uint uid)
3511 if (is(T:const(char)[]) || is(T:uint))
3513 chiroSetReadOrUnreadMessageAppearance(tagidname, uid, Appearance.Unread);
3518 purge all messages with the given tag.
3520 this removes messages from all view tables, removes content from
3521 the "messages" table, and sets "messages" table tags to NULL.
3523 public void chiroDeletePurgedWithTag(T) (T tagidname)
3524 if (is(T:const(char)[]) || is(T:uint))
3526 static if (is(T:const(char)[])) {
3527 immutable uint tid = chiroGetTagUid(tagidname);
3528 if (!tid) return 0;
3529 enum selHdr = ``;
3530 } else {
3531 alias tid = tagidname;
3534 if (!tid) return;
3536 static auto statCountPurged = LazyStatement!"View"(`
3537 SELECT COUNT(uid) AS pcount FROM threads
3538 WHERE tagid=:tagid AND appearance=:appr
3541 uint purgedCount = 0;
3542 foreach (auto row; statCountPurged.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).range) {
3543 purgedCount = row.pcount!uint;
3545 if (!purgedCount) return;
3547 // we will need this to clear storage
3548 uint[] plist;
3549 scope(exit) delete plist;
3550 plist.reserve(purgedCount);
3552 static auto statListPurged = LazyStatement!"View"(`
3553 SELECT uid AS uid FROM threads
3554 WHERE tagid=:tagid AND appearance=:appr
3555 ORDER BY uid
3558 foreach (auto row; statListPurged.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).range) {
3559 plist ~= row.uid!uint;
3561 if (plist.length == 0) return; // just in case
3563 static auto statClearStorage = LazyStatement!"Store"(`
3564 UPDATE messages
3565 SET tags=NULL, data=NULL
3566 WHERE uid=:uid
3567 ;`);
3569 enum BulkClearSQL(string table) = `
3570 DELETE FROM `~table~`
3571 WHERE
3572 uid IN (SELECT uid FROM threads WHERE tagid=:tagid AND appearance=:appr)
3575 // bulk clearing of info
3576 static auto statClearInfo = LazyStatement!"View"(BulkClearSQL!"info");
3577 // bulk clearing of msgids
3578 static auto statClearMsgids = LazyStatement!"View"(BulkClearSQL!"msgids");
3579 // bulk clearing of refids
3580 static auto statClearRefids = LazyStatement!"View"(BulkClearSQL!"refids");
3581 // bulk clearing of text
3582 static auto statClearText = LazyStatement!"View"(BulkClearSQL!"content_text");
3583 // bulk clearing of html
3584 static auto statClearHtml = LazyStatement!"View"(BulkClearSQL!"content_html");
3585 // bulk clearing of attaches
3586 static auto statClearAttach = LazyStatement!"View"(BulkClearSQL!"attaches");
3587 // bulk clearing of threads
3588 static auto statClearThreads = LazyStatement!"View"(`
3589 DELETE FROM threads
3590 WHERE tagid=:tagid AND appearance=:appr
3591 ;`);
3593 static if (is(T:const(char)[])) {
3594 conwriteln("removing ", plist.length, " message", (plist.length != 1 ? "s" : ""), " from '", tagidname, "'...");
3595 } else {
3596 DynStr tname = chiroGetTagName(tid);
3597 conwriteln("removing ", plist.length, " message", (plist.length != 1 ? "s" : ""), " from '", tname.getData, "'...");
3600 // WARNING! "info" must be cleared FIRST, and "threads" LAST
3601 transacted!"View"{
3602 statClearInfo.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3603 statClearMsgids.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3604 statClearRefids.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3605 statClearText.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3606 statClearHtml.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3607 statClearAttach.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3608 statClearThreads.st.bind(":tagid", tid).bind(":appr", Appearance.SoftDeletePurge).doAll();
3609 // relink tag threads
3610 chiroSupportRelinkTagThreads(tid);
3613 // now clear the storage
3614 conwriteln("clearing the storage...");
3615 transacted!"Store"{
3616 foreach (immutable uint uid; plist) {
3617 statClearStorage.st.bind(":uid", uid).doAll();
3621 conwriteln("done purging.");
3626 creates "treepane" table for the given tag. that table can be used to
3627 render threaded listview.
3629 returns max id of the existing item. can be used for pagination.
3630 item ids are guaranteed to be sequential, and without any holes.
3631 the first id is `1`.
3633 returned table has "rowid", and two integer fields: "uid" (message uid), and
3634 "level" (message depth, starting from 0).
3636 public uint chiroCreateTreePaneTable(T) (T tagidname, int lastmonthes=12, bool allowThreading=true)
3637 if (is(T:const(char)[]) || is(T:uint))
3639 auto ctm = Timer(true);
3641 // shrink temp table to the bare minimum, because each field costs several msecs
3642 // we don't need parent and time here, because we can easily select them with inner joins
3644 dbView.execute(`
3645 DROP TABLE IF EXISTS treepane;
3646 CREATE TEMP TABLE IF NOT EXISTS treepane (
3647 iid INTEGER PRIMARY KEY
3648 , uid INTEGER
3649 , level INTEGER
3650 -- to make joins easier
3651 , tagid INTEGER
3656 // clear it (should be faster than dropping and recreating)
3657 dbView.execute(`DELETE FROM treepane;`);
3659 static auto statTrd = LazyStatement!"View"(`
3660 INSERT INTO treepane
3661 (uid, level, tagid)
3662 WITH tree(uid, parent, level, time, path) AS (
3663 WITH RECURSIVE fulltree(uid, parent, level, time, path) AS (
3664 SELECT t.uid AS uid, t.parent AS parent, 1 AS level, t.time AS time, printf('%08X', t.time) AS path
3665 FROM threads t
3666 WHERE t.time>=:starttime AND parent=0 AND t.tagid=:tagidname AND t.appearance <> -1
3667 UNION ALL
3668 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
3669 FROM threads t, fulltree ft
3670 WHERE t.time>=:starttime AND t.parent=ft.uid AND t.tagid=:tagidname AND t.appearance <> -1
3672 SELECT * FROM fulltree
3674 SELECT
3675 tree.uid AS uid
3676 , tree.level-1 AS level
3677 , :tagidname AS tagid
3678 FROM tree
3679 ORDER BY path
3680 ;`);
3682 static auto statNoTrd = LazyStatement!"View"(`
3683 INSERT INTO treepane
3684 (uid, level, tagid)
3685 SELECT
3686 threads.uid AS uid
3687 , 0 AS level
3688 , :tagidname AS tagid
3689 FROM threads
3690 WHERE
3691 threads.time>=:starttime AND threads.tagid=:tagidname AND threads.appearance <> -1
3692 ORDER BY
3693 threads.time
3694 ;`);
3696 // this need to add answers to some ancient crap
3697 static auto statFuckMore = LazyStatement!"View"(`
3698 INSERT INTO treepane
3699 (uid, level, tagid)
3700 SELECT
3701 uid, 0, :tagidname
3702 FROM threads
3703 WHERE
3704 tagid=:tagidname AND time>=:starttime AND appearance=:app
3705 AND NOT EXISTS(SELECT 1 FROM treepane AS tp WHERE tp.uid=threads.uid)
3706 ;`);
3708 static if (is(T:const(char)[])) {
3709 immutable uint tid = chiroGetTagUid(tagidname);
3710 if (!tid) return 0;
3711 enum selHdr = ``;
3712 } else {
3713 alias tid = tagidname;
3716 uint startTime = 0;
3718 if (lastmonthes > 0) {
3719 if (lastmonthes > 12*100) {
3720 startTime = 0;
3721 } else {
3722 // show last `lastmonthes` (full monthes)
3723 import std.datetime;
3724 import core.time : Duration;
3726 SysTime now = Clock.currTime().toUTC();
3727 int year = now.year;
3728 int month = now.month; // from 1
3729 --lastmonthes;
3730 // yes, i am THAT lazy
3731 while (lastmonthes > 0) {
3732 if (month > lastmonthes) { month -= lastmonthes; break; }
3733 lastmonthes -= month;
3734 month = 12;
3735 --year;
3737 // construct unix time
3738 now.fracSecs = Duration.zero;
3739 now.second = 0;
3740 now.hour = 0;
3741 now.minute = 0;
3742 now.day = 1;
3743 now.month = cast(Month)month;
3744 now.year = year;
3745 startTime = cast(uint)now.toUnixTime();
3749 // this "%08X" will do up to 2038; i'm fine with it
3750 if (allowThreading) {
3751 statTrd.st.bind(":tagidname", tid).bind(":starttime", startTime).doAll();
3752 statFuckMore.st.bind(":tagidname", tid).bind(":starttime", startTime).bind(":app", Appearance.Unread).doAll();
3753 } else {
3754 statNoTrd.st.bind(":tagidname", tid).bind(":starttime", startTime).doAll();
3756 ctm.stop;
3757 if (ChiroTimerEnabled) writeln("creating treepane time: ", ctm);
3759 immutable uint res = cast(uint)dbView.lastRowId;
3761 version(chidb_drop_pane_table) {
3762 dbView.execute(`CREATE INDEX treepane_uid ON treepane(uid);`);
3765 return res;
3770 returns current treepane tagid.
3772 public uint chiroGetTreePaneTableTagId () {
3773 static auto stat = LazyStatement!"View"(`SELECT tagid AS tagid FROM treepane WHERE iid=1 LIMIT 1;`);
3774 foreach (auto row; stat.st.range) return row.tagid!uint;
3775 return 0;
3780 returns current treepane max uid.
3782 public uint chiroGetTreePaneTableMaxUId () {
3783 static auto stat = LazyStatement!"View"(`SELECT MAX(uid) AS uid FROM treepane LIMIT 1;`);
3784 foreach (auto row; stat.st.range) return row.uid!uint;
3785 return 0;
3790 returns number of items in the current treepane.
3792 public uint chiroGetTreePaneTableCount () {
3793 static auto stat = LazyStatement!"View"(`SELECT COUNT(*) AS total FROM treepane;`);
3794 foreach (auto row; stat.st.range) return row.total!uint;
3795 return 0;
3800 returns index of the given uid in the treepane.
3802 public bool chiroIsTreePaneTableUidValid (uint uid) {
3803 static auto stat = LazyStatement!"View"(`SELECT iid AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
3804 if (uid == 0) return false;
3805 foreach (auto row; stat.st.bind(":uid", uid).range) return true;
3806 return false;
3811 returns first treepane uid.
3813 public uint chiroGetTreePaneTableFirstUid () {
3814 static auto stmt = LazyStatement!"View"(`SELECT uid AS uid FROM treepane WHERE iid=1 LIMIT 1;`);
3815 foreach (auto row; stmt.st.range) return row.uid!uint;
3816 return 0;
3821 returns last treepane uid.
3823 public uint chiroGetTreePaneTableLastUid () {
3824 static auto stmt = LazyStatement!"View"(`SELECT MAX(iid), uid AS uid FROM treepane LIMIT 1;`);
3825 foreach (auto row; stmt.st.range) return row.uid!uint;
3826 return 0;
3831 returns index of the given uid in the treepane.
3833 public int chiroGetTreePaneTableUid2Index (uint uid) {
3834 static auto stmt = LazyStatement!"View"(`SELECT iid-1 AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
3835 if (uid == 0) return -1;
3836 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.idx!int;
3837 return -1;
3842 returns uid of the given index in the treepane.
3844 public uint chiroGetTreePaneTableIndex2Uid (int index) {
3845 static auto stmt = LazyStatement!"View"(`SELECT uid AS uid FROM treepane WHERE iid=:idx+1 LIMIT 1;`);
3846 if (index < 0 || index == int.max) return 0;
3847 foreach (auto row; stmt.st.bind(":idx", index).range) return row.uid!uint;
3848 return 0;
3853 returns previous uid in the treepane.
3855 public uint chiroGetTreePaneTablePrevUid (uint uid) {
3856 static auto stmt = LazyStatement!"View"(`
3857 SELECT uid AS uid FROM treepane
3858 WHERE iid IN (SELECT iid-1 FROM treepane WHERE uid=:uid LIMIT 1)
3859 LIMIT 1
3860 ;`);
3861 if (uid == 0) return chiroGetTreePaneTableFirstUid();
3862 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.uid!uint;
3863 return 0;
3868 returns uid of the given index in the treepane.
3870 public uint chiroGetTreePaneTableNextUid (uint uid) {
3871 static auto stmt = LazyStatement!"View"(`
3872 SELECT uid AS uid FROM treepane
3873 WHERE iid IN (SELECT iid+1 FROM treepane WHERE uid=:uid LIMIT 1)
3874 LIMIT 1
3875 ;`);
3876 if (uid == 0) return chiroGetTreePaneTableFirstUid();
3877 foreach (auto row; stmt.st.bind(":uid", uid).range) return row.uid!uint;
3878 return 0;
3883 releases (drops) "treepane" table.
3885 can be called several times, but usually you don't need to call this at all.
3887 public void chiroClearTreePaneTable () {
3888 //dbView.execute(`DROP TABLE IF EXISTS treepane;`);
3889 dbView.execute(`DELETE FROM treepane;`);
3894 return next unread message uid in treepane, or 0.
3896 public uint chiroGetPaneNextUnread (uint curruid) {
3897 static auto stmtNext = LazyStatement!"View"(`
3898 SELECT treepane.uid AS uid FROM treepane
3899 INNER JOIN threads USING(uid, tagid)
3900 WHERE treepane.iid-1 > :cidx AND threads.appearance=:appr
3901 ORDER BY iid
3902 LIMIT 1
3903 ;`);
3904 immutable int cidx = chiroGetTreePaneTableUid2Index(curruid);
3905 foreach (auto row; stmtNext.st.bind(":cidx", cidx).bind(":appr", Appearance.Unread).range) return row.uid!uint;
3906 if (curruid) {
3907 // try from the beginning
3908 foreach (auto row; stmtNext.st.bind(":cidx", -1).bind(":appr", Appearance.Unread).range) return row.uid!uint;
3910 return 0;
3915 selects given number of items starting with the given item id.
3917 returns numer of selected items.
3919 `stiid` counts from zero
3921 WARNING! "treepane" table must be prepared with `chiroCreateTreePaneTable()`!
3923 WARNING! [i]dup `SQ3Text` arguments if necessary, they won't survive the `cb` return!
3925 public int chiroGetPaneTablePage (int stiid, int limit,
3926 void delegate (int pgofs, /* offset from the page start, from zero and up to `limit` */
3927 int iid, /* item id, counts from zero*/
3928 uint uid, /* msguid, never zero */
3929 uint parentuid, /* parent msguid, may be zero */
3930 uint level, /* threading level, from zero */
3931 Appearance appearance, /* see above */
3932 Mute mute, /* see above */
3933 SQ3Text date, /* string representation of receiving date and time */
3934 SQ3Text subj, /* message subject, can be empty string */
3935 SQ3Text fromName, /* message sender name, can be empty string */
3936 SQ3Text fromMail, /* message sender email, can be empty string */
3937 SQ3Text title) cb /* title from twiting */
3939 static auto stat = LazyStatement!"View"(`
3940 SELECT
3941 treepane.iid AS iid
3942 , treepane.uid AS uid
3943 , treepane.level AS level
3944 , threads.parent AS parent
3945 , threads.appearance AS appearance
3946 , threads.mute AS mute
3947 , datetime(threads.time, 'unixepoch') AS time
3948 , info.subj AS subj
3949 , info.from_name AS from_name
3950 , info.from_mail AS from_mail
3951 , threads.title AS title
3952 FROM treepane
3953 INNER JOIN info USING(uid)
3954 INNER JOIN threads USING(uid, tagid)
3955 WHERE treepane.iid >= :stiid
3956 ORDER BY treepane.iid
3957 LIMIT :limit
3960 if (limit <= 0) return 0;
3961 if (stiid < 0) {
3962 if (stiid == int.min) return 0;
3963 limit += stiid;
3964 if (limit <= 0) return 0;
3965 stiid = 0;
3967 int total = 0;
3968 foreach (auto row; stat.st.bind(":stiid", stiid+1).bind(":limit", limit).range)
3970 if (cb !is null) {
3971 cb(total, row.iid!int, row.uid!uint, row.parent!uint, row.level!uint,
3972 cast(Appearance)row.appearance!int, cast(Mute)row.mute!int,
3973 row.time!SQ3Text, row.subj!SQ3Text, row.from_name!SQ3Text, row.from_mail!SQ3Text, row.title!SQ3Text);
3975 ++total;
3977 return total;
3981 // ////////////////////////////////////////////////////////////////////////// //
3982 /** returns full content of the messare or `null` if no message found (or it was deleted).
3984 * you can safely `delete` the result
3986 public char[] GetFullMessageContent (uint uid) {
3987 if (uid == 0) return null;
3988 foreach (auto row; dbStore.statement(`SELECT ChiroUnpack(data) AS content FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid).range) {
3989 auto ct = row.content!SQ3Text;
3990 if (!ct.length) return null;
3991 char[] content = new char[ct.length];
3992 content[] = ct[];
3993 return content;
3995 return null;
3999 // ////////////////////////////////////////////////////////////////////////// //
4000 public enum Bogo {
4001 Error, // some error occured
4002 Ham,
4003 Unsure,
4004 Spam,
4007 public Bogo messageBogoCheck (uint uid) {
4008 if (uid == 0) return Bogo.Error;
4009 char[] content = GetFullMessageContent(uid);
4010 scope(exit) delete content;
4011 if (content is null) return Bogo.Error;
4013 try {
4014 import std.process;
4015 //{ auto fo = VFile("/tmp/zzzz", "w"); fo.rawWriteExact(art.data); }
4016 auto pipes = pipeProcess(["/usr/bin/bogofilter", "-T"]);
4017 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4018 //pipes.stdin.writeln();
4019 //foreach (string s; art.text) pipes.stdin.writeln(s);
4020 pipes.stdin.writeln(content.xstripright);
4021 pipes.stdin.flush();
4022 pipes.stdin.close();
4023 auto res = pipes.stdout.readln();
4024 wait(pipes.pid);
4025 //conwriteln("RESULT: [", res, "]");
4026 if (res.length == 0) {
4027 //conwriteln("ERROR: bogofilter returned nothing");
4028 return Bogo.Error;
4030 if (res[0] == 'H') return Bogo.Ham;
4031 if (res[0] == 'U') return Bogo.Unsure;
4032 if (res[0] == 'S') return Bogo.Spam;
4033 //while (res.length && res[$-1] <= ' ') res = res[0..$-1];
4034 //conwriteln("ERROR: bogofilter returned some shit: [", res, "]");
4035 } catch (Exception e) { // sorry
4036 //conwriteln("ERROR bogofiltering: ", e.msg);
4039 return Bogo.Error;
4043 // ////////////////////////////////////////////////////////////////////////// //
4044 private void messageBogoMarkSpamHam(bool spam) (uint uid) {
4045 if (uid == 0) return;
4046 char[] content = GetFullMessageContent(uid);
4047 scope(exit) delete content;
4048 if (content is null) return;
4050 static if (spam) enum arg = "-s"; else enum arg = "-n";
4051 try {
4052 import std.process;
4053 auto pipes = pipeProcess(["/usr/bin/bogofilter", arg]);
4054 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4055 //pipes.stdin.writeln();
4056 //foreach (string s; art.text) pipes.stdin.writeln(s);
4057 pipes.stdin.writeln(content.xstripright);
4058 pipes.stdin.flush();
4059 pipes.stdin.close();
4060 wait(pipes.pid);
4061 } catch (Exception e) { // sorry
4062 //conwriteln("ERROR bogofiltering: ", e.msg);
4067 public void messageBogoMarkHam (uint uid) { messageBogoMarkSpamHam!false(uid); }
4068 public void messageBogoMarkSpam (uint uid) { messageBogoMarkSpamHam!true(uid); }
4071 // ////////////////////////////////////////////////////////////////////////// //
4072 public alias TwitProcessCallback = void delegate (const(char)[] msg, uint curr, uint total);
4074 void processEmailTwits (TwitProcessCallback cb) {
4075 enum Message = "processing email twits";
4077 auto stFindTwitNameEmail = LazyStatement!"View"(`
4078 SELECT
4079 threads.uid AS uid
4080 , threads.tagid AS tagid
4081 FROM threads
4082 INNER JOIN info AS ii ON
4083 ii.uid=threads.uid AND
4084 ii.from_mail=:email AND
4085 ii.from_name=:name
4086 WHERE mute=0
4087 ;`);
4089 auto stFindTwitEmail = LazyStatement!"View"(`
4090 SELECT
4091 threads.uid AS uid
4092 , threads.tagid AS tagid
4093 FROM threads
4094 INNER JOIN info AS ii ON
4095 ii.uid=threads.uid AND
4096 ii.from_mail=:email
4097 WHERE mute=0
4098 ;`);
4100 auto stFindTwitName = LazyStatement!"View"(`
4101 SELECT
4102 threads.uid AS uid
4103 , threads.tagid AS tagid
4104 FROM threads
4105 INNER JOIN info AS ii ON
4106 ii.uid=threads.uid AND
4107 ii.from_name=:name
4108 WHERE mute=0
4109 ;`);
4112 auto stFindTwitNameEmailMasked = LazyStatement!"View"(`
4113 SELECT
4114 threads.uid AS uid
4115 , threads.tagid AS tagid
4116 FROM threads
4117 INNER JOIN info AS ii ON
4118 ii.uid=threads.uid AND
4119 ii.from_name=:name AND
4120 ii.from_mail GLOB :email
4121 WHERE mute=0
4122 ;`);
4124 auto stFindTwitEmailMasked = LazyStatement!"View"(`
4125 SELECT
4126 threads.uid AS uid
4127 , threads.tagid AS tagid
4128 FROM threads
4129 INNER JOIN info AS ii ON
4130 ii.uid=threads.uid AND
4131 ii.from_mail GLOB :email
4132 WHERE mute=0
4133 ;`);
4136 auto stUpdateMute = LazyStatement!"View"(`
4137 UPDATE threads
4138 SET mute=:mute, title=:title
4139 WHERE uid=:uid AND tagid=:tagid AND mute=0
4140 ;`);
4142 static struct UidTag {
4143 uint uid;
4144 uint tagid;
4147 uint twitcount = 0;
4148 foreach (auto trow; dbConf.statement(`SELECT COUNT(*) AS twitcount FROM emailtwits;`).range) twitcount = trow.twitcount!uint;
4150 if (cb !is null) cb(Message, 0, twitcount);
4152 dbView.execute(`
4153 CREATE TEMP TABLE IF NOT EXISTS disemails(
4154 email TEXT NOT NULL UNIQUE
4157 scope(exit) {
4158 dbView.execute(`DROP TABLE IF EXISTS disemails;`);
4161 transacted!"View"{
4162 uint twitdone = 0;
4163 foreach (auto trow; dbConf.statement(`
4164 SELECT
4165 tagglob AS tagglob
4166 , email AS email
4167 , name AS name
4168 , title AS title
4169 FROM emailtwits
4170 WHERE email NOT LIKE '%*%'
4171 ;`).range)
4173 ++twitdone;
4174 auto title = trow.title!SQ3Text;
4175 if (title.length == 0) continue;
4176 auto email = trow.email!SQ3Text;
4177 auto name = trow.name!SQ3Text;
4178 assert(email.indexOf('*') < 0);
4179 DBStatement st;
4180 if (email.length && name.length) {
4181 st = stFindTwitNameEmail.st;
4182 st.bindConstText(":email", email).bindConstText(":name", name);
4183 } else if (email.length) {
4184 st = stFindTwitEmail.st;
4185 st.bindConstText(":email", email);
4186 } else if (name.length) {
4187 st = stFindTwitName.st;
4188 st.bindConstText(":name", name);
4189 } else {
4190 continue;
4192 UidTag[] msguids;
4193 msguids.reserve(128);
4194 scope(exit) delete msguids;
4195 //writeln("::: ", email, " : ", name);
4196 foreach (auto mrow; st.range) {
4197 auto tname = chiroGetTagName(mrow.tagid!uint);
4198 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4199 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4200 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4202 if (msguids.length == 0) continue;
4203 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4204 immutable bool muteAllow = title.startsWith("!"); // allow this
4205 //transacted!"View"{
4206 foreach (immutable pair; msguids) {
4207 stUpdateMute.st
4208 .bind(":uid", pair.uid)
4209 .bind(":tagid", pair.tagid)
4210 .bind(":mute", (muteAllow ? Mute.Never : Mute.ThreadStart))
4211 .bindConstText(":title", title)
4212 .doAll();
4214 //};
4215 if (cb !is null) cb(Message, twitdone, twitcount);
4218 dbView.execute(`
4219 DELETE FROM disemails;
4220 INSERT INTO disemails
4221 SELECT DISTINCT(from_mail) FROM info;
4224 version(all) {
4225 foreach (auto trow; dbConf.statement(`
4226 SELECT
4227 tagglob AS tagglob
4228 , email AS email
4229 , name AS name
4230 , title AS title
4231 FROM emailtwits
4232 WHERE email LIKE '%*%'
4233 ;`).range)
4235 ++twitdone;
4236 auto title = trow.title!SQ3Text;
4237 if (title.length == 0) continue;
4238 auto email = trow.email!SQ3Text;
4239 auto name = trow.name!SQ3Text;
4240 assert(email.indexOf('*') >= 0);
4241 assert(email.length);
4243 foreach (auto drow; dbView.statement(`SELECT email AS demail FROM disemails WHERE email GLOB :email;`)
4244 .bindConstText(":email", email).range)
4246 DBStatement st;
4247 if (name.length) {
4248 st = stFindTwitNameEmail.st;
4249 st.bindConstText(":email", drow.demail!SQ3Text).bindConstText(":name", name);
4250 } else {
4251 st = stFindTwitEmail.st;
4252 st.bindConstText(":email", drow.demail!SQ3Text);
4254 UidTag[] msguids;
4255 msguids.reserve(128);
4256 scope(exit) delete msguids;
4257 //writeln("::: ", email, " : ", name);
4258 foreach (auto mrow; st.range) {
4259 auto tname = chiroGetTagName(mrow.tagid!uint);
4260 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4261 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4262 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4264 if (msguids.length == 0) continue;
4265 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4266 immutable bool muteAllow = title.startsWith("!"); // allow this
4267 //transacted!"View"{
4268 foreach (immutable pair; msguids) {
4269 stUpdateMute.st
4270 .bind(":uid", pair.uid)
4271 .bind(":tagid", pair.tagid)
4272 .bind(":mute", (muteAllow ? Mute.Never : Mute.ThreadStart))
4273 .bindConstText(":title", title)
4274 .doAll();
4276 //};
4279 if (cb !is null) cb(Message, twitdone, twitcount);
4281 } else {
4282 foreach (auto trow; dbConf.statement(`
4283 SELECT
4284 tagglob AS tagglob
4285 , email AS email
4286 , name AS name
4287 , title AS title
4288 FROM emailtwits
4289 WHERE email LIKE '%*%'
4290 ;`).range)
4292 ++twitdone;
4293 auto title = trow.title!SQ3Text;
4294 if (title.length == 0) continue;
4295 auto email = trow.email!SQ3Text;
4296 auto name = trow.name!SQ3Text;
4297 assert(email.indexOf('*') >= 0);
4298 assert(email.length);
4299 DBStatement st;
4300 if (email.length && name.length) {
4301 st = stFindTwitNameEmailMasked.st;
4302 st.bindConstText(":email", email).bindConstText(":name", name);
4303 } else {
4304 st = stFindTwitEmailMasked.st;
4305 st.bindConstText(":email", email);
4307 UidTag[] msguids;
4308 msguids.reserve(128);
4309 scope(exit) delete msguids;
4310 //writeln("::: ", email, " : ", name);
4311 foreach (auto mrow; st.range) {
4312 auto tname = chiroGetTagName(mrow.tagid!uint);
4313 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4314 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4315 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4317 if (msguids.length == 0) continue;
4318 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4319 immutable bool muteAllow = title.startsWith("!"); // allow this
4320 //transacted!"View"{
4321 foreach (immutable pair; msguids) {
4322 stUpdateMute.st
4323 .bind(":uid", pair.uid)
4324 .bind(":tagid", pair.tagid)
4325 .bind(":mute", (muteAllow ? Mute.Never : Mute.ThreadStart))
4326 .bindConstText(":title", title)
4327 .doAll();
4329 //};
4330 if (cb !is null) cb(Message, twitdone, twitcount);
4335 if (cb !is null) cb(Message, twitcount, twitcount);
4339 void processMsgidTwits (TwitProcessCallback cb) {
4340 enum Message = "processing msgid twits";
4342 auto stUpdateMute = LazyStatement!"View"(`
4343 UPDATE threads
4344 SET mute=:mute, title=NULL
4345 WHERE uid=:uid AND tagid=:tagid AND mute=0
4346 ;`);
4348 static struct UidTag {
4349 uint uid;
4350 uint tagid;
4353 uint twitcount = 0;
4354 foreach (auto trow; dbConf.statement(`SELECT COUNT(*) AS twitcount FROM msgidtwits;`).range) twitcount = trow.twitcount!uint;
4356 if (cb !is null) cb(Message, 0, twitcount);
4358 transacted!"View"{
4359 uint twitdone = 0;
4360 foreach (auto trow; dbConf.statement(`SELECT msgid AS msgid, tagglob AS tagglob FROM msgidtwits;`).range) {
4361 ++twitdone;
4362 UidTag[] msguids;
4363 msguids.reserve(128);
4364 scope(exit) delete msguids;
4366 foreach (auto mrow; dbView.statement(`
4367 SELECT threads.uid AS uid, threads.tagid AS tagid
4368 FROM threads
4369 INNER JOIN msgids AS mm
4370 ON mm.msgid=:msgid AND mm.uid=threads.uid
4371 WHERE mute=0
4372 ;`).bindConstText(":msgid", trow.msgid!SQ3Text).range)
4374 auto tname = chiroGetTagName(mrow.tagid!uint);
4375 if (tname.length == 0 || !globmatch(tname, trow.tagglob!SQ3Text)) continue;
4376 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4377 msguids ~= UidTag(mrow.uid!uint, mrow.tagid!uint);
4379 if (msguids.length == 0) continue;
4380 //conwriteln("updating ", msguids.length, " messages for msgid <", trow.msgid!SQ3Text, ">");
4381 //transacted!"View"{
4382 foreach (immutable pair; msguids) {
4383 stUpdateMute.st
4384 .bind(":uid", pair.uid)
4385 .bind(":tagid", pair.tagid)
4386 .bind(":mute", Mute.ThreadStart)
4387 .doAll();
4389 //};
4390 if (cb !is null) cb(Message, twitdone, twitcount);
4394 if (cb !is null) cb(Message, twitcount, twitcount);
4398 void processThreadMutes (TwitProcessCallback cb) {
4399 enum Message = "processing thread mutes";
4401 if (cb !is null) cb(Message, 0, 0);
4403 dbConf.execute(`
4404 ATTACH DATABASE '`~MailDBPath~`chiview.db' AS chiview;
4406 BEGIN TRANSACTION;
4408 --------------------------------------------------------------------------------
4409 -- create temp table with mute pairs
4410 SELECT ChiroTimerStart('creating mute pairs');
4411 CREATE TEMP TABLE mutepairs AS
4412 WITH RECURSIVE children(muid, paruid, mtagid) AS (
4413 SELECT 0, chiview.threads.uid, chiview.threads.tagid
4414 FROM chiview.threads
4415 WHERE chiview.threads.parent=0 AND chiview.threads.mute=2
4416 AND EXISTS (SELECT uid FROM chiview.threads AS tx WHERE tx.tagid=chiview.threads.tagid AND tx.parent=chiview.threads.uid)
4417 UNION ALL
4418 SELECT
4419 tt.uid, tt.uid, mtagid
4420 FROM children AS cc
4421 INNER JOIN chiview.threads AS tt
4423 tt.tagid=cc.mtagid AND
4424 tt.parent=cc.paruid AND
4425 tt.uid<>cc.muid AND
4426 tt.uid<>cc.paruid
4428 SELECT
4429 muid AS muid
4430 , mtagid AS mtagid
4431 FROM children
4432 WHERE muid<>0
4434 SELECT ChiroTimerStop();
4437 SELECT 'nested mute pairs to skip:', COUNT(uid)
4438 FROM chiview.threads
4439 INNER JOIN mutepairs AS tt
4441 tagid=tt.mtagid AND
4442 uid=tt.muid
4443 WHERE mute<>0
4447 SELECT ChiroTimerStart('updating thread mutes');
4448 UPDATE chiview.threads
4450 mute=3 -- child
4451 , appearance=(SELECT CASE WHEN appearance=0 THEN 1 ELSE appearance END)
4452 FROM (SELECT muid, mtagid FROM mutepairs) AS cc
4453 WHERE uid=cc.muid AND tagid=cc.mtagid AND mute=0
4455 SELECT ChiroTimerStop();
4457 DROP TABLE mutepairs;
4460 --SELECT 'secondary mutes:', COUNT(mute) FROM threads WHERE mute=3;
4463 COMMIT TRANSACTION;
4465 DETACH DATABASE chiview;
4470 public void chiroRecalcAllTwits (TwitProcessCallback cb) {
4471 // clear all twits
4472 conwriteln("clearing all mutes...");
4473 if (cb !is null) cb("clearing mutes", 0, 0);
4474 dbView.execute(`
4475 UPDATE threads
4476 SET mute=0, title=NULL
4477 ;`);
4478 conwriteln("processing email twits...");
4479 processEmailTwits(cb);
4480 conwriteln("processing msgid twits...");
4481 processMsgidTwits(cb);
4482 conwriteln("propagating thread twits...");
4483 processThreadMutes(cb);
4484 conwriteln("twit recalculation complete.");