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