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 iv
.encoding
;
70 private import iv
.cmdcon
;
71 private import iv
.ripemd160
;
72 private import iv
.strex
;
73 private import iv
.sq3
;
74 private import iv
.timer
;
75 private import iv
.vfs
.io
;
76 private import iv
.vfs
.util
;
78 private import iv
.dlzma
;
80 private import chibackend
.mbuilder
: DynStr
;
81 private import chibackend
.parse
;
82 private import chibackend
.decode
;
83 //private import iv.utfutil;
84 //private import iv.vfs.io;
86 version(use_libdeflate
) private import chibackend
.pack
.libdeflate
;
87 else version(use_balz
) private import iv
.balz
;
88 else version(use_libxpack
) private import chibackend
.pack
.libxpack
;
89 else version(use_libbrieflz
) private import chibackend
.pack
.libbrieflz
;
90 else version(use_liblzfse
) private import chibackend
.pack
.liblzfse
;
91 else version(use_lzjb
) private import chibackend
.pack
.lzjb
;
92 else version(use_libwim_lzms
) private import chibackend
.pack
.libwim
;
93 else version(use_libwim_lzx
) private import chibackend
.pack
.libwim
;
94 else version(use_libwim_xpress
) private import chibackend
.pack
.libwim
;
95 else version(use_lz4
) private import chibackend
.pack
.liblz4
;
96 else version(use_zstd
) private import chibackend
.pack
.libzstd
;
100 public enum ChiroDefaultPackLevel
= 6;
102 public enum ChiroDefaultPackLevel
= 9;
106 // use `MailDBPath()` to get/set it
107 private __gshared string ExpandedMailDBPath
= null;
108 public __gshared
int ChiroCompressionLevel
= ChiroDefaultPackLevel
;
109 public __gshared
bool ChiroSQLiteSilent
= false;
111 // if `true`, will try both ZLib and LZMA
112 public __gshared
bool ChiroPackTryHard
= false;
114 public __gshared
bool ChiroTimerEnabled
= false;
115 private __gshared Timer chiTimer
= Timer(false);
116 private __gshared
char[] chiTimerMsg
= null;
119 public __gshared Database dbStore
; // message store db
120 public __gshared Database dbView
; // message view db
121 public __gshared Database dbConf
; // config/options db
124 public enum Appearance
{
125 Ignore
= -1, // can be used to ignore messages in thread view
128 SoftDeleteFilter
= 2, // soft-delete from filter
129 SoftDeleteUser
= 3, // soft-delete by user
130 SoftDeletePurge
= 4, // soft-delete by user (will be purged on folder change)
136 Message
= 1, /* single message */
137 ThreadStart
= 2, /* all children starting from this */
138 ThreadOther
= 3, /* muted by some parent */
141 public bool isSoftDeleted (const int appearance
) pure nothrow @safe @nogc {
142 pragma(inline
, true);
144 appearance
>= Appearance
.SoftDeleteFilter
&&
145 appearance
<= Appearance
.SoftDeletePurge
;
150 There are several added SQLite functions:
152 ChiroPack(data[, compratio])
155 This tries to compress the given data, and returns a compressed blob.
156 If `compratio` is negative or zero, do not compress anything.
162 This decompresses the blob compressed with `ChiroPack()`. It is (usually) safe to pass
163 non-compressed data to this function.
166 ChiroNormCRLF(content)
167 ======================
169 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
170 Removes trailing blanks.
173 ChiroNormHeaders(content)
174 =========================
176 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
177 Then replaces 'space, LF' with a single space (joins multiline headers).
178 Removes trailing blanks.
181 ChiroExtractHeaders(content)
182 ============================
184 Can be used to extract headers from the message.
185 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
186 Then replaces 'space, LF' with a single space (joins multiline headers).
187 Removes trailing blanks.
190 ChiroExtractBody(content)
191 =========================
193 Can be used to extract body from the message.
194 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
195 Then replaces 'space, LF' with a single space (joins multiline headers).
196 Removes trailing blanks and final dot.
200 public enum OptionsDBName
= "chiroptera.db";
201 public enum StorageDBName
= "chistore.db";
202 public enum SupportDBName
= "chiview.db";
205 // ////////////////////////////////////////////////////////////////////////// //
206 private enum CommonPragmas
= `
207 PRAGMA case_sensitive_like = OFF;
208 PRAGMA foreign_keys = OFF;
209 PRAGMA locking_mode = NORMAL; /*EXCLUSIVE;*/
210 PRAGMA secure_delete = OFF;
212 PRAGMA trusted_schema = OFF;
213 PRAGMA writable_schema = OFF;
216 enum CommonPragmasRO
= CommonPragmas
~`
217 PRAGMA temp_store = MEMORY; /*DEFAULT*/ /*FILE*/
220 enum CommonPragmasRW
= CommonPragmas
~`
221 PRAGMA application_id = 1128810834; /*CHIR*/
222 PRAGMA auto_vacuum = NONE;
223 PRAGMA encoding = "UTF-8";
224 PRAGMA temp_store = DEFAULT;
225 --PRAGMA journal_mode = WAL; /*OFF;*/
226 --PRAGMA journal_mode = DELETE; /*OFF;*/
227 PRAGMA synchronous = NORMAL; /*OFF;*/
230 enum CommonPragmasRecreate
= `
231 PRAGMA locking_mode = EXCLUSIVE;
232 PRAGMA journal_mode = OFF;
233 PRAGMA synchronous = OFF;
236 static immutable dbpragmasRO
= CommonPragmasRO
;
238 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
239 // use the smallest page size, because we don't need to perform alot of selects here
240 static immutable dbpragmasRWStorage
= "PRAGMA page_size = 512;"~CommonPragmasRW
~"PRAGMA journal_mode = DELETE;";
241 static immutable dbpragmasRWStorageRecreate
= dbpragmasRWStorage
~CommonPragmasRecreate
;
243 // use slightly bigger pages
244 // funny, smaller pages leads to bigger files
245 static immutable dbpragmasRWSupport
= "PRAGMA page_size = 4096;"~CommonPragmasRW
~"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;";
246 static immutable dbpragmasRWSupportRecreate
= dbpragmasRWSupport
~CommonPragmasRecreate
;
248 // smaller page size is ok
249 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
250 static immutable dbpragmasRWOptions
= "PRAGMA page_size = 512;"~CommonPragmasRW
~"PRAGMA journal_mode = /*DELETE*/WAL; PRAGMA synchronous = NORMAL;";
251 static immutable dbpragmasRWOptionsRecreate
= dbpragmasRWOptions
~CommonPragmasRecreate
;
254 enum msgTagNameCheckSQL
= `
255 WITH RECURSIVE tagtable(tag, rest) AS (
256 VALUES('', NEW.tags||'|')
259 SUBSTR(rest, 0, INSTR(rest, '|')),
260 SUBSTR(rest, INSTR(rest, '|')+1)
265 WHEN tag = '/' THEN RAISE(FAIL, 'tag name violation (root tags are not allowed)')
266 WHEN LENGTH(tag) = 1 THEN RAISE(FAIL, 'tag name violation (too short tag name)')
267 WHEN SUBSTR(tag, LENGTH(tag)) = '/' THEN RAISE(FAIL, 'tag name violation (tag should not end with a slash)')
273 // main storage and support databases will be in different files
274 static immutable string schemaStorage
= `
275 -- deleted messages have empty headers and body
276 -- this is so uids will remain unique on inserting
277 -- tags are used to associate the message with various folders, and stored here for rebuild purposes
278 -- the frontend will use the separate "tags" table to select messages
279 -- deleted messages must not have any tags, and should contain no other data
280 -- (keeping the data is harmless, it simply sits there and takes space)
281 CREATE TABLE IF NOT EXISTS messages (
282 uid INTEGER PRIMARY KEY /* rowid, never zero */
283 , tags TEXT DEFAULT NULL /* associated message tags, '|'-separated; case-sensitive, no extra whitespaces or '||'! */
284 -- article data; MUST contain the ending dot, and be properly dot-stuffed
285 -- basically, this is "what we had received, as is" (*WITH* the ending dot!)
286 -- there is no need to normalize it in any way (and you *SHOULD NOT* do it!)
287 -- it should be compressed with "ChiroPack()", and extracted with "ChiroUnpack()"
291 -- check tag constraints
292 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_insert
293 BEFORE INSERT ON messages
295 BEGIN`~msgTagNameCheckSQL
~`
298 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_update_tags
299 BEFORE UPDATE OF tags ON messages
301 BEGIN`~msgTagNameCheckSQL
~`
305 static immutable string schemaStorageIndex
= `
309 static immutable string schemaOptions
= `
310 -- use "autoincrement" to allow account deletion
311 CREATE TABLE IF NOT EXISTS accounts (
312 accid INTEGER PRIMARY KEY AUTOINCREMENT /* unique, never zero */
313 , checktime INTEGER NOT NULL DEFAULT 15 /* check time, in minutes */
314 , nosendauth INTEGER NOT NULL DEFAULT 0 /* turn off authentication on sending? */
315 , debuglog INTEGER NOT NULL DEFAULT 0 /* do debug logging? */
316 , nocheck INTEGER NOT NULL DEFAULT 0 /* disable checking? */
317 , nntplastindex INTEGER NOT NULL DEFAULT 0 /* last seen article index for NNTP groups */
318 , name TEXT NOT NULL UNIQUE /* account name; lowercase alphanum, '_', '-', '.' */
319 , recvserver TEXT NOT NULL /* server for receiving messages */
320 , sendserver TEXT NOT NULL /* server for sending messages */
321 , user TEXT NOT NULL /* pop3 user name */
322 , pass TEXT NOT NULL /* pop3 password, empty for no authorisation */
323 , realname TEXT NOT NULL /* user name for e-mail headers */
324 , email TEXT NOT NULL /* account e-mail address (full, name@host) */
325 , inbox TEXT NOT NULL /* inbox tag, usually "/accname/inbox", or folder for nntp */
326 , nntpgroup TEXT NOT NULL DEFAULT '' /* nntp group name for NNTP accounts; if empty, this is POP3 account */
330 CREATE TABLE IF NOT EXISTS options (
331 name TEXT NOT NULL UNIQUE
336 CREATE TABLE IF NOT EXISTS addressbook (
337 nick TEXT NOT NULL UNIQUE /* short nick for this address book entry */
338 , name TEXT NOT NULL DEFAULT ''
339 , email TEXT NOT NULL
340 , notes TEXT DEFAULT NULL
344 -- twits by email/name
345 CREATE TABLE IF NOT EXISTS emailtwits (
346 etwitid INTEGER PRIMARY KEY
347 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
348 , email TEXT /* if both name and email present, use only email */
349 , name TEXT /* name to twit by */
350 , title TEXT /* optional title */
351 , notes TEXT /* notes; often URL */
355 CREATE TABLE IF NOT EXISTS msgidtwits (
356 mtwitid INTEGER PRIMARY KEY
357 , etwitid INTEGER /* parent mail twit, if any */
358 , automatic INTEGER DEFAULT 1 /* added by message filtering, not from .rc? */
359 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
360 , msgid TEXT /* message used to set twit */
365 CREATE TABLE IF NOT EXISTS filters (
366 filterid INTEGER PRIMARY KEY
367 , valid INTEGER NOT NULL DEFAULT 1 /* is this filter valid? used to skip bad filters */
368 , idx INTEGER NOT NULL DEFAULT 0 /* used for ordering */
369 , post INTEGER NOT NULL DEFAULT 0 /* post-spamcheck filter? */
370 , hitcount INTEGER NOT NULL DEFAULT 0 /* for statistics */
371 , name TEXT NOT NULL UNIQUE /* filter name */
372 , body TEXT /* filter text */
375 CREATE TRIGGER IF NOT EXISTS filters_new_index
376 AFTER INSERT ON filters
379 UPDATE filters SET idx=(SELECT MAX(idx)+10 FROM filters)
380 WHERE NEW.idx=0 AND filterid=NEW.filterid;
384 static immutable string schemaOptionsIndex
= `
385 -- no need to, "UNIQUE" automaptically creates it
386 --CREATE INDEX IF NOT EXISTS accounts_name ON accounts(name);
388 -- this index in implicit
389 --CREATE INDEX IF NOT EXISTS options_name ON options(name);
391 CREATE INDEX IF NOT EXISTS emailtwits_email ON emailtwits(email);
392 CREATE INDEX IF NOT EXISTS emailtwits_name ON emailtwits(name);
393 CREATE UNIQUE INDEX IF NOT EXISTS emailtwits_email_name ON emailtwits(email, name);
395 CREATE INDEX IF NOT EXISTS msgidtwits_msgid ON msgidtwits(msgid);
397 CREATE INDEX IF NOT EXISTS filters_idx_post_valid ON filters(idx, post, valid);
401 enum schemaSupportTable
= `
402 -- tag <-> messageid correspondence
403 -- note that one message can be tagged with more than one tag
404 -- there is always tag with "uid=0", to keep all tags alive
406 -- account:name -- received via this account
407 -- #spam -- this is spam message
408 -- #hobo -- will be autoassigned to messages without any tags (created on demand)
409 CREATE TABLE IF NOT EXISTS tagnames (
410 tagid INTEGER PRIMARY KEY
411 , hidden INTEGER NOT NULL DEFAULT 0 /* deleting tags may cause 'tagid' reuse, so it's better to hide them instead */
412 , threading INTEGER NOT NULL DEFAULT 1 /* enable threaded view? */
413 , noattaches INTEGER NOT NULL DEFAULT 0 /* ignore non-text attachments? */
414 , tag TEXT NOT NULL UNIQUE
417 -- it is here, because we don't have a lot of tags, and inserts are slightly faster this way
418 -- it's not required, because "UNIQUE" constraint will create automatic index
419 --CREATE INDEX IF NOT EXISTS tagname_tag ON tagnames(tag);
421 --CREATE INDEX IF NOT EXISTS tagname_tag_uid ON tagnames(tag, tagid);
424 -- each tag has its own unique threads (so uids can be duplicated, but (uid,tagid) paris cannot
425 -- see above for "apearance" and "mute" values
426 CREATE TABLE IF NOT EXISTS threads (
427 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
428 , tagid INTEGER /* we need separate threads for each tag */
429 , time INTEGER DEFAULT 0 /* unixtime -- creation/send/receive */
431 , parent INTEGER DEFAULT 0 /* uid: parent message in thread, or 0 */
433 , appearance INTEGER DEFAULT 0 /* how the message should look */
434 , mute INTEGER DEFAULT 0 /* 1: only this message, 2: the whole thread */
435 , title TEXT DEFAULT NULL /* title from the filter */
440 -- for FTS5 to work, this table must be:
441 -- updated LAST on INSERT
442 -- updated FIRST on DELETE
443 -- this is due to FTS5 triggers
444 -- message texts should NEVER be updated!
445 -- if you want to do update a message:
446 -- first, DELETE the old one from this table
447 -- then, update textx
448 -- then, INSERT here again
449 -- doing it like that will keep FTS5 in sync
450 CREATE TABLE IF NOT EXISTS info (
451 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
452 , from_name TEXT /* can be empty */
453 , from_mail TEXT /* can be empty */
454 , subj TEXT /* can be empty */
455 , to_name TEXT /* can be empty */
456 , to_mail TEXT /* can be empty */
461 -- moved to separate table, because this info is used only when inserting new messages
462 CREATE TABLE IF NOT EXISTS msgids (
463 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
464 , time INTEGER /* so we can select the most recent message */
465 , msgid TEXT /* message id */
469 -- this holds in-reply-to, and references
470 -- moved to separate table, because this info is used only when inserting new messages
471 CREATE TABLE IF NOT EXISTS refids (
472 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
473 , idx INTEGER /* internal index in headers, cannot have gaps, starts from 0 */
474 , msgid TEXT /* message id */
478 -- this ALWAYS contain an entry (yet content may be empty string)
479 CREATE TABLE IF NOT EXISTS content_text (
480 uid INTEGER PRIMARY KEY /* owner message uid */
481 , format TEXT NOT NULL /* optional format, like 'flowed' */
482 , content TEXT NOT NULL /* properly decoded; packed */
486 -- this ALWAYS contain an entry (yet content may be empty string)
487 CREATE TABLE IF NOT EXISTS content_html (
488 uid INTEGER PRIMARY KEY /* owner message uid */
489 , format TEXT NOT NULL /* optional format, like 'flowed' */
490 , content TEXT NOT NULL /* properly decoded; packed */
494 -- this DOES NOT include text and html contents (and may exclude others)
495 CREATE TABLE IF NOT EXISTS attaches (
496 uid INTEGER /* owner message uid */
497 , idx INTEGER /* code should take care of proper autoincrementing this */
498 , mime TEXT NOT NULL /* always lowercased */
499 , name TEXT NOT NULL /* attachment name; always empty for inline content, never empty for non-inline content */
500 , format TEXT NOT NULL /* optional format, like 'flowed' */
501 , content BLOB /* properly decoded; packed; NULL if the attach was dropped */
505 -- this view is used for FTS5 content queries
506 -- it is harmless to keep it here even if FTS5 is not used
507 --DROP VIEW IF EXISTS fts5_msgview;
508 CREATE VIEW IF NOT EXISTS fts5_msgview (uid, sender, subj, text, html)
512 , info.from_name||' '||CHAR(26)||' '||info.from_mail AS sender
514 , ChiroUnpack(content_text.content) AS text
515 , ChiroUnpack(content_html.content) AS html
517 INNER JOIN content_text USING(uid)
518 INNER JOIN content_html USING(uid)
522 -- this table holds all unsent messages
523 -- they are put in the storage and properly inserted,
524 -- but also put in this table, for the receiver to send them
525 -- also note that NNTP messages will be put in the storage without any tags, but with a content
526 -- (this is because we will receive them back from NNTP server later)
527 -- succesfully sent messages will be marked with non-zero sendtime
528 CREATE TABLE IF NOT EXISTS unsent (
529 uid INTEGER PRIMARY KEY /* the same as in the storage, not automatic */
530 , accid INTEGER /* account from which this message should be sent */
531 , from_pop3 TEXT /* "from" for POP3 */
532 , to_pop3 TEXT /* "to" for POP3 */
533 , data TEXT /* PACKED data to send */
534 , sendtime INTEGER DEFAULT 0 /* 0: not yet; unixtime */
535 , lastsendtime INTEGER DEFAULT 0 /* when we last tried to send it? 0 means "not yet" */
539 static immutable string schemaSupportTempTables
= `
540 --DROP TABLE IF EXISTS treepane;
541 CREATE TEMP TABLE IF NOT EXISTS treepane (
542 iid INTEGER PRIMARY KEY
545 -- to make joins easier
549 CREATE INDEX IF NOT EXISTS treepane_uid ON treepane(uid);
552 enum schemaSupportIndex
= `
553 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_tag_uid ON threads(tagid, uid);
554 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_uid_tag ON threads(uid, tagid);
556 -- this is for views where threading is disabled
557 CREATE INDEX IF NOT EXISTS trd_by_tag_time ON threads(tagid, time);
558 --CREATE INDEX IF NOT EXISTS trd_by_tag_time_parent ON threads(tagid, time, parent);
559 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent_time ON threads(tagid, parent, time);
560 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent ON threads(tagid, parent);
561 CREATE INDEX IF NOT EXISTS trd_by_parent_tag ON threads(parent, tagid);
563 -- this is for test if we have any unread articles (we don't mind the exact numbers, tho)
564 CREATE INDEX IF NOT EXISTS trd_by_appearance ON threads(appearance);
565 -- this is for removing purged messages
566 CREATE INDEX IF NOT EXISTS trd_by_tag_appearance ON threads(tagid, appearance);
567 -- was used in table view creation, not used anymore
568 --CREATE INDEX IF NOT EXISTS trd_by_parent_tag_appearance ON threads(parent, tagid, appearance);
571 -- was used in table view creation, not used anymore
572 --CREATE INDEX IF NOT EXISTS trd_by_tag_appearance_time ON threads(tagid, appearance, time);
574 CREATE INDEX IF NOT EXISTS msgid_by_msgid_time ON msgids(msgid, time DESC);
576 CREATE INDEX IF NOT EXISTS refid_by_refids_idx ON refids(msgid, idx);
577 CREATE INDEX IF NOT EXISTS refid_by_uid_idx ON refids(uid, idx);
579 CREATE INDEX IF NOT EXISTS content_text_by_uid ON content_text(uid);
580 CREATE INDEX IF NOT EXISTS content_html_by_uid ON content_html(uid);
582 CREATE INDEX IF NOT EXISTS attaches_by_uid_name ON attaches(uid, name);
583 CREATE INDEX IF NOT EXISTS attaches_by_uid_idx ON attaches(uid, idx);
585 -- "info" indicies for twits
586 CREATE INDEX IF NOT EXISTS info_by_from_mail_name ON info(from_mail, from_name);
587 --CREATE INDEX IF NOT EXISTS info_by_from_mail ON info(from_mail);
588 CREATE INDEX IF NOT EXISTS info_by_from_name ON info(from_name);
591 CREATE INDEX IF NOT EXISTS unsent_by_accid ON unsent(accid);
594 static immutable string schemaSupport
= schemaSupportTable
~schemaSupportIndex
;
597 version(fts5_use_porter
) {
598 enum FTS5_Tokenizer
= "porter unicode61 remove_diacritics 2";
600 enum FTS5_Tokenizer
= "unicode61 remove_diacritics 2";
603 static immutable string recreateFTS5
= `
604 DROP TABLE IF EXISTS fts5_messages;
605 CREATE VIRTUAL TABLE fts5_messages USING fts5(
606 sender /* sender name and email, separated by " \x1a " (dec 26) (substitute char) */
607 , subj /* email subject */
608 , text /* email body, text/plain */
609 , html /* email body, text/html */
610 --, uid UNINDEXED /* message uid this comes from (not needed, use "rowid" instead */
611 , tokenize = '`~FTS5_Tokenizer
~`'
612 , content = 'fts5_msgview'
613 , content_rowid = 'uid'
615 /* sender, subj, text, html */
616 INSERT INTO fts5_messages(fts5_messages, rank) VALUES('rank', 'bm25(1.0, 3.0, 10.0, 6.0)');
619 static immutable string repopulateFTS5
= `
620 SELECT ChiroTimerStart('updating FTS5');
623 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
624 SELECT uid, sender, subj, text, html
628 SELECT threads.tagid FROM threads
629 INNER JOIN tagnames USING(tagid)
631 threads.uid=fts5_msgview.uid AND
632 tagnames.hidden=0 AND SUBSTR(tagnames.tag, 1, 1)='/'
636 SELECT ChiroTimerStop();
640 static immutable string recreateFTS5Triggers
= `
641 -- triggers to keep the FTS index up to date
643 -- this rely on the proper "info" table update order
644 -- info must be inserted LAST
645 DROP TRIGGER IF EXISTS fts5xtrig_insert;
646 CREATE TRIGGER fts5xtrig_insert
649 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
650 SELECT uid, sender, subj, text, html FROM fts5_msgview WHERE uid=NEW.uid LIMIT 1;
653 -- not AFTER, because we still need a valid view!
654 -- this rely on the proper "info" table update order
655 -- info must be deleted FIRST
656 DROP TRIGGER IF EXISTS fts5xtrig_delete;
657 CREATE TRIGGER fts5xtrig_delete
658 BEFORE DELETE ON info
660 INSERT INTO fts5_messages(fts5_messages, rowid, sender, subj, text, html)
661 SELECT 'delete', uid, sender, subj, text, html FROM fts5_msgview WHERE uid=OLD.uid LIMIT 1;
664 -- message texts should NEVER be updated, so no ON UPDATE trigger
668 // ////////////////////////////////////////////////////////////////////////// //
669 // not properly implemented yet
670 //version = lazy_mt_safe;
672 version(lazy_mt_safe
) {
673 enum lazy_mt_safe_flag
= true;
675 enum lazy_mt_safe_flag
= false;
678 public struct LazyStatement(string dbname
) {
688 DBStatement st
= void;
689 version(lazy_mt_safe
) {
690 sqlite3_mutex
* mutex
= void;
693 usize sqlsize
= void;
694 uint compiled
= void;
700 string delayInit
= null;
703 inout(Data
)* datap () inout pure nothrow @trusted @nogc { pragma(inline
, true); return cast(Data
*)udata
; }
704 void datap (Data
*v
) pure nothrow @trusted @nogc { pragma(inline
, true); udata
= cast(usize
)v
; }
708 @disable this (this);
714 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
715 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
716 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
717 else static assert(0, "invalid db name: '"~dbname~"'");
718 import core.stdc.stdlib : calloc;
719 Data* dp = cast(Data*)calloc(1, Data.sizeof);
720 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
722 dp.sql = cast(char*)calloc(1, sql.length);
723 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
724 dp.sql[0..sql.length] = sql[];
725 dp.sqlsize = sql.length;
726 version(lazy_mt_safe) {
727 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
728 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
732 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
736 import core
.stdc
.stdlib
: free
;
739 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===DEINIT===\n%s\n============\n", dp.sql); }
740 dp
.st
= DBStatement
.init
;
742 version(lazy_mt_safe
) {
743 sqlite3_mutex_free(dp
.mutex
);
749 bool valid () pure nothrow @safe @nogc { pragma(inline
, true); return (udata
!= 0 || delayInit
.length
); }
751 private void setupWith (const(char)[] sql
) {
752 if (udata
) throw new Exception("statement already inited");
754 static if (dbname
== "View" || dbname
== "view") dbtype
= DB
.View
;
755 else static if (dbname
== "Store" || dbname
== "store") dbtype
= DB
.Store
;
756 else static if (dbname
== "Conf" || dbname
== "conf") dbtype
= DB
.Conf
;
757 else static assert(0, "invalid db name: '"~dbname
~"'");
758 import core
.stdc
.stdlib
: calloc
;
759 Data
* dp
= cast(Data
*)calloc(1, Data
.sizeof
);
760 if (dp
is null) { import core
.exception
: onOutOfMemoryErrorNoGC
; onOutOfMemoryErrorNoGC(); }
762 dp
.sql
= cast(char*)calloc(1, sql
.length
);
763 if (dp
.sql
is null) { import core
.exception
: onOutOfMemoryErrorNoGC
; onOutOfMemoryErrorNoGC(); }
764 dp
.sql
[0..sql
.length
] = sql
[];
765 dp
.sqlsize
= sql
.length
;
766 version(lazy_mt_safe
) {
767 dp
.mutex
= sqlite3_mutex_alloc(SQLITE_MUTEX_FAST
);
768 if (dp
.mutex
is null) { import core
.exception
: onOutOfMemoryErrorNoGC
; onOutOfMemoryErrorNoGC(); }
771 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
774 ref DBStatement
st () {
776 //throw new Exception("no statement set");
777 setupWith(delayInit
);
781 version(lazy_mt_safe
) {
782 sqlite3_mutex_enter(dp
.mutex
);
785 version(lazy_mt_safe
) {
786 sqlite3_mutex_leave(dp
.mutex
);
789 //{ import core.stdc.stdio : printf; printf("***compiling:\n%s\n=====\n", dp.sql); }
790 final switch (dbtype
) {
791 case DB
.Store
: dp
.st
= dbStore
.persistentStatement(dp
.sql
[0..dp
.sqlsize
]); break;
792 case DB
.View
: dp
.st
= dbView
.persistentStatement(dp
.sql
[0..dp
.sqlsize
]); break;
793 case DB
.Conf
: dp
.st
= dbConf
.persistentStatement(dp
.sql
[0..dp
.sqlsize
]); break;
796 //assert(dp.st.valid);
798 //assert(dp.st.valid);
804 // ////////////////////////////////////////////////////////////////////////// //
805 private bool isGoodText (const(void)[] buf
) pure nothrow @safe @nogc {
806 foreach (immutable ubyte ch
; cast(const(ubyte)[])buf
) {
808 if (ch
!= 9 && ch
!= 10 && ch
!= 13 && ch
!= 27) return false;
810 if (ch
== 127) return false;
814 //return utf8ValidText(buf);
818 // ////////////////////////////////////////////////////////////////////////// //
819 private bool isBadPrefix (const(char)[] buf
) pure nothrow @trusted @nogc {
820 if (buf
.length
< 5) return false;
822 buf
.ptr
[0] == '\x1b' &&
823 buf
.ptr
[1] >= 'A' && buf
.ptr
[1] <= 'Z' &&
824 buf
.ptr
[2] >= 'A' && buf
.ptr
[2] <= 'Z' &&
825 buf
.ptr
[3] >= 'A' && buf
.ptr
[3] <= 'Z' &&
826 buf
.ptr
[4] >= 'A' && buf
.ptr
[4] <= 'Z';
830 /* two high bits of the first byte holds the size:
831 00: fit into 6 bits: [0.. 0x3f] (1 byte)
832 01: fit into 14 bits: [0.. 0x3fff] (2 bytes)
833 10: fit into 22 bits: [0.. 0x3f_ffff] (3 bytes)
834 11: fit into 30 bits: [0..0x3fff_ffff] (4 bytes)
836 number is stored as big-endian.
837 will not write anything to `dest` if there is not enough room.
839 returns number of bytes, or 0 if the number is too big.
841 private uint encodeUInt (void[] dest
, uint v
) nothrow @trusted @nogc {
842 if (v
> 0x3fff_ffffU
) return 0;
843 ubyte[] d
= cast(ubyte[])dest
;
845 if (v
> 0x3f_ffffU
) {
848 d
.ptr
[0] = cast(ubyte)(v
>>24);
849 d
.ptr
[1] = cast(ubyte)(v
>>16);
850 d
.ptr
[2] = cast(ubyte)(v
>>8);
851 d
.ptr
[3] = cast(ubyte)v
;
859 d
.ptr
[0] = cast(ubyte)(v
>>16);
860 d
.ptr
[1] = cast(ubyte)(v
>>8);
861 d
.ptr
[2] = cast(ubyte)v
;
869 d
.ptr
[0] = cast(ubyte)(v
>>8);
870 d
.ptr
[1] = cast(ubyte)v
;
875 if (d
.length
>= 1) d
.ptr
[0] = cast(ubyte)v
;
880 private uint decodeUIntLength (const(void)[] dest
) pure nothrow @trusted @nogc {
881 const(ubyte)[] d
= cast(const(ubyte)[])dest
;
882 if (d
.length
== 0) return 0;
883 switch (d
.ptr
[0]&0xc0) {
885 case 0x40: return (d
.length
>= 2 ?
2 : 0);
886 case 0x80: return (d
.length
>= 3 ?
3 : 0);
889 return (d
.length
>= 4 ?
4 : 0);
893 // returns uint.max on error (impossible value)
894 private uint decodeUInt (const(void)[] dest
) pure nothrow @trusted @nogc {
895 const(ubyte)[] d
= cast(const(ubyte)[])dest
;
896 if (d
.length
== 0) return uint.max
;
898 switch (d
.ptr
[0]&0xc0) {
903 if (d
.length
< 2) return uint.max
;
904 res
= ((d
.ptr
[0]&0x3fU
)<<8)|d
.ptr
[1];
907 if (d
.length
< 3) return uint.max
;
908 res
= ((d
.ptr
[0]&0x3fU
)<<16)|
(d
.ptr
[1]<<8)|d
.ptr
[2];
911 if (d
.length
< 4) return uint.max
;
912 res
= ((d
.ptr
[0]&0x3fU
)<<24)|
(d
.ptr
[1]<<16)|
(d
.ptr
[2]<<8)|d
.ptr
[3];
919 // returns position AFTER the headers (empty line is skipped too)
920 // returned value is safe for slicing
921 private int sq3Supp_FindHeadersEnd (const(char)* vs
, const int sz
) {
922 import core
.stdc
.string
: memchr
;
923 if (sz
<= 0) return 0;
924 const(char)* eptr
= cast(const(char)*)memchr(vs
, '\n', cast(uint)sz
);
925 while (eptr
!is null) {
927 int epos
= cast(int)cast(usize
)(eptr
-vs
);
928 if (sz
-epos
< 1) break;
930 if (sz
-epos
< 2) break;
934 if (*eptr
== '\n') return epos
+1;
936 eptr
= cast(const(char)*)memchr(eptr
, '\n', cast(uint)(sz
-epos
));
942 // hack for some invalid dates
943 uint parseMailDate (const(char)[] s
) nothrow {
945 if (s
.length
== 0) return 0;
947 return cast(uint)(parseRFC822DateTime(s
).toUTC
.toUnixTime
);
948 } catch (Exception
) {}
949 // sometimes this helps
951 foreach_reverse (immutable char ch
; s
) {
952 if (ch
< '0' || ch
> '9') break;
955 if (dcount
> 4) return 0;
956 s
~= "0000"[0..4-dcount
];
958 return cast(uint)(parseRFC822DateTime(s
).toUTC
.toUnixTime
);
959 } catch (Exception
) {}
964 // ////////////////////////////////////////////////////////////////////////// //
968 ** ChiroPackLZMA(content)
969 ** ChiroPackLZMA(content, packflag)
971 ** second form accepts int flag; 0 means "don't pack"
973 private void sq3Fn_ChiroPackLZMA (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) nothrow @trusted {
974 if (argc
< 1 || argc
> 2) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroPackLZMA()`", -1); return; }
975 int packlevel
= (argc
> 1 ?
sqlite3_value_int(argv
[1]) : ChiroDefaultPackLevel
);
976 if (packlevel
< 0) packlevel
= 5/*lzma default*/; else if (packlevel
> 9) packlevel
= 9;
978 sqlite3_value
*val
= argv
[0];
980 immutable int sz
= sqlite3_value_bytes(val
);
981 if (sz
< 0 || sz
> 0x3fffffff-4) { sqlite3_result_error_toobig(ctx
); return; }
983 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
985 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(val
);
986 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroPackLZMA()`", -1); return; }
988 if (sz
>= 0x3fffffff-8) {
989 if (isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_error_toobig(ctx
); return; }
990 sqlite3_result_value(ctx
, val
);
994 import core
.stdc
.stdlib
: malloc
, free
;
995 import core
.stdc
.string
: memcpy
;
997 if (packlevel
> 0 && sz
> 8) {
998 import core
.stdc
.stdio
: snprintf
;
1000 xsz
[0..5] = "\x1bLZMA";
1001 uint xszlen
= encodeUInt(xsz
[5..$], cast(uint)sz
);
1004 immutable uint bsz
= cast(uint)sz
;
1005 char* cbuf
= cast(char*)malloc(bsz
+xszlen
+LZMA_PROPS_SIZE
+1+16);
1007 if (isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_error_nomem(ctx
); return; }
1009 cbuf
[0..xszlen
] = xsz
[0..xszlen
];
1010 usize destLen
= bsz
;
1011 ubyte[LZMA_PROPS_SIZE
+8] hdr
= void;
1012 uint hdrSize
= cast(uint)hdr
.sizeof
;
1014 CLzmaEncProps props
;
1015 props
.level
= packlevel
;
1016 props
.dictSize
= 1<<22; //4MB
1017 props
.reduceSize
= bsz
;
1019 SRes res
= LzmaEncode(cast(ubyte*)cbuf
+xszlen
+LZMA_PROPS_SIZE
+1, &destLen
, cast(const(ubyte)*)vs
, bsz
, &props
, hdr
.ptr
, &hdrSize
, 0/*writeEndMark*/, null, &lzmaDefAllocator
, &lzmaDefAllocator
);
1020 assert(hdrSize
== LZMA_PROPS_SIZE
);
1021 if (res
== SZ_OK
&& destLen
+xszlen
+LZMA_PROPS_SIZE
+1 < cast(usize
)sz
) {
1022 import core
.stdc
.string
: memcpy
;
1023 cbuf
[xszlen
] = LZMA_PROPS_SIZE
;
1024 memcpy(cbuf
+xszlen
+1, hdr
.ptr
, LZMA_PROPS_SIZE
);
1025 sqlite3_result_blob(ctx
, cbuf
, destLen
+xszlen
+LZMA_PROPS_SIZE
+1, &free
);
1033 if (isBadPrefix(vs
[0..cast(uint)sz
])) {
1034 char *res
= cast(char *)malloc(sz
+5);
1035 if (res
is null) { sqlite3_result_error_nomem(ctx
); return; }
1036 res
[0..5] = "\x1bRAWB";
1037 res
[5..sz
+5] = vs
[0..sz
];
1038 if (isGoodText(vs
[0..cast(usize
)sz
])) {
1039 sqlite3_result_text(ctx
, res
, sz
+5, &free
);
1041 sqlite3_result_blob(ctx
, res
, sz
+5, &free
);
1044 immutable bool wantBlob
= !isGoodText(vs
[0..cast(usize
)sz
]);
1045 immutable int tp
= sqlite3_value_type(val
);
1046 if ((wantBlob
&& tp
== SQLITE_BLOB
) ||
(!wantBlob
&& tp
== SQLITE3_TEXT
)) {
1047 sqlite3_result_value(ctx
, val
);
1048 } else if (wantBlob
) {
1049 sqlite3_result_blob(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1051 sqlite3_result_text(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1058 ** ChiroPack(content)
1059 ** ChiroPack(content, packflag)
1061 ** second form accepts int flag; 0 means "don't pack"
1063 private void sq3Fn_ChiroPackCommon (sqlite3_context
*ctx
, sqlite3_value
*val
, int packlevel
) nothrow @trusted {
1064 immutable int sz
= sqlite3_value_bytes(val
);
1065 if (sz
< 0 || sz
> 0x3fffffff-4) { sqlite3_result_error_toobig(ctx
); return; }
1067 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1069 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(val
);
1070 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroPack()`", -1); return; }
1072 if (sz
>= 0x3fffffff-8) {
1073 if (isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_error_toobig(ctx
); return; }
1074 sqlite3_result_value(ctx
, val
);
1078 import core
.stdc
.stdlib
: malloc
, free
;
1079 import core
.stdc
.string
: memcpy
;
1081 if (packlevel
> 0 && sz
> 8) {
1082 import core
.stdc
.stdio
: snprintf
;
1083 char[16] xsz
= void;
1085 xsz
[0..5] = "\x1bBALZ";
1086 } else version(use_libxpack
) {
1087 xsz
[0..5] = "\x1bXPAK";
1088 } else version(use_libbrieflz
) {
1089 xsz
[0..5] = "\x1bBRLZ";
1090 } else version(use_liblzfse
) {
1091 xsz
[0..5] = "\x1bLZFS";
1092 } else version(use_lzjb
) {
1093 xsz
[0..5] = "\x1bLZJB";
1094 } else version(use_libwim_lzms
) {
1095 xsz
[0..5] = "\x1bLZMS";
1096 } else version(use_libwim_lzx
) {
1097 xsz
[0..5] = "\x1bLZMX";
1098 } else version(use_libwim_xpress
) {
1099 xsz
[0..5] = "\x1bXPRS";
1100 } else version(use_lz4
) {
1101 xsz
[0..5] = "\x1bLZ4D";
1102 } else version(use_zstd
) {
1103 xsz
[0..5] = "\x1bZSTD";
1105 xsz
[0..5] = "\x1bZLIB";
1107 immutable uint xszlenNum
= encodeUInt(xsz
[5..$], cast(uint)sz
);
1109 immutable uint xszlen
= xszlenNum
+5;
1110 //xsz[xszlen++] = ':';
1111 version(use_libbrieflz
) {
1112 immutable usize bsz
= blz_max_packed_size(cast(usize
)sz
);
1113 } else version(use_lzjb
) {
1114 immutable uint bsz
= cast(uint)sz
+1024;
1115 } else version(use_lz4
) {
1116 immutable uint bsz
= cast(uint)LZ4_compressBound(sz
)+1024;
1118 immutable uint bsz
= cast(uint)sz
;
1120 char* cbuf
= cast(char*)malloc(bsz
+xszlen
+64);
1122 if (isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_error_nomem(ctx
); return; }
1124 cbuf
[0..xszlen
] = xsz
[0..xszlen
];
1128 usize dpos
= xszlen
;
1133 if (spos
>= cast(usize
)sz
) return 0;
1134 usize left
= cast(usize
)sz
-spos
;
1135 if (left
> buf
.length
) left
= buf
.length
;
1136 if (left
) memcpy(buf
.ptr
, vs
+spos
, left
);
1142 if (dpos
+buf
.length
>= cast(usize
)sz
) throw new Exception("uncompressible");
1143 memcpy(cbuf
+dpos
, buf
.ptr
, buf
.length
);
1146 // maximum compression?
1149 } catch(Exception
) {
1152 if (dpos
< cast(usize
)sz
) {
1153 sqlite3_result_blob(ctx
, cbuf
, dpos
, &free
);
1156 } else version(use_libdeflate
) {
1157 if (packlevel
> 12) packlevel
= 12;
1158 libdeflate_compressor
*cpr
= libdeflate_alloc_compressor(packlevel
);
1159 if (cpr
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1160 usize dsize
= libdeflate_zlib_compress(cpr
, vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
);
1161 libdeflate_free_compressor(cpr
);
1162 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1163 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1166 } else version(use_libxpack
) {
1167 // 2^19 (524288) bytes. This is definitely a big problem and I am planning to address it.
1168 // https://github.com/ebiggers/xpack/issues/1
1169 if (sz
< 524288-64) {
1170 if (packlevel
> 9) packlevel
= 9;
1171 xpack_compressor
*cpr
= xpack_alloc_compressor(cast(usize
)sz
, packlevel
);
1172 if (cpr
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1173 usize dsize
= xpack_compress(cpr
, vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
);
1174 xpack_free_compressor(cpr
);
1175 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1176 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1180 } else version(use_libbrieflz
) {
1181 if (packlevel
> 10) packlevel
= 10;
1182 immutable usize wbsize
= blz_workmem_size_level(cast(usize
)sz
, packlevel
);
1183 void* wbuf
= cast(void*)malloc(wbsize
+!wbsize
);
1184 if (wbuf
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1185 uint dsize
= blz_pack_level(vs
, cbuf
+xszlen
, cast(uint)sz
, wbuf
, packlevel
);
1187 if (dsize
+xszlen
< cast(usize
)sz
) {
1188 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1191 } else version(use_liblzfse
) {
1192 immutable usize wbsize
= lzfse_encode_scratch_size();
1193 void* wbuf
= cast(void*)malloc(wbsize
+!wbsize
);
1194 if (wbuf
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1195 usize dsize
= lzfse_encode_buffer(cbuf
+xszlen
, bsz
, vs
, cast(uint)sz
, wbuf
);
1197 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1198 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1201 } else version(use_lzjb
) {
1202 usize dsize
= lzjb_compress(vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
);
1203 if (dsize
== usize
.max
) dsize
= 0;
1204 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1205 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1208 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "LZJB FAILED!\n"); }
1209 } else version(use_libwim_lzms
) {
1210 wimlib_compressor
* cpr
;
1211 uint clevel
= (packlevel
< 10 ?
50 : 1000);
1212 int rc
= wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZMS
, cast(usize
)sz
, clevel
, &cpr
);
1213 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1214 usize dsize
= wimlib_compress(vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
, cpr
);
1215 wimlib_free_compressor(cpr
);
1216 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1217 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1220 } else version(use_libwim_lzx
) {
1221 if (sz
<= WIMLIB_LZX_MAX_CHUNK
) {
1222 wimlib_compressor
* cpr
;
1223 uint clevel
= (packlevel
< 10 ?
50 : 1000);
1224 int rc
= wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZX
, cast(usize
)sz
, clevel
, &cpr
);
1225 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1226 usize dsize
= wimlib_compress(vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
, cpr
);
1227 wimlib_free_compressor(cpr
);
1228 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1229 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1233 } else version(use_libwim_xpress
) {
1234 if (sz
<= WIMLIB_XPRESS_MAX_CHUNK
) {
1235 wimlib_compressor
* cpr
;
1236 uint clevel
= (packlevel
< 10 ?
50 : 1000);
1237 uint csz
= WIMLIB_XPRESS_MIN_CHUNK
;
1238 while (csz
< WIMLIB_XPRESS_MAX_CHUNK
&& csz
< cast(uint)sz
) csz
*= 2U;
1239 int rc
= wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_XPRESS
, csz
, clevel
, &cpr
);
1240 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1241 usize dsize
= wimlib_compress(vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
, cpr
);
1242 wimlib_free_compressor(cpr
);
1243 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1244 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1248 } else version(use_lz4
) {
1249 int dsize
= LZ4_compress_default(vs
, cbuf
+xszlen
, sz
, cast(int)bsz
);
1250 if (dsize
> 0 && dsize
+xszlen
< sz
) {
1251 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1254 } else version(use_zstd
) {
1255 immutable int clev
=
1256 packlevel
<= 3 ?
ZSTD_minCLevel() :
1257 packlevel
<= 6 ?
ZSTD_defaultCLevel() :
1258 packlevel
< 10 ?
19 :
1260 usize dsize
= ZSTD_compress(cbuf
+xszlen
, cast(int)bsz
, vs
, sz
, clev
);
1261 if (!ZSTD_isError(dsize
) && dsize
> 0 && dsize
+xszlen
< sz
) {
1262 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1266 import etc
.c
.zlib
: /*compressBound,*/ compress2
, Z_OK
;
1267 //uint bsz = cast(uint)compressBound(cast(uint)sz);
1268 if (packlevel
> 9) packlevel
= 9;
1270 immutable int zres
= compress2(cast(ubyte *)(cbuf
+xszlen
), &dsize
, cast(const(ubyte) *)vs
, sz
, packlevel
);
1271 if (zres
== Z_OK
&& dsize
+xszlen
< cast(usize
)sz
) {
1272 if (!ChiroPackTryHard
) {
1273 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1281 if (ChiroPackTryHard
) {
1282 char* lzmabuf
= cast(char*)malloc(bsz
+xszlen
+LZMA_PROPS_SIZE
+1+64);
1283 if (lzmabuf
!is null) {
1284 lzmabuf
[0..xszlen
] = xsz
[0..xszlen
];
1285 lzmabuf
[1..5] = "LZMA";
1286 usize destLen
= (cbuf
is null ? bsz
: dsize
); // do not take more than zlib
1287 if (destLen
> bsz
) destLen
= bsz
; // just in case
1288 ubyte[LZMA_PROPS_SIZE
+8] hdr
= void;
1289 uint hdrSize
= cast(uint)hdr
.sizeof
;
1291 CLzmaEncProps props
;
1292 props
.level
= packlevel
;
1293 props
.dictSize
= 1<<22; //4MB
1294 props
.reduceSize
= bsz
;
1296 immutable SRes nres
= LzmaEncode(cast(ubyte*)(lzmabuf
+xszlen
+LZMA_PROPS_SIZE
+1), &destLen
, cast(const(ubyte)*)vs
, bsz
, &props
, hdr
.ptr
, &hdrSize
, 0/*writeEndMark*/, null, &lzmaDefAllocator
, &lzmaDefAllocator
);
1297 assert(hdrSize
== LZMA_PROPS_SIZE
);
1298 if (nres
== SZ_OK
&& destLen
+xszlen
+LZMA_PROPS_SIZE
+1 < cast(usize
)sz
) {
1299 if (cbuf
is null || destLen
+xszlen
+LZMA_PROPS_SIZE
+1 < dsize
+xszlen
) {
1300 if (cbuf
!is null) free(cbuf
); // free zlib result
1301 import core
.stdc
.string
: memcpy
;
1302 lzmabuf
[xszlen
] = LZMA_PROPS_SIZE
;
1303 memcpy(lzmabuf
+xszlen
+1, hdr
.ptr
, LZMA_PROPS_SIZE
);
1304 sqlite3_result_blob(ctx
, lzmabuf
, destLen
+xszlen
+LZMA_PROPS_SIZE
+1, &free
);
1311 // return zlib result?
1312 if (cbuf
!is null) {
1313 assert(dsize
< bsz
);
1314 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1318 if (cbuf
!is null) free(cbuf
);
1323 if (isBadPrefix(vs
[0..cast(uint)sz
])) {
1324 char *res
= cast(char *)malloc(sz
+5);
1325 if (res
is null) { sqlite3_result_error_nomem(ctx
); return; }
1326 res
[0..5] = "\x1bRAWB";
1327 res
[5..sz
+5] = vs
[0..sz
];
1328 if (isGoodText(vs
[0..cast(usize
)sz
])) {
1329 sqlite3_result_text(ctx
, res
, sz
+5, &free
);
1331 sqlite3_result_blob(ctx
, res
, sz
+5, &free
);
1334 immutable bool wantBlob
= !isGoodText(vs
[0..cast(usize
)sz
]);
1335 immutable int tp
= sqlite3_value_type(val
);
1336 if ((wantBlob
&& tp
== SQLITE_BLOB
) ||
(!wantBlob
&& tp
== SQLITE3_TEXT
)) {
1337 sqlite3_result_value(ctx
, val
);
1338 } else if (wantBlob
) {
1339 sqlite3_result_blob(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1341 sqlite3_result_text(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1348 ** ChiroPack(content)
1350 private void sq3Fn_ChiroPack (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) nothrow @trusted {
1351 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroPack()`", -1); return; }
1352 return sq3Fn_ChiroPackCommon(ctx
, argv
[0], ChiroCompressionLevel
);
1357 ** ChiroPack(content, packlevel)
1359 ** `packlevel` == 0 means "don't pack"
1360 ** `packlevel` == 9 means "maximum compression"
1362 private void sq3Fn_ChiroPackDPArg (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) nothrow @trusted {
1363 if (argc
!= 2) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroPack()`", -1); return; }
1364 return sq3Fn_ChiroPackCommon(ctx
, argv
[0], sqlite3_value_int(argv
[1]));
1369 ** ChiroGetPackType(content)
1371 private void sq3Fn_ChiroGetPackType (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1372 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroGetPackType()`", -1); return; }
1374 int sz
= sqlite3_value_bytes(argv
[0]);
1375 if (sz
< 5 || sz
> 0x3fffffff-4) { sqlite3_result_text(ctx
, "RAWB", 4, SQLITE_STATIC
); return; }
1377 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1378 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1380 if (!isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_text(ctx
, "RAWB", 4, SQLITE_STATIC
); return; }
1382 sqlite3_result_text(ctx
, vs
+1, 4, SQLITE_TRANSIENT
);
1387 ** ChiroUnpack(content)
1389 ** it is (almost) safe to pass non-packed content here
1391 private void sq3Fn_ChiroUnpack (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1392 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!000\n"); }
1393 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroUnpack()`", -1); return; }
1395 int sz
= sqlite3_value_bytes(argv
[0]);
1396 if (sz
< 0 || sz
> 0x3fffffff-4) { sqlite3_result_error_toobig(ctx
); return; }
1398 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1400 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1401 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1403 if (!isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_value(ctx
, argv
[0]); return; }
1404 if (vs
[0..5] == "\x1bRAWB") { sqlite3_result_blob(ctx
, vs
+5, sz
-5, SQLITE_TRANSIENT
); return; }
1405 if (sz
< 6) { sqlite3_result_error(ctx
, "invalid data in `ChiroUnpack()`", -1); return; }
1422 int codec
= Codec_ZLIB
;
1423 if (vs
[0..5] != "\x1bZLIB") {
1424 if (vs
[0..5] == "\x1bLZMA") codec
= Codec_LZMA
;
1426 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bBALZ") codec
= Codec_BALZ
;
1428 version(use_libxpack
) {
1429 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bXPAK") codec
= Codec_XPAK
;
1431 version(use_libxpack
) {
1432 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bXPAK") codec
= Codec_XPAK
;
1434 version(use_libbrieflz
) {
1435 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bBRLZ") codec
= Codec_BRLZ
;
1437 version(use_liblzfse
) {
1438 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZFS") codec
= Codec_LZFS
;
1441 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZJB") codec
= Codec_LZJB
;
1443 version(use_libwim_lzms
) {
1444 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZMS") codec
= Codec_LZMS
;
1446 version(use_libwim_lzx
) {
1447 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZMX") codec
= Codec_LZMX
;
1449 version(use_libwim_xpress
) {
1450 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bXPRS") codec
= Codec_XPRS
;
1453 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZ4D") codec
= Codec_LZ4D
;
1456 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bZSTD") codec
= Codec_ZSTD
;
1458 if (codec
== Codec_ZLIB
) { sqlite3_result_error(ctx
, "invalid codec in `ChiroUnpack()`", -1); return; }
1462 // size is guaranteed to be at least 6 here
1466 immutable uint numsz
= decodeUIntLength(vs
[0..cast(uint)sz
]);
1467 //{ import core.stdc.stdio : printf; printf("sz=%d; numsz=%u; %02X %02X %02X %02X\n", sz, numsz, cast(uint)vs[5], cast(uint)vs[6], cast(uint)vs[7], cast(uint)vs[8]); }
1468 //writeln("sq3Fn_ChiroUnpack: nsz=", sz-5);
1469 if (numsz
== 0 || numsz
> cast(uint)sz
) { sqlite3_result_error(ctx
, "invalid data in `ChiroUnpack()`", -1); return; }
1470 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!100\n"); }
1471 immutable uint rsize
= decodeUInt(vs
[0..cast(uint)sz
]);
1472 if (rsize
== uint.max
) { sqlite3_result_error(ctx
, "invalid data in `ChiroUnpack()`", -1); return; }
1473 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!101:rsize=%u\n", rsize); }
1474 if (rsize
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1477 sz
-= cast(int)numsz
;
1478 //{ import core.stdc.stdio : printf; printf("sz=%d; rsize=%u\n", sz, rsize, dpos); }
1480 import core
.stdc
.stdlib
: malloc
, free
;
1481 import core
.stdc
.string
: memcpy
;
1483 char* cbuf
= cast(char*)malloc(rsize
);
1484 if (cbuf
is null) { sqlite3_result_error_nomem(ctx
); return; }
1485 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz-dpos);
1487 usize dsize
= rsize
;
1488 final switch (codec
) {
1490 version(use_libdeflate
) {
1491 libdeflate_decompressor
*dcp
= libdeflate_alloc_decompressor();
1492 if (dcp
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1493 auto rc
= libdeflate_zlib_decompress(dcp
, vs
, cast(usize
)sz
, cbuf
, rsize
, null);
1494 if (rc
!= LIBDEFLATE_SUCCESS
) {
1496 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1500 import etc
.c
.zlib
: uncompress
, Z_OK
;
1501 int zres
= uncompress(cast(ubyte *)cbuf
, &dsize
, cast(const(ubyte) *)vs
, sz
);
1502 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz, "; dsize=", dsize, "; zres=", zres);
1503 if (zres
!= Z_OK || dsize
!= rsize
) {
1505 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1512 if (sz
< LZMA_PROPS_SIZE
+1 || vs
[0] != LZMA_PROPS_SIZE
) {
1514 sqlite3_result_error(ctx
, "broken LZMA data in `ChiroUnpack()`", -1);
1518 usize srcSize
= sz
-vs
[0]-1;
1520 SRes zres
= LzmaDecode(cast(ubyte *)cbuf
, &dsize
, cast(const(ubyte) *)vs
+vs
[0]+1, &srcSize
,
1521 cast(const(ubyte)*)(vs
+1)/*propData*/, vs
[0]/*propSize*/, LZMA_FINISH_ANY
, &status
, &lzmaDefAllocator
);
1522 if (zres
!= SZ_OK || dsize
!= rsize || status
== LZMA_STATUS_FINISHED_WITH_MARK || status
== LZMA_STATUS_NEEDS_MORE_INPUT
) {
1524 sqlite3_result_error(ctx
, "broken LZMA data in `ChiroUnpack()`", -1);
1535 auto dc
= bz
.decompress(
1538 uint left
= cast(uint)sz
-spos
;
1539 if (left
> buf
.length
) left
= cast(uint)buf
.length
;
1540 if (left
!= 0) memcpy(buf
.ptr
, vs
, left
);
1546 uint left
= rsize
-outpos
;
1547 if (left
== 0) throw new Exception("broken data");
1548 if (left
> buf
.length
) left
= cast(uint)buf
.length
;
1549 if (left
) memcpy(cbuf
+outpos
, buf
.ptr
, left
);
1553 if (dc
!= rsize
) throw new Exception("broken data");
1554 } catch (Exception
) {
1557 if (outpos
== uint.max
) {
1559 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1565 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1570 version(use_libxpack
) {
1571 xpack_decompressor
*dcp
= xpack_alloc_decompressor();
1572 if (dcp
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1573 auto rc
= xpack_decompress(dcp
, vs
, cast(usize
)sz
, cbuf
, rsize
, null);
1574 if (rc
!= DECOMPRESS_SUCCESS
) {
1576 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1581 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1586 version(use_libbrieflz
) {
1587 dsize
= blz_depack_safe(vs
, cast(uint)sz
, cbuf
, rsize
);
1588 if (dsize
!= rsize
) {
1590 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1595 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1600 version(use_liblzfse
) {
1601 immutable usize wbsize
= lzfse_decode_scratch_size();
1602 void* wbuf
= cast(void*)malloc(wbsize
+!wbsize
);
1603 if (wbuf
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1604 dsize
= lzfse_decode_buffer(cbuf
, cast(usize
)rsize
, vs
, cast(usize
)sz
, wbuf
);
1606 if (dsize
== 0 || dsize
!= rsize
) {
1608 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1613 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1619 dsize
= lzjb_decompress(vs
, cast(usize
)sz
, cbuf
, rsize
);
1620 if (dsize
!= rsize
) {
1622 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1627 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1632 version(use_libwim_lzms
) {
1633 wimlib_decompressor
* dpr
;
1634 int rc
= wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZMS
, rsize
, &dpr
);
1635 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1636 rc
= wimlib_decompress(vs
, cast(usize
)sz
, cbuf
, rsize
, dpr
);
1637 wimlib_free_decompressor(dpr
);
1640 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1645 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1650 version(use_libwim_lzx
) {
1651 wimlib_decompressor
* dpr
;
1652 int rc
= wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZX
, rsize
, &dpr
);
1653 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1654 rc
= wimlib_decompress(vs
, cast(usize
)sz
, cbuf
, rsize
, dpr
);
1655 wimlib_free_decompressor(dpr
);
1658 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1663 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1668 version(use_libwim_xpress
) {
1669 wimlib_decompressor
* dpr
;
1670 uint csz
= WIMLIB_XPRESS_MIN_CHUNK
;
1671 while (csz
< WIMLIB_XPRESS_MAX_CHUNK
&& csz
< rsize
) csz
*= 2U;
1672 int rc
= wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_XPRESS
, csz
, &dpr
);
1673 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1674 rc
= wimlib_decompress(vs
, cast(usize
)sz
, cbuf
, rsize
, dpr
);
1675 wimlib_free_decompressor(dpr
);
1678 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1683 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1689 dsize
= LZ4_decompress_safe(vs
, cbuf
, sz
, rsize
);
1690 if (dsize
!= rsize
) {
1692 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1697 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1703 dsize
= ZSTD_decompress(cbuf
, rsize
, vs
, sz
);
1704 if (ZSTD_isError(dsize
) || dsize
!= rsize
) {
1706 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1711 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1717 if (isGoodText(cbuf
[0..dsize
])) {
1718 sqlite3_result_text(ctx
, cbuf
, cast(int)dsize
, &free
);
1720 sqlite3_result_blob(ctx
, cbuf
, cast(int)dsize
, &free
);
1726 ** ChiroNormCRLF(content)
1728 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1729 ** Removes trailing blanks.
1731 private void sq3Fn_ChiroNormCRLF (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1732 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroNormCRLF()`", -1); return; }
1734 int sz
= sqlite3_value_bytes(argv
[0]);
1735 if (sz
< 0 || sz
> 0x3fffffff) { sqlite3_result_error_toobig(ctx
); return; }
1737 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1739 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1740 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroNormCRLF()`", -1); return; }
1742 // check if we have something to do, and calculate new string size
1743 bool needwork
= false;
1744 if (vs
[cast(uint)sz
-1] <= 32) {
1746 while (sz
> 0 && vs
[cast(uint)sz
-1] <= 32) --sz
;
1747 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1749 uint newsz
= cast(uint)sz
;
1750 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1753 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) --newsz
;
1754 } else if (!needwork
) {
1755 needwork
= ((ch
< 32 && ch
!= 9 && ch
!= 10) || ch
== 127);
1760 if (sqlite3_value_type(argv
[0]) == SQLITE3_TEXT
) sqlite3_result_value(ctx
, argv
[0]);
1761 else sqlite3_result_text(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1765 assert(newsz
&& newsz
<= cast(uint)sz
);
1767 // need a new string
1768 import core
.stdc
.stdlib
: malloc
, free
;
1769 char* newstr
= cast(char*)malloc(newsz
);
1770 if (newstr
is null) { sqlite3_result_error_nomem(ctx
); return; }
1771 char* dest
= newstr
;
1772 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1774 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1776 if (ch
== 127) *dest
++ = '~';
1777 else if (ch
== 11 || ch
== 12) *dest
++ = '\n';
1778 else if (ch
< 32 && ch
!= 9 && ch
!= 10) *dest
++ = ' ';
1782 assert(dest
== newstr
+newsz
);
1784 sqlite3_result_text(ctx
, newstr
, cast(int)newsz
, &free
);
1789 ** ChiroNormHeaders(content)
1791 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1792 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1793 ** Removes trailing blanks.
1795 private void sq3Fn_ChiroNormHeaders (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1796 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroNormHeaders()`", -1); return; }
1798 int sz
= sqlite3_value_bytes(argv
[0]);
1799 if (sz
< 0 || sz
> 0x3fffffff) { sqlite3_result_error_toobig(ctx
); return; }
1801 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1803 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1804 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroNormHeaders()`", -1); return; }
1806 // check if we have something to do, and calculate new string size
1807 bool needwork
= false;
1808 if (vs
[cast(uint)sz
-1] <= 32) {
1810 while (sz
> 0 && vs
[cast(uint)sz
-1] <= 32) --sz
;
1811 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1813 uint newsz
= cast(uint)sz
;
1814 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1817 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) --newsz
;
1818 } else if (ch
== 10) {
1819 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] <= 32) { needwork
= true; --newsz
; }
1820 } else if (!needwork
) {
1821 needwork
= ((ch
< 32 && ch
!= 10) || ch
== 127);
1826 if (sqlite3_value_type(argv
[0]) == SQLITE3_TEXT
) sqlite3_result_value(ctx
, argv
[0]);
1827 else sqlite3_result_text(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1831 assert(newsz
&& newsz
<= cast(uint)sz
);
1833 // need a new string
1834 import core
.stdc
.stdlib
: malloc
, free
;
1835 char* newstr
= cast(char*)malloc(newsz
);
1836 if (newstr
is null) { sqlite3_result_error_nomem(ctx
); return; }
1837 char* dest
= newstr
;
1838 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1840 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1841 } else if (ch
== 10) {
1842 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] <= 32) {} else *dest
++ = '\n';
1844 if (ch
== 127) *dest
++ = '~';
1845 else if (ch
< 32 && ch
!= 10) *dest
++ = ' ';
1849 assert(dest
== newstr
+newsz
);
1851 sqlite3_result_text(ctx
, newstr
, cast(int)newsz
, &free
);
1856 ** ChiroExtractHeaders(content)
1858 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1859 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1860 ** Removes trailing blanks.
1862 private void sq3Fn_ChiroExtractHeaders (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1863 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1865 int sz
= sqlite3_value_bytes(argv
[0]);
1866 if (sz
< 0 || sz
> 0x3fffffff) { sqlite3_result_error_toobig(ctx
); return; }
1868 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1870 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1871 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1874 sz
= sq3Supp_FindHeadersEnd(vs
, sz
);
1876 // strip trailing blanks
1877 while (sz
> 0 && vs
[cast(uint)sz
-1U] <= 32) --sz
;
1878 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1880 // allocate new string (it can be smaller, but will never be bigger)
1881 import core
.stdc
.stdlib
: malloc
, free
;
1882 char* newstr
= cast(char*)malloc(cast(uint)sz
);
1883 if (newstr
is null) { sqlite3_result_error_nomem(ctx
); return; }
1884 char* dest
= newstr
;
1885 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1887 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1888 } else if (ch
== 10) {
1889 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] <= 32) {} else *dest
++ = '\n';
1891 if (ch
== 127) *dest
++ = '~';
1892 else if (ch
< 32 && ch
!= 10) *dest
++ = ' ';
1896 assert(dest
<= newstr
+cast(uint)sz
);
1897 sz
= cast(int)cast(usize
)(dest
-newstr
);
1898 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1899 sqlite3_result_text(ctx
, newstr
, sz
, &free
);
1904 ** ChiroExtractBody(content)
1906 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1907 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1908 ** Removes trailing blanks and final dot.
1910 private void sq3Fn_ChiroExtractBody (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1911 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1913 int sz
= sqlite3_value_bytes(argv
[0]);
1914 if (sz
< 0 || sz
> 0x3fffffff) { sqlite3_result_error_toobig(ctx
); return; }
1916 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1918 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1919 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1922 immutable int bstart
= sq3Supp_FindHeadersEnd(vs
, sz
);
1923 if (bstart
>= sz
) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1927 // strip trailing dot
1928 if (sz
>= 2 && vs
[cast(uint)sz
-2U] == '\r' && vs
[cast(uint)sz
-1U] == '\n') sz
-= 2;
1929 else if (sz
>= 1 && vs
[cast(uint)sz
-1U] == '\n') --sz
;
1930 if (sz
== 1 && vs
[0] == '.') sz
= 0;
1931 else if (sz
>= 2 && vs
[cast(uint)sz
-2U] == '\n' && vs
[cast(uint)sz
-1U] == '.') --sz
;
1932 else if (sz
>= 2 && vs
[cast(uint)sz
-2U] == '\r' && vs
[cast(uint)sz
-1U] == '.') --sz
;
1934 // strip trailing blanks
1935 while (sz
> 0 && vs
[cast(uint)sz
-1U] <= 32) --sz
;
1936 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1938 // allocate new string (it can be smaller, but will never be bigger)
1939 import core
.stdc
.stdlib
: malloc
, free
;
1940 char* newstr
= cast(char*)malloc(cast(uint)sz
);
1941 if (newstr
is null) { sqlite3_result_error_nomem(ctx
); return; }
1942 char* dest
= newstr
;
1943 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1945 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1947 if (ch
== 127) *dest
++ = '~';
1948 else if (ch
== 11 || ch
== 12) *dest
++ = '\n';
1949 else if (ch
< 32 && ch
!= 9 && ch
!= 10) *dest
++ = ' ';
1953 assert(dest
<= newstr
+cast(uint)sz
);
1954 sz
= cast(int)cast(usize
)(dest
-newstr
);
1955 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1956 sqlite3_result_text(ctx
, newstr
, sz
, &free
);
1961 ** ChiroRIPEMD160(content)
1963 ** Calculates RIPEMD160 hash over the given content.
1965 ** Returns BINARY BLOB! You can use `tolower(hex(ChiroRIPEMD160(contents)))`
1966 ** to get lowercased hex hash string.
1968 private void sq3Fn_ChiroRIPEMD160 (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1969 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroRIPEMD160()`", -1); return; }
1971 immutable int sz
= sqlite3_value_bytes(argv
[0]);
1972 if (sz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
1974 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1975 if (!vs
&& sz
== 0) vs
= "";
1976 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroRIPEMD160()`", -1); return; }
1979 ripemd160_put(ref rmd
, vs
[0..cast(uint)sz
]);
1980 ubyte[RIPEMD160_BYTES
] hash
= ripemd160_finish(ref rmd
);
1981 sqlite3_result_blob(ctx
, cast(const(char)*)hash
.ptr
, cast(int)hash
.length
, SQLITE_TRANSIENT
);
1985 enum HeaderProcStartTpl(string fnname
) = `
1986 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"`~fnname
~`()\"", -1); return; }
1988 immutable int sz = sqlite3_value_bytes(argv[0]);
1989 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1991 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1992 if (!vs && sz == 0) vs = "";
1993 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"`~fnname
~`()\"", -1); return; }
1995 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
2000 ** ChiroHdr_NNTPIndex(headers)
2002 ** The content must be email with headers (or headers only).
2003 ** Returns "NNTP-Index" field or zero (int).
2005 private void sq3Fn_ChiroHdr_NNTPIndex (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2006 mixin(HeaderProcStartTpl
!"ChiroHdr_NNTPIndex");
2010 auto nntpidxfld
= findHeaderField(hdrs
, "NNTP-Index");
2011 if (nntpidxfld
.length
) {
2012 auto id
= nntpidxfld
.getFieldValue
;
2014 foreach (immutable ch
; id
) {
2015 if (ch
< '0' || ch
> '9') { nntpidx
= 0; break; }
2016 if (nntpidx
== 0 && ch
== '0') continue;
2017 immutable uint nn
= nntpidx
*10u+(ch
-'0');
2018 if (nn
<= nntpidx
) nntpidx
= 0x7fffffff; else nntpidx
= nn
;
2023 // it is safe, it can't overflow
2024 sqlite3_result_int(ctx
, cast(int)nntpidx
);
2029 ** ChiroHdr_RecvTime(headers)
2031 ** The content must be email with headers (or headers only).
2032 ** Returns unixtime (can be zero).
2034 private void sq3Fn_ChiroHdr_RecvTime (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2035 mixin(HeaderProcStartTpl
!"ChiroHdr_RecvTime");
2037 uint msgtime
= 0; // message receiving time
2039 auto datefld
= findHeaderField(hdrs
, "Injection-Date");
2040 if (datefld
.length
!= 0) {
2041 auto v
= datefld
.getFieldValue
;
2043 msgtime
= parseMailDate(v
);
2044 } catch (Exception
) {
2045 //writeln("UID=", uid, ": FUCKED INJECTION-DATE: |", v, "|");
2046 msgtime
= 0; // just in case
2051 // obsolete NNTP date field, because why not?
2052 datefld
= findHeaderField(hdrs
, "NNTP-Posting-Date");
2053 if (datefld
.length
!= 0) {
2054 auto v
= datefld
.getFieldValue
;
2056 msgtime
= parseMailDate(v
);
2057 } catch (Exception
) {
2058 //writeln("UID=", uid, ": FUCKED NNTP-POSTING-DATE: |", v, "|");
2059 msgtime
= 0; // just in case
2065 datefld
= findHeaderField(hdrs
, "Date");
2066 if (datefld
.length
!= 0) {
2067 auto v
= datefld
.getFieldValue
;
2069 msgtime
= parseMailDate(v
);
2070 } catch (Exception
) {
2071 //writeln("UID=", uid, ": FUCKED DATE: |", v, "|");
2072 msgtime
= 0; // just in case
2077 // finally, try to get time from "Received:"
2078 //Received: from dns9.fly.us ([131.103.96.154]) by np5-d2.fly.us with Microsoft SMTPSVC(5.0.2195.6824); Tue, 21 Mar 2017 17:35:54 -0400
2080 //writeln("!!! --- !!!");
2081 uint lowesttime
= uint.max
;
2082 foreach (uint fidx
; 0..uint.max
) {
2083 auto recvfld
= findHeaderField(hdrs
, "Received", fidx
);
2084 if (recvfld
.length
== 0) break;
2085 auto lsemi
= recvfld
.lastIndexOf(';');
2086 if (lsemi
>= 0) recvfld
= recvfld
[lsemi
+1..$].xstrip
;
2087 if (recvfld
.length
!= 0) {
2088 auto v
= recvfld
.getFieldValue
;
2091 tm
= parseMailDate(v
);
2092 } catch (Exception
) {
2093 //writeln("UID=", uid, ": FUCKED RECV DATE: |", v, "|");
2094 tm
= 0; // just in case
2096 //writeln(tm, " : ", lowesttime);
2097 if (tm
&& tm
< lowesttime
) lowesttime
= tm
;
2100 if (lowesttime
!= uint.max
) msgtime
= lowesttime
;
2103 sqlite3_result_int64(ctx
, msgtime
);
2108 ** ChiroHdr_FromEmail(headers)
2110 ** The content must be email with headers (or headers only).
2111 ** Returns email "From" field.
2113 private void sq3Fn_ChiroHdr_FromEmail (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2114 mixin(HeaderProcStartTpl
!"ChiroHdr_FromEmail");
2115 auto from
= findHeaderField(hdrs
, "From").extractMail
;
2116 if (from
.length
== 0) {
2117 sqlite3_result_text(ctx
, "nobody@nowhere", -1, SQLITE_STATIC
);
2119 sqlite3_result_text(ctx
, from
.ptr
, cast(int)from
.length
, SQLITE_TRANSIENT
);
2125 ** ChiroHdr_ToEmail(headers)
2127 ** The content must be email with headers (or headers only).
2128 ** Returns email "From" field.
2130 private void sq3Fn_ChiroHdr_ToEmail (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2131 mixin(HeaderProcStartTpl
!"ChiroHdr_ToEmail");
2132 auto to
= findHeaderField(hdrs
, "To").extractMail
;
2133 if (to
.length
== 0) {
2134 sqlite3_result_text(ctx
, "nobody@nowhere", -1, SQLITE_STATIC
);
2136 sqlite3_result_text(ctx
, to
.ptr
, cast(int)to
.length
, SQLITE_TRANSIENT
);
2142 ** ChiroHdr_Subj(headers)
2144 ** The content must be email with headers (or headers only).
2145 ** Returns email "From" field.
2147 private void sq3Fn_ChiroHdr_Subj (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2148 mixin(HeaderProcStartTpl
!"sq3Fn_ChiroHdr_Subj");
2149 auto subj
= findHeaderField(hdrs
, "Subject").decodeSubj
.subjRemoveRe
;
2150 if (subj
.length
== 0) {
2151 sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
);
2153 sqlite3_result_text(ctx
, subj
.ptr
, cast(int)subj
.length
, SQLITE_TRANSIENT
);
2159 ** ChiroHdr_Field(headers, fieldname)
2161 ** The content must be email with headers (or headers only).
2162 ** Returns field value as text, or NULL if there is no such field.
2164 private void sq3Fn_ChiroHdr_Field (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2165 if (argc
!= 2) { sqlite3_result_error(ctx
, "invalid number of arguments to \"ChiroHdr_Field()\"", -1); return; }
2167 immutable int sz
= sqlite3_value_bytes(argv
[0]);
2168 if (sz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
2170 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
2171 if (!vs
&& sz
== 0) vs
= "";
2172 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
2174 immutable int fldsz
= sqlite3_value_bytes(argv
[1]);
2175 if (fldsz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
2177 const(char)* fldname
= cast(const(char) *)sqlite3_value_blob(argv
[1]);
2178 if (!fldname
&& fldsz
== 0) fldname
= "";
2179 if (!fldname
) { sqlite3_result_error(ctx
, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
2181 const(char)[] hdrs
= vs
[0..cast(usize
)sq3Supp_FindHeadersEnd(vs
, sz
)];
2182 auto value
= findHeaderField(hdrs
, fldname
[0..fldsz
]);
2183 if (value
is null) {
2184 sqlite3_result_null(ctx
);
2185 } else if (value
.length
== 0) {
2186 sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
);
2188 sqlite3_result_text(ctx
, value
.ptr
, cast(int)value
.length
, SQLITE_TRANSIENT
);
2194 ** ChiroTimerStart([msg])
2196 ** The content must be email with headers (or headers only).
2197 ** Returns email "From" field.
2199 private void sq3Fn_ChiroTimerStart (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2200 if (argc
> 1) { sqlite3_result_error(ctx
, "invalid number of arguments to \"ChiroTimerStart()\"", -1); return; }
2205 immutable int sz
= sqlite3_value_bytes(argv
[0]);
2207 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
2209 chiTimerMsg
= new char[cast(usize
)sz
];
2210 chiTimerMsg
[0..cast(usize
)sz
] = vs
[0..cast(usize
)sz
];
2211 writeln("started ", chiTimerMsg
, "...");
2216 sqlite3_result_int(ctx
, 1);
2222 ** ChiroTimerStop([msg])
2224 ** The content must be email with headers (or headers only).
2225 ** Returns email "From" field.
2227 private void sq3Fn_ChiroTimerStop (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2229 if (argc
> 1) { sqlite3_result_error(ctx
, "invalid number of arguments to \"ChiroTimerStop()\"", -1); return; }
2231 if (ChiroTimerEnabled
) {
2234 immutable int sz
= sqlite3_value_bytes(argv
[0]);
2236 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
2238 chiTimerMsg
= new char[cast(usize
)sz
];
2239 chiTimerMsg
[0..cast(usize
)sz
] = vs
[0..cast(usize
)sz
];
2245 auto tstr
= chiTimer
.toBuffer(buf
[]);
2246 if (chiTimerMsg
.length
) {
2247 writeln("done ", chiTimerMsg
, ": ", tstr
);
2249 writeln("time: ", tstr
);
2255 sqlite3_result_int(ctx
, 1);
2260 ** ChiroGlob(pat, str)
2262 ** GLOB replacement, with extended word matching.
2264 private void sq3Fn_ChiroGlob_common (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
, int casesens
,
2265 uint stridx
=1, uint patidx
=0)
2267 if (argc
!= 2) { sqlite3_result_error(ctx
, "invalid number of arguments to \"ChiroGlob()\"", -1); return; }
2269 immutable int patsz
= sqlite3_value_bytes(argv
[patidx
]);
2270 if (patsz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
2272 const(char)* pat
= cast(const(char) *)sqlite3_value_blob(argv
[patidx
]);
2273 if (!pat
&& patsz
== 0) pat
= "";
2274 if (!pat
) { sqlite3_result_error(ctx
, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2276 immutable int strsz
= sqlite3_value_bytes(argv
[stridx
]);
2277 if (strsz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
2279 const(char)* str = cast(const(char) *)sqlite3_value_blob(argv
[stridx
]);
2280 if (!str && strsz
== 0) str = "";
2281 if (!str) { sqlite3_result_error(ctx
, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2283 immutable bool res
=
2285 globmatch(str[0..cast(usize
)strsz
], pat
[0..cast(usize
)patsz
]) :
2286 globmatchCI(str[0..cast(usize
)strsz
], pat
[0..cast(usize
)patsz
]);
2288 sqlite3_result_int(ctx
, (res ?
1 : 0));
2293 ** ChiroGlobSQL(pat, str)
2295 ** GLOB replacement, with extended word matching.
2297 private void sq3Fn_ChiroGlobSQL (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2298 sq3Fn_ChiroGlob_common(ctx
, argc
, argv
, casesens
:1);
2302 ** ChiroGlob(str, pat)
2304 ** GLOB replacement, with extended word matching.
2306 private void sq3Fn_ChiroGlob (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2307 sq3Fn_ChiroGlob_common(ctx
, argc
, argv
, casesens
:1, stridx
:0, patidx
:1);
2311 ** ChiroGlobCI(str, pat)
2313 ** GLOB replacement, with extended word matching.
2315 private void sq3Fn_ChiroGlobCI (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2316 sq3Fn_ChiroGlob_common(ctx
, argc
, argv
, casesens
:0, stridx
:0, patidx
:1);
2320 // ////////////////////////////////////////////////////////////////////////// //
2324 // ////////////////////////////////////////////////////////////////////////// //
2325 public void chiroRegisterSQLite3Functions (ref Database
db) {
2326 sqlite3_busy_timeout(db.getHandle
, 20000); // busy timeout: 20 seconds
2328 immutable int rc
= sqlite3_extended_result_codes(db.getHandle
, 1);
2329 if (rc
!= SQLITE_OK
) {
2330 import core
.stdc
.stdio
: stderr
, fprintf
;
2331 fprintf(stderr
, "SQLITE WARNING: cannot enable extended result codes (this is harmless).\n");
2333 db.createFunction("glob", 2, &sq3Fn_ChiroGlobSQL
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2334 db.createFunction("ChiroGlob", 2, &sq3Fn_ChiroGlob
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2335 db.createFunction("ChiroGlobCI", 2, &sq3Fn_ChiroGlobCI
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2337 db.createFunction("ChiroPack", 1, &sq3Fn_ChiroPack
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2338 db.createFunction("ChiroPack", 2, &sq3Fn_ChiroPackDPArg
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2339 db.createFunction("ChiroUnpack", 1, &sq3Fn_ChiroUnpack
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2341 db.createFunction("ChiroPackLZMA", 1, &sq3Fn_ChiroPackLZMA
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2342 db.createFunction("ChiroPackLZMA", 2, &sq3Fn_ChiroPackLZMA
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2344 db.createFunction("ChiroGetPackType", 1, &sq3Fn_ChiroGetPackType
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2346 db.createFunction("ChiroNormCRLF", 1, &sq3Fn_ChiroNormCRLF
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2347 db.createFunction("ChiroNormHeaders", 1, &sq3Fn_ChiroNormHeaders
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2348 db.createFunction("ChiroExtractHeaders", 1, &sq3Fn_ChiroExtractHeaders
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2349 db.createFunction("ChiroExtractBody", 1, &sq3Fn_ChiroExtractBody
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2350 db.createFunction("ChiroRIPEMD160", 1, &sq3Fn_ChiroRIPEMD160
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2352 db.createFunction("ChiroHdr_NNTPIndex", 1, &sq3Fn_ChiroHdr_NNTPIndex
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2353 db.createFunction("ChiroHdr_RecvTime", 1, &sq3Fn_ChiroHdr_RecvTime
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2354 db.createFunction("ChiroHdr_FromEmail", 1, &sq3Fn_ChiroHdr_FromEmail
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2355 db.createFunction("ChiroHdr_ToEmail", 1, &sq3Fn_ChiroHdr_ToEmail
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2356 db.createFunction("ChiroHdr_Subj", 1, &sq3Fn_ChiroHdr_Subj
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2357 db.createFunction("ChiroHdr_Field", 2, &sq3Fn_ChiroHdr_Field
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2359 db.createFunction("ChiroTimerStart", 0, &sq3Fn_ChiroTimerStart
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2360 db.createFunction("ChiroTimerStart", 1, &sq3Fn_ChiroTimerStart
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2361 db.createFunction("ChiroTimerStop", 0, &sq3Fn_ChiroTimerStop
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2362 db.createFunction("ChiroTimerStop", 1, &sq3Fn_ChiroTimerStop
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2366 // ////////////////////////////////////////////////////////////////////////// //
2367 public void chiroRecreateStorageDB (const(char)[] dbname
=ExpandedMailDBPath
~StorageDBName
) {
2368 try { import std
.file
: remove
; remove(dbname
); } catch (Exception
) {}
2369 dbStore
= Database(dbname
, Database
.Mode
.ReadWriteCreate
, dbpragmasRWStorageRecreate
, schemaStorage
);
2370 chiroRegisterSQLite3Functions(dbStore
);
2371 dbStore
.setOnClose(schemaStorageIndex
~dbpragmasRWStorage
~"ANALYZE;");
2375 // ////////////////////////////////////////////////////////////////////////// //
2376 public void chiroRecreateViewDB (const(char)[] dbname
=ExpandedMailDBPath
~SupportDBName
) {
2377 try { import std
.file
: remove
; remove(dbname
); } catch (Exception
) {}
2378 dbView
= Database(dbname
, Database
.Mode
.ReadWriteCreate
, dbpragmasRWSupportRecreate
, schemaSupportTable
);
2379 chiroRegisterSQLite3Functions(dbView
);
2380 dbView
.setOnClose(schemaSupportIndex
~dbpragmasRWSupport
~"ANALYZE;");
2384 public void chiroCreateViewIndiciesDB () {
2385 dbView
.setOnClose(dbpragmasRWSupport
~"ANALYZE;");
2386 dbView
.execute(schemaSupportIndex
);
2390 // ////////////////////////////////////////////////////////////////////////// //
2391 public void chiroRecreateConfDB (const(char)[] dbname
=ExpandedMailDBPath
~OptionsDBName
) {
2392 try { import std
.file
: remove
; remove(dbname
); } catch (Exception
) {}
2393 dbConf
= Database(dbname
, Database
.Mode
.ReadWriteCreate
, dbpragmasRWOptionsRecreate
, schemaOptions
);
2394 chiroRegisterSQLite3Functions(dbConf
);
2395 dbConf
.setOnClose(schemaOptionsIndex
~dbpragmasRWOptions
~"ANALYZE;");
2399 // ////////////////////////////////////////////////////////////////////////// //
2400 public void chiroOpenStorageDB (const(char)[] dbname
=ExpandedMailDBPath
~StorageDBName
, bool readonly
=false) {
2401 dbStore
= Database(dbname
, (readonly ? Database
.Mode
.ReadOnly
: Database
.Mode
.ReadWrite
), (readonly ? dbpragmasRO
: dbpragmasRWStorage
), schemaStorage
);
2402 chiroRegisterSQLite3Functions(dbStore
);
2403 if (!readonly
) dbStore
.setOnClose("PRAGMA optimize;");
2407 // ////////////////////////////////////////////////////////////////////////// //
2408 public void chiroOpenViewDB (const(char)[] dbname
=ExpandedMailDBPath
~SupportDBName
, bool readonly
=false) {
2409 dbView
= Database(dbname
, (readonly ? Database
.Mode
.ReadOnly
: Database
.Mode
.ReadWrite
), (readonly ? dbpragmasRO
: dbpragmasRWSupport
), schemaSupport
);
2410 chiroRegisterSQLite3Functions(dbView
);
2412 dbView
.execute(schemaSupportTempTables
);
2413 dbView
.setOnClose("PRAGMA optimize;");
2418 // ////////////////////////////////////////////////////////////////////////// //
2419 public void chiroOpenConfDB (const(char)[] dbname
=ExpandedMailDBPath
~OptionsDBName
, bool readonly
=false) {
2420 dbConf
= Database(dbname
, (readonly ? Database
.Mode
.ReadOnly
: Database
.Mode
.ReadWrite
), (readonly ? dbpragmasRO
: dbpragmasRWOptions
), schemaOptions
);
2421 chiroRegisterSQLite3Functions(dbConf
);
2422 if (!readonly
) dbConf
.setOnClose("PRAGMA optimize;");
2426 // ////////////////////////////////////////////////////////////////////////// //
2428 recreates FTS5 (full-text search) info.
2430 public void chiroRecreateFTS5 (bool repopulate
=true) {
2431 dbView
.execute(recreateFTS5
);
2432 if (repopulate
) dbView
.execute(repopulateFTS5
);
2433 dbView
.execute(recreateFTS5Triggers
);
2437 // ////////////////////////////////////////////////////////////////////////// //
2439 static void errorLogCallback (void *pArg
, int rc
, const char *zMsg
) {
2440 if (ChiroSQLiteSilent
) return;
2441 import core
.stdc
.stdio
: stderr
, fprintf
;
2443 case SQLITE_NOTICE
: fprintf(stderr
, "***SQLITE NOTICE: %s\n", zMsg
); break;
2444 case SQLITE_NOTICE_RECOVER_WAL
: fprintf(stderr
, "***SQLITE NOTICE (WAL RECOVER): %s\n", zMsg
); break;
2445 case SQLITE_NOTICE_RECOVER_ROLLBACK
: fprintf(stderr
, "***SQLITE NOTICE (ROLLBACK RECOVER): %s\n", zMsg
); break;
2447 case SQLITE_WARNING
: fprintf(stderr
, "***SQLITE WARNING: %s\n", zMsg
); break;
2448 case SQLITE_WARNING_AUTOINDEX
: fprintf(stderr
, "***SQLITE AUTOINDEX WARNING: %s\n", zMsg
); break;
2450 case SQLITE_CANTOPEN
:
2452 break; // ignore those
2454 default: fprintf(stderr
, "***SQLITE LOG(%d) [%s]: %s\n", rc
, sqlite3_errstr(rc
), zMsg
); break;
2460 static string
sqerrstr (immutable int rc
) nothrow @trusted {
2461 const(char)* msg
= sqlite3_errstr(rc
);
2462 if (!msg ||
!msg
[0]) return null;
2463 import core
.stdc
.string
: strlen
;
2464 return msg
[0..strlen(msg
)].idup
;
2468 static void sqconfigcheck (immutable int rc
, string msg
, bool fatal
) {
2469 if (rc
== SQLITE_OK
) return;
2471 string errmsg
= sqerrstr(rc
);
2472 throw new Exception("FATAL: "~msg
~": "~errmsg
);
2474 if (msg
is null) msg
= "";
2475 import core
.stdc
.stdio
: stderr
, fprintf
;
2476 fprintf(stderr
, "SQLITE WARNING: %.*s (this is harmless): %s\n", cast(uint)msg
.length
, msg
.ptr
, sqlite3_errstr(rc
));
2481 // call this BEFORE opening any SQLite database connection!
2482 public void chiroSwitchToSingleThread () {
2483 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SINGLETHREAD
), "cannot set single-threaded mode", fatal
:false);
2487 public string
MailDBPath () nothrow @trusted @nogc { return ExpandedMailDBPath
; }
2490 public void MailDBPath(T
:const(char)[]) (T mailpath
) nothrow @trusted {
2491 while (mailpath
.length
> 1 && mailpath
[$-1] == '/') mailpath
= mailpath
[0..$-1];
2493 if (mailpath
.length
== 0 || mailpath
== ".") {
2494 ExpandedMailDBPath
= "";
2498 if (mailpath
[0] == '~') {
2499 char[] dpath
= new char[mailpath
.length
+4096];
2500 dpath
= expandTilde(dpath
, mailpath
);
2502 while (dpath
.length
> 1 && dpath
[$-1] == '/') dpath
= dpath
[0..$-1];
2504 ExpandedMailDBPath
= cast(string
)dpath
; // it is safe to cast here
2506 char[] dpath
= new char[mailpath
.length
+1];
2507 dpath
[0..$-1] = mailpath
[];
2509 ExpandedMailDBPath
= cast(string
)dpath
; // it is safe to cast here
2514 shared static this () {
2516 SQLITE_CONFIG_STMTJRNL_SPILL
= 26, /* int nByte */
2517 SQLITE_CONFIG_SMALL_MALLOC
= 27, /* boolean */
2520 if (!sqlite3_threadsafe()) {
2521 throw new Exception("FATAL: SQLite must be compiled with threading support!");
2524 // we are interested in all errors
2525 sqlite3_config(SQLITE_CONFIG_LOG
, &errorLogCallback
, null);
2527 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SERIALIZED
), "cannot set SQLite serialized threading mode", fatal
:true);
2528 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SMALL_MALLOC
, 0), "cannot enable SQLite unrestriced malloc mode", fatal
:false);
2529 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_URI
, 1), "cannot enable SQLite URI handling", fatal
:false);
2530 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_COVERING_INDEX_SCAN
, 1), "cannot enable SQLite covering index scan", fatal
:false);
2531 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_STMTJRNL_SPILL
, 512*1024), "cannot set SQLite statement journal spill threshold", fatal
:false);
2533 MailDBPath
= "~/Mail";
2537 shared static ~this () {
2544 // ////////////////////////////////////////////////////////////////////////// //
2545 public void transacted(string dbname
) (void delegate () dg
) {
2546 if (dg
is null) return;
2547 static if (dbname
== "View" || dbname
== "view") alias db = dbView
;
2548 else static if (dbname
== "Store" || dbname
== "store") alias db = dbStore
;
2549 else static if (dbname
== "Conf" || dbname
== "conf") alias db = dbConf
;
2550 else static assert(0, "invalid db name: '"~dbname
~"'");
2555 // ////////////////////////////////////////////////////////////////////////// //
2556 public DynStr
chiroGetTagMonthLimitEx(T
) (T tagnameid
, out int val
, int defval
=6)
2557 if (is(T
:const(char)[]) ||
is(T
:uint))
2559 static if (is(T
:const(char)[])) {
2560 alias tagname
= tagnameid
;
2563 static auto stGetTagName
= LazyStatement
!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
2564 foreach (auto row
; stGetTagName
.st
.bind(":tagid", tagnameid
).range
) {
2565 tagnameStr
= row
.tagname
!SQ3Text
;
2567 const(char)[] tagname
= tagnameStr
.getData
;
2570 static auto stGetMLimit
= LazyStatement
!"Conf"(`
2571 WITH RECURSIVE pth(path) AS (
2572 VALUES('/mainpane/msgview/monthlimit'||:tagslash||:tagname)
2575 SUBSTR(path, 1, LENGTH(path)-LENGTH(REPLACE(path, RTRIM(path, REPLACE(path, '/', '')), ''))-1)
2577 WHERE path LIKE '/mainpane/msgview/monthlimit%'
2582 , opt.value AS value
2584 INNER JOIN options AS opt ON opt.name=pth.path
2585 WHERE pth.path LIKE '/mainpane/msgview/monthlimit%'
2590 .bindConstText(":tagslash", (tagname
.length
&& tagname
[0] != '/' ?
"/" : ""))
2591 .bindConstText(":tagname", tagname
);
2593 foreach (auto row
; stGetMLimit
.st
.range
) {
2594 //conwriteln("TAGNAME=<", tagname, ">; val=", row.value!int, "; sres=<", row.name!SQ3Text, ">");
2595 val
= row
.value
!int;
2596 DynStr sres
= row
.name
!SQ3Text
;
2605 public int chiroGetTagMonthLimit(T
) (T tagnameid
, int defval
=6)
2606 if (is(T
:const(char)[]) ||
is(T
:uint))
2608 static if (is(T
:const(char)[])) {
2609 alias tagname
= tagnameid
;
2612 static auto stGetTagName
= LazyStatement
!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
2613 foreach (auto row
; stGetTagName
.st
.bind(":tagid", tagnameid
).range
) {
2614 tagnameStr
= row
.tagname
!SQ3Text
;
2616 const(char)[] tagname
= tagnameStr
.getData
;
2619 static auto stGetMLimit
= LazyStatement
!"Conf"(`
2620 WITH RECURSIVE pth(path) AS (
2621 VALUES('/mainpane/msgview/monthlimit'||:tagslash||:tagname)
2624 SUBSTR(path, 1, LENGTH(path)-LENGTH(REPLACE(path, RTRIM(path, REPLACE(path, '/', '')), ''))-1)
2626 WHERE path LIKE '/mainpane/msgview/monthlimit%'
2633 INNER JOIN options AS opt ON opt.name=pth.path
2634 WHERE pth.path LIKE '/mainpane/msgview/monthlimit%'
2639 .bindConstText(":tagslash", (tagname
.length
&& tagname
[0] != '/' ?
"/" : ""))
2640 .bindConstText(":tagname", tagname
);
2642 foreach (auto row
; stGetMLimit
.st
.range
) return row
.value
!int;
2648 public void chiroDeleteOption (const(char)[] name
) {
2649 assert(name
.length
!= 0);
2650 static auto stat
= LazyStatement
!"Conf"(`DELETE FROM options WHERE name=:name;`);
2651 stat
.st
.bindConstText(":name", name
).doAll();
2654 public void chiroSetOption(T
) (const(char)[] name
, T value
)
2655 if (!is(T
:const(DynStr
)) && (__traits(isIntegral
, T
) ||
is(T
:const(char)[])))
2657 assert(name
.length
!= 0);
2658 static auto stat
= LazyStatement
!"Conf"(`
2661 VALUES(:name,:value)
2663 DO UPDATE SET value=:value
2665 stat
.st
.bindConstText(":name", name
);
2666 static if (is(T
== typeof(null))) {
2667 stat
.st
.bindConstText(":value", "");
2668 } else static if (__traits(isIntegral
, T
)) {
2669 stat
.st
.bind(":value", value
);
2670 } else static if (is(T
:const(char)[])) {
2671 stat
.st
.bindConstText(":value", value
);
2673 static assert(0, "oops");
2678 public void chiroSetOption (const(char)[] name
, DynStr value
) {
2679 assert(name
.length
!= 0);
2680 //{ import std.stdio; writeln("SETOPTION(", name, "): <", value.getData, ">"); }
2681 static auto stat
= LazyStatement
!"Conf"(`
2684 VALUES(:name,:value)
2686 DO UPDATE SET value=:value
2689 .bindConstText(":name", name
)
2690 .bindConstText(":value", value
.getData
)
2695 public void chiroSetOptionUInts (const(char)[] name
, uint v0
, uint v1
) {
2696 assert(name
.length
!= 0);
2697 static auto stat
= LazyStatement
!"Conf"(`
2700 VALUES(:name,:value)
2702 DO UPDATE SET value=:value
2704 import core
.stdc
.stdio
: snprintf
;
2705 char[64] value
= void;
2706 auto vlen
= snprintf(value
.ptr
, value
.sizeof
, "%u,%u", v0
, v1
);
2708 .bindConstText(":name", name
)
2709 .bindConstText(":value", value
[0..vlen
])
2714 public T
chiroGetOptionEx(T
) (const(char)[] name
, out bool exists
, T defval
=T
.init
)
2715 if (!is(T
:const(DynStr
)) && (__traits(isIntegral
, T
) ||
is(T
:const(char)[])))
2717 static auto stat
= LazyStatement
!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2718 assert(name
.length
!= 0);
2720 foreach (auto row
; stat
.st
.bindConstText(":name", name
).range
) {
2727 public T
chiroGetOption(T
) (const(char)[] name
, T defval
=T
.init
)
2728 if (!is(T
:const(DynStr
)) && (__traits(isIntegral
, T
) ||
is(T
:const(char)[])))
2730 static auto stat
= LazyStatement
!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2731 assert(name
.length
!= 0);
2732 foreach (auto row
; stat
.st
.bindConstText(":name", name
).range
) {
2738 public void chiroGetOption (ref DynStr s
, const(char)[] name
, const(char)[] defval
=null) {
2739 static auto stat
= LazyStatement
!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2740 assert(name
.length
!= 0);
2741 foreach (auto row
; stat
.st
.bindConstText(":name", name
).range
) {
2742 s
= row
.value
!SQ3Text
;
2749 private uint parseUInt (ref SQ3Text s
) {
2751 if (s
.length
== 0 ||
!isdigit(s
[0])) return uint.max
;
2754 immutable int dg
= s
[0].digitInBase(10);
2756 immutable uint nr
= res
*10U+cast(uint)dg
;
2757 if (nr
< res
) return uint.max
;
2761 if (s
.length
&& s
[0] == ',') s
= s
[1..$];
2767 public void chiroGetOptionUInts (ref uint v0
, ref uint v1
, const(char)[] name
) {
2768 static auto stat
= LazyStatement
!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2769 assert(name
.length
!= 0);
2770 foreach (auto row
; stat
.st
.bindConstText(":name", name
).range
) {
2771 auto s
= row
.value
!SQ3Text
;
2772 immutable uint rv0
= parseUInt(s
);
2773 immutable uint rv1
= parseUInt(s
);
2774 if (rv0
!= uint.max
&& rv1
!= uint.max
&& s
.length
== 0) {
2783 // ////////////////////////////////////////////////////////////////////////// //
2784 // append tag if necessary, return tagid
2785 // tag name must be valid: not empty, and not end with a '/'
2786 // returns 0 on invalid tag name
2787 public uint chiroAppendTag (const(char)[] tagname
, int hidden
=0) {
2788 tagname
= tagname
.xstrip
;
2789 while (tagname
.length
&& tagname
[$-1] == '/') tagname
= tagname
[0..$-1];
2790 tagname
= tagname
.xstrip
;
2791 if (tagname
.length
== 0) return 0;
2792 if (tagname
.indexOf('|') >= 0) return 0;
2794 static auto stAppendTag
= LazyStatement
!"View"(`
2795 INSERT INTO tagnames(tag, hidden, threading) VALUES(:tagname,:hidden,:threading)
2797 DO UPDATE SET hidden=hidden -- this is for "returning"
2798 RETURNING tagid AS tagid
2801 // alphanum tags must start with '/'
2803 if (tagname
[0].isalnum
&& tagname
.indexOf(':') < 0) {
2806 stAppendTag
.st
.bindConstText(":tagname", tn
);
2808 stAppendTag
.st
.bindConstText(":tagname", tagname
);
2811 .bind(":hidden", hidden
)
2812 .bind(":threading", (hidden ?
0 : 1));
2813 foreach (auto row
; stAppendTag
.st
.range
) return row
.tagid
!uint;
2819 // ////////////////////////////////////////////////////////////////////////// //
2820 /// returns `true` if we need to update pane
2821 /// if message is left without any tags, it will be tagged with "#hobo"
2822 public bool chiroMessageRemoveTag (uint uid
, const(char)[] tagname
) {
2823 if (uid
== 0) return false;
2824 tagname
= tagname
.xstrip
;
2825 while (tagname
.length
&& tagname
[$-1] == '/') tagname
= tagname
[0..$-1];
2826 tagname
= tagname
.xstrip
;
2827 if (tagname
.length
== 0) return false;
2828 if (tagname
.indexOf('|') >= 0) return false;
2830 immutable tagid
= chiroGetTagUid(tagname
);
2831 if (tagid
== 0) return false;
2833 static auto stUpdateStorageTags
= LazyStatement
!"Store"(`
2834 UPDATE messages SET tags=:tags WHERE uid=:uid
2837 static auto stUidHasTag
= LazyStatement
!"View"(`
2838 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2841 static auto stInsertIntoThreads
= LazyStatement
!"View"(`
2842 INSERT INTO threads(uid, tagid,appearance,time)
2843 VALUES(:uid, :tagid, :appr, (SELECT time FROM info WHERE uid=:uid LIMIT 1))
2846 // delete message from threads
2847 static auto stClearThreads
= LazyStatement
!"View"(`
2848 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2851 static auto stGetMsgTags
= LazyStatement
!"View"(`
2852 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
2854 INNER JOIN tagnames AS tt USING(tagid)
2859 immutable bool updatePane
= (chiroGetTreePaneTableTagId() == tagid
);
2860 bool wasChanges
= false;
2863 // get tagid (possibly appending the tag)
2865 foreach (auto row
; stUidHasTag
.st
.bind(":uid", uid
).bind(":tagid", tagid
).range
) hasit
= true;
2868 stClearThreads
.st
.bind(":uid", uid
).bind(":tagid", tagid
).doAll((stmt
) { wasChanges
= true; });
2870 // if there were any changes, rebuild message tags
2871 if (!wasChanges
) return;
2874 foreach (auto trow
; stGetMsgTags
.st
.bind(":uid", uid
).range
) {
2875 auto tname
= trow
.name
!SQ3Text
;
2876 if (tname
.length
== 0) continue;
2877 if (newtags
.length
) newtags
~= "|";
2881 // if there is no tags, assign "#hobo"
2882 // this should not happen, but...
2883 if (newtags
.length
== 0) {
2885 auto hobo
= chiroAppendTag(newtags
, hidden
:1);
2887 // append record for this tag to threads
2888 // note that there is no need to relink hobos, they should not be threaded
2889 //FIXME: this clears message appearance
2890 stInsertIntoThreads
.st
2892 .bind(":tagid", hobo
)
2893 .bind(":appr", Appearance
.Read
)
2897 // update storage with new tag names
2898 assert(newtags
.length
);
2899 stUpdateStorageTags
.st
.bindConstText(":tags", newtags
).doAll();
2901 // and relink threads for this tagid
2902 chiroSupportRelinkTagThreads(tagid
);
2905 return (wasChanges
&& updatePane
);
2909 // ////////////////////////////////////////////////////////////////////////// //
2910 /// returns `true` if we need to update pane
2911 public bool chiroMessageAddTag (uint uid
, const(char)[] tagname
) {
2912 if (uid
== 0) return false;
2913 tagname
= tagname
.xstrip
;
2914 while (tagname
.length
&& tagname
[$-1] == '/') tagname
= tagname
[0..$-1];
2915 tagname
= tagname
.xstrip
;
2916 if (tagname
.length
== 0) return false;
2917 if (tagname
.indexOf('|') >= 0) return false;
2919 static auto stUpdateStorageTags
= LazyStatement
!"Store"(`
2920 UPDATE messages SET tags=tags||'|'||:tagname WHERE uid=:uid
2923 static auto stUidExists
= LazyStatement
!"View"(`
2924 SELECT 1 FROM threads WHERE uid=:uid LIMIT 1
2927 static auto stUidHasTag
= LazyStatement
!"View"(`
2928 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2931 static auto stInsertIntoThreads
= LazyStatement
!"View"(`
2932 INSERT INTO threads(uid, tagid, appearance, time)
2933 VALUES(:uid, :tagid, :appr, (SELECT time FROM threads WHERE uid=:uid LIMIT 1))
2936 static auto stUnHobo
= LazyStatement
!"View"(`
2937 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2940 bool hasuid
= false;
2941 foreach (auto row
; stUidExists
.st
.bind(":uid", uid
).range
) hasuid
= true;
2942 if (!hasuid
) return false; // nothing to do
2944 immutable paneTagId
= chiroGetTreePaneTableTagId();
2945 bool updatePane
= false;
2948 // get tagid (possibly appending the tag)
2949 uint tagid
= chiroAppendTag(tagname
);
2951 conwriteln("ERROR: cannot append tag name '", tagname
, "'!");
2956 foreach (auto row
; stUidHasTag
.st
.bind(":uid", uid
).bind(":tagid", tagid
).range
) hasit
= true;
2959 // append this tag to the message in the storage
2960 stUpdateStorageTags
.st
.bind(":uid", uid
).bindConstText(":tagname", tagname
).doAll();
2962 // append record for this tag to threads
2963 stInsertIntoThreads
.st
2965 .bind(":tagid", tagid
)
2966 .bind(":appr", Appearance
.Read
)
2969 // and relink threads for this tagid
2970 chiroSupportRelinkTagThreads(tagid
);
2972 // remove this message from "#hobo", if there is any
2973 auto hobo
= chiroGetTagUid("#hobo");
2974 if (hobo
&& hobo
!= tagid
) {
2975 stUnHobo
.st
.bind(":tagid", hobo
).bind(":uid", uid
).doAll();
2976 // there's no need to relink hobos, because they should have no links
2979 updatePane
= (tagid
== paneTagId
);
2987 inserts the one message from the message storage with the given id into view storage.
2988 parses it and such, and optionally updates threads.
2990 doesn't updates NNTP indicies and such, never relinks anything.
2992 invalid (unknown) tags will be ignored.
2994 returns number of processed messages.
2996 doesn't start/end any transactions, so wrap it yourself.
2998 public bool chiroParseAndInsertOneMessage (uint uid
, uint msgtime
, int appearance
,
2999 const(char)[] hdrs
, const(char)[] body, const(char)[] tags
)
3001 auto stInsThreads
= dbView
.statement(`
3003 ( uid, tagid, time, appearance)
3004 VALUES(:uid,:tagid,:time,:appearance)
3007 auto stInsInfo
= dbView
.statement(`
3009 ( uid, from_name, from_mail, subj, to_name, to_mail)
3010 VALUES(:uid,:from_name,:from_mail,:subj,:to_name,:to_mail)
3013 auto stInsMsgId
= dbView
.statement(`
3016 VALUES(:uid,:msgid,:time)
3019 auto stInsMsgRefId
= dbView
.statement(`
3022 VALUES(:uid,:idx,:msgid)
3025 auto stInsContentText
= dbView
.statement(`
3026 INSERT INTO content_text
3027 ( uid, format, content)
3028 VALUES(:uid,:format, ChiroPack(:content))
3031 auto stInsContentHtml
= dbView
.statement(`
3032 INSERT INTO content_html
3033 ( uid, format, content)
3034 VALUES(:uid,:format, ChiroPack(:content))
3037 auto stInsAttach
= dbView
.statement(`
3038 INSERT INTO attaches
3039 ( uid, idx, mime, name, format, content)
3040 VALUES(:uid,:idx,:mime,:name,:format, ChiroPack(:content))
3043 bool noattaches
= false; // do not store attaches?
3045 // create thread record for each tag (and update max nntp index)
3047 int noAttachCount
= 0;
3048 while (tags
.length
) {
3049 auto eep
= tags
.indexOf('|');
3050 auto tagname
= (eep
>= 0 ? tags
[0..eep
] : tags
[0..$]);
3051 tags
= (eep
>= 0 ? tags
[eep
+1..$] : tags
[0..0]);
3052 if (tagname
.length
== 0) continue;
3054 //immutable uint tuid = chiroGetTagUid(tagname);
3055 immutable uint tuid
= chiroAppendTag(tagname
, (tagname
== "#hobo" ?
1 : 0));
3056 if (tuid
== 0) continue;
3059 if (nntpidx > 0 && tagname.startsWith("account:")) {
3060 auto accname = tagname[8..$];
3062 .bindConstText(":accname", accname)
3063 .bind(":nntpidx", nntpidx)
3068 if (!chiroIsTagAllowAttaches(tuid
)) ++noAttachCount
;
3071 int app
= appearance
;
3072 if (app
== Appearance
.Unread
) {
3073 if (tagname
.startsWith("account:") ||
3074 tagname
.startsWith("#spam") ||
3075 tagname
.startsWith("#hobo"))
3077 app
= Appearance
.Read
;
3083 .bind(":tagid", tuid
)
3084 .bind(":time", msgtime
)
3085 .bind(":appearance", app
)
3088 if (!tagCount
) return false;
3089 noattaches
= (noAttachCount
&& noAttachCount
== tagCount
);
3093 bool hasmsgid
= false;
3094 auto msgidfield
= findHeaderField(hdrs
, "Message-Id");
3095 if (msgidfield
.length
) {
3096 auto id
= msgidfield
.getFieldValue
;
3101 .bind("time", msgtime
)
3102 .bindConstText(":msgid", id
)
3106 // if there is no msgid, create one
3109 ripemd160_put(ref rmd
, hdrs
[]);
3110 ripemd160_put(ref rmd
, body[]);
3111 ubyte[RIPEMD160_BYTES
] digest
= ripemd160_finish(ref rmd
);
3112 char[20*2+2+16] buf
= 0;
3113 import core
.stdc
.stdio
: snprintf
;
3114 import core
.stdc
.string
: strcat
;
3115 foreach (immutable idx
, ubyte b
; digest
[]) snprintf(buf
.ptr
+idx
*2, 3, "%02x", b
);
3116 strcat(buf
.ptr
, "@artificial"); // it is safe, there is enough room for it
3119 .bind("time", msgtime
)
3120 .bindConstText(":msgid", buf
[0..20*2])
3125 // insert references
3128 auto inreplyfld
= findHeaderField(hdrs
, "In-Reply-To");
3129 while (inreplyfld
.length
) {
3130 auto id
= getNextFieldValue(inreplyfld
);
3134 .bind(":idx", refidx
++)
3140 inreplyfld
= findHeaderField(hdrs
, "References");
3141 while (inreplyfld
.length
) {
3142 auto id
= getNextFieldValue(inreplyfld
);
3146 .bind(":idx", refidx
++)
3153 // insert base content and attaches
3156 parseContent(ref content
, hdrs
, body, noattaches
);
3157 // insert text and html
3158 bool wasText
= false, wasHtml
= false;
3159 foreach (const ref Content cc
; content
) {
3160 if (cc
.name
.length
) continue;
3161 if (noattaches
&& !cc
.mime
.startsWith("text/")) continue;
3162 if (!wasText
&& cc
.mime
== "text/plain") {
3166 .bindConstText(":format", cc
.format
)
3167 .bindConstBlob(":content", cc
.data
)
3169 } else if (!wasHtml
&& cc
.mime
== "text/html") {
3173 .bindConstText(":format", cc
.format
)
3174 .bindConstBlob(":content", cc
.data
)
3181 .bindConstText(":format", "")
3182 .bindConstBlob(":content", "")
3188 .bindConstText(":format", "")
3189 .bindConstBlob(":content", "")
3192 // insert everything
3194 foreach (const ref Content cc
; content
) {
3195 if (cc
.name
.length
== 0 && cc
.mime
.startsWith("text/")) continue;
3196 // for "no attaches" mode, still record the attach, but ignore its contents
3199 .bind(":idx", cidx
++)
3200 .bindConstText(":mime", cc
.mime
)
3201 .bindConstText(":name", cc
.name
)
3202 .bindConstText(":format", cc
.name
)
3203 .bindConstBlob(":content", (noattaches ?
null : cc
.data
), allowNull
:true)
3208 // insert from/to/subj info
3209 // this must be done last to keep FTS5 in sync
3211 auto subj
= findHeaderField(hdrs
, "Subject").decodeSubj
.subjRemoveRe
;
3212 auto from
= findHeaderField(hdrs
, "From");
3213 auto to
= findHeaderField(hdrs
, "To");
3216 .bind(":from_name", from
.extractName
)
3217 .bind(":from_mail", from
.extractMail
)
3218 .bind(":subj", subj
)
3219 .bind(":to_name", to
.extractName
)
3220 .bind(":to_mail", to
.extractMail
)
3229 inserts the messages from the message storage with the given id into view storage.
3230 parses it and such, and optionally updates threads.
3232 WARNING! DOESN'T UPDATE NNTP INDICIES! this should be done by the downloader.
3234 invalid (unknown) tags will be ignored.
3236 returns number of processed messages.
3238 public uint chiroParseAndInsertMessages (uint stmsgid
,
3239 void delegate (uint count
, uint total
, uint nntpidx
, const(char)[] tags
) progresscb
=null,
3240 uint emsgid
=uint.max
, bool relink
=true, bool asread
=false)
3242 if (emsgid
< stmsgid
) return 0; // nothing to do
3246 if (progresscb
!is null) {
3247 // find total number of messages to process
3248 foreach (auto row
; dbStore
.statement(`
3249 SELECT count(uid) AS total FROM messages WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
3250 ;`).bind(":msglo", stmsgid
).bind(":msghi", emsgid
).range
)
3252 total
= row
.total
!uint;
3255 if (total
== 0) return 0; // why not?
3260 if (relink
) uptagids
.reserve(128);
3261 scope(exit
) delete uptagids
;
3263 foreach (auto mrow
; dbStore
.statement(`
3264 -- this should cache unpack results
3265 WITH msgunpacked(msguid, msgdata, msgtags) AS (
3266 SELECT uid AS msguid, ChiroUnpack(data) AS msgdata, tags AS msgtags
3268 WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
3274 , ChiroExtractHeaders(msgdata) AS headers
3275 , ChiroExtractBody(msgdata) AS body
3276 , ChiroHdr_NNTPIndex(msgdata) AS nntpidx
3277 , ChiroHdr_RecvTime(msgdata) AS msgtime
3279 ;`).bind(":msglo", stmsgid
).bind(":msghi", emsgid
).range
)
3282 auto hdrs
= mrow
.headers
!SQ3Text
;
3283 auto body = mrow
.body!SQ3Text
;
3284 auto tags
= mrow
.tags
!SQ3Text
;
3285 uint uid
= mrow
.uid
!uint;
3286 uint nntpidx
= mrow
.nntpidx
!uint;
3287 uint msgtime
= mrow
.msgtime
!uint;
3288 assert(tags
.length
);
3290 chiroParseAndInsertOneMessage(uid
, msgtime
, (asread ?
1 : 0), hdrs
, body, tags
);
3292 if (progresscb
!is null) progresscb(count
, total
, nntpidx
, tags
);
3295 while (tags
.length
) {
3296 auto eep
= tags
.indexOf('|');
3297 auto tagname
= (eep
>= 0 ? tags
[0..eep
] : tags
[0..$]);
3298 tags
= (eep
>= 0 ? tags
[eep
+1..$] : tags
[0..0]);
3299 if (tagname
.length
== 0) continue;
3301 immutable uint tuid
= chiroGetTagUid(tagname
);
3302 if (tuid
== 0) continue;
3305 foreach (immutable n
; uptagids
) if (n
== tuid
) { found
= true; break; }
3306 if (!found
) uptagids
~= tuid
;
3311 if (relink
&& uptagids
.length
) {
3312 foreach (immutable tagid
; uptagids
) chiroSupportRelinkTagThreads(tagid
);
3321 returns accouint uid (accid) or 0.
3323 public uint chiroGetAccountUid (const(char)[] accname
) {
3324 static auto stat
= LazyStatement
!"Conf"(`SELECT accid AS accid FROM accounts WHERE name=:accname LIMIT 1;`);
3325 foreach (auto row
; stat
.st
.bindConstText(":accname", accname
).range
) return row
.accid
!uint;
3331 returns accouint name, or empty string.
3333 public DynStr
chiroGetAccountName (uint accid
) {
3334 static auto stat
= LazyStatement
!"Conf"(`SELECT name AS name FROM accounts WHERE accid=:accid LIMIT 1;`);
3336 if (accid
== 0) return res
;
3337 foreach (auto row
; stat
.st
.bind(":accid", accid
).range
) {
3338 res
= row
.name
!SQ3Text
;
3345 public struct AccountInfo
{
3353 @property bool isValid () const pure nothrow @safe @nogc { return (accid
!= 0); }
3356 public bool chiroGetAccountInfo (uint accid
, out AccountInfo nfo
) {
3357 static auto stat
= LazyStatement
!"Conf"(`
3358 SELECT name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup, inbox AS inbox
3360 WHERE accid=:accid LIMIT 1
3362 if (accid
== 0) return false;
3363 foreach (auto row
; stat
.st
.bind(":accid", accid
).range
) {
3365 nfo
.name
= row
.name
!SQ3Text
;
3366 nfo
.realname
= row
.realname
!SQ3Text
;
3367 nfo
.email
= row
.email
!SQ3Text
;
3368 nfo
.nntpgroup
= row
.nntpgroup
!SQ3Text
;
3369 nfo
.inbox
= row
.inbox
!SQ3Text
;
3375 public bool chiroGetAccountInfo (const(char)[] accname
, out AccountInfo nfo
) {
3376 static auto stat
= LazyStatement
!"Conf"(`
3377 SELECT accid AS accid, name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup, inbox AS inbox
3379 WHERE name=:name LIMIT 1
3381 if (accname
.length
== 0) return false;
3382 foreach (auto row
; stat
.st
.bindConstText(":name", accname
).range
) {
3383 nfo
.accid
= row
.accid
!uint;
3384 nfo
.name
= row
.name
!SQ3Text
;
3385 nfo
.realname
= row
.realname
!SQ3Text
;
3386 nfo
.email
= row
.email
!SQ3Text
;
3387 nfo
.nntpgroup
= row
.nntpgroup
!SQ3Text
;
3388 nfo
.inbox
= row
.inbox
!SQ3Text
;
3396 returns list of known tags, sorted by name.
3398 public DynStr
[] chiroGetTagList () {
3399 static auto stat
= LazyStatement
!"View"(`SELECT tag AS tagname FROM tagnames WHERE hidden=0 ORDER BY tag;`);
3401 foreach (auto row
; stat
.st
.range
) res
~= DynStr(row
.tagname
!SQ3Text
);
3407 returns tag uid (tagid) or 0.
3409 public uint chiroGetTagUid (const(char)[] tagname
) {
3410 static auto stat
= LazyStatement
!"View"(`SELECT tagid AS tagid FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3411 foreach (auto row
; stat
.st
.bindConstText(":tagname", tagname
).range
) {
3412 return row
.tagid
!uint;
3419 returns tag name or empty string.
3421 public DynStr
chiroGetTagName (uint tagid
) {
3422 static auto stat
= LazyStatement
!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3424 foreach (auto row
; stat
.st
.bind(":tagid", tagid
).range
) {
3425 s
= row
.tagname
!SQ3Text
;
3433 returns `true` if the given tag supports threads.
3435 this is used only when adding new messages, to set all parents to 0.
3437 public bool chiroIsTagThreaded(T
) (T tagnameid
)
3438 if (is(T
:const(char)[]) ||
is(T
:uint))
3440 static if (is(T
:const(char)[])) {
3441 static auto stat
= LazyStatement
!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3442 foreach (auto row
; stat
.st
.bindConstText(":tagname", tagnameid
).range
) {
3443 return (row
.threading
!uint == 1);
3446 static auto xstat
= LazyStatement
!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3447 foreach (auto row
; xstat
.st
.bind(":tagid", tagnameid
).range
) {
3448 return (row
.threading
!uint == 1);
3456 returns `true` if the given tag allows attaches.
3458 this is used only when adding new messages, to set all parents to 0.
3460 public bool chiroIsTagAllowAttaches(T
) (T tagnameid
)
3461 if (is(T
:const(char)[]) ||
is(T
:uint))
3463 static if (is(T
:const(char)[])) {
3464 static auto stat
= LazyStatement
!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3465 foreach (auto row
; stat
.st
.bindConstText(":tagname", tagnameid
).range
) {
3466 return (row
.threading
!uint == 1);
3469 static auto xstat
= LazyStatement
!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3470 foreach (auto row
; xstat
.st
.bind(":tagid", tagnameid
).range
) {
3471 return (row
.threading
!uint == 1);
3479 relinks all messages in all threads suitable for relinking, and
3480 sets parents to zero otherwise.
3482 public void chiroSupportRelinkAllThreads () {
3483 // yeah, that's it: a single SQL statement
3485 -- clear parents where threading is disabled
3486 SELECT ChiroTimerStart('clearing parents');
3491 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=0)
3494 SELECT ChiroTimerStop();
3496 SELECT ChiroTimerStart('relinking threads');
3501 SELECT uid FROM msgids
3503 -- find MSGID for any of our current references
3504 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3505 -- check if UID for that MSGID has the valid tag
3506 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=threads.tagid)
3512 -- do not process messages with non-threading tags
3513 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=1)
3515 SELECT ChiroTimerStop();
3521 relinks all messages for the given tag, or sets parents to zero if
3522 threading for that tag is disabled.
3524 public void chiroSupportRelinkTagThreads(T
) (T tagnameid
)
3525 if (is(T
:const(char)[]) ||
is(T
:uint))
3527 static if (is(T
:const(char)[])) {
3528 immutable uint tid
= chiroGetTagUid(tagnameid
);
3531 alias tid
= tagnameid
;
3534 static auto statNoTrd
= LazyStatement
!"View"(`
3539 tagid = :tagid AND parent <> 0
3542 static auto statTrd
= LazyStatement
!"View"(`
3547 SELECT uid FROM msgids
3549 -- find MSGID for any of our current references
3550 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3551 -- check if UID for that MSGID has the valid tag
3552 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=:tagid)
3558 threads.tagid = :tagid
3561 if (!chiroIsTagThreaded(tid
)) {
3562 // clear parents (just in case)
3563 statNoTrd
.st
.bind(":tagid", tid
).doAll();
3565 // yeah, that's it: a single SQL statement
3566 statTrd
.st
.bind(":tagid", tid
).doAll();
3572 * get "from info" for the given message.
3574 * returns `false` if there is no such message.
3576 public bool chiroGetMessageFrom (uint uid
, ref DynStr fromMail
, ref DynStr fromName
) {
3577 static auto statGetFrom
= LazyStatement
!"View"(`
3579 from_name AS fromName
3580 , from_mail AS fromMail
3587 foreach (auto row
; statGetFrom
.st
.bind(":uid", uid
).range
) {
3588 fromMail
= row
.fromMail
!SQ3Text
;
3589 fromName
= row
.fromName
!SQ3Text
;
3597 gets twit title and state for the given (tagid, uid) message.
3599 returns -666 if there is no such message.
3601 public DynStr
chiroGetMessageTwit(T
) (T tagidname
, uint uid
, out bool twited
)
3602 if (is(T
:const(char)[]) ||
is(T
:uint))
3606 if (!uid
) return res
;
3608 static if (is(T
:const(char)[])) {
3609 immutable uint tid
= chiroGetTagUid(tagidname
);
3613 alias tid
= tagidname
;
3616 if (!tid
) return res
;
3618 static auto statGetTwit
= LazyStatement
!"View"(`
3619 SELECT title AS title
3621 WHERE uid=:uid AND tagid=:tagid AND mute>0
3627 .bind(":tagid", tid
);
3628 foreach (auto row
; statGetTwit
.st
.range
) {
3630 res
= row
.title
!SQ3Text
;
3638 gets mute state for the given (tagid, uid) message.
3640 returns -666 if there is no such message.
3642 public int chiroGetMessageMute(T
) (T tagidname
, uint uid
)
3643 if (is(T
:const(char)[]) ||
is(T
:uint))
3645 if (!uid
) return -666;
3647 static if (is(T
:const(char)[])) {
3648 immutable uint tid
= chiroGetTagUid(tagidname
);
3652 alias tid
= tagidname
;
3655 if (!tid
) return -666;
3657 static auto statGetApp
= LazyStatement
!"View"(`
3660 WHERE uid=:uid AND tagid=:tagid
3666 .bind(":tagid", tid
);
3667 foreach (auto row
; statGetApp
.st
.range
) return row
.mute
!int;
3673 sets mute state the given (tagid, uid) message.
3675 doesn't change children states.
3677 public void chiroSetMessageMute(T
) (T tagidname
, uint uid
, Mute mute
)
3678 if (is(T
:const(char)[]) ||
is(T
:uint))
3682 static if (is(T
:const(char)[])) {
3683 immutable uint tid
= chiroGetTagUid(tagidname
);
3687 alias tid
= tagidname
;
3692 static auto statSetApp
= LazyStatement
!"View"(`
3697 uid=:uid AND tagid=:tagid
3700 static auto statSetAppRead
= LazyStatement
!"View"(`
3704 , appearance=iif(appearance=0,1,appearance)
3706 uid=:uid AND tagid=:tagid
3709 if (mute
> Mute
.Normal
) {
3711 .bind(":mute", cast(int)mute
)
3713 .bind(":tagid", tid
)
3717 .bind(":mute", cast(int)mute
)
3719 .bind(":tagid", tid
)
3726 gets appearance for the given (tagid, uid) message.
3728 returns -666 if there is no such message.
3730 public int chiroGetMessageAppearance(T
) (T tagidname
, uint uid
)
3731 if (is(T
:const(char)[]) ||
is(T
:uint))
3733 if (!uid
) return -666;
3735 static if (is(T
:const(char)[])) {
3736 immutable uint tid
= chiroGetTagUid(tagidname
);
3740 alias tid
= tagidname
;
3743 if (!tid
) return -666;
3745 static auto statGetApp
= LazyStatement
!"View"(`
3746 SELECT appearance AS appearance
3748 WHERE uid=:uid AND tagid=:tagid
3754 .bind(":tagid", tid
);
3755 foreach (auto row
; statGetApp
.st
.range
) return row
.appearance
!int;
3761 gets appearance for the given (tagid, uid) message.
3763 public bool chiroGetMessageUnread(T
) (T tagidname
, uint uid
)
3764 if (is(T
:const(char)[]) ||
is(T
:uint))
3766 return (chiroGetMessageAppearance(tagidname
, uid
) == Appearance
.Unread
);
3771 gets appearance for the given (tagid, uid) message.
3773 public bool chiroGetMessageExactRead(T
) (T tagidname
, uint uid
)
3774 if (is(T
:const(char)[]) ||
is(T
:uint))
3776 return (chiroGetMessageAppearance(tagidname
, uid
) == Appearance
.Read
);
3781 sets appearance for the given (tagid, uid) message.
3783 public void chiroSetMessageAppearance(T
) (T tagidname
, uint uid
, Appearance appearance
)
3784 if (is(T
:const(char)[]) ||
is(T
:uint))
3788 static if (is(T
:const(char)[])) {
3789 immutable uint tid
= chiroGetTagUid(tagidname
);
3793 alias tid
= tagidname
;
3798 static auto statSetApp
= LazyStatement
!"View"(`
3801 appearance=:appearance
3803 uid=:uid AND tagid=:tagid
3807 .bind(":appearance", cast(int)appearance
)
3809 .bind(":tagid", tid
)
3815 mark (tagid, uid) message as read.
3817 public void chiroSetReadOrUnreadMessageAppearance(T
) (T tagidname
, uint uid
, Appearance appearance
)
3818 if (is(T
:const(char)[]) ||
is(T
:uint))
3822 static if (is(T
:const(char)[])) {
3823 immutable uint tid
= chiroGetTagUid(tagidname
);
3827 alias tid
= tagidname
;
3832 static auto statSetApp
= LazyStatement
!"View"(`
3837 uid=:uid AND tagid=:tagid AND (appearance=:checkapp0 OR appearance=:checkapp1)
3842 .bind(":tagid", tid
)
3843 .bind(":setapp", cast(int)appearance
)
3844 .bind(":checkapp0", Appearance
.Read
)
3845 .bind(":checkapp1", Appearance
.Unread
)
3851 mark (tagid, uid) message as read.
3853 public void chiroSetMessageRead(T
) (T tagidname
, uint uid
)
3854 if (is(T
:const(char)[]) ||
is(T
:uint))
3856 chiroSetReadOrUnreadMessageAppearance(tagidname
, uid
, Appearance
.Read
);
3860 public void chiroSetMessageUnread(T
) (T tagidname
, uint uid
)
3861 if (is(T
:const(char)[]) ||
is(T
:uint))
3863 chiroSetReadOrUnreadMessageAppearance(tagidname
, uid
, Appearance
.Unread
);
3868 purge all messages with the given tag.
3870 this removes messages from all view tables, removes content from
3871 the "messages" table, and sets "messages" table tags to NULL.
3873 public void chiroDeletePurgedWithTag(T
) (T tagidname
)
3874 if (is(T
:const(char)[]) ||
is(T
:uint))
3876 static if (is(T
:const(char)[])) {
3877 immutable uint tid
= chiroGetTagUid(tagidname
);
3881 alias tid
= tagidname
;
3886 static auto statCountPurged
= LazyStatement
!"View"(`
3887 SELECT COUNT(uid) AS pcount FROM threads
3888 WHERE tagid=:tagid AND appearance=:appr
3891 uint purgedCount
= 0;
3892 foreach (auto row
; statCountPurged
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).range
) {
3893 purgedCount
= row
.pcount
!uint;
3895 if (!purgedCount
) return;
3897 // we will need this to clear storage
3899 scope(exit
) delete plist
;
3900 plist
.reserve(purgedCount
);
3902 static auto statListPurged
= LazyStatement
!"View"(`
3903 SELECT uid AS uid FROM threads
3904 WHERE tagid=:tagid AND appearance=:appr
3908 foreach (auto row
; statListPurged
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).range
) {
3909 plist
~= row
.uid
!uint;
3911 if (plist
.length
== 0) return; // just in case
3913 static auto statClearStorage
= LazyStatement
!"Store"(`
3915 SET tags=NULL, data=NULL
3919 enum BulkClearSQL(string table
) = `
3920 DELETE FROM `~table
~`
3922 uid IN (SELECT uid FROM threads WHERE tagid=:tagid AND appearance=:appr)
3925 // bulk clearing of info
3926 static auto statClearInfo
= LazyStatement
!"View"(BulkClearSQL
!"info");
3927 // bulk clearing of msgids
3928 static auto statClearMsgids
= LazyStatement
!"View"(BulkClearSQL
!"msgids");
3929 // bulk clearing of refids
3930 static auto statClearRefids
= LazyStatement
!"View"(BulkClearSQL
!"refids");
3931 // bulk clearing of text
3932 static auto statClearText
= LazyStatement
!"View"(BulkClearSQL
!"content_text");
3933 // bulk clearing of html
3934 static auto statClearHtml
= LazyStatement
!"View"(BulkClearSQL
!"content_html");
3935 // bulk clearing of attaches
3936 static auto statClearAttach
= LazyStatement
!"View"(BulkClearSQL
!"attaches");
3937 // bulk clearing of threads
3938 static auto statClearThreads
= LazyStatement
!"View"(`
3940 WHERE tagid=:tagid AND appearance=:appr
3943 static if (is(T
:const(char)[])) {
3944 conwriteln("removing ", plist
.length
, " message", (plist
.length
!= 1 ?
"s" : ""), " from '", tagidname
, "'...");
3946 DynStr tname
= chiroGetTagName(tid
);
3947 conwriteln("removing ", plist
.length
, " message", (plist
.length
!= 1 ?
"s" : ""), " from '", tname
.getData
, "'...");
3950 // WARNING! "info" must be cleared FIRST, and "threads" LAST
3952 statClearInfo
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3953 statClearMsgids
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3954 statClearRefids
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3955 statClearText
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3956 statClearHtml
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3957 statClearAttach
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3958 statClearThreads
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3959 // relink tag threads
3960 chiroSupportRelinkTagThreads(tid
);
3963 // now clear the storage
3964 conwriteln("clearing the storage...");
3966 foreach (immutable uint uid
; plist
) {
3967 statClearStorage
.st
.bind(":uid", uid
).doAll();
3971 conwriteln("done purging.");
3976 creates "treepane" table for the given tag. that table can be used to
3977 render threaded listview.
3979 returns max id of the existing item. can be used for pagination.
3980 item ids are guaranteed to be sequential, and without any holes.
3981 the first id is `1`.
3983 returned table has "rowid", and two integer fields: "uid" (message uid), and
3984 "level" (message depth, starting from 0).
3986 public uint chiroCreateTreePaneTable(T
) (T tagidname
, int lastmonthes
=12, bool allowThreading
=true)
3987 if (is(T
:const(char)[]) ||
is(T
:uint))
3989 auto ctm
= Timer(true);
3991 // shrink temp table to the bare minimum, because each field costs several msecs
3992 // we don't need parent and time here, because we can easily select them with inner joins
3995 DROP TABLE IF EXISTS treepane;
3996 CREATE TEMP TABLE IF NOT EXISTS treepane (
3997 iid INTEGER PRIMARY KEY
4000 -- to make joins easier
4006 // this need to add answers to some ancient crap
4007 static auto statFirstUnreadTime
= LazyStatement
!"View"(`
4008 SELECT MIN(time) AS time, parent AS parent
4010 WHERE tagid=:tagidname AND appearance=:app
4013 static auto statFindParentFor
= LazyStatement
!"View"(`
4014 SELECT time AS time, parent AS parent
4016 WHERE tagid=:tagidname AND uid=:uid
4020 // clear it (should be faster than dropping and recreating)
4021 dbView
.execute(`DELETE FROM treepane;`);
4023 // this "%08X" will do up to 2038; i'm fine with it
4024 static auto statTrd
= LazyStatement
!"View"(`
4025 INSERT INTO treepane
4027 WITH tree(uid, parent, level, time, path) AS (
4028 WITH RECURSIVE fulltree(uid, parent, level, time, path) AS (
4029 SELECT t.uid AS uid, t.parent AS parent, 1 AS level, t.time AS time, printf('%08X', t.time) AS path
4031 WHERE t.time>=:starttime AND parent=0 AND t.tagid=:tagidname AND t.appearance <> -1
4033 SELECT t.uid AS uid, t.parent AS parent, ft.level+1 AS level, t.time AS time, printf('%s|%08X', ft.path, t.time) AS path
4034 FROM threads t, fulltree ft
4035 WHERE t.time>=:starttime AND t.parent=ft.uid AND t.tagid=:tagidname AND t.appearance <> -1
4037 SELECT * FROM fulltree
4041 , tree.level-1 AS level
4042 , :tagidname AS tagid
4047 static auto statNoTrd
= LazyStatement
!"View"(`
4048 INSERT INTO treepane
4053 , :tagidname AS tagid
4056 threads.time>=:starttime AND threads.tagid=:tagidname AND threads.appearance <> -1
4061 static if (is(T
:const(char)[])) {
4062 immutable uint tid
= chiroGetTagUid(tagidname
);
4066 alias tid
= tagidname
;
4071 if (lastmonthes
> 0) {
4072 if (lastmonthes
> 12*100) {
4075 // show last `lastmonthes` (full monthes)
4076 import std
.datetime
;
4077 import core
.time
: Duration
;
4079 SysTime now
= Clock
.currTime().toUTC();
4080 int year
= now
.year
;
4081 int month
= now
.month
; // from 1
4083 // yes, i am THAT lazy
4084 while (lastmonthes
> 0) {
4085 if (month
> lastmonthes
) { month
-= lastmonthes
; break; }
4086 lastmonthes
-= month
;
4090 // construct unix time
4091 now
.fracSecs
= Duration
.zero
;
4096 now
.month
= cast(Month
)month
;
4098 startTime
= cast(uint)now
.toUnixTime();
4102 // check if we need to fix unread time
4103 // required to show the whole ancient thread if somebody answered
4104 if (startTime
> 0) {
4107 foreach (auto row
; statFirstUnreadTime
.st
.bind(":tagidname", tid
).bind(":app", Appearance
.Unread
).range
) {
4108 unTime
= row
.time
!uint;
4109 unParent
= row
.parent
!uint;
4111 if (unTime
> 0 && unTime
< startTime
) {
4112 // find root message, and start from it
4114 while (unParent
&& allowThreading
) {
4115 statFindParentFor
.st
4116 .bind(":tagidname", tid
)
4117 .bind(":uid", unParent
);
4120 foreach (auto row
; statFindParentFor
.st
.range
) {
4121 unTime
= row
.time
!uint;
4122 unParent
= row
.parent
!uint;
4124 if (unTime
> 0 && unTime
< startTime
) startTime
= unTime
;
4129 if (allowThreading
) {
4130 statTrd
.st
.bind(":tagidname", tid
).bind(":starttime", startTime
).doAll();
4132 statNoTrd
.st
.bind(":tagidname", tid
).bind(":starttime", startTime
).doAll();
4135 if (ChiroTimerEnabled
) writeln("creating treepane time: ", ctm
);
4137 immutable uint res
= cast(uint)dbView
.lastRowId
;
4139 version(chidb_drop_pane_table
) {
4140 dbView
.execute(`CREATE INDEX treepane_uid ON treepane(uid);`);
4148 returns current treepane tagid.
4150 public uint chiroGetTreePaneTableTagId () {
4151 static auto stat
= LazyStatement
!"View"(`SELECT tagid AS tagid FROM treepane WHERE iid=1 LIMIT 1;`);
4152 foreach (auto row
; stat
.st
.range
) return row
.tagid
!uint;
4158 returns current treepane max uid.
4160 public uint chiroGetTreePaneTableMaxUId () {
4161 static auto stat
= LazyStatement
!"View"(`SELECT MAX(uid) AS uid FROM treepane LIMIT 1;`);
4162 foreach (auto row
; stat
.st
.range
) return row
.uid
!uint;
4168 returns number of items in the current treepane.
4170 public uint chiroGetTreePaneTableCount () {
4171 static auto stat
= LazyStatement
!"View"(`SELECT COUNT(*) AS total FROM treepane;`);
4172 foreach (auto row
; stat
.st
.range
) return row
.total
!uint;
4178 returns index of the given uid in the treepane.
4180 public bool chiroIsTreePaneTableUidValid (uint uid
) {
4181 static auto stat
= LazyStatement
!"View"(`SELECT iid AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
4182 if (uid
== 0) return false;
4183 foreach (auto row
; stat
.st
.bind(":uid", uid
).range
) return true;
4189 returns first treepane uid.
4191 public uint chiroGetTreePaneTableFirstUid () {
4192 static auto stmt
= LazyStatement
!"View"(`SELECT uid AS uid FROM treepane WHERE iid=1 LIMIT 1;`);
4193 foreach (auto row
; stmt
.st
.range
) return row
.uid
!uint;
4199 returns last treepane uid.
4201 public uint chiroGetTreePaneTableLastUid () {
4202 static auto stmt
= LazyStatement
!"View"(`SELECT MAX(iid), uid AS uid FROM treepane LIMIT 1;`);
4203 foreach (auto row
; stmt
.st
.range
) return row
.uid
!uint;
4209 returns index of the given uid in the treepane.
4211 public int chiroGetTreePaneTableUid2Index (uint uid
) {
4212 static auto stmt
= LazyStatement
!"View"(`SELECT iid-1 AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
4213 if (uid
== 0) return -1;
4214 foreach (auto row
; stmt
.st
.bind(":uid", uid
).range
) return row
.idx
!int;
4220 returns uid of the given index in the treepane.
4222 public uint chiroGetTreePaneTableIndex2Uid (int index
) {
4223 static auto stmt
= LazyStatement
!"View"(`SELECT uid AS uid FROM treepane WHERE iid=:idx+1 LIMIT 1;`);
4224 if (index
< 0 || index
== int.max
) return 0;
4225 foreach (auto row
; stmt
.st
.bind(":idx", index
).range
) return row
.uid
!uint;
4231 returns previous uid in the treepane.
4233 public uint chiroGetTreePaneTablePrevUid (uint uid
) {
4234 static auto stmt
= LazyStatement
!"View"(`
4235 SELECT uid AS uid FROM treepane
4236 WHERE iid IN (SELECT iid-1 FROM treepane WHERE uid=:uid LIMIT 1)
4239 if (uid
== 0) return chiroGetTreePaneTableFirstUid();
4240 foreach (auto row
; stmt
.st
.bind(":uid", uid
).range
) return row
.uid
!uint;
4246 returns uid of the given index in the treepane.
4248 public uint chiroGetTreePaneTableNextUid (uint uid
) {
4249 static auto stmt
= LazyStatement
!"View"(`
4250 SELECT uid AS uid FROM treepane
4251 WHERE iid IN (SELECT iid+1 FROM treepane WHERE uid=:uid LIMIT 1)
4254 if (uid
== 0) return chiroGetTreePaneTableFirstUid();
4255 foreach (auto row
; stmt
.st
.bind(":uid", uid
).range
) return row
.uid
!uint;
4261 releases (drops) "treepane" table.
4263 can be called several times, but usually you don't need to call this at all.
4265 public void chiroClearTreePaneTable () {
4266 //dbView.execute(`DROP TABLE IF EXISTS treepane;`);
4267 dbView
.execute(`DELETE FROM treepane;`);
4272 return next unread message uid in treepane, or 0.
4274 public uint chiroGetPaneNextUnread (uint curruid
) {
4275 static auto stmtNext
= LazyStatement
!"View"(`
4276 SELECT treepane.uid AS uid FROM treepane
4277 INNER JOIN threads USING(uid, tagid)
4278 WHERE treepane.iid-1 > :cidx AND threads.appearance=:appr
4282 immutable int cidx
= chiroGetTreePaneTableUid2Index(curruid
);
4283 foreach (auto row
; stmtNext
.st
.bind(":cidx", cidx
).bind(":appr", Appearance
.Unread
).range
) return row
.uid
!uint;
4285 // try from the beginning
4286 foreach (auto row
; stmtNext
.st
.bind(":cidx", -1).bind(":appr", Appearance
.Unread
).range
) return row
.uid
!uint;
4293 selects given number of items starting with the given item id.
4295 returns numer of selected items.
4297 `stiid` counts from zero
4299 WARNING! "treepane" table must be prepared with `chiroCreateTreePaneTable()`!
4301 WARNING! [i]dup `SQ3Text` arguments if necessary, they won't survive the `cb` return!
4303 public int chiroGetPaneTablePage (int stiid
, int limit
,
4304 void delegate (int pgofs
, /* offset from the page start, from zero and up to `limit` */
4305 int iid
, /* item id, counts from zero*/
4306 uint uid
, /* msguid, never zero */
4307 uint parentuid
, /* parent msguid, may be zero */
4308 uint level
, /* threading level, from zero */
4309 Appearance appearance
, /* see above */
4310 Mute mute
, /* see above */
4311 SQ3Text date
, /* string representation of receiving date and time */
4312 SQ3Text subj
, /* message subject, can be empty string */
4313 SQ3Text fromName
, /* message sender name, can be empty string */
4314 SQ3Text fromMail
, /* message sender email, can be empty string */
4315 SQ3Text title
) cb
/* title from twiting */
4317 static auto stat
= LazyStatement
!"View"(`
4320 , treepane.uid AS uid
4321 , treepane.level AS level
4322 , threads.parent AS parent
4323 , threads.appearance AS appearance
4324 , threads.mute AS mute
4325 , datetime(threads.time, 'unixepoch') AS time
4327 , info.from_name AS from_name
4328 , info.from_mail AS from_mail
4329 , threads.title AS title
4331 INNER JOIN info USING(uid)
4332 INNER JOIN threads USING(uid, tagid)
4333 WHERE treepane.iid >= :stiid
4334 ORDER BY treepane.iid
4338 if (limit
<= 0) return 0;
4340 if (stiid
== int.min
) return 0;
4342 if (limit
<= 0) return 0;
4346 foreach (auto row
; stat
.st
.bind(":stiid", stiid
+1).bind(":limit", limit
).range
)
4349 cb(total
, row
.iid
!int, row
.uid
!uint, row
.parent
!uint, row
.level
!uint,
4350 cast(Appearance
)row
.appearance
!int, cast(Mute
)row
.mute
!int,
4351 row
.time
!SQ3Text
, row
.subj
!SQ3Text
, row
.from_name
!SQ3Text
, row
.from_mail
!SQ3Text
, row
.title
!SQ3Text
);
4359 // ////////////////////////////////////////////////////////////////////////// //
4360 /** returns full content of the messare or `null` if no message found (or it was deleted).
4362 public DynStr
chiroGetFullMessageContent (uint uid
) {
4364 if (uid
== 0) return res
;
4365 foreach (auto row
; dbStore
.statement(`SELECT ChiroUnpack(data) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid
).range
) {
4366 res
= row
.result
!SQ3Text
;
4373 /** returns full content of the messare or `null` if no message found (or it was deleted).
4375 public DynStr
chiroMessageHeaders (uint uid
) {
4377 if (uid
== 0) return res
;
4378 foreach (auto row
; dbStore
.statement(`SELECT ChiroExtractHeaders(ChiroUnpack(data)) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid
).range
) {
4379 res
= row
.result
!SQ3Text
;
4386 /** returns full content of the messare or `null` if no message found (or it was deleted).
4388 public DynStr
chiroMessageBody (uint uid
) {
4390 if (uid
== 0) return res
;
4391 foreach (auto row
; dbStore
.statement(`SELECT ChiroExtractBody(ChiroUnpack(data)) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid
).range
) {
4392 res
= row
.result
!SQ3Text
;
4399 // ////////////////////////////////////////////////////////////////////////// //
4401 Error
, // some error occured
4407 public Bogo
messageBogoCheck (uint uid
) {
4408 if (uid
== 0) return Bogo
.Error
;
4409 DynStr content
= chiroGetFullMessageContent(uid
);
4410 if (content
.length
== 0) return Bogo
.Error
;
4414 //{ auto fo = VFile("/tmp/zzzz", "w"); fo.rawWriteExact(art.data); }
4415 auto pipes
= pipeProcess(["/usr/bin/bogofilter", "-T"]);
4416 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4417 //pipes.stdin.writeln();
4418 //foreach (string s; art.text) pipes.stdin.writeln(s);
4419 pipes
.stdin
.writeln(content
.getData
.xstripright
);
4420 pipes
.stdin
.flush();
4421 pipes
.stdin
.close();
4422 auto res
= pipes
.stdout
.readln();
4424 //conwriteln("RESULT: [", res, "]");
4425 if (res
.length
== 0) {
4426 //conwriteln("ERROR: bogofilter returned nothing");
4429 if (res
[0] == 'H') return Bogo
.Ham
;
4430 if (res
[0] == 'U') return Bogo
.Unsure
;
4431 if (res
[0] == 'S') return Bogo
.Spam
;
4432 //while (res.length && res[$-1] <= ' ') res = res[0..$-1];
4433 //conwriteln("ERROR: bogofilter returned some shit: [", res, "]");
4434 } catch (Exception e
) { // sorry
4435 //conwriteln("ERROR bogofiltering: ", e.msg);
4442 // ////////////////////////////////////////////////////////////////////////// //
4443 private void messageBogoMarkSpamHam(bool spam
) (uint uid
) {
4444 if (uid
== 0) return;
4445 DynStr content
= chiroGetFullMessageContent(uid
);
4446 if (content
.length
== 0) return;
4448 static if (spam
) enum arg
= "-s"; else enum arg
= "-n";
4451 auto pipes
= pipeProcess(["/usr/bin/bogofilter", arg
]);
4452 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4453 //pipes.stdin.writeln();
4454 //foreach (string s; art.text) pipes.stdin.writeln(s);
4455 pipes
.stdin
.writeln(content
.getData
.xstripright
);
4456 pipes
.stdin
.flush();
4457 pipes
.stdin
.close();
4459 } catch (Exception e
) { // sorry
4460 //conwriteln("ERROR bogofiltering: ", e.msg);
4465 public void messageBogoMarkHam (uint uid
) { messageBogoMarkSpamHam
!false(uid
); }
4466 public void messageBogoMarkSpam (uint uid
) { messageBogoMarkSpamHam
!true(uid
); }
4469 // ////////////////////////////////////////////////////////////////////////// //
4470 public alias TwitProcessCallback
= void delegate (const(char)[] msg
, uint curr
, uint total
);
4472 void processEmailTwits (TwitProcessCallback cb
) {
4473 enum Message
= "processing email twits";
4475 auto stFindTwitNameEmail
= LazyStatement
!"View"(`
4478 , threads.tagid AS tagid
4480 INNER JOIN info AS ii ON
4481 ii.uid=threads.uid AND
4482 ii.from_mail=:email AND
4487 auto stFindTwitEmail
= LazyStatement
!"View"(`
4490 , threads.tagid AS tagid
4492 INNER JOIN info AS ii ON
4493 ii.uid=threads.uid AND
4498 auto stFindTwitName
= LazyStatement
!"View"(`
4501 , threads.tagid AS tagid
4503 INNER JOIN info AS ii ON
4504 ii.uid=threads.uid AND
4510 auto stFindTwitNameEmailMasked
= LazyStatement
!"View"(`
4513 , threads.tagid AS tagid
4515 INNER JOIN info AS ii ON
4516 ii.uid=threads.uid AND
4517 ii.from_name=:name AND
4518 ii.from_mail GLOB :email
4522 auto stFindTwitEmailMasked
= LazyStatement
!"View"(`
4525 , threads.tagid AS tagid
4527 INNER JOIN info AS ii ON
4528 ii.uid=threads.uid AND
4529 ii.from_mail GLOB :email
4534 auto stUpdateMute
= LazyStatement
!"View"(`
4536 SET mute=:mute, title=:title
4537 WHERE uid=:uid AND tagid=:tagid AND mute=0
4540 static struct UidTag
{
4546 foreach (auto trow
; dbConf
.statement(`SELECT COUNT(*) AS twitcount FROM emailtwits;`).range
) twitcount
= trow
.twitcount
!uint;
4548 if (cb
!is null) cb(Message
, 0, twitcount
);
4551 CREATE TEMP TABLE IF NOT EXISTS disemails(
4552 email TEXT NOT NULL UNIQUE
4556 if (cb
!is null) cb("dropping temp tables", twitcount
, twitcount
);
4557 dbView
.execute(`DROP TABLE IF EXISTS disemails;`);
4562 foreach (auto trow
; dbConf
.statement(`
4569 WHERE email NOT LIKE '%*%'
4573 auto title
= trow
.title
!SQ3Text
;
4574 if (title
.length
== 0) continue;
4575 auto email
= trow
.email
!SQ3Text
;
4576 auto name
= trow
.name
!SQ3Text
;
4577 assert(email
.indexOf('*') < 0);
4579 if (email
.length
&& name
.length
) {
4580 st
= stFindTwitNameEmail
.st
;
4581 st
.bindConstText(":email", email
).bindConstText(":name", name
);
4582 } else if (email
.length
) {
4583 st
= stFindTwitEmail
.st
;
4584 st
.bindConstText(":email", email
);
4585 } else if (name
.length
) {
4586 st
= stFindTwitName
.st
;
4587 st
.bindConstText(":name", name
);
4592 msguids
.reserve(128);
4593 scope(exit
) delete msguids
;
4594 //writeln("::: ", email, " : ", name);
4595 foreach (auto mrow
; st
.range
) {
4596 auto tname
= chiroGetTagName(mrow
.tagid
!uint);
4597 if (tname
.length
== 0 ||
!globmatch(tname
, trow
.tagglob
!SQ3Text
)) continue;
4598 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4599 msguids
~= UidTag(mrow
.uid
!uint, mrow
.tagid
!uint);
4601 if (msguids
.length
== 0) continue;
4602 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4603 immutable bool muteAllow
= title
.startsWith("!"); // allow this
4604 //transacted!"View"{
4605 foreach (immutable pair
; msguids
) {
4607 .bind(":uid", pair
.uid
)
4608 .bind(":tagid", pair
.tagid
)
4609 .bind(":mute", (muteAllow ? Mute
.Never
: Mute
.ThreadStart
))
4610 .bindConstText(":title", title
)
4614 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4617 if (cb
!is null) cb("selecting distinct emails", twitdone
, twitcount
);
4619 DELETE FROM disemails;
4620 INSERT INTO disemails
4621 SELECT DISTINCT(from_mail) FROM info;
4623 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4626 foreach (auto trow
; dbConf
.statement(`
4633 WHERE email LIKE '%*%'
4637 auto title
= trow
.title
!SQ3Text
;
4638 if (title
.length
== 0) continue;
4639 auto email
= trow
.email
!SQ3Text
;
4640 auto name
= trow
.name
!SQ3Text
;
4641 assert(email
.indexOf('*') >= 0);
4642 assert(email
.length
);
4644 foreach (auto drow
; dbView
.statement(`SELECT email AS demail FROM disemails WHERE email GLOB :email;`)
4645 .bindConstText(":email", email
).range
)
4649 st
= stFindTwitNameEmail
.st
;
4650 st
.bindConstText(":email", drow
.demail
!SQ3Text
).bindConstText(":name", name
);
4652 st
= stFindTwitEmail
.st
;
4653 st
.bindConstText(":email", drow
.demail
!SQ3Text
);
4656 msguids
.reserve(128);
4657 scope(exit
) delete msguids
;
4658 //writeln("::: ", email, " : ", name);
4659 foreach (auto mrow
; st
.range
) {
4660 auto tname
= chiroGetTagName(mrow
.tagid
!uint);
4661 if (tname
.length
== 0 ||
!globmatch(tname
, trow
.tagglob
!SQ3Text
)) continue;
4662 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4663 msguids
~= UidTag(mrow
.uid
!uint, mrow
.tagid
!uint);
4665 if (msguids
.length
== 0) continue;
4666 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4667 immutable bool muteAllow
= title
.startsWith("!"); // allow this
4668 //transacted!"View"{
4669 foreach (immutable pair
; msguids
) {
4671 .bind(":uid", pair
.uid
)
4672 .bind(":tagid", pair
.tagid
)
4673 .bind(":mute", (muteAllow ? Mute
.Never
: Mute
.ThreadStart
))
4674 .bindConstText(":title", title
)
4680 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4683 foreach (auto trow
; dbConf
.statement(`
4690 WHERE email LIKE '%*%'
4694 auto title
= trow
.title
!SQ3Text
;
4695 if (title
.length
== 0) continue;
4696 auto email
= trow
.email
!SQ3Text
;
4697 auto name
= trow
.name
!SQ3Text
;
4698 assert(email
.indexOf('*') >= 0);
4699 assert(email
.length
);
4701 if (email
.length
&& name
.length
) {
4702 st
= stFindTwitNameEmailMasked
.st
;
4703 st
.bindConstText(":email", email
).bindConstText(":name", name
);
4705 st
= stFindTwitEmailMasked
.st
;
4706 st
.bindConstText(":email", email
);
4709 msguids
.reserve(128);
4710 scope(exit
) delete msguids
;
4711 //writeln("::: ", email, " : ", name);
4712 foreach (auto mrow
; st
.range
) {
4713 auto tname
= chiroGetTagName(mrow
.tagid
!uint);
4714 if (tname
.length
== 0 ||
!globmatch(tname
, trow
.tagglob
!SQ3Text
)) continue;
4715 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4716 msguids
~= UidTag(mrow
.uid
!uint, mrow
.tagid
!uint);
4718 if (msguids
.length
== 0) continue;
4719 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4720 immutable bool muteAllow
= title
.startsWith("!"); // allow this
4721 //transacted!"View"{
4722 foreach (immutable pair
; msguids
) {
4724 .bind(":uid", pair
.uid
)
4725 .bind(":tagid", pair
.tagid
)
4726 .bind(":mute", (muteAllow ? Mute
.Never
: Mute
.ThreadStart
))
4727 .bindConstText(":title", title
)
4731 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4736 //if (cb !is null) cb(Message, twitcount, twitcount);
4740 void processMsgidTwits (TwitProcessCallback cb
) {
4741 enum Message
= "processing msgid twits";
4743 auto stUpdateMute
= LazyStatement
!"View"(`
4745 SET mute=:mute, title=NULL
4746 WHERE uid=:uid AND tagid=:tagid AND mute=0
4749 static struct UidTag
{
4755 foreach (auto trow
; dbConf
.statement(`SELECT COUNT(*) AS twitcount FROM msgidtwits;`).range
) twitcount
= trow
.twitcount
!uint;
4757 if (cb
!is null) cb(Message
, 0, twitcount
);
4761 foreach (auto trow
; dbConf
.statement(`SELECT msgid AS msgid, tagglob AS tagglob FROM msgidtwits;`).range
) {
4764 msguids
.reserve(128);
4765 scope(exit
) delete msguids
;
4767 foreach (auto mrow
; dbView
.statement(`
4768 SELECT threads.uid AS uid, threads.tagid AS tagid
4770 INNER JOIN msgids AS mm
4771 ON mm.msgid=:msgid AND mm.uid=threads.uid
4773 ;`).bindConstText(":msgid", trow
.msgid
!SQ3Text
).range
)
4775 auto tname
= chiroGetTagName(mrow
.tagid
!uint);
4776 if (tname
.length
== 0 ||
!globmatch(tname
, trow
.tagglob
!SQ3Text
)) continue;
4777 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4778 msguids
~= UidTag(mrow
.uid
!uint, mrow
.tagid
!uint);
4780 if (msguids
.length
== 0) continue;
4781 //conwriteln("updating ", msguids.length, " messages for msgid <", trow.msgid!SQ3Text, ">");
4782 //transacted!"View"{
4783 foreach (immutable pair
; msguids
) {
4785 .bind(":uid", pair
.uid
)
4786 .bind(":tagid", pair
.tagid
)
4787 .bind(":mute", Mute
.ThreadStart
)
4791 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4795 if (cb
!is null) cb(Message
, twitcount
, twitcount
);
4799 void processThreadMutes (TwitProcessCallback cb
) {
4800 enum Message
= "processing thread mutes";
4802 if (cb
!is null) cb(Message
, 0, 0);
4805 ATTACH DATABASE '`~MailDBPath
~`chiview.db' AS chiview;
4809 --------------------------------------------------------------------------------
4810 -- create temp table with mute pairs
4811 SELECT ChiroTimerStart('creating mute pairs');
4812 CREATE TEMP TABLE mutepairs AS
4813 WITH RECURSIVE children(muid, paruid, mtagid) AS (
4814 SELECT 0, chiview.threads.uid, chiview.threads.tagid
4815 FROM chiview.threads
4816 WHERE chiview.threads.parent=0 AND chiview.threads.mute=2
4817 AND EXISTS (SELECT uid FROM chiview.threads AS tx WHERE tx.tagid=chiview.threads.tagid AND tx.parent=chiview.threads.uid)
4820 tt.uid, tt.uid, mtagid
4822 INNER JOIN chiview.threads AS tt
4824 tt.tagid=cc.mtagid AND
4825 tt.parent=cc.paruid AND
4835 SELECT ChiroTimerStop();
4838 SELECT 'nested mute pairs to skip:', COUNT(uid)
4839 FROM chiview.threads
4840 INNER JOIN mutepairs AS tt
4848 SELECT ChiroTimerStart('updating thread mutes');
4849 UPDATE chiview.threads
4852 , appearance=(SELECT CASE WHEN appearance=0 THEN 1 ELSE appearance END)
4853 FROM (SELECT muid, mtagid FROM mutepairs) AS cc
4854 WHERE uid=cc.muid AND tagid=cc.mtagid AND mute=0
4856 SELECT ChiroTimerStop();
4858 DROP TABLE mutepairs;
4861 --SELECT 'secondary mutes:', COUNT(mute) FROM threads WHERE mute=3;
4866 DETACH DATABASE chiview;
4871 public void chiroRecalcAllTwits (TwitProcessCallback cb
) {
4873 conwriteln("clearing all mutes...");
4874 if (cb
!is null) cb("clearing mutes", 0, 0);
4877 SET mute=0, title=NULL
4879 conwriteln("processing email twits...");
4880 processEmailTwits(cb
);
4881 conwriteln("processing msgid twits...");
4882 processMsgidTwits(cb
);
4883 conwriteln("propagating thread twits...");
4884 processThreadMutes(cb
);
4885 conwriteln("twit recalculation complete.");