2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module chibackend
.sqbase
is aliced
;
20 //version = fts5_use_porter;
22 // do not use, for testing only!
23 // and it seems to generate bigger files, lol
26 // use libdeflate instead of zlib
27 // see https://github.com/ebiggers/libdeflate
28 // around 2 times slower on level 9 than zlib, resulting size is 5MB less
29 // around 3 times slower on level 12, resulting size it 10MB less
30 // totally doesn't worth it
31 //version = use_libdeflate;
33 // use libxpack instead of zlib
34 // see https://github.com/ebiggers/xpack
35 // it supports buffers up to 2^19 (524288) bytes (see https://github.com/ebiggers/xpack/issues/1)
36 // therefore it is useless (the resulting file is bigger than with zlib)
37 //version = use_libxpack;
40 // see https://github.com/jibsen/brieflz
41 // it has spurious slowdowns, and so is 4 times slower than zlib, with worser compression
42 //version = use_libbrieflz;
44 // apple crap; i just wanted to see how bad it is ;-)
45 // speed is comparable with zlib, compression is shittier by 60MB; crap
46 //version = use_liblzfse;
51 // just for fun, slightly better than lzjb
54 // some compressors from wimlib
55 // see https://wimlib.net/
56 // only one can be selected!
57 // 15 times slower than zlib, much worser compression (~100MB bigger)
58 //version = use_libwim_lzms; // this supports chunks up to our maximum blob size
59 // two times faster than lzms, compression is still awful
60 //version = use_libwim_lzx; // this supports chunks up to 2MB; more-or-less useful
61 // quite fast (because it refuses to compress anything bigger than 64KB); compression is most awful
62 //version = use_libwim_xpress; // this supports chunks up to 64KB; useless
64 // oh, because why not?
65 // surprisingly good (but not as good as zlib), and lightning fast on default compression level
66 // sadly, requires external lib
69 private import std
.digest
.ripemd
;
70 private import iv
.encoding
;
71 private import iv
.cmdcon
;
72 private import iv
.strex
;
73 private import iv
.sq3
;
74 private import iv
.timer
;
75 private import iv
.vfs
.io
;
76 private import iv
.vfs
.util
;
78 private import iv
.dlzma
;
80 private import chibackend
.mbuilder
: DynStr
;
81 private import chibackend
.parse
;
82 private import chibackend
.decode
;
83 //private import iv.utfutil;
84 //private import iv.vfs.io;
86 version(use_libdeflate
) private import chibackend
.pack
.libdeflate
;
87 else version(use_balz
) private import iv
.balz
;
88 else version(use_libxpack
) private import chibackend
.pack
.libxpack
;
89 else version(use_libbrieflz
) private import chibackend
.pack
.libbrieflz
;
90 else version(use_liblzfse
) private import chibackend
.pack
.liblzfse
;
91 else version(use_lzjb
) private import chibackend
.pack
.lzjb
;
92 else version(use_libwim_lzms
) private import chibackend
.pack
.libwim
;
93 else version(use_libwim_lzx
) private import chibackend
.pack
.libwim
;
94 else version(use_libwim_xpress
) private import chibackend
.pack
.libwim
;
95 else version(use_lz4
) private import chibackend
.pack
.liblz4
;
96 else version(use_zstd
) private import chibackend
.pack
.libzstd
;
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 the contents)
526 -- (this is because we will receive them back from NNTP server later)
527 -- succesfully sent messages will be simply DELETEd
528 CREATE TABLE IF NOT EXISTS unsent (
529 uid INTEGER PRIMARY KEY /* the same as in the storage, not automatic */
530 , accid INTEGER /* account from which this message should be sent */
531 , from_pop3 TEXT /* "from" for POP3 */
532 , to_pop3 TEXT /* "to" for POP3 */
533 , data TEXT /* PACKED data to send */
537 static immutable string schemaSupportTempTables
= `
538 --DROP TABLE IF EXISTS treepane;
539 CREATE TEMP TABLE IF NOT EXISTS treepane (
540 iid INTEGER PRIMARY KEY
543 -- to make joins easier
547 CREATE INDEX IF NOT EXISTS treepane_uid ON treepane(uid);
550 enum schemaSupportIndex
= `
551 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_tag_uid ON threads(tagid, uid);
552 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_uid_tag ON threads(uid, tagid);
554 -- this is for views where threading is disabled
555 CREATE INDEX IF NOT EXISTS trd_by_tag_time ON threads(tagid, time);
556 --CREATE INDEX IF NOT EXISTS trd_by_tag_time_parent ON threads(tagid, time, parent);
557 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent_time ON threads(tagid, parent, time);
558 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent ON threads(tagid, parent);
559 CREATE INDEX IF NOT EXISTS trd_by_parent_tag ON threads(parent, tagid);
561 -- this is for test if we have any unread articles (we don't mind the exact numbers, tho)
562 CREATE INDEX IF NOT EXISTS trd_by_appearance ON threads(appearance);
563 -- this is for removing purged messages
564 CREATE INDEX IF NOT EXISTS trd_by_tag_appearance ON threads(tagid, appearance);
565 -- was used in table view creation, not used anymore
566 --CREATE INDEX IF NOT EXISTS trd_by_parent_tag_appearance ON threads(parent, tagid, appearance);
569 -- was used in table view creation, not used anymore
570 --CREATE INDEX IF NOT EXISTS trd_by_tag_appearance_time ON threads(tagid, appearance, time);
572 CREATE INDEX IF NOT EXISTS msgid_by_msgid_time ON msgids(msgid, time DESC);
574 CREATE INDEX IF NOT EXISTS refid_by_refids_idx ON refids(msgid, idx);
575 CREATE INDEX IF NOT EXISTS refid_by_uid_idx ON refids(uid, idx);
577 CREATE INDEX IF NOT EXISTS content_text_by_uid ON content_text(uid);
578 CREATE INDEX IF NOT EXISTS content_html_by_uid ON content_html(uid);
580 CREATE INDEX IF NOT EXISTS attaches_by_uid_name ON attaches(uid, name);
581 CREATE INDEX IF NOT EXISTS attaches_by_uid_idx ON attaches(uid, idx);
583 -- "info" indicies for twits
584 CREATE INDEX IF NOT EXISTS info_by_from_mail_name ON info(from_mail, from_name);
585 --CREATE INDEX IF NOT EXISTS info_by_from_mail ON info(from_mail);
586 CREATE INDEX IF NOT EXISTS info_by_from_name ON info(from_name);
589 CREATE INDEX IF NOT EXISTS unsent_by_accid ON unsent(accid);
592 static immutable string schemaSupport
= schemaSupportTable
~schemaSupportIndex
;
595 version(fts5_use_porter
) {
596 enum FTS5_Tokenizer
= "porter unicode61 remove_diacritics 2";
598 enum FTS5_Tokenizer
= "unicode61 remove_diacritics 2";
601 static immutable string recreateFTS5
= `
602 DROP TABLE IF EXISTS fts5_messages;
603 CREATE VIRTUAL TABLE fts5_messages USING fts5(
604 sender /* sender name and email, separated by " \x1a " (dec 26) (substitute char) */
605 , subj /* email subject */
606 , text /* email body, text/plain */
607 , html /* email body, text/html */
608 --, uid UNINDEXED /* message uid this comes from (not needed, use "rowid" instead */
609 , tokenize = '`~FTS5_Tokenizer
~`'
610 , content = 'fts5_msgview'
611 , content_rowid = 'uid'
613 /* sender, subj, text, html */
614 INSERT INTO fts5_messages(fts5_messages, rank) VALUES('rank', 'bm25(1.0, 3.0, 10.0, 6.0)');
617 static immutable string repopulateFTS5
= `
618 SELECT ChiroTimerStart('updating FTS5');
621 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
622 SELECT uid, sender, subj, text, html
626 SELECT threads.tagid FROM threads
627 INNER JOIN tagnames USING(tagid)
629 threads.uid=fts5_msgview.uid AND
630 tagnames.hidden=0 AND SUBSTR(tagnames.tag, 1, 1)='/'
634 SELECT ChiroTimerStop();
638 static immutable string recreateFTS5Triggers
= `
639 -- triggers to keep the FTS index up to date
641 -- this rely on the proper "info" table update order
642 -- info must be inserted LAST
643 DROP TRIGGER IF EXISTS fts5xtrig_insert;
644 CREATE TRIGGER fts5xtrig_insert
647 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
648 SELECT uid, sender, subj, text, html FROM fts5_msgview WHERE uid=NEW.uid LIMIT 1;
651 -- not AFTER, because we still need a valid view!
652 -- this rely on the proper "info" table update order
653 -- info must be deleted FIRST
654 DROP TRIGGER IF EXISTS fts5xtrig_delete;
655 CREATE TRIGGER fts5xtrig_delete
656 BEFORE DELETE ON info
658 INSERT INTO fts5_messages(fts5_messages, rowid, sender, subj, text, html)
659 SELECT 'delete', uid, sender, subj, text, html FROM fts5_msgview WHERE uid=OLD.uid LIMIT 1;
662 -- message texts should NEVER be updated, so no ON UPDATE trigger
666 // ////////////////////////////////////////////////////////////////////////// //
667 // not properly implemented yet
668 //version = lazy_mt_safe;
670 version(lazy_mt_safe
) {
671 enum lazy_mt_safe_flag
= true;
673 enum lazy_mt_safe_flag
= false;
676 public struct LazyStatement(string dbname
) {
686 DBStatement st
= void;
687 version(lazy_mt_safe
) {
688 sqlite3_mutex
* mutex
= void;
691 usize sqlsize
= void;
692 uint compiled
= void;
698 string delayInit
= null;
701 inout(Data
)* datap () inout pure nothrow @trusted @nogc { pragma(inline
, true); return cast(Data
*)udata
; }
702 void datap (Data
*v
) pure nothrow @trusted @nogc { pragma(inline
, true); udata
= cast(usize
)v
; }
706 @disable this (this);
712 static if (dbname == "View" || dbname == "view") dbtype = DB.View;
713 else static if (dbname == "Store" || dbname == "store") dbtype = DB.Store;
714 else static if (dbname == "Conf" || dbname == "conf") dbtype = DB.Conf;
715 else static assert(0, "invalid db name: '"~dbname~"'");
716 import core.stdc.stdlib : calloc;
717 Data* dp = cast(Data*)calloc(1, Data.sizeof);
718 if (dp is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
720 dp.sql = cast(char*)calloc(1, sql.length);
721 if (dp.sql is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
722 dp.sql[0..sql.length] = sql[];
723 dp.sqlsize = sql.length;
724 version(lazy_mt_safe) {
725 dp.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
726 if (dp.mutex is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); }
730 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
734 import core
.stdc
.stdlib
: free
;
737 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===DEINIT===\n%s\n============\n", dp.sql); }
738 dp
.st
= DBStatement
.init
;
740 version(lazy_mt_safe
) {
741 sqlite3_mutex_free(dp
.mutex
);
747 bool valid () pure nothrow @safe @nogc { pragma(inline
, true); return (udata
!= 0 || delayInit
.length
); }
749 private void setupWith (const(char)[] sql
) {
750 if (udata
) throw new Exception("statement already inited");
752 static if (dbname
== "View" || dbname
== "view") dbtype
= DB
.View
;
753 else static if (dbname
== "Store" || dbname
== "store") dbtype
= DB
.Store
;
754 else static if (dbname
== "Conf" || dbname
== "conf") dbtype
= DB
.Conf
;
755 else static assert(0, "invalid db name: '"~dbname
~"'");
756 import core
.stdc
.stdlib
: calloc
;
757 Data
* dp
= cast(Data
*)calloc(1, Data
.sizeof
);
758 if (dp
is null) { import core
.exception
: onOutOfMemoryErrorNoGC
; onOutOfMemoryErrorNoGC(); }
760 dp
.sql
= cast(char*)calloc(1, sql
.length
);
761 if (dp
.sql
is null) { import core
.exception
: onOutOfMemoryErrorNoGC
; onOutOfMemoryErrorNoGC(); }
762 dp
.sql
[0..sql
.length
] = sql
[];
763 dp
.sqlsize
= sql
.length
;
764 version(lazy_mt_safe
) {
765 dp
.mutex
= sqlite3_mutex_alloc(SQLITE_MUTEX_FAST
);
766 if (dp
.mutex
is null) { import core
.exception
: onOutOfMemoryErrorNoGC
; onOutOfMemoryErrorNoGC(); }
769 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===INIT===\n%s\n==========\n", dp.sql); }
772 ref DBStatement
st () {
774 //throw new Exception("no statement set");
775 setupWith(delayInit
);
779 version(lazy_mt_safe
) {
780 sqlite3_mutex_enter(dp
.mutex
);
783 version(lazy_mt_safe
) {
784 sqlite3_mutex_leave(dp
.mutex
);
787 //{ import core.stdc.stdio : printf; printf("***compiling:\n%s\n=====\n", dp.sql); }
788 final switch (dbtype
) {
789 case DB
.Store
: dp
.st
= dbStore
.persistentStatement(dp
.sql
[0..dp
.sqlsize
]); break;
790 case DB
.View
: dp
.st
= dbView
.persistentStatement(dp
.sql
[0..dp
.sqlsize
]); break;
791 case DB
.Conf
: dp
.st
= dbConf
.persistentStatement(dp
.sql
[0..dp
.sqlsize
]); break;
794 //assert(dp.st.valid);
796 //assert(dp.st.valid);
802 // ////////////////////////////////////////////////////////////////////////// //
803 private bool isGoodText (const(void)[] buf
) pure nothrow @safe @nogc {
804 foreach (immutable ubyte ch
; cast(const(ubyte)[])buf
) {
806 if (ch
!= 9 && ch
!= 10 && ch
!= 13 && ch
!= 27) return false;
808 if (ch
== 127) return false;
812 //return utf8ValidText(buf);
816 // ////////////////////////////////////////////////////////////////////////// //
817 private bool isBadPrefix (const(char)[] buf
) pure nothrow @trusted @nogc {
818 if (buf
.length
< 5) return false;
820 buf
.ptr
[0] == '\x1b' &&
821 buf
.ptr
[1] >= 'A' && buf
.ptr
[1] <= 'Z' &&
822 buf
.ptr
[2] >= 'A' && buf
.ptr
[2] <= 'Z' &&
823 buf
.ptr
[3] >= 'A' && buf
.ptr
[3] <= 'Z' &&
824 buf
.ptr
[4] >= 'A' && buf
.ptr
[4] <= 'Z';
828 /* two high bits of the first byte holds the size:
829 00: fit into 6 bits: [0.. 0x3f] (1 byte)
830 01: fit into 14 bits: [0.. 0x3fff] (2 bytes)
831 10: fit into 22 bits: [0.. 0x3f_ffff] (3 bytes)
832 11: fit into 30 bits: [0..0x3fff_ffff] (4 bytes)
834 number is stored as big-endian.
835 will not write anything to `dest` if there is not enough room.
837 returns number of bytes, or 0 if the number is too big.
839 private uint encodeUInt (void[] dest
, uint v
) nothrow @trusted @nogc {
840 if (v
> 0x3fff_ffffU
) return 0;
841 ubyte[] d
= cast(ubyte[])dest
;
843 if (v
> 0x3f_ffffU
) {
846 d
.ptr
[0] = cast(ubyte)(v
>>24);
847 d
.ptr
[1] = cast(ubyte)(v
>>16);
848 d
.ptr
[2] = cast(ubyte)(v
>>8);
849 d
.ptr
[3] = cast(ubyte)v
;
857 d
.ptr
[0] = cast(ubyte)(v
>>16);
858 d
.ptr
[1] = cast(ubyte)(v
>>8);
859 d
.ptr
[2] = cast(ubyte)v
;
867 d
.ptr
[0] = cast(ubyte)(v
>>8);
868 d
.ptr
[1] = cast(ubyte)v
;
873 if (d
.length
>= 1) d
.ptr
[0] = cast(ubyte)v
;
878 private uint decodeUIntLength (const(void)[] dest
) pure nothrow @trusted @nogc {
879 const(ubyte)[] d
= cast(const(ubyte)[])dest
;
880 if (d
.length
== 0) return 0;
881 switch (d
.ptr
[0]&0xc0) {
883 case 0x40: return (d
.length
>= 2 ?
2 : 0);
884 case 0x80: return (d
.length
>= 3 ?
3 : 0);
887 return (d
.length
>= 4 ?
4 : 0);
891 // returns uint.max on error (impossible value)
892 private uint decodeUInt (const(void)[] dest
) pure nothrow @trusted @nogc {
893 const(ubyte)[] d
= cast(const(ubyte)[])dest
;
894 if (d
.length
== 0) return uint.max
;
896 switch (d
.ptr
[0]&0xc0) {
901 if (d
.length
< 2) return uint.max
;
902 res
= ((d
.ptr
[0]&0x3fU
)<<8)|d
.ptr
[1];
905 if (d
.length
< 3) return uint.max
;
906 res
= ((d
.ptr
[0]&0x3fU
)<<16)|
(d
.ptr
[1]<<8)|d
.ptr
[2];
909 if (d
.length
< 4) return uint.max
;
910 res
= ((d
.ptr
[0]&0x3fU
)<<24)|
(d
.ptr
[1]<<16)|
(d
.ptr
[2]<<8)|d
.ptr
[3];
917 // returns position AFTER the headers (empty line is skipped too)
918 // returned value is safe for slicing
919 private int sq3Supp_FindHeadersEnd (const(char)* vs
, const int sz
) {
920 import core
.stdc
.string
: memchr
;
921 if (sz
<= 0) return 0;
922 const(char)* eptr
= cast(const(char)*)memchr(vs
, '\n', cast(uint)sz
);
923 while (eptr
!is null) {
925 int epos
= cast(int)cast(usize
)(eptr
-vs
);
926 if (sz
-epos
< 1) break;
928 if (sz
-epos
< 2) break;
932 if (*eptr
== '\n') return epos
+1;
934 eptr
= cast(const(char)*)memchr(eptr
, '\n', cast(uint)(sz
-epos
));
940 // hack for some invalid dates
941 uint parseMailDate (const(char)[] s
) nothrow {
943 if (s
.length
== 0) return 0;
945 return cast(uint)(parseRFC822DateTime(s
).toUTC
.toUnixTime
);
946 } catch (Exception
) {}
947 // sometimes this helps
949 foreach_reverse (immutable char ch
; s
) {
950 if (ch
< '0' || ch
> '9') break;
953 if (dcount
> 4) return 0;
954 s
~= "0000"[0..4-dcount
];
956 return cast(uint)(parseRFC822DateTime(s
).toUTC
.toUnixTime
);
957 } catch (Exception
) {}
962 // ////////////////////////////////////////////////////////////////////////// //
966 ** ChiroPackLZMA(content)
967 ** ChiroPackLZMA(content, packflag)
969 ** second form accepts int flag; 0 means "don't pack"
971 private void sq3Fn_ChiroPackLZMA (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) nothrow @trusted {
972 if (argc
< 1 || argc
> 2) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroPackLZMA()`", -1); return; }
973 int packlevel
= (argc
> 1 ?
sqlite3_value_int(argv
[1]) : ChiroDefaultPackLevel
);
974 if (packlevel
< 0) packlevel
= 5/*lzma default*/; else if (packlevel
> 9) packlevel
= 9;
976 sqlite3_value
*val
= argv
[0];
978 immutable int sz
= sqlite3_value_bytes(val
);
979 if (sz
< 0 || sz
> 0x3fffffff-4) { sqlite3_result_error_toobig(ctx
); return; }
981 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
983 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(val
);
984 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroPackLZMA()`", -1); return; }
986 if (sz
>= 0x3fffffff-8) {
987 if (isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_error_toobig(ctx
); return; }
988 sqlite3_result_value(ctx
, val
);
992 import core
.stdc
.stdlib
: malloc
, free
;
993 import core
.stdc
.string
: memcpy
;
995 if (packlevel
> 0 && sz
> 8) {
996 import core
.stdc
.stdio
: snprintf
;
998 xsz
[0..5] = "\x1bLZMA";
999 uint xszlen
= encodeUInt(xsz
[5..$], cast(uint)sz
);
1002 immutable uint bsz
= cast(uint)sz
;
1003 char* cbuf
= cast(char*)malloc(bsz
+xszlen
+LZMA_PROPS_SIZE
+1+16);
1005 if (isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_error_nomem(ctx
); return; }
1007 cbuf
[0..xszlen
] = xsz
[0..xszlen
];
1008 usize destLen
= bsz
;
1009 ubyte[LZMA_PROPS_SIZE
+8] hdr
= void;
1010 uint hdrSize
= cast(uint)hdr
.sizeof
;
1012 CLzmaEncProps props
;
1013 props
.level
= packlevel
;
1014 props
.dictSize
= 1<<22; //4MB
1015 props
.reduceSize
= bsz
;
1017 SRes res
= LzmaEncode(cast(ubyte*)cbuf
+xszlen
+LZMA_PROPS_SIZE
+1, &destLen
, cast(const(ubyte)*)vs
, bsz
, &props
, hdr
.ptr
, &hdrSize
, 0/*writeEndMark*/, null, &lzmaDefAllocator
, &lzmaDefAllocator
);
1018 assert(hdrSize
== LZMA_PROPS_SIZE
);
1019 if (res
== SZ_OK
&& destLen
+xszlen
+LZMA_PROPS_SIZE
+1 < cast(usize
)sz
) {
1020 import core
.stdc
.string
: memcpy
;
1021 cbuf
[xszlen
] = LZMA_PROPS_SIZE
;
1022 memcpy(cbuf
+xszlen
+1, hdr
.ptr
, LZMA_PROPS_SIZE
);
1023 sqlite3_result_blob(ctx
, cbuf
, destLen
+xszlen
+LZMA_PROPS_SIZE
+1, &free
);
1031 if (isBadPrefix(vs
[0..cast(uint)sz
])) {
1032 char *res
= cast(char *)malloc(sz
+5);
1033 if (res
is null) { sqlite3_result_error_nomem(ctx
); return; }
1034 res
[0..5] = "\x1bRAWB";
1035 res
[5..sz
+5] = vs
[0..sz
];
1036 if (isGoodText(vs
[0..cast(usize
)sz
])) {
1037 sqlite3_result_text(ctx
, res
, sz
+5, &free
);
1039 sqlite3_result_blob(ctx
, res
, sz
+5, &free
);
1042 immutable bool wantBlob
= !isGoodText(vs
[0..cast(usize
)sz
]);
1043 immutable int tp
= sqlite3_value_type(val
);
1044 if ((wantBlob
&& tp
== SQLITE_BLOB
) ||
(!wantBlob
&& tp
== SQLITE3_TEXT
)) {
1045 sqlite3_result_value(ctx
, val
);
1046 } else if (wantBlob
) {
1047 sqlite3_result_blob(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1049 sqlite3_result_text(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1056 ** ChiroPack(content)
1057 ** ChiroPack(content, packflag)
1059 ** second form accepts int flag; 0 means "don't pack"
1061 private void sq3Fn_ChiroPackCommon (sqlite3_context
*ctx
, sqlite3_value
*val
, int packlevel
) nothrow @trusted {
1062 immutable int sz
= sqlite3_value_bytes(val
);
1063 if (sz
< 0 || sz
> 0x3fffffff-4) { sqlite3_result_error_toobig(ctx
); return; }
1065 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1067 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(val
);
1068 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroPack()`", -1); return; }
1070 if (sz
>= 0x3fffffff-8) {
1071 if (isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_error_toobig(ctx
); return; }
1072 sqlite3_result_value(ctx
, val
);
1076 import core
.stdc
.stdlib
: malloc
, free
;
1077 import core
.stdc
.string
: memcpy
;
1079 if (packlevel
> 0 && sz
> 8) {
1080 import core
.stdc
.stdio
: snprintf
;
1081 char[16] xsz
= void;
1083 xsz
[0..5] = "\x1bBALZ";
1084 } else version(use_libxpack
) {
1085 xsz
[0..5] = "\x1bXPAK";
1086 } else version(use_libbrieflz
) {
1087 xsz
[0..5] = "\x1bBRLZ";
1088 } else version(use_liblzfse
) {
1089 xsz
[0..5] = "\x1bLZFS";
1090 } else version(use_lzjb
) {
1091 xsz
[0..5] = "\x1bLZJB";
1092 } else version(use_libwim_lzms
) {
1093 xsz
[0..5] = "\x1bLZMS";
1094 } else version(use_libwim_lzx
) {
1095 xsz
[0..5] = "\x1bLZMX";
1096 } else version(use_libwim_xpress
) {
1097 xsz
[0..5] = "\x1bXPRS";
1098 } else version(use_lz4
) {
1099 xsz
[0..5] = "\x1bLZ4D";
1100 } else version(use_zstd
) {
1101 xsz
[0..5] = "\x1bZSTD";
1103 xsz
[0..5] = "\x1bZLIB";
1105 immutable uint xszlenNum
= encodeUInt(xsz
[5..$], cast(uint)sz
);
1107 immutable uint xszlen
= xszlenNum
+5;
1108 //xsz[xszlen++] = ':';
1109 version(use_libbrieflz
) {
1110 immutable usize bsz
= blz_max_packed_size(cast(usize
)sz
);
1111 } else version(use_lzjb
) {
1112 immutable uint bsz
= cast(uint)sz
+1024;
1113 } else version(use_lz4
) {
1114 immutable uint bsz
= cast(uint)LZ4_compressBound(sz
)+1024;
1116 immutable uint bsz
= cast(uint)sz
;
1118 char* cbuf
= cast(char*)malloc(bsz
+xszlen
+64);
1120 if (isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_error_nomem(ctx
); return; }
1122 cbuf
[0..xszlen
] = xsz
[0..xszlen
];
1126 usize dpos
= xszlen
;
1131 if (spos
>= cast(usize
)sz
) return 0;
1132 usize left
= cast(usize
)sz
-spos
;
1133 if (left
> buf
.length
) left
= buf
.length
;
1134 if (left
) memcpy(buf
.ptr
, vs
+spos
, left
);
1140 if (dpos
+buf
.length
>= cast(usize
)sz
) throw new Exception("uncompressible");
1141 memcpy(cbuf
+dpos
, buf
.ptr
, buf
.length
);
1144 // maximum compression?
1147 } catch(Exception
) {
1150 if (dpos
< cast(usize
)sz
) {
1151 sqlite3_result_blob(ctx
, cbuf
, dpos
, &free
);
1154 } else version(use_libdeflate
) {
1155 if (packlevel
> 12) packlevel
= 12;
1156 libdeflate_compressor
*cpr
= libdeflate_alloc_compressor(packlevel
);
1157 if (cpr
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1158 usize dsize
= libdeflate_zlib_compress(cpr
, vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
);
1159 libdeflate_free_compressor(cpr
);
1160 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1161 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1164 } else version(use_libxpack
) {
1165 // 2^19 (524288) bytes. This is definitely a big problem and I am planning to address it.
1166 // https://github.com/ebiggers/xpack/issues/1
1167 if (sz
< 524288-64) {
1168 if (packlevel
> 9) packlevel
= 9;
1169 xpack_compressor
*cpr
= xpack_alloc_compressor(cast(usize
)sz
, packlevel
);
1170 if (cpr
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1171 usize dsize
= xpack_compress(cpr
, vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
);
1172 xpack_free_compressor(cpr
);
1173 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1174 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1178 } else version(use_libbrieflz
) {
1179 if (packlevel
> 10) packlevel
= 10;
1180 immutable usize wbsize
= blz_workmem_size_level(cast(usize
)sz
, packlevel
);
1181 void* wbuf
= cast(void*)malloc(wbsize
+!wbsize
);
1182 if (wbuf
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1183 uint dsize
= blz_pack_level(vs
, cbuf
+xszlen
, cast(uint)sz
, wbuf
, packlevel
);
1185 if (dsize
+xszlen
< cast(usize
)sz
) {
1186 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1189 } else version(use_liblzfse
) {
1190 immutable usize wbsize
= lzfse_encode_scratch_size();
1191 void* wbuf
= cast(void*)malloc(wbsize
+!wbsize
);
1192 if (wbuf
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1193 usize dsize
= lzfse_encode_buffer(cbuf
+xszlen
, bsz
, vs
, cast(uint)sz
, wbuf
);
1195 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1196 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1199 } else version(use_lzjb
) {
1200 usize dsize
= lzjb_compress(vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
);
1201 if (dsize
== usize
.max
) dsize
= 0;
1202 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1203 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1206 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "LZJB FAILED!\n"); }
1207 } else version(use_libwim_lzms
) {
1208 wimlib_compressor
* cpr
;
1209 uint clevel
= (packlevel
< 10 ?
50 : 1000);
1210 int rc
= wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZMS
, cast(usize
)sz
, clevel
, &cpr
);
1211 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1212 usize dsize
= wimlib_compress(vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
, cpr
);
1213 wimlib_free_compressor(cpr
);
1214 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1215 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1218 } else version(use_libwim_lzx
) {
1219 if (sz
<= WIMLIB_LZX_MAX_CHUNK
) {
1220 wimlib_compressor
* cpr
;
1221 uint clevel
= (packlevel
< 10 ?
50 : 1000);
1222 int rc
= wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZX
, cast(usize
)sz
, clevel
, &cpr
);
1223 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1224 usize dsize
= wimlib_compress(vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
, cpr
);
1225 wimlib_free_compressor(cpr
);
1226 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1227 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1231 } else version(use_libwim_xpress
) {
1232 if (sz
<= WIMLIB_XPRESS_MAX_CHUNK
) {
1233 wimlib_compressor
* cpr
;
1234 uint clevel
= (packlevel
< 10 ?
50 : 1000);
1235 uint csz
= WIMLIB_XPRESS_MIN_CHUNK
;
1236 while (csz
< WIMLIB_XPRESS_MAX_CHUNK
&& csz
< cast(uint)sz
) csz
*= 2U;
1237 int rc
= wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_XPRESS
, csz
, clevel
, &cpr
);
1238 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1239 usize dsize
= wimlib_compress(vs
, cast(usize
)sz
, cbuf
+xszlen
, bsz
, cpr
);
1240 wimlib_free_compressor(cpr
);
1241 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
1242 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1246 } else version(use_lz4
) {
1247 int dsize
= LZ4_compress_default(vs
, cbuf
+xszlen
, sz
, cast(int)bsz
);
1248 if (dsize
> 0 && dsize
+xszlen
< sz
) {
1249 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1252 } else version(use_zstd
) {
1253 immutable int clev
=
1254 packlevel
<= 3 ?
ZSTD_minCLevel() :
1255 packlevel
<= 6 ?
ZSTD_defaultCLevel() :
1256 packlevel
< 10 ?
19 :
1258 usize dsize
= ZSTD_compress(cbuf
+xszlen
, cast(int)bsz
, vs
, sz
, clev
);
1259 if (!ZSTD_isError(dsize
) && dsize
> 0 && dsize
+xszlen
< sz
) {
1260 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1264 import etc
.c
.zlib
: /*compressBound,*/ compress2
, Z_OK
;
1265 //uint bsz = cast(uint)compressBound(cast(uint)sz);
1266 if (packlevel
> 9) packlevel
= 9;
1268 immutable int zres
= compress2(cast(ubyte *)(cbuf
+xszlen
), &dsize
, cast(const(ubyte) *)vs
, sz
, packlevel
);
1269 if (zres
== Z_OK
&& dsize
+xszlen
< cast(usize
)sz
) {
1270 if (!ChiroPackTryHard
) {
1271 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1279 if (ChiroPackTryHard
) {
1280 char* lzmabuf
= cast(char*)malloc(bsz
+xszlen
+LZMA_PROPS_SIZE
+1+64);
1281 if (lzmabuf
!is null) {
1282 lzmabuf
[0..xszlen
] = xsz
[0..xszlen
];
1283 lzmabuf
[1..5] = "LZMA";
1284 usize destLen
= (cbuf
is null ? bsz
: dsize
); // do not take more than zlib
1285 if (destLen
> bsz
) destLen
= bsz
; // just in case
1286 ubyte[LZMA_PROPS_SIZE
+8] hdr
= void;
1287 uint hdrSize
= cast(uint)hdr
.sizeof
;
1289 CLzmaEncProps props
;
1290 props
.level
= packlevel
;
1291 props
.dictSize
= 1<<22; //4MB
1292 props
.reduceSize
= bsz
;
1294 immutable SRes nres
= LzmaEncode(cast(ubyte*)(lzmabuf
+xszlen
+LZMA_PROPS_SIZE
+1), &destLen
, cast(const(ubyte)*)vs
, bsz
, &props
, hdr
.ptr
, &hdrSize
, 0/*writeEndMark*/, null, &lzmaDefAllocator
, &lzmaDefAllocator
);
1295 assert(hdrSize
== LZMA_PROPS_SIZE
);
1296 if (nres
== SZ_OK
&& destLen
+xszlen
+LZMA_PROPS_SIZE
+1 < cast(usize
)sz
) {
1297 if (cbuf
is null || destLen
+xszlen
+LZMA_PROPS_SIZE
+1 < dsize
+xszlen
) {
1298 if (cbuf
!is null) free(cbuf
); // free zlib result
1299 import core
.stdc
.string
: memcpy
;
1300 lzmabuf
[xszlen
] = LZMA_PROPS_SIZE
;
1301 memcpy(lzmabuf
+xszlen
+1, hdr
.ptr
, LZMA_PROPS_SIZE
);
1302 sqlite3_result_blob(ctx
, lzmabuf
, destLen
+xszlen
+LZMA_PROPS_SIZE
+1, &free
);
1309 // return zlib result?
1310 if (cbuf
!is null) {
1311 assert(dsize
< bsz
);
1312 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
1316 if (cbuf
!is null) free(cbuf
);
1321 if (isBadPrefix(vs
[0..cast(uint)sz
])) {
1322 char *res
= cast(char *)malloc(sz
+5);
1323 if (res
is null) { sqlite3_result_error_nomem(ctx
); return; }
1324 res
[0..5] = "\x1bRAWB";
1325 res
[5..sz
+5] = vs
[0..sz
];
1326 if (isGoodText(vs
[0..cast(usize
)sz
])) {
1327 sqlite3_result_text(ctx
, res
, sz
+5, &free
);
1329 sqlite3_result_blob(ctx
, res
, sz
+5, &free
);
1332 immutable bool wantBlob
= !isGoodText(vs
[0..cast(usize
)sz
]);
1333 immutable int tp
= sqlite3_value_type(val
);
1334 if ((wantBlob
&& tp
== SQLITE_BLOB
) ||
(!wantBlob
&& tp
== SQLITE3_TEXT
)) {
1335 sqlite3_result_value(ctx
, val
);
1336 } else if (wantBlob
) {
1337 sqlite3_result_blob(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1339 sqlite3_result_text(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1346 ** ChiroPack(content)
1348 private void sq3Fn_ChiroPack (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) nothrow @trusted {
1349 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroPack()`", -1); return; }
1350 return sq3Fn_ChiroPackCommon(ctx
, argv
[0], ChiroCompressionLevel
);
1355 ** ChiroPack(content, packlevel)
1357 ** `packlevel` == 0 means "don't pack"
1358 ** `packlevel` == 9 means "maximum compression"
1360 private void sq3Fn_ChiroPackDPArg (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) nothrow @trusted {
1361 if (argc
!= 2) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroPack()`", -1); return; }
1362 return sq3Fn_ChiroPackCommon(ctx
, argv
[0], sqlite3_value_int(argv
[1]));
1367 ** ChiroGetPackType(content)
1369 private void sq3Fn_ChiroGetPackType (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1370 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroGetPackType()`", -1); return; }
1372 int sz
= sqlite3_value_bytes(argv
[0]);
1373 if (sz
< 5 || sz
> 0x3fffffff-4) { sqlite3_result_text(ctx
, "RAWB", 4, SQLITE_STATIC
); return; }
1375 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1376 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1378 if (!isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_text(ctx
, "RAWB", 4, SQLITE_STATIC
); return; }
1380 sqlite3_result_text(ctx
, vs
+1, 4, SQLITE_TRANSIENT
);
1385 ** ChiroUnpack(content)
1387 ** it is (almost) safe to pass non-packed content here
1389 private void sq3Fn_ChiroUnpack (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1390 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!000\n"); }
1391 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroUnpack()`", -1); return; }
1393 int sz
= sqlite3_value_bytes(argv
[0]);
1394 if (sz
< 0 || sz
> 0x3fffffff-4) { sqlite3_result_error_toobig(ctx
); return; }
1396 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1398 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1399 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1401 if (!isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_value(ctx
, argv
[0]); return; }
1402 if (vs
[0..5] == "\x1bRAWB") { sqlite3_result_blob(ctx
, vs
+5, sz
-5, SQLITE_TRANSIENT
); return; }
1403 if (sz
< 6) { sqlite3_result_error(ctx
, "invalid data in `ChiroUnpack()`", -1); return; }
1420 int codec
= Codec_ZLIB
;
1421 if (vs
[0..5] != "\x1bZLIB") {
1422 if (vs
[0..5] == "\x1bLZMA") codec
= Codec_LZMA
;
1424 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bBALZ") codec
= Codec_BALZ
;
1426 version(use_libxpack
) {
1427 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bXPAK") codec
= Codec_XPAK
;
1429 version(use_libxpack
) {
1430 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bXPAK") codec
= Codec_XPAK
;
1432 version(use_libbrieflz
) {
1433 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bBRLZ") codec
= Codec_BRLZ
;
1435 version(use_liblzfse
) {
1436 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZFS") codec
= Codec_LZFS
;
1439 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZJB") codec
= Codec_LZJB
;
1441 version(use_libwim_lzms
) {
1442 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZMS") codec
= Codec_LZMS
;
1444 version(use_libwim_lzx
) {
1445 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZMX") codec
= Codec_LZMX
;
1447 version(use_libwim_xpress
) {
1448 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bXPRS") codec
= Codec_XPRS
;
1451 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZ4D") codec
= Codec_LZ4D
;
1454 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bZSTD") codec
= Codec_ZSTD
;
1456 if (codec
== Codec_ZLIB
) { sqlite3_result_error(ctx
, "invalid codec in `ChiroUnpack()`", -1); return; }
1460 // size is guaranteed to be at least 6 here
1464 immutable uint numsz
= decodeUIntLength(vs
[0..cast(uint)sz
]);
1465 //{ import core.stdc.stdio : printf; printf("sz=%d; numsz=%u; %02X %02X %02X %02X\n", sz, numsz, cast(uint)vs[5], cast(uint)vs[6], cast(uint)vs[7], cast(uint)vs[8]); }
1466 //writeln("sq3Fn_ChiroUnpack: nsz=", sz-5);
1467 if (numsz
== 0 || numsz
> cast(uint)sz
) { sqlite3_result_error(ctx
, "invalid data in `ChiroUnpack()`", -1); return; }
1468 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!100\n"); }
1469 immutable uint rsize
= decodeUInt(vs
[0..cast(uint)sz
]);
1470 if (rsize
== uint.max
) { sqlite3_result_error(ctx
, "invalid data in `ChiroUnpack()`", -1); return; }
1471 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!101:rsize=%u\n", rsize); }
1472 if (rsize
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1475 sz
-= cast(int)numsz
;
1476 //{ import core.stdc.stdio : printf; printf("sz=%d; rsize=%u\n", sz, rsize, dpos); }
1478 import core
.stdc
.stdlib
: malloc
, free
;
1479 import core
.stdc
.string
: memcpy
;
1481 char* cbuf
= cast(char*)malloc(rsize
);
1482 if (cbuf
is null) { sqlite3_result_error_nomem(ctx
); return; }
1483 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz-dpos);
1485 usize dsize
= rsize
;
1486 final switch (codec
) {
1488 version(use_libdeflate
) {
1489 libdeflate_decompressor
*dcp
= libdeflate_alloc_decompressor();
1490 if (dcp
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1491 auto rc
= libdeflate_zlib_decompress(dcp
, vs
, cast(usize
)sz
, cbuf
, rsize
, null);
1492 if (rc
!= LIBDEFLATE_SUCCESS
) {
1494 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1498 import etc
.c
.zlib
: uncompress
, Z_OK
;
1499 int zres
= uncompress(cast(ubyte *)cbuf
, &dsize
, cast(const(ubyte) *)vs
, sz
);
1500 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz, "; dsize=", dsize, "; zres=", zres);
1501 if (zres
!= Z_OK || dsize
!= rsize
) {
1503 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1510 if (sz
< LZMA_PROPS_SIZE
+1 || vs
[0] != LZMA_PROPS_SIZE
) {
1512 sqlite3_result_error(ctx
, "broken LZMA data in `ChiroUnpack()`", -1);
1516 usize srcSize
= sz
-vs
[0]-1;
1518 SRes zres
= LzmaDecode(cast(ubyte *)cbuf
, &dsize
, cast(const(ubyte) *)vs
+vs
[0]+1, &srcSize
,
1519 cast(const(ubyte)*)(vs
+1)/*propData*/, vs
[0]/*propSize*/, LZMA_FINISH_ANY
, &status
, &lzmaDefAllocator
);
1520 if (zres
!= SZ_OK || dsize
!= rsize || status
== LZMA_STATUS_FINISHED_WITH_MARK || status
== LZMA_STATUS_NEEDS_MORE_INPUT
) {
1522 sqlite3_result_error(ctx
, "broken LZMA data in `ChiroUnpack()`", -1);
1533 auto dc
= bz
.decompress(
1536 uint left
= cast(uint)sz
-spos
;
1537 if (left
> buf
.length
) left
= cast(uint)buf
.length
;
1538 if (left
!= 0) memcpy(buf
.ptr
, vs
, left
);
1544 uint left
= rsize
-outpos
;
1545 if (left
== 0) throw new Exception("broken data");
1546 if (left
> buf
.length
) left
= cast(uint)buf
.length
;
1547 if (left
) memcpy(cbuf
+outpos
, buf
.ptr
, left
);
1551 if (dc
!= rsize
) throw new Exception("broken data");
1552 } catch (Exception
) {
1555 if (outpos
== uint.max
) {
1557 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1563 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1568 version(use_libxpack
) {
1569 xpack_decompressor
*dcp
= xpack_alloc_decompressor();
1570 if (dcp
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1571 auto rc
= xpack_decompress(dcp
, vs
, cast(usize
)sz
, cbuf
, rsize
, null);
1572 if (rc
!= DECOMPRESS_SUCCESS
) {
1574 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1579 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1584 version(use_libbrieflz
) {
1585 dsize
= blz_depack_safe(vs
, cast(uint)sz
, cbuf
, rsize
);
1586 if (dsize
!= rsize
) {
1588 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1593 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1598 version(use_liblzfse
) {
1599 immutable usize wbsize
= lzfse_decode_scratch_size();
1600 void* wbuf
= cast(void*)malloc(wbsize
+!wbsize
);
1601 if (wbuf
is null) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1602 dsize
= lzfse_decode_buffer(cbuf
, cast(usize
)rsize
, vs
, cast(usize
)sz
, wbuf
);
1604 if (dsize
== 0 || dsize
!= rsize
) {
1606 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1611 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1617 dsize
= lzjb_decompress(vs
, cast(usize
)sz
, cbuf
, rsize
);
1618 if (dsize
!= rsize
) {
1620 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1625 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1630 version(use_libwim_lzms
) {
1631 wimlib_decompressor
* dpr
;
1632 int rc
= wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZMS
, rsize
, &dpr
);
1633 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1634 rc
= wimlib_decompress(vs
, cast(usize
)sz
, cbuf
, rsize
, dpr
);
1635 wimlib_free_decompressor(dpr
);
1638 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1643 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1648 version(use_libwim_lzx
) {
1649 wimlib_decompressor
* dpr
;
1650 int rc
= wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZX
, rsize
, &dpr
);
1651 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1652 rc
= wimlib_decompress(vs
, cast(usize
)sz
, cbuf
, rsize
, dpr
);
1653 wimlib_free_decompressor(dpr
);
1656 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1661 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1666 version(use_libwim_xpress
) {
1667 wimlib_decompressor
* dpr
;
1668 uint csz
= WIMLIB_XPRESS_MIN_CHUNK
;
1669 while (csz
< WIMLIB_XPRESS_MAX_CHUNK
&& csz
< rsize
) csz
*= 2U;
1670 int rc
= wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_XPRESS
, csz
, &dpr
);
1671 if (rc
!= 0) { free(cbuf
); sqlite3_result_error_nomem(ctx
); return; }
1672 rc
= wimlib_decompress(vs
, cast(usize
)sz
, cbuf
, rsize
, dpr
);
1673 wimlib_free_decompressor(dpr
);
1676 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1681 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1687 dsize
= LZ4_decompress_safe(vs
, cbuf
, sz
, rsize
);
1688 if (dsize
!= rsize
) {
1690 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1695 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1701 dsize
= ZSTD_decompress(cbuf
, rsize
, vs
, sz
);
1702 if (ZSTD_isError(dsize
) || dsize
!= rsize
) {
1704 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1709 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1715 if (isGoodText(cbuf
[0..dsize
])) {
1716 sqlite3_result_text(ctx
, cbuf
, cast(int)dsize
, &free
);
1718 sqlite3_result_blob(ctx
, cbuf
, cast(int)dsize
, &free
);
1724 ** ChiroNormCRLF(content)
1726 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1727 ** Removes trailing blanks.
1729 private void sq3Fn_ChiroNormCRLF (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1730 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroNormCRLF()`", -1); return; }
1732 int sz
= sqlite3_value_bytes(argv
[0]);
1733 if (sz
< 0 || sz
> 0x3fffffff) { sqlite3_result_error_toobig(ctx
); return; }
1735 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1737 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1738 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroNormCRLF()`", -1); return; }
1740 // check if we have something to do, and calculate new string size
1741 bool needwork
= false;
1742 if (vs
[cast(uint)sz
-1] <= 32) {
1744 while (sz
> 0 && vs
[cast(uint)sz
-1] <= 32) --sz
;
1745 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1747 uint newsz
= cast(uint)sz
;
1748 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1751 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) --newsz
;
1752 } else if (!needwork
) {
1753 needwork
= ((ch
< 32 && ch
!= 9 && ch
!= 10) || ch
== 127);
1758 if (sqlite3_value_type(argv
[0]) == SQLITE3_TEXT
) sqlite3_result_value(ctx
, argv
[0]);
1759 else sqlite3_result_text(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1763 assert(newsz
&& newsz
<= cast(uint)sz
);
1765 // need a new string
1766 import core
.stdc
.stdlib
: malloc
, free
;
1767 char* newstr
= cast(char*)malloc(newsz
);
1768 if (newstr
is null) { sqlite3_result_error_nomem(ctx
); return; }
1769 char* dest
= newstr
;
1770 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1772 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1774 if (ch
== 127) *dest
++ = '~';
1775 else if (ch
== 11 || ch
== 12) *dest
++ = '\n';
1776 else if (ch
< 32 && ch
!= 9 && ch
!= 10) *dest
++ = ' ';
1780 assert(dest
== newstr
+newsz
);
1782 sqlite3_result_text(ctx
, newstr
, cast(int)newsz
, &free
);
1787 ** ChiroNormHeaders(content)
1789 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1790 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1791 ** Removes trailing blanks.
1793 private void sq3Fn_ChiroNormHeaders (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1794 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroNormHeaders()`", -1); return; }
1796 int sz
= sqlite3_value_bytes(argv
[0]);
1797 if (sz
< 0 || sz
> 0x3fffffff) { sqlite3_result_error_toobig(ctx
); return; }
1799 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1801 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1802 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroNormHeaders()`", -1); return; }
1804 // check if we have something to do, and calculate new string size
1805 bool needwork
= false;
1806 if (vs
[cast(uint)sz
-1] <= 32) {
1808 while (sz
> 0 && vs
[cast(uint)sz
-1] <= 32) --sz
;
1809 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1811 uint newsz
= cast(uint)sz
;
1812 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1815 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) --newsz
;
1816 } else if (ch
== 10) {
1817 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] <= 32) { needwork
= true; --newsz
; }
1818 } else if (!needwork
) {
1819 needwork
= ((ch
< 32 && ch
!= 10) || ch
== 127);
1824 if (sqlite3_value_type(argv
[0]) == SQLITE3_TEXT
) sqlite3_result_value(ctx
, argv
[0]);
1825 else sqlite3_result_text(ctx
, vs
, sz
, SQLITE_TRANSIENT
);
1829 assert(newsz
&& newsz
<= cast(uint)sz
);
1831 // need a new string
1832 import core
.stdc
.stdlib
: malloc
, free
;
1833 char* newstr
= cast(char*)malloc(newsz
);
1834 if (newstr
is null) { sqlite3_result_error_nomem(ctx
); return; }
1835 char* dest
= newstr
;
1836 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1838 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1839 } else if (ch
== 10) {
1840 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] <= 32) {} else *dest
++ = '\n';
1842 if (ch
== 127) *dest
++ = '~';
1843 else if (ch
< 32 && ch
!= 10) *dest
++ = ' ';
1847 assert(dest
== newstr
+newsz
);
1849 sqlite3_result_text(ctx
, newstr
, cast(int)newsz
, &free
);
1854 ** ChiroExtractHeaders(content)
1856 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1857 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1858 ** Removes trailing blanks.
1860 private void sq3Fn_ChiroExtractHeaders (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1861 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1863 int sz
= sqlite3_value_bytes(argv
[0]);
1864 if (sz
< 0 || sz
> 0x3fffffff) { sqlite3_result_error_toobig(ctx
); return; }
1866 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1868 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1869 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1872 sz
= sq3Supp_FindHeadersEnd(vs
, sz
);
1874 // strip trailing blanks
1875 while (sz
> 0 && vs
[cast(uint)sz
-1U] <= 32) --sz
;
1876 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1878 // allocate new string (it can be smaller, but will never be bigger)
1879 import core
.stdc
.stdlib
: malloc
, free
;
1880 char* newstr
= cast(char*)malloc(cast(uint)sz
);
1881 if (newstr
is null) { sqlite3_result_error_nomem(ctx
); return; }
1882 char* dest
= newstr
;
1883 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1885 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1886 } else if (ch
== 10) {
1887 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] <= 32) {} else *dest
++ = '\n';
1889 if (ch
== 127) *dest
++ = '~';
1890 else if (ch
< 32 && ch
!= 10) *dest
++ = ' ';
1894 assert(dest
<= newstr
+cast(uint)sz
);
1895 sz
= cast(int)cast(usize
)(dest
-newstr
);
1896 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1897 sqlite3_result_text(ctx
, newstr
, sz
, &free
);
1902 ** ChiroExtractBody(content)
1904 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1905 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1906 ** Removes trailing blanks and final dot.
1908 private void sq3Fn_ChiroExtractBody (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1909 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1911 int sz
= sqlite3_value_bytes(argv
[0]);
1912 if (sz
< 0 || sz
> 0x3fffffff) { sqlite3_result_error_toobig(ctx
); return; }
1914 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1916 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1917 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1920 immutable int bstart
= sq3Supp_FindHeadersEnd(vs
, sz
);
1921 if (bstart
>= sz
) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1925 // strip trailing dot
1926 if (sz
>= 2 && vs
[cast(uint)sz
-2U] == '\r' && vs
[cast(uint)sz
-1U] == '\n') sz
-= 2;
1927 else if (sz
>= 1 && vs
[cast(uint)sz
-1U] == '\n') --sz
;
1928 if (sz
== 1 && vs
[0] == '.') sz
= 0;
1929 else if (sz
>= 2 && vs
[cast(uint)sz
-2U] == '\n' && vs
[cast(uint)sz
-1U] == '.') --sz
;
1930 else if (sz
>= 2 && vs
[cast(uint)sz
-2U] == '\r' && vs
[cast(uint)sz
-1U] == '.') --sz
;
1932 // strip trailing blanks
1933 while (sz
> 0 && vs
[cast(uint)sz
-1U] <= 32) --sz
;
1934 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1936 // allocate new string (it can be smaller, but will never be bigger)
1937 import core
.stdc
.stdlib
: malloc
, free
;
1938 char* newstr
= cast(char*)malloc(cast(uint)sz
);
1939 if (newstr
is null) { sqlite3_result_error_nomem(ctx
); return; }
1940 char* dest
= newstr
;
1941 foreach (immutable idx
, immutable char ch
; vs
[0..cast(uint)sz
]) {
1943 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1945 if (ch
== 127) *dest
++ = '~';
1946 else if (ch
== 11 || ch
== 12) *dest
++ = '\n';
1947 else if (ch
< 32 && ch
!= 9 && ch
!= 10) *dest
++ = ' ';
1951 assert(dest
<= newstr
+cast(uint)sz
);
1952 sz
= cast(int)cast(usize
)(dest
-newstr
);
1953 if (sz
== 0) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
1954 sqlite3_result_text(ctx
, newstr
, sz
, &free
);
1959 ** ChiroRIPEMD160(content)
1961 ** Calculates RIPEMD160 hash over the given content.
1963 ** Returns BINARY BLOB! You can use `tolower(hex(ChiroRIPEMD160(contents)))`
1964 ** to get lowercased hex hash string.
1966 private void sq3Fn_ChiroRIPEMD160 (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
1967 if (argc
!= 1) { sqlite3_result_error(ctx
, "invalid number of arguments to `ChiroRIPEMD160()`", -1); return; }
1969 immutable int sz
= sqlite3_value_bytes(argv
[0]);
1970 if (sz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
1972 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1973 if (!vs
&& sz
== 0) vs
= "";
1974 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in `ChiroRIPEMD160()`", -1); return; }
1976 ubyte[20] hash
= ripemd160Of(vs
[0..cast(uint)sz
]);
1977 sqlite3_result_blob(ctx
, cast(const(char)*)hash
.ptr
, cast(int)hash
.length
, SQLITE_TRANSIENT
);
1981 enum HeaderProcStartTpl(string fnname
) = `
1982 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"`~fnname
~`()\"", -1); return; }
1984 immutable int sz = sqlite3_value_bytes(argv[0]);
1985 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1987 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1988 if (!vs && sz == 0) vs = "";
1989 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"`~fnname
~`()\"", -1); return; }
1991 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
1996 ** ChiroHdr_NNTPIndex(headers)
1998 ** The content must be email with headers (or headers only).
1999 ** Returns "NNTP-Index" field or zero (int).
2001 private void sq3Fn_ChiroHdr_NNTPIndex (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2002 mixin(HeaderProcStartTpl
!"ChiroHdr_NNTPIndex");
2006 auto nntpidxfld
= findHeaderField(hdrs
, "NNTP-Index");
2007 if (nntpidxfld
.length
) {
2008 auto id
= nntpidxfld
.getFieldValue
;
2010 foreach (immutable ch
; id
) {
2011 if (ch
< '0' || ch
> '9') { nntpidx
= 0; break; }
2012 if (nntpidx
== 0 && ch
== '0') continue;
2013 immutable uint nn
= nntpidx
*10u+(ch
-'0');
2014 if (nn
<= nntpidx
) nntpidx
= 0x7fffffff; else nntpidx
= nn
;
2019 // it is safe, it can't overflow
2020 sqlite3_result_int(ctx
, cast(int)nntpidx
);
2025 ** ChiroHdr_RecvTime(headers)
2027 ** The content must be email with headers (or headers only).
2028 ** Returns unixtime (can be zero).
2030 private void sq3Fn_ChiroHdr_RecvTime (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2031 mixin(HeaderProcStartTpl
!"ChiroHdr_RecvTime");
2033 uint msgtime
= 0; // message receiving time
2035 auto datefld
= findHeaderField(hdrs
, "Injection-Date");
2036 if (datefld
.length
!= 0) {
2037 auto v
= datefld
.getFieldValue
;
2039 msgtime
= parseMailDate(v
);
2040 } catch (Exception
) {
2041 //writeln("UID=", uid, ": FUCKED INJECTION-DATE: |", v, "|");
2042 msgtime
= 0; // just in case
2047 // obsolete NNTP date field, because why not?
2048 datefld
= findHeaderField(hdrs
, "NNTP-Posting-Date");
2049 if (datefld
.length
!= 0) {
2050 auto v
= datefld
.getFieldValue
;
2052 msgtime
= parseMailDate(v
);
2053 } catch (Exception
) {
2054 //writeln("UID=", uid, ": FUCKED NNTP-POSTING-DATE: |", v, "|");
2055 msgtime
= 0; // just in case
2061 datefld
= findHeaderField(hdrs
, "Date");
2062 if (datefld
.length
!= 0) {
2063 auto v
= datefld
.getFieldValue
;
2065 msgtime
= parseMailDate(v
);
2066 } catch (Exception
) {
2067 //writeln("UID=", uid, ": FUCKED DATE: |", v, "|");
2068 msgtime
= 0; // just in case
2073 // finally, try to get time from "Received:"
2074 //Received: from dns9.fly.us ([131.103.96.154]) by np5-d2.fly.us with Microsoft SMTPSVC(5.0.2195.6824); Tue, 21 Mar 2017 17:35:54 -0400
2076 //writeln("!!! --- !!!");
2077 uint lowesttime
= uint.max
;
2078 foreach (uint fidx
; 0..uint.max
) {
2079 auto recvfld
= findHeaderField(hdrs
, "Received", fidx
);
2080 if (recvfld
.length
== 0) break;
2081 auto lsemi
= recvfld
.lastIndexOf(';');
2082 if (lsemi
>= 0) recvfld
= recvfld
[lsemi
+1..$].xstrip
;
2083 if (recvfld
.length
!= 0) {
2084 auto v
= recvfld
.getFieldValue
;
2087 tm
= parseMailDate(v
);
2088 } catch (Exception
) {
2089 //writeln("UID=", uid, ": FUCKED RECV DATE: |", v, "|");
2090 tm
= 0; // just in case
2092 //writeln(tm, " : ", lowesttime);
2093 if (tm
&& tm
< lowesttime
) lowesttime
= tm
;
2096 if (lowesttime
!= uint.max
) msgtime
= lowesttime
;
2099 sqlite3_result_int64(ctx
, msgtime
);
2104 ** ChiroHdr_FromEmail(headers)
2106 ** The content must be email with headers (or headers only).
2107 ** Returns email "From" field.
2109 private void sq3Fn_ChiroHdr_FromEmail (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2110 mixin(HeaderProcStartTpl
!"ChiroHdr_FromEmail");
2111 auto from
= findHeaderField(hdrs
, "From").extractMail
;
2112 if (from
.length
== 0) {
2113 sqlite3_result_text(ctx
, "nobody@nowhere", -1, SQLITE_STATIC
);
2115 sqlite3_result_text(ctx
, from
.ptr
, cast(int)from
.length
, SQLITE_TRANSIENT
);
2121 ** ChiroHdr_ToEmail(headers)
2123 ** The content must be email with headers (or headers only).
2124 ** Returns email "From" field.
2126 private void sq3Fn_ChiroHdr_ToEmail (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2127 mixin(HeaderProcStartTpl
!"ChiroHdr_ToEmail");
2128 auto to
= findHeaderField(hdrs
, "To").extractMail
;
2129 if (to
.length
== 0) {
2130 sqlite3_result_text(ctx
, "nobody@nowhere", -1, SQLITE_STATIC
);
2132 sqlite3_result_text(ctx
, to
.ptr
, cast(int)to
.length
, SQLITE_TRANSIENT
);
2138 ** ChiroHdr_Subj(headers)
2140 ** The content must be email with headers (or headers only).
2141 ** Returns email "From" field.
2143 private void sq3Fn_ChiroHdr_Subj (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2144 mixin(HeaderProcStartTpl
!"sq3Fn_ChiroHdr_Subj");
2145 auto subj
= findHeaderField(hdrs
, "Subject").decodeSubj
.subjRemoveRe
;
2146 if (subj
.length
== 0) {
2147 sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
);
2149 sqlite3_result_text(ctx
, subj
.ptr
, cast(int)subj
.length
, SQLITE_TRANSIENT
);
2155 ** ChiroHdr_Field(headers, fieldname)
2157 ** The content must be email with headers (or headers only).
2158 ** Returns field value as text, or NULL if there is no such field.
2160 private void sq3Fn_ChiroHdr_Field (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2161 if (argc
!= 2) { sqlite3_result_error(ctx
, "invalid number of arguments to \"ChiroHdr_Field()\"", -1); return; }
2163 immutable int sz
= sqlite3_value_bytes(argv
[0]);
2164 if (sz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
2166 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
2167 if (!vs
&& sz
== 0) vs
= "";
2168 if (!vs
) { sqlite3_result_error(ctx
, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
2170 immutable int fldsz
= sqlite3_value_bytes(argv
[1]);
2171 if (fldsz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
2173 const(char)* fldname
= cast(const(char) *)sqlite3_value_blob(argv
[1]);
2174 if (!fldname
&& fldsz
== 0) fldname
= "";
2175 if (!fldname
) { sqlite3_result_error(ctx
, "cannot get blob data in \"ChiroHdr_Field()\"", -1); return; }
2177 const(char)[] hdrs
= vs
[0..cast(usize
)sq3Supp_FindHeadersEnd(vs
, sz
)];
2178 auto value
= findHeaderField(hdrs
, fldname
[0..fldsz
]);
2179 if (value
is null) {
2180 sqlite3_result_null(ctx
);
2181 } else if (value
.length
== 0) {
2182 sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
);
2184 sqlite3_result_text(ctx
, value
.ptr
, cast(int)value
.length
, SQLITE_TRANSIENT
);
2190 ** ChiroTimerStart([msg])
2192 ** The content must be email with headers (or headers only).
2193 ** Returns email "From" field.
2195 private void sq3Fn_ChiroTimerStart (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2196 if (argc
> 1) { sqlite3_result_error(ctx
, "invalid number of arguments to \"ChiroTimerStart()\"", -1); return; }
2201 immutable int sz
= sqlite3_value_bytes(argv
[0]);
2203 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
2205 chiTimerMsg
= new char[cast(usize
)sz
];
2206 chiTimerMsg
[0..cast(usize
)sz
] = vs
[0..cast(usize
)sz
];
2207 writeln("started ", chiTimerMsg
, "...");
2212 sqlite3_result_int(ctx
, 1);
2218 ** ChiroTimerStop([msg])
2220 ** The content must be email with headers (or headers only).
2221 ** Returns email "From" field.
2223 private void sq3Fn_ChiroTimerStop (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2225 if (argc
> 1) { sqlite3_result_error(ctx
, "invalid number of arguments to \"ChiroTimerStop()\"", -1); return; }
2227 if (ChiroTimerEnabled
) {
2230 immutable int sz
= sqlite3_value_bytes(argv
[0]);
2232 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
2234 chiTimerMsg
= new char[cast(usize
)sz
];
2235 chiTimerMsg
[0..cast(usize
)sz
] = vs
[0..cast(usize
)sz
];
2241 auto tstr
= chiTimer
.toBuffer(buf
[]);
2242 if (chiTimerMsg
.length
) {
2243 writeln("done ", chiTimerMsg
, ": ", tstr
);
2245 writeln("time: ", tstr
);
2251 sqlite3_result_int(ctx
, 1);
2256 ** ChiroGlob(pat, str)
2258 ** GLOB replacement, with extended word matching.
2260 private void sq3Fn_ChiroGlob_common (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
, int casesens
,
2261 uint stridx
=1, uint patidx
=0)
2263 if (argc
!= 2) { sqlite3_result_error(ctx
, "invalid number of arguments to \"ChiroGlob()\"", -1); return; }
2265 immutable int patsz
= sqlite3_value_bytes(argv
[patidx
]);
2266 if (patsz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
2268 const(char)* pat
= cast(const(char) *)sqlite3_value_blob(argv
[patidx
]);
2269 if (!pat
&& patsz
== 0) pat
= "";
2270 if (!pat
) { sqlite3_result_error(ctx
, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2272 immutable int strsz
= sqlite3_value_bytes(argv
[stridx
]);
2273 if (strsz
< 0) { sqlite3_result_error_toobig(ctx
); return; }
2275 const(char)* str = cast(const(char) *)sqlite3_value_blob(argv
[stridx
]);
2276 if (!str && strsz
== 0) str = "";
2277 if (!str) { sqlite3_result_error(ctx
, "cannot get blob data in \"ChiroGlob()\"", -1); return; }
2279 immutable bool res
=
2281 globmatch(str[0..cast(usize
)strsz
], pat
[0..cast(usize
)patsz
]) :
2282 globmatchCI(str[0..cast(usize
)strsz
], pat
[0..cast(usize
)patsz
]);
2284 sqlite3_result_int(ctx
, (res ?
1 : 0));
2289 ** ChiroGlobSQL(pat, str)
2291 ** GLOB replacement, with extended word matching.
2293 private void sq3Fn_ChiroGlobSQL (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2294 sq3Fn_ChiroGlob_common(ctx
, argc
, argv
, casesens
:1);
2298 ** ChiroGlob(str, pat)
2300 ** GLOB replacement, with extended word matching.
2302 private void sq3Fn_ChiroGlob (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2303 sq3Fn_ChiroGlob_common(ctx
, argc
, argv
, casesens
:1, stridx
:0, patidx
:1);
2307 ** ChiroGlobCI(str, pat)
2309 ** GLOB replacement, with extended word matching.
2311 private void sq3Fn_ChiroGlobCI (sqlite3_context
*ctx
, int argc
, sqlite3_value
**argv
) {
2312 sq3Fn_ChiroGlob_common(ctx
, argc
, argv
, casesens
:0, stridx
:0, patidx
:1);
2316 // ////////////////////////////////////////////////////////////////////////// //
2320 // ////////////////////////////////////////////////////////////////////////// //
2321 public void chiroRegisterSQLite3Functions (ref Database
db) {
2322 sqlite3_busy_timeout(db.getHandle
, 20000); // busy timeout: 20 seconds
2324 immutable int rc
= sqlite3_extended_result_codes(db.getHandle
, 1);
2325 if (rc
!= SQLITE_OK
) {
2326 import core
.stdc
.stdio
: stderr
, fprintf
;
2327 fprintf(stderr
, "SQLITE WARNING: cannot enable extended result codes (this is harmless).\n");
2329 db.createFunction("glob", 2, &sq3Fn_ChiroGlobSQL
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2330 db.createFunction("ChiroGlob", 2, &sq3Fn_ChiroGlob
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2331 db.createFunction("ChiroGlobCI", 2, &sq3Fn_ChiroGlobCI
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2333 db.createFunction("ChiroPack", 1, &sq3Fn_ChiroPack
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2334 db.createFunction("ChiroPack", 2, &sq3Fn_ChiroPackDPArg
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2335 db.createFunction("ChiroUnpack", 1, &sq3Fn_ChiroUnpack
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2337 db.createFunction("ChiroPackLZMA", 1, &sq3Fn_ChiroPackLZMA
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2338 db.createFunction("ChiroPackLZMA", 2, &sq3Fn_ChiroPackLZMA
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2340 db.createFunction("ChiroGetPackType", 1, &sq3Fn_ChiroGetPackType
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2342 db.createFunction("ChiroNormCRLF", 1, &sq3Fn_ChiroNormCRLF
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2343 db.createFunction("ChiroNormHeaders", 1, &sq3Fn_ChiroNormHeaders
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2344 db.createFunction("ChiroExtractHeaders", 1, &sq3Fn_ChiroExtractHeaders
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2345 db.createFunction("ChiroExtractBody", 1, &sq3Fn_ChiroExtractBody
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2346 db.createFunction("ChiroRIPEMD160", 1, &sq3Fn_ChiroRIPEMD160
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2348 db.createFunction("ChiroHdr_NNTPIndex", 1, &sq3Fn_ChiroHdr_NNTPIndex
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2349 db.createFunction("ChiroHdr_RecvTime", 1, &sq3Fn_ChiroHdr_RecvTime
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2350 db.createFunction("ChiroHdr_FromEmail", 1, &sq3Fn_ChiroHdr_FromEmail
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2351 db.createFunction("ChiroHdr_ToEmail", 1, &sq3Fn_ChiroHdr_ToEmail
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2352 db.createFunction("ChiroHdr_Subj", 1, &sq3Fn_ChiroHdr_Subj
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2353 db.createFunction("ChiroHdr_Field", 2, &sq3Fn_ChiroHdr_Field
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2355 db.createFunction("ChiroTimerStart", 0, &sq3Fn_ChiroTimerStart
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2356 db.createFunction("ChiroTimerStart", 1, &sq3Fn_ChiroTimerStart
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2357 db.createFunction("ChiroTimerStop", 0, &sq3Fn_ChiroTimerStop
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2358 db.createFunction("ChiroTimerStop", 1, &sq3Fn_ChiroTimerStop
, moreflags
:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS
);
2362 // ////////////////////////////////////////////////////////////////////////// //
2363 public void chiroRecreateStorageDB (const(char)[] dbname
=ExpandedMailDBPath
~StorageDBName
) {
2364 try { import std
.file
: remove
; remove(dbname
); } catch (Exception
) {}
2365 dbStore
= Database(dbname
, Database
.Mode
.ReadWriteCreate
, dbpragmasRWStorageRecreate
, schemaStorage
);
2366 chiroRegisterSQLite3Functions(dbStore
);
2367 dbStore
.setOnClose(schemaStorageIndex
~dbpragmasRWStorage
~"ANALYZE;");
2371 // ////////////////////////////////////////////////////////////////////////// //
2372 public void chiroRecreateViewDB (const(char)[] dbname
=ExpandedMailDBPath
~SupportDBName
) {
2373 try { import std
.file
: remove
; remove(dbname
); } catch (Exception
) {}
2374 dbView
= Database(dbname
, Database
.Mode
.ReadWriteCreate
, dbpragmasRWSupportRecreate
, schemaSupportTable
);
2375 chiroRegisterSQLite3Functions(dbView
);
2376 dbView
.setOnClose(schemaSupportIndex
~dbpragmasRWSupport
~"ANALYZE;");
2380 public void chiroCreateViewIndiciesDB () {
2381 dbView
.setOnClose(dbpragmasRWSupport
~"ANALYZE;");
2382 dbView
.execute(schemaSupportIndex
);
2386 // ////////////////////////////////////////////////////////////////////////// //
2387 public void chiroRecreateConfDB (const(char)[] dbname
=ExpandedMailDBPath
~OptionsDBName
) {
2388 try { import std
.file
: remove
; remove(dbname
); } catch (Exception
) {}
2389 dbConf
= Database(dbname
, Database
.Mode
.ReadWriteCreate
, dbpragmasRWOptionsRecreate
, schemaOptions
);
2390 chiroRegisterSQLite3Functions(dbConf
);
2391 dbConf
.setOnClose(schemaOptionsIndex
~dbpragmasRWOptions
~"ANALYZE;");
2395 // ////////////////////////////////////////////////////////////////////////// //
2396 public void chiroOpenStorageDB (const(char)[] dbname
=ExpandedMailDBPath
~StorageDBName
, bool readonly
=false) {
2397 dbStore
= Database(dbname
, (readonly ? Database
.Mode
.ReadOnly
: Database
.Mode
.ReadWrite
), (readonly ? dbpragmasRO
: dbpragmasRWStorage
), schemaStorage
);
2398 chiroRegisterSQLite3Functions(dbStore
);
2399 if (!readonly
) dbStore
.setOnClose("PRAGMA optimize;");
2403 // ////////////////////////////////////////////////////////////////////////// //
2404 public void chiroOpenViewDB (const(char)[] dbname
=ExpandedMailDBPath
~SupportDBName
, bool readonly
=false) {
2405 dbView
= Database(dbname
, (readonly ? Database
.Mode
.ReadOnly
: Database
.Mode
.ReadWrite
), (readonly ? dbpragmasRO
: dbpragmasRWSupport
), schemaSupport
);
2406 chiroRegisterSQLite3Functions(dbView
);
2408 dbView
.execute(schemaSupportTempTables
);
2409 dbView
.setOnClose("PRAGMA optimize;");
2414 // ////////////////////////////////////////////////////////////////////////// //
2415 public void chiroOpenConfDB (const(char)[] dbname
=ExpandedMailDBPath
~OptionsDBName
, bool readonly
=false) {
2416 dbConf
= Database(dbname
, (readonly ? Database
.Mode
.ReadOnly
: Database
.Mode
.ReadWrite
), (readonly ? dbpragmasRO
: dbpragmasRWOptions
), schemaOptions
);
2417 chiroRegisterSQLite3Functions(dbConf
);
2418 if (!readonly
) dbConf
.setOnClose("PRAGMA optimize;");
2422 // ////////////////////////////////////////////////////////////////////////// //
2424 recreates FTS5 (full-text search) info.
2426 public void chiroRecreateFTS5 (bool repopulate
=true) {
2427 dbView
.execute(recreateFTS5
);
2428 if (repopulate
) dbView
.execute(repopulateFTS5
);
2429 dbView
.execute(recreateFTS5Triggers
);
2433 // ////////////////////////////////////////////////////////////////////////// //
2435 static void errorLogCallback (void *pArg
, int rc
, const char *zMsg
) {
2436 if (ChiroSQLiteSilent
) return;
2437 import core
.stdc
.stdio
: stderr
, fprintf
;
2439 case SQLITE_NOTICE
: fprintf(stderr
, "***SQLITE NOTICE: %s\n", zMsg
); break;
2440 case SQLITE_NOTICE_RECOVER_WAL
: fprintf(stderr
, "***SQLITE NOTICE (WAL RECOVER): %s\n", zMsg
); break;
2441 case SQLITE_NOTICE_RECOVER_ROLLBACK
: fprintf(stderr
, "***SQLITE NOTICE (ROLLBACK RECOVER): %s\n", zMsg
); break;
2443 case SQLITE_WARNING
: fprintf(stderr
, "***SQLITE WARNING: %s\n", zMsg
); break;
2444 case SQLITE_WARNING_AUTOINDEX
: fprintf(stderr
, "***SQLITE AUTOINDEX WARNING: %s\n", zMsg
); break;
2446 case SQLITE_CANTOPEN
:
2448 break; // ignore those
2450 default: fprintf(stderr
, "***SQLITE LOG(%d) [%s]: %s\n", rc
, sqlite3_errstr(rc
), zMsg
); break;
2456 static string
sqerrstr (immutable int rc
) nothrow @trusted {
2457 const(char)* msg
= sqlite3_errstr(rc
);
2458 if (!msg ||
!msg
[0]) return null;
2459 import core
.stdc
.string
: strlen
;
2460 return msg
[0..strlen(msg
)].idup
;
2464 static void sqconfigcheck (immutable int rc
, string msg
, bool fatal
) {
2465 if (rc
== SQLITE_OK
) return;
2467 string errmsg
= sqerrstr(rc
);
2468 throw new Exception("FATAL: "~msg
~": "~errmsg
);
2470 if (msg
is null) msg
= "";
2471 import core
.stdc
.stdio
: stderr
, fprintf
;
2472 fprintf(stderr
, "SQLITE WARNING: %.*s (this is harmless): %s\n", cast(uint)msg
.length
, msg
.ptr
, sqlite3_errstr(rc
));
2477 // call this BEFORE opening any SQLite database connection!
2478 public void chiroSwitchToSingleThread () {
2479 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SINGLETHREAD
), "cannot set single-threaded mode", fatal
:false);
2483 public string
MailDBPath () nothrow @trusted @nogc { return ExpandedMailDBPath
; }
2486 public void MailDBPath(T
:const(char)[]) (T mailpath
) nothrow @trusted {
2487 while (mailpath
.length
> 1 && mailpath
[$-1] == '/') mailpath
= mailpath
[0..$-1];
2489 if (mailpath
.length
== 0 || mailpath
== ".") {
2490 ExpandedMailDBPath
= "";
2494 if (mailpath
[0] == '~') {
2495 char[] dpath
= new char[mailpath
.length
+4096];
2496 dpath
= expandTilde(dpath
, mailpath
);
2498 while (dpath
.length
> 1 && dpath
[$-1] == '/') dpath
= dpath
[0..$-1];
2500 ExpandedMailDBPath
= cast(string
)dpath
; // it is safe to cast here
2502 char[] dpath
= new char[mailpath
.length
+1];
2503 dpath
[0..$-1] = mailpath
[];
2505 ExpandedMailDBPath
= cast(string
)dpath
; // it is safe to cast here
2510 shared static this () {
2512 SQLITE_CONFIG_STMTJRNL_SPILL
= 26, /* int nByte */
2513 SQLITE_CONFIG_SMALL_MALLOC
= 27, /* boolean */
2516 if (!sqlite3_threadsafe()) {
2517 throw new Exception("FATAL: SQLite must be compiled with threading support!");
2520 // we are interested in all errors
2521 sqlite3_config(SQLITE_CONFIG_LOG
, &errorLogCallback
, null);
2523 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SERIALIZED
), "cannot set SQLite serialized threading mode", fatal
:true);
2524 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SMALL_MALLOC
, 0), "cannot enable SQLite unrestriced malloc mode", fatal
:false);
2525 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_URI
, 1), "cannot enable SQLite URI handling", fatal
:false);
2526 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_COVERING_INDEX_SCAN
, 1), "cannot enable SQLite covering index scan", fatal
:false);
2527 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_STMTJRNL_SPILL
, 512*1024), "cannot set SQLite statement journal spill threshold", fatal
:false);
2529 MailDBPath
= "~/Mail";
2533 shared static ~this () {
2540 // ////////////////////////////////////////////////////////////////////////// //
2541 public void transacted(string dbname
) (void delegate () dg
) {
2542 if (dg
is null) return;
2543 static if (dbname
== "View" || dbname
== "view") alias db = dbView
;
2544 else static if (dbname
== "Store" || dbname
== "store") alias db = dbStore
;
2545 else static if (dbname
== "Conf" || dbname
== "conf") alias db = dbConf
;
2546 else static assert(0, "invalid db name: '"~dbname
~"'");
2551 // ////////////////////////////////////////////////////////////////////////// //
2552 public DynStr
chiroGetTagMonthLimitEx(T
) (T tagnameid
, out int val
, int defval
=6)
2553 if (is(T
:const(char)[]) ||
is(T
:uint))
2555 static if (is(T
:const(char)[])) {
2556 alias tagname
= tagnameid
;
2559 static auto stGetTagName
= LazyStatement
!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
2560 foreach (auto row
; stGetTagName
.st
.bind(":tagid", tagnameid
).range
) {
2561 tagnameStr
= row
.tagname
!SQ3Text
;
2563 const(char)[] tagname
= tagnameStr
.getData
;
2566 static auto stGetMLimit
= LazyStatement
!"Conf"(`
2567 WITH RECURSIVE pth(path) AS (
2568 VALUES('/mainpane/msgview/monthlimit'||:tagslash||:tagname)
2571 SUBSTR(path, 1, LENGTH(path)-LENGTH(REPLACE(path, RTRIM(path, REPLACE(path, '/', '')), ''))-1)
2573 WHERE path LIKE '/mainpane/msgview/monthlimit%'
2578 , opt.value AS value
2580 INNER JOIN options AS opt ON opt.name=pth.path
2581 WHERE pth.path LIKE '/mainpane/msgview/monthlimit%'
2586 .bindConstText(":tagslash", (tagname
.length
&& tagname
[0] != '/' ?
"/" : ""))
2587 .bindConstText(":tagname", tagname
);
2589 foreach (auto row
; stGetMLimit
.st
.range
) {
2590 //conwriteln("TAGNAME=<", tagname, ">; val=", row.value!int, "; sres=<", row.name!SQ3Text, ">");
2591 val
= row
.value
!int;
2592 DynStr sres
= row
.name
!SQ3Text
;
2601 public int chiroGetTagMonthLimit(T
) (T tagnameid
, int defval
=6)
2602 if (is(T
:const(char)[]) ||
is(T
:uint))
2604 static if (is(T
:const(char)[])) {
2605 alias tagname
= tagnameid
;
2608 static auto stGetTagName
= LazyStatement
!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
2609 foreach (auto row
; stGetTagName
.st
.bind(":tagid", tagnameid
).range
) {
2610 tagnameStr
= row
.tagname
!SQ3Text
;
2612 const(char)[] tagname
= tagnameStr
.getData
;
2615 static auto stGetMLimit
= LazyStatement
!"Conf"(`
2616 WITH RECURSIVE pth(path) AS (
2617 VALUES('/mainpane/msgview/monthlimit'||:tagslash||:tagname)
2620 SUBSTR(path, 1, LENGTH(path)-LENGTH(REPLACE(path, RTRIM(path, REPLACE(path, '/', '')), ''))-1)
2622 WHERE path LIKE '/mainpane/msgview/monthlimit%'
2629 INNER JOIN options AS opt ON opt.name=pth.path
2630 WHERE pth.path LIKE '/mainpane/msgview/monthlimit%'
2635 .bindConstText(":tagslash", (tagname
.length
&& tagname
[0] != '/' ?
"/" : ""))
2636 .bindConstText(":tagname", tagname
);
2638 foreach (auto row
; stGetMLimit
.st
.range
) return row
.value
!int;
2644 public void chiroDeleteOption (const(char)[] name
) {
2645 assert(name
.length
!= 0);
2646 static auto stat
= LazyStatement
!"Conf"(`DELETE FROM options WHERE name=:name;`);
2647 stat
.st
.bindConstText(":name", name
).doAll();
2650 public void chiroSetOption(T
) (const(char)[] name
, T value
)
2651 if (!is(T
:const(DynStr
)) && (__traits(isIntegral
, T
) ||
is(T
:const(char)[])))
2653 assert(name
.length
!= 0);
2654 static auto stat
= LazyStatement
!"Conf"(`
2657 VALUES(:name,:value)
2659 DO UPDATE SET value=:value
2661 stat
.st
.bindConstText(":name", name
);
2662 static if (is(T
== typeof(null))) {
2663 stat
.st
.bindConstText(":value", "");
2664 } else static if (__traits(isIntegral
, T
)) {
2665 stat
.st
.bind(":value", value
);
2666 } else static if (is(T
:const(char)[])) {
2667 stat
.st
.bindConstText(":value", value
);
2669 static assert(0, "oops");
2674 public void chiroSetOption (const(char)[] name
, DynStr value
) {
2675 assert(name
.length
!= 0);
2676 //{ import std.stdio; writeln("SETOPTION(", name, "): <", value.getData, ">"); }
2677 static auto stat
= LazyStatement
!"Conf"(`
2680 VALUES(:name,:value)
2682 DO UPDATE SET value=:value
2685 .bindConstText(":name", name
)
2686 .bindConstText(":value", value
.getData
)
2691 public void chiroSetOptionUInts (const(char)[] name
, uint v0
, uint v1
) {
2692 assert(name
.length
!= 0);
2693 static auto stat
= LazyStatement
!"Conf"(`
2696 VALUES(:name,:value)
2698 DO UPDATE SET value=:value
2700 import core
.stdc
.stdio
: snprintf
;
2701 char[64] value
= void;
2702 auto vlen
= snprintf(value
.ptr
, value
.sizeof
, "%u,%u", v0
, v1
);
2704 .bindConstText(":name", name
)
2705 .bindConstText(":value", value
[0..vlen
])
2710 public T
chiroGetOptionEx(T
) (const(char)[] name
, out bool exists
, T defval
=T
.init
)
2711 if (!is(T
:const(DynStr
)) && (__traits(isIntegral
, T
) ||
is(T
:const(char)[])))
2713 static auto stat
= LazyStatement
!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2714 assert(name
.length
!= 0);
2716 foreach (auto row
; stat
.st
.bindConstText(":name", name
).range
) {
2723 public T
chiroGetOption(T
) (const(char)[] name
, T defval
=T
.init
)
2724 if (!is(T
:const(DynStr
)) && (__traits(isIntegral
, T
) ||
is(T
:const(char)[])))
2726 static auto stat
= LazyStatement
!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2727 assert(name
.length
!= 0);
2728 foreach (auto row
; stat
.st
.bindConstText(":name", name
).range
) {
2734 public void chiroGetOption (ref DynStr s
, const(char)[] name
, const(char)[] defval
=null) {
2735 static auto stat
= LazyStatement
!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2736 assert(name
.length
!= 0);
2737 foreach (auto row
; stat
.st
.bindConstText(":name", name
).range
) {
2738 s
= row
.value
!SQ3Text
;
2745 private uint parseUInt (ref SQ3Text s
) {
2747 if (s
.length
== 0 ||
!isdigit(s
[0])) return uint.max
;
2750 immutable int dg
= s
[0].digitInBase(10);
2752 immutable uint nr
= res
*10U+cast(uint)dg
;
2753 if (nr
< res
) return uint.max
;
2757 if (s
.length
&& s
[0] == ',') s
= s
[1..$];
2763 public void chiroGetOptionUInts (ref uint v0
, ref uint v1
, const(char)[] name
) {
2764 static auto stat
= LazyStatement
!"Conf"(`SELECT value AS value FROM options WHERE name=:name LIMIT 1;`);
2765 assert(name
.length
!= 0);
2766 foreach (auto row
; stat
.st
.bindConstText(":name", name
).range
) {
2767 auto s
= row
.value
!SQ3Text
;
2768 immutable uint rv0
= parseUInt(s
);
2769 immutable uint rv1
= parseUInt(s
);
2770 if (rv0
!= uint.max
&& rv1
!= uint.max
&& s
.length
== 0) {
2779 // ////////////////////////////////////////////////////////////////////////// //
2780 // append tag if necessary, return tagid
2781 // tag name must be valid: not empty, and not end with a '/'
2782 // returns 0 on invalid tag name
2783 public uint chiroAppendTag (const(char)[] tagname
, int hidden
=0) {
2784 tagname
= tagname
.xstrip
;
2785 while (tagname
.length
&& tagname
[$-1] == '/') tagname
= tagname
[0..$-1];
2786 tagname
= tagname
.xstrip
;
2787 if (tagname
.length
== 0) return 0;
2788 if (tagname
.indexOf('|') >= 0) return 0;
2790 static auto stAppendTag
= LazyStatement
!"View"(`
2791 INSERT INTO tagnames(tag, hidden, threading) VALUES(:tagname,:hidden,:threading)
2793 DO UPDATE SET hidden=hidden -- this is for "returning"
2794 RETURNING tagid AS tagid
2797 // alphanum tags must start with '/'
2799 if (tagname
[0].isalnum
&& tagname
.indexOf(':') < 0) {
2802 stAppendTag
.st
.bindConstText(":tagname", tn
);
2804 stAppendTag
.st
.bindConstText(":tagname", tagname
);
2807 .bind(":hidden", hidden
)
2808 .bind(":threading", (hidden ?
0 : 1));
2809 foreach (auto row
; stAppendTag
.st
.range
) return row
.tagid
!uint;
2815 // ////////////////////////////////////////////////////////////////////////// //
2816 /// returns `true` if we need to update pane
2817 /// if message is left without any tags, it will be tagged with "#hobo"
2818 public bool chiroMessageRemoveTag (uint uid
, const(char)[] tagname
) {
2819 if (uid
== 0) return false;
2820 tagname
= tagname
.xstrip
;
2821 while (tagname
.length
&& tagname
[$-1] == '/') tagname
= tagname
[0..$-1];
2822 tagname
= tagname
.xstrip
;
2823 if (tagname
.length
== 0) return false;
2824 if (tagname
.indexOf('|') >= 0) return false;
2826 immutable tagid
= chiroGetTagUid(tagname
);
2827 if (tagid
== 0) return false;
2829 static auto stUpdateStorageTags
= LazyStatement
!"Store"(`
2830 UPDATE messages SET tags=:tags WHERE uid=:uid
2833 static auto stUidHasTag
= LazyStatement
!"View"(`
2834 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2837 static auto stInsertIntoThreads
= LazyStatement
!"View"(`
2838 INSERT INTO threads(uid, tagid,appearance,time)
2839 VALUES(:uid, :tagid, :appr, (SELECT time FROM info WHERE uid=:uid LIMIT 1))
2842 // delete message from threads
2843 static auto stClearThreads
= LazyStatement
!"View"(`
2844 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2847 static auto stGetMsgTags
= LazyStatement
!"View"(`
2848 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
2850 INNER JOIN tagnames AS tt USING(tagid)
2855 immutable bool updatePane
= (chiroGetTreePaneTableTagId() == tagid
);
2856 bool wasChanges
= false;
2859 // get tagid (possibly appending the tag)
2861 foreach (auto row
; stUidHasTag
.st
.bind(":uid", uid
).bind(":tagid", tagid
).range
) hasit
= true;
2864 stClearThreads
.st
.bind(":uid", uid
).bind(":tagid", tagid
).doAll((stmt
) { wasChanges
= true; });
2866 // if there were any changes, rebuild message tags
2867 if (!wasChanges
) return;
2870 foreach (auto trow
; stGetMsgTags
.st
.bind(":uid", uid
).range
) {
2871 auto tname
= trow
.name
!SQ3Text
;
2872 if (tname
.length
== 0) continue;
2873 if (newtags
.length
) newtags
~= "|";
2877 // if there is no tags, assign "#hobo"
2878 // this should not happen, but...
2879 if (newtags
.length
== 0) {
2881 auto hobo
= chiroAppendTag(newtags
, hidden
:1);
2883 // append record for this tag to threads
2884 // note that there is no need to relink hobos, they should not be threaded
2885 //FIXME: this clears message appearance
2886 stInsertIntoThreads
.st
2888 .bind(":tagid", hobo
)
2889 .bind(":appr", Appearance
.Read
)
2893 // update storage with new tag names
2894 assert(newtags
.length
);
2895 stUpdateStorageTags
.st
.bindConstText(":tags", newtags
).doAll();
2897 // and relink threads for this tagid
2898 chiroSupportRelinkTagThreads(tagid
);
2901 return (wasChanges
&& updatePane
);
2905 // ////////////////////////////////////////////////////////////////////////// //
2906 /// returns `true` if we need to update pane
2907 public bool chiroMessageAddTag (uint uid
, const(char)[] tagname
) {
2908 if (uid
== 0) return false;
2909 tagname
= tagname
.xstrip
;
2910 while (tagname
.length
&& tagname
[$-1] == '/') tagname
= tagname
[0..$-1];
2911 tagname
= tagname
.xstrip
;
2912 if (tagname
.length
== 0) return false;
2913 if (tagname
.indexOf('|') >= 0) return false;
2915 static auto stUpdateStorageTags
= LazyStatement
!"Store"(`
2916 UPDATE messages SET tags=tags||'|'||:tagname WHERE uid=:uid
2919 static auto stUidExists
= LazyStatement
!"View"(`
2920 SELECT 1 FROM threads WHERE uid=:uid LIMIT 1
2923 static auto stUidHasTag
= LazyStatement
!"View"(`
2924 SELECT uid AS uid FROM threads WHERE tagid=:tagid AND uid=:uid LIMIT 1
2927 static auto stInsertIntoThreads
= LazyStatement
!"View"(`
2928 INSERT INTO threads(uid, tagid, appearance, time)
2929 VALUES(:uid, :tagid, :appr, (SELECT time FROM threads WHERE uid=:uid LIMIT 1))
2932 static auto stUnHobo
= LazyStatement
!"View"(`
2933 DELETE FROM threads WHERE tagid=:tagid AND uid=:uid
2936 bool hasuid
= false;
2937 foreach (auto row
; stUidExists
.st
.bind(":uid", uid
).range
) hasuid
= true;
2938 if (!hasuid
) return false; // nothing to do
2940 immutable paneTagId
= chiroGetTreePaneTableTagId();
2941 bool updatePane
= false;
2944 // get tagid (possibly appending the tag)
2945 uint tagid
= chiroAppendTag(tagname
);
2947 conwriteln("ERROR: cannot append tag name '", tagname
, "'!");
2952 foreach (auto row
; stUidHasTag
.st
.bind(":uid", uid
).bind(":tagid", tagid
).range
) hasit
= true;
2955 // append this tag to the message in the storage
2956 stUpdateStorageTags
.st
.bind(":uid", uid
).bindConstText(":tagname", tagname
).doAll();
2958 // append record for this tag to threads
2959 stInsertIntoThreads
.st
2961 .bind(":tagid", tagid
)
2962 .bind(":appr", Appearance
.Read
)
2965 // and relink threads for this tagid
2966 chiroSupportRelinkTagThreads(tagid
);
2968 // remove this message from "#hobo", if there is any
2969 auto hobo
= chiroGetTagUid("#hobo");
2970 if (hobo
&& hobo
!= tagid
) {
2971 stUnHobo
.st
.bind(":tagid", hobo
).bind(":uid", uid
).doAll();
2972 // there's no need to relink hobos, because they should have no links
2975 updatePane
= (tagid
== paneTagId
);
2983 inserts the one message from the message storage with the given id into view storage.
2984 parses it and such, and optionally updates threads.
2986 doesn't updates NNTP indicies and such, never relinks anything.
2988 invalid (unknown) tags will be ignored.
2990 returns number of processed messages.
2992 doesn't start/end any transactions, so wrap it yourself.
2994 public bool chiroParseAndInsertOneMessage (uint uid
, uint msgtime
, int appearance
,
2995 const(char)[] hdrs
, const(char)[] body, const(char)[] tags
)
2997 auto stInsThreads
= dbView
.statement(`
2999 ( uid, tagid, time, appearance)
3000 VALUES(:uid,:tagid,:time,:appearance)
3003 auto stInsInfo
= dbView
.statement(`
3005 ( uid, from_name, from_mail, subj, to_name, to_mail)
3006 VALUES(:uid,:from_name,:from_mail,:subj,:to_name,:to_mail)
3009 auto stInsMsgId
= dbView
.statement(`
3012 VALUES(:uid,:msgid,:time)
3015 auto stInsMsgRefId
= dbView
.statement(`
3018 VALUES(:uid,:idx,:msgid)
3021 auto stInsContentText
= dbView
.statement(`
3022 INSERT INTO content_text
3023 ( uid, format, content)
3024 VALUES(:uid,:format, ChiroPack(:content))
3027 auto stInsContentHtml
= dbView
.statement(`
3028 INSERT INTO content_html
3029 ( uid, format, content)
3030 VALUES(:uid,:format, ChiroPack(:content))
3033 auto stInsAttach
= dbView
.statement(`
3034 INSERT INTO attaches
3035 ( uid, idx, mime, name, format, content)
3036 VALUES(:uid,:idx,:mime,:name,:format, ChiroPack(:content))
3039 bool noattaches
= false; // do not store attaches?
3041 // create thread record for each tag (and update max nntp index)
3043 int noAttachCount
= 0;
3044 while (tags
.length
) {
3045 auto eep
= tags
.indexOf('|');
3046 auto tagname
= (eep
>= 0 ? tags
[0..eep
] : tags
[0..$]);
3047 tags
= (eep
>= 0 ? tags
[eep
+1..$] : tags
[0..0]);
3048 if (tagname
.length
== 0) continue;
3050 //immutable uint tuid = chiroGetTagUid(tagname);
3051 immutable uint tuid
= chiroAppendTag(tagname
, (tagname
== "#hobo" ?
1 : 0));
3052 if (tuid
== 0) continue;
3055 if (nntpidx > 0 && tagname.startsWith("account:")) {
3056 auto accname = tagname[8..$];
3058 .bindConstText(":accname", accname)
3059 .bind(":nntpidx", nntpidx)
3064 if (!chiroIsTagAllowAttaches(tuid
)) ++noAttachCount
;
3067 int app
= appearance
;
3068 if (app
== Appearance
.Unread
) {
3069 if (tagname
.startsWith("account:") ||
3070 tagname
.startsWith("#spam") ||
3071 tagname
.startsWith("#hobo"))
3073 app
= Appearance
.Read
;
3079 .bind(":tagid", tuid
)
3080 .bind(":time", msgtime
)
3081 .bind(":appearance", app
)
3084 if (!tagCount
) return false;
3085 noattaches
= (noAttachCount
&& noAttachCount
== tagCount
);
3089 bool hasmsgid
= false;
3090 auto msgidfield
= findHeaderField(hdrs
, "Message-Id");
3091 if (msgidfield
.length
) {
3092 auto id
= msgidfield
.getFieldValue
;
3097 .bind("time", msgtime
)
3098 .bindConstText(":msgid", id
)
3102 // if there is no msgid, create one
3106 hash
.put(cast(const(ubyte)[])hdrs
);
3107 hash
.put(cast(const(ubyte)[])body);
3108 ubyte[20] digest
= hash
.finish();
3109 char[20*2+2+16] buf
;
3110 import core
.stdc
.stdio
: snprintf
;
3111 import core
.stdc
.string
: strcat
;
3112 foreach (immutable idx
, ubyte b
; digest
[]) snprintf(buf
.ptr
+idx
*2, 3, "%02x", b
);
3113 strcat(buf
.ptr
, "@artificial"); // it is safe, there is enough room for it
3116 .bind("time", msgtime
)
3117 .bindConstText(":msgid", buf
[0..20*2])
3122 // insert references
3125 auto inreplyfld
= findHeaderField(hdrs
, "In-Reply-To");
3126 while (inreplyfld
.length
) {
3127 auto id
= getNextFieldValue(inreplyfld
);
3131 .bind(":idx", refidx
++)
3137 inreplyfld
= findHeaderField(hdrs
, "References");
3138 while (inreplyfld
.length
) {
3139 auto id
= getNextFieldValue(inreplyfld
);
3143 .bind(":idx", refidx
++)
3150 // insert base content and attaches
3153 parseContent(ref content
, hdrs
, body, noattaches
);
3154 // insert text and html
3155 bool wasText
= false, wasHtml
= false;
3156 foreach (const ref Content cc
; content
) {
3157 if (cc
.name
.length
) continue;
3158 if (noattaches
&& !cc
.mime
.startsWith("text/")) continue;
3159 if (!wasText
&& cc
.mime
== "text/plain") {
3163 .bindConstText(":format", cc
.format
)
3164 .bindConstBlob(":content", cc
.data
)
3166 } else if (!wasHtml
&& cc
.mime
== "text/html") {
3170 .bindConstText(":format", cc
.format
)
3171 .bindConstBlob(":content", cc
.data
)
3178 .bindConstText(":format", "")
3179 .bindConstBlob(":content", "")
3185 .bindConstText(":format", "")
3186 .bindConstBlob(":content", "")
3189 // insert everything
3191 foreach (const ref Content cc
; content
) {
3192 if (cc
.name
.length
== 0 && cc
.mime
.startsWith("text/")) continue;
3193 // for "no attaches" mode, still record the attach, but ignore its contents
3196 .bind(":idx", cidx
++)
3197 .bindConstText(":mime", cc
.mime
)
3198 .bindConstText(":name", cc
.name
)
3199 .bindConstText(":format", cc
.name
)
3200 .bindConstBlob(":content", (noattaches ?
null : cc
.data
), allowNull
:true)
3205 // insert from/to/subj info
3206 // this must be done last to keep FTS5 in sync
3208 auto subj
= findHeaderField(hdrs
, "Subject").decodeSubj
.subjRemoveRe
;
3209 auto from
= findHeaderField(hdrs
, "From");
3210 auto to
= findHeaderField(hdrs
, "To");
3213 .bind(":from_name", from
.extractName
)
3214 .bind(":from_mail", from
.extractMail
)
3215 .bind(":subj", subj
)
3216 .bind(":to_name", to
.extractName
)
3217 .bind(":to_mail", to
.extractMail
)
3226 inserts the messages from the message storage with the given id into view storage.
3227 parses it and such, and optionally updates threads.
3229 WARNING! DOESN'T UPDATE NNTP INDICIES! this should be done by the downloader.
3231 invalid (unknown) tags will be ignored.
3233 returns number of processed messages.
3235 public uint chiroParseAndInsertMessages (uint stmsgid
,
3236 void delegate (uint count
, uint total
, uint nntpidx
, const(char)[] tags
) progresscb
=null,
3237 uint emsgid
=uint.max
, bool relink
=true, bool asread
=false)
3239 if (emsgid
< stmsgid
) return 0; // nothing to do
3243 if (progresscb
!is null) {
3244 // find total number of messages to process
3245 foreach (auto row
; dbStore
.statement(`
3246 SELECT count(uid) AS total FROM messages WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
3247 ;`).bind(":msglo", stmsgid
).bind(":msghi", emsgid
).range
)
3249 total
= row
.total
!uint;
3252 if (total
== 0) return 0; // why not?
3257 if (relink
) uptagids
.reserve(128);
3258 scope(exit
) delete uptagids
;
3260 foreach (auto mrow
; dbStore
.statement(`
3261 -- this should cache unpack results
3262 WITH msgunpacked(msguid, msgdata, msgtags) AS (
3263 SELECT uid AS msguid, ChiroUnpack(data) AS msgdata, tags AS msgtags
3265 WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
3271 , ChiroExtractHeaders(msgdata) AS headers
3272 , ChiroExtractBody(msgdata) AS body
3273 , ChiroHdr_NNTPIndex(msgdata) AS nntpidx
3274 , ChiroHdr_RecvTime(msgdata) AS msgtime
3276 ;`).bind(":msglo", stmsgid
).bind(":msghi", emsgid
).range
)
3279 auto hdrs
= mrow
.headers
!SQ3Text
;
3280 auto body = mrow
.body!SQ3Text
;
3281 auto tags
= mrow
.tags
!SQ3Text
;
3282 uint uid
= mrow
.uid
!uint;
3283 uint nntpidx
= mrow
.nntpidx
!uint;
3284 uint msgtime
= mrow
.msgtime
!uint;
3285 assert(tags
.length
);
3287 chiroParseAndInsertOneMessage(uid
, msgtime
, (asread ?
1 : 0), hdrs
, body, tags
);
3289 if (progresscb
!is null) progresscb(count
, total
, nntpidx
, tags
);
3292 while (tags
.length
) {
3293 auto eep
= tags
.indexOf('|');
3294 auto tagname
= (eep
>= 0 ? tags
[0..eep
] : tags
[0..$]);
3295 tags
= (eep
>= 0 ? tags
[eep
+1..$] : tags
[0..0]);
3296 if (tagname
.length
== 0) continue;
3298 immutable uint tuid
= chiroGetTagUid(tagname
);
3299 if (tuid
== 0) continue;
3302 foreach (immutable n
; uptagids
) if (n
== tuid
) { found
= true; break; }
3303 if (!found
) uptagids
~= tuid
;
3308 if (relink
&& uptagids
.length
) {
3309 foreach (immutable tagid
; uptagids
) chiroSupportRelinkTagThreads(tagid
);
3318 returns accouint uid (accid) or 0.
3320 public uint chiroGetAccountUid (const(char)[] accname
) {
3321 static auto stat
= LazyStatement
!"Conf"(`SELECT accid AS accid FROM accounts WHERE name=:accname LIMIT 1;`);
3322 foreach (auto row
; stat
.st
.bindConstText(":accname", accname
).range
) return row
.accid
!uint;
3328 returns accouint name, or empty string.
3330 public DynStr
chiroGetAccountName (uint accid
) {
3331 static auto stat
= LazyStatement
!"Conf"(`SELECT name AS name FROM accounts WHERE accid=:accid LIMIT 1;`);
3333 if (accid
== 0) return res
;
3334 foreach (auto row
; stat
.st
.bind(":accid", accid
).range
) {
3335 res
= row
.name
!SQ3Text
;
3343 returns list of known tags, sorted by name.
3345 public string
[] chiroGetTagList () {
3346 static auto stat
= LazyStatement
!"View"(`SELECT tag AS tagname FROM tagnames WHERE hidden=0 ORDER BY tag;`);
3348 foreach (auto row
; stat
.st
.range
) res
~= row
.tagname
!string
;
3354 returns tag uid (tagid) or 0.
3356 public uint chiroGetTagUid (const(char)[] tagname
) {
3357 static auto stat
= LazyStatement
!"View"(`SELECT tagid AS tagid FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3358 foreach (auto row
; stat
.st
.bindConstText(":tagname", tagname
).range
) {
3359 return row
.tagid
!uint;
3366 returns tag name or empty string.
3368 public DynStr
chiroGetTagName (uint tagid
) {
3369 static auto stat
= LazyStatement
!"View"(`SELECT tag AS tagname FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3371 foreach (auto row
; stat
.st
.bind(":tagid", tagid
).range
) {
3372 s
= row
.tagname
!SQ3Text
;
3380 returns `true` if the given tag supports threads.
3382 this is used only when adding new messages, to set all parents to 0.
3384 public bool chiroIsTagThreaded(T
) (T tagnameid
)
3385 if (is(T
:const(char)[]) ||
is(T
:uint))
3387 static if (is(T
:const(char)[])) {
3388 static auto stat
= LazyStatement
!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3389 foreach (auto row
; stat
.st
.bindConstText(":tagname", tagnameid
).range
) {
3390 return (row
.threading
!uint == 1);
3393 static auto xstat
= LazyStatement
!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3394 foreach (auto row
; xstat
.st
.bind(":tagid", tagnameid
).range
) {
3395 return (row
.threading
!uint == 1);
3403 returns `true` if the given tag allows attaches.
3405 this is used only when adding new messages, to set all parents to 0.
3407 public bool chiroIsTagAllowAttaches(T
) (T tagnameid
)
3408 if (is(T
:const(char)[]) ||
is(T
:uint))
3410 static if (is(T
:const(char)[])) {
3411 static auto stat
= LazyStatement
!"View"(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`);
3412 foreach (auto row
; stat
.st
.bindConstText(":tagname", tagnameid
).range
) {
3413 return (row
.threading
!uint == 1);
3416 static auto xstat
= LazyStatement
!"View"(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`);
3417 foreach (auto row
; xstat
.st
.bind(":tagid", tagnameid
).range
) {
3418 return (row
.threading
!uint == 1);
3426 relinks all messages in all threads suitable for relinking, and
3427 sets parents to zero otherwise.
3429 public void chiroSupportRelinkAllThreads () {
3430 // yeah, that's it: a single SQL statement
3432 -- clear parents where threading is disabled
3433 SELECT ChiroTimerStart('clearing parents');
3438 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=0)
3441 SELECT ChiroTimerStop();
3443 SELECT ChiroTimerStart('relinking threads');
3448 SELECT uid FROM msgids
3450 -- find MSGID for any of our current references
3451 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3452 -- check if UID for that MSGID has the valid tag
3453 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=threads.tagid)
3459 -- do not process messages with non-threading tags
3460 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=1)
3462 SELECT ChiroTimerStop();
3468 relinks all messages for the given tag, or sets parents to zero if
3469 threading for that tag is disabled.
3471 public void chiroSupportRelinkTagThreads(T
) (T tagnameid
)
3472 if (is(T
:const(char)[]) ||
is(T
:uint))
3474 static if (is(T
:const(char)[])) {
3475 immutable uint tid
= chiroGetTagUid(tagnameid
);
3478 alias tid
= tagnameid
;
3481 static auto statNoTrd
= LazyStatement
!"View"(`
3486 tagid = :tagid AND parent <> 0
3489 static auto statTrd
= LazyStatement
!"View"(`
3494 SELECT uid FROM msgids
3496 -- find MSGID for any of our current references
3497 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
3498 -- check if UID for that MSGID has the valid tag
3499 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=:tagid)
3505 threads.tagid = :tagid
3508 if (!chiroIsTagThreaded(tid
)) {
3509 // clear parents (just in case)
3510 statNoTrd
.st
.bind(":tagid", tid
).doAll();
3512 // yeah, that's it: a single SQL statement
3513 statTrd
.st
.bind(":tagid", tid
).doAll();
3519 * get "from info" for the given message.
3521 * returns `false` if there is no such message.
3523 public bool chiroGetMessageFrom (uint uid
, ref DynStr fromMail
, ref DynStr fromName
) {
3524 static auto statGetFrom
= LazyStatement
!"View"(`
3526 from_name AS fromName
3527 , from_mail AS fromMail
3534 foreach (auto row
; statGetFrom
.st
.bind(":uid", uid
).range
) {
3535 fromMail
= row
.fromMail
!SQ3Text
;
3536 fromName
= row
.fromName
!SQ3Text
;
3544 gets twit title and state for the given (tagid, uid) message.
3546 returns -666 if there is no such message.
3548 public DynStr
chiroGetMessageTwit(T
) (T tagidname
, uint uid
, out bool twited
)
3549 if (is(T
:const(char)[]) ||
is(T
:uint))
3553 if (!uid
) return res
;
3555 static if (is(T
:const(char)[])) {
3556 immutable uint tid
= chiroGetTagUid(tagidname
);
3560 alias tid
= tagidname
;
3563 if (!tid
) return res
;
3565 static auto statGetTwit
= LazyStatement
!"View"(`
3566 SELECT title AS title
3568 WHERE uid=:uid AND tagid=:tagid AND mute>0
3574 .bind(":tagid", tid
);
3575 foreach (auto row
; statGetTwit
.st
.range
) {
3577 res
= row
.title
!SQ3Text
;
3585 gets mute state for the given (tagid, uid) message.
3587 returns -666 if there is no such message.
3589 public int chiroGetMessageMute(T
) (T tagidname
, uint uid
)
3590 if (is(T
:const(char)[]) ||
is(T
:uint))
3592 if (!uid
) return -666;
3594 static if (is(T
:const(char)[])) {
3595 immutable uint tid
= chiroGetTagUid(tagidname
);
3599 alias tid
= tagidname
;
3602 if (!tid
) return -666;
3604 static auto statGetApp
= LazyStatement
!"View"(`
3607 WHERE uid=:uid AND tagid=:tagid
3613 .bind(":tagid", tid
);
3614 foreach (auto row
; statGetApp
.st
.range
) return row
.mute
!int;
3620 sets mute state the given (tagid, uid) message.
3622 doesn't change children states.
3624 public void chiroSetMessageMute(T
) (T tagidname
, uint uid
, Mute mute
)
3625 if (is(T
:const(char)[]) ||
is(T
:uint))
3629 static if (is(T
:const(char)[])) {
3630 immutable uint tid
= chiroGetTagUid(tagidname
);
3634 alias tid
= tagidname
;
3639 static auto statSetApp
= LazyStatement
!"View"(`
3644 uid=:uid AND tagid=:tagid
3647 static auto statSetAppRead
= LazyStatement
!"View"(`
3651 , appearance=iif(appearance=0,1,appearance)
3653 uid=:uid AND tagid=:tagid
3656 if (mute
> Mute
.Normal
) {
3658 .bind(":mute", cast(int)mute
)
3660 .bind(":tagid", tid
)
3664 .bind(":mute", cast(int)mute
)
3666 .bind(":tagid", tid
)
3673 gets appearance for the given (tagid, uid) message.
3675 returns -666 if there is no such message.
3677 public int chiroGetMessageAppearance(T
) (T tagidname
, uint uid
)
3678 if (is(T
:const(char)[]) ||
is(T
:uint))
3680 if (!uid
) return -666;
3682 static if (is(T
:const(char)[])) {
3683 immutable uint tid
= chiroGetTagUid(tagidname
);
3687 alias tid
= tagidname
;
3690 if (!tid
) return -666;
3692 static auto statGetApp
= LazyStatement
!"View"(`
3693 SELECT appearance AS appearance
3695 WHERE uid=:uid AND tagid=:tagid
3701 .bind(":tagid", tid
);
3702 foreach (auto row
; statGetApp
.st
.range
) return row
.appearance
!int;
3708 gets appearance for the given (tagid, uid) message.
3710 public bool chiroGetMessageUnread(T
) (T tagidname
, uint uid
)
3711 if (is(T
:const(char)[]) ||
is(T
:uint))
3713 return (chiroGetMessageAppearance(tagidname
, uid
) == Appearance
.Unread
);
3718 gets appearance for the given (tagid, uid) message.
3720 public bool chiroGetMessageExactRead(T
) (T tagidname
, uint uid
)
3721 if (is(T
:const(char)[]) ||
is(T
:uint))
3723 return (chiroGetMessageAppearance(tagidname
, uid
) == Appearance
.Read
);
3728 sets appearance for the given (tagid, uid) message.
3730 public void chiroSetMessageAppearance(T
) (T tagidname
, uint uid
, Appearance appearance
)
3731 if (is(T
:const(char)[]) ||
is(T
:uint))
3735 static if (is(T
:const(char)[])) {
3736 immutable uint tid
= chiroGetTagUid(tagidname
);
3740 alias tid
= tagidname
;
3745 static auto statSetApp
= LazyStatement
!"View"(`
3748 appearance=:appearance
3750 uid=:uid AND tagid=:tagid
3754 .bind(":appearance", cast(int)appearance
)
3756 .bind(":tagid", tid
)
3762 mark (tagid, uid) message as read.
3764 public void chiroSetReadOrUnreadMessageAppearance(T
) (T tagidname
, uint uid
, Appearance appearance
)
3765 if (is(T
:const(char)[]) ||
is(T
:uint))
3769 static if (is(T
:const(char)[])) {
3770 immutable uint tid
= chiroGetTagUid(tagidname
);
3774 alias tid
= tagidname
;
3779 static auto statSetApp
= LazyStatement
!"View"(`
3784 uid=:uid AND tagid=:tagid AND (appearance=:checkapp0 OR appearance=:checkapp1)
3789 .bind(":tagid", tid
)
3790 .bind(":setapp", cast(int)appearance
)
3791 .bind(":checkapp0", Appearance
.Read
)
3792 .bind(":checkapp1", Appearance
.Unread
)
3798 mark (tagid, uid) message as read.
3800 public void chiroSetMessageRead(T
) (T tagidname
, uint uid
)
3801 if (is(T
:const(char)[]) ||
is(T
:uint))
3803 chiroSetReadOrUnreadMessageAppearance(tagidname
, uid
, Appearance
.Read
);
3807 public void chiroSetMessageUnread(T
) (T tagidname
, uint uid
)
3808 if (is(T
:const(char)[]) ||
is(T
:uint))
3810 chiroSetReadOrUnreadMessageAppearance(tagidname
, uid
, Appearance
.Unread
);
3815 purge all messages with the given tag.
3817 this removes messages from all view tables, removes content from
3818 the "messages" table, and sets "messages" table tags to NULL.
3820 public void chiroDeletePurgedWithTag(T
) (T tagidname
)
3821 if (is(T
:const(char)[]) ||
is(T
:uint))
3823 static if (is(T
:const(char)[])) {
3824 immutable uint tid
= chiroGetTagUid(tagidname
);
3828 alias tid
= tagidname
;
3833 static auto statCountPurged
= LazyStatement
!"View"(`
3834 SELECT COUNT(uid) AS pcount FROM threads
3835 WHERE tagid=:tagid AND appearance=:appr
3838 uint purgedCount
= 0;
3839 foreach (auto row
; statCountPurged
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).range
) {
3840 purgedCount
= row
.pcount
!uint;
3842 if (!purgedCount
) return;
3844 // we will need this to clear storage
3846 scope(exit
) delete plist
;
3847 plist
.reserve(purgedCount
);
3849 static auto statListPurged
= LazyStatement
!"View"(`
3850 SELECT uid AS uid FROM threads
3851 WHERE tagid=:tagid AND appearance=:appr
3855 foreach (auto row
; statListPurged
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).range
) {
3856 plist
~= row
.uid
!uint;
3858 if (plist
.length
== 0) return; // just in case
3860 static auto statClearStorage
= LazyStatement
!"Store"(`
3862 SET tags=NULL, data=NULL
3866 enum BulkClearSQL(string table
) = `
3867 DELETE FROM `~table
~`
3869 uid IN (SELECT uid FROM threads WHERE tagid=:tagid AND appearance=:appr)
3872 // bulk clearing of info
3873 static auto statClearInfo
= LazyStatement
!"View"(BulkClearSQL
!"info");
3874 // bulk clearing of msgids
3875 static auto statClearMsgids
= LazyStatement
!"View"(BulkClearSQL
!"msgids");
3876 // bulk clearing of refids
3877 static auto statClearRefids
= LazyStatement
!"View"(BulkClearSQL
!"refids");
3878 // bulk clearing of text
3879 static auto statClearText
= LazyStatement
!"View"(BulkClearSQL
!"content_text");
3880 // bulk clearing of html
3881 static auto statClearHtml
= LazyStatement
!"View"(BulkClearSQL
!"content_html");
3882 // bulk clearing of attaches
3883 static auto statClearAttach
= LazyStatement
!"View"(BulkClearSQL
!"attaches");
3884 // bulk clearing of threads
3885 static auto statClearThreads
= LazyStatement
!"View"(`
3887 WHERE tagid=:tagid AND appearance=:appr
3890 static if (is(T
:const(char)[])) {
3891 conwriteln("removing ", plist
.length
, " message", (plist
.length
!= 1 ?
"s" : ""), " from '", tagidname
, "'...");
3893 DynStr tname
= chiroGetTagName(tid
);
3894 conwriteln("removing ", plist
.length
, " message", (plist
.length
!= 1 ?
"s" : ""), " from '", tname
.getData
, "'...");
3897 // WARNING! "info" must be cleared FIRST, and "threads" LAST
3899 statClearInfo
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3900 statClearMsgids
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3901 statClearRefids
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3902 statClearText
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3903 statClearHtml
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3904 statClearAttach
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3905 statClearThreads
.st
.bind(":tagid", tid
).bind(":appr", Appearance
.SoftDeletePurge
).doAll();
3906 // relink tag threads
3907 chiroSupportRelinkTagThreads(tid
);
3910 // now clear the storage
3911 conwriteln("clearing the storage...");
3913 foreach (immutable uint uid
; plist
) {
3914 statClearStorage
.st
.bind(":uid", uid
).doAll();
3918 conwriteln("done purging.");
3923 creates "treepane" table for the given tag. that table can be used to
3924 render threaded listview.
3926 returns max id of the existing item. can be used for pagination.
3927 item ids are guaranteed to be sequential, and without any holes.
3928 the first id is `1`.
3930 returned table has "rowid", and two integer fields: "uid" (message uid), and
3931 "level" (message depth, starting from 0).
3933 public uint chiroCreateTreePaneTable(T
) (T tagidname
, int lastmonthes
=12, bool allowThreading
=true)
3934 if (is(T
:const(char)[]) ||
is(T
:uint))
3936 auto ctm
= Timer(true);
3938 // shrink temp table to the bare minimum, because each field costs several msecs
3939 // we don't need parent and time here, because we can easily select them with inner joins
3942 DROP TABLE IF EXISTS treepane;
3943 CREATE TEMP TABLE IF NOT EXISTS treepane (
3944 iid INTEGER PRIMARY KEY
3947 -- to make joins easier
3953 // this need to add answers to some ancient crap
3954 static auto statFirstUnreadTime
= LazyStatement
!"View"(`
3955 SELECT MIN(time) AS time, parent AS parent
3957 WHERE tagid=:tagidname AND appearance=:app
3960 static auto statFindParentFor
= LazyStatement
!"View"(`
3961 SELECT time AS time, parent AS parent
3963 WHERE tagid=:tagidname AND uid=:uid
3967 // clear it (should be faster than dropping and recreating)
3968 dbView
.execute(`DELETE FROM treepane;`);
3970 // this "%08X" will do up to 2038; i'm fine with it
3971 static auto statTrd
= LazyStatement
!"View"(`
3972 INSERT INTO treepane
3974 WITH tree(uid, parent, level, time, path) AS (
3975 WITH RECURSIVE fulltree(uid, parent, level, time, path) AS (
3976 SELECT t.uid AS uid, t.parent AS parent, 1 AS level, t.time AS time, printf('%08X', t.time) AS path
3978 WHERE t.time>=:starttime AND parent=0 AND t.tagid=:tagidname AND t.appearance <> -1
3980 SELECT t.uid AS uid, t.parent AS parent, ft.level+1 AS level, t.time AS time, printf('%s|%08X', ft.path, t.time) AS path
3981 FROM threads t, fulltree ft
3982 WHERE t.time>=:starttime AND t.parent=ft.uid AND t.tagid=:tagidname AND t.appearance <> -1
3984 SELECT * FROM fulltree
3988 , tree.level-1 AS level
3989 , :tagidname AS tagid
3994 static auto statNoTrd
= LazyStatement
!"View"(`
3995 INSERT INTO treepane
4000 , :tagidname AS tagid
4003 threads.time>=:starttime AND threads.tagid=:tagidname AND threads.appearance <> -1
4008 static if (is(T
:const(char)[])) {
4009 immutable uint tid
= chiroGetTagUid(tagidname
);
4013 alias tid
= tagidname
;
4018 if (lastmonthes
> 0) {
4019 if (lastmonthes
> 12*100) {
4022 // show last `lastmonthes` (full monthes)
4023 import std
.datetime
;
4024 import core
.time
: Duration
;
4026 SysTime now
= Clock
.currTime().toUTC();
4027 int year
= now
.year
;
4028 int month
= now
.month
; // from 1
4030 // yes, i am THAT lazy
4031 while (lastmonthes
> 0) {
4032 if (month
> lastmonthes
) { month
-= lastmonthes
; break; }
4033 lastmonthes
-= month
;
4037 // construct unix time
4038 now
.fracSecs
= Duration
.zero
;
4043 now
.month
= cast(Month
)month
;
4045 startTime
= cast(uint)now
.toUnixTime();
4049 // check if we need to fix unread time
4050 // required to show the whole ancient thread if somebody answered
4051 if (startTime
> 0) {
4054 foreach (auto row
; statFirstUnreadTime
.st
.bind(":tagidname", tid
).bind(":app", Appearance
.Unread
).range
) {
4055 unTime
= row
.time
!uint;
4056 unParent
= row
.parent
!uint;
4058 if (unTime
> 0 && unTime
< startTime
) {
4059 // find root message, and start from it
4061 while (unParent
&& allowThreading
) {
4062 statFindParentFor
.st
4063 .bind(":tagidname", tid
)
4064 .bind(":uid", unParent
);
4067 foreach (auto row
; statFindParentFor
.st
.range
) {
4068 unTime
= row
.time
!uint;
4069 unParent
= row
.parent
!uint;
4071 if (unTime
> 0 && unTime
< startTime
) startTime
= unTime
;
4076 if (allowThreading
) {
4077 statTrd
.st
.bind(":tagidname", tid
).bind(":starttime", startTime
).doAll();
4079 statNoTrd
.st
.bind(":tagidname", tid
).bind(":starttime", startTime
).doAll();
4082 if (ChiroTimerEnabled
) writeln("creating treepane time: ", ctm
);
4084 immutable uint res
= cast(uint)dbView
.lastRowId
;
4086 version(chidb_drop_pane_table
) {
4087 dbView
.execute(`CREATE INDEX treepane_uid ON treepane(uid);`);
4095 returns current treepane tagid.
4097 public uint chiroGetTreePaneTableTagId () {
4098 static auto stat
= LazyStatement
!"View"(`SELECT tagid AS tagid FROM treepane WHERE iid=1 LIMIT 1;`);
4099 foreach (auto row
; stat
.st
.range
) return row
.tagid
!uint;
4105 returns current treepane max uid.
4107 public uint chiroGetTreePaneTableMaxUId () {
4108 static auto stat
= LazyStatement
!"View"(`SELECT MAX(uid) AS uid FROM treepane LIMIT 1;`);
4109 foreach (auto row
; stat
.st
.range
) return row
.uid
!uint;
4115 returns number of items in the current treepane.
4117 public uint chiroGetTreePaneTableCount () {
4118 static auto stat
= LazyStatement
!"View"(`SELECT COUNT(*) AS total FROM treepane;`);
4119 foreach (auto row
; stat
.st
.range
) return row
.total
!uint;
4125 returns index of the given uid in the treepane.
4127 public bool chiroIsTreePaneTableUidValid (uint uid
) {
4128 static auto stat
= LazyStatement
!"View"(`SELECT iid AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
4129 if (uid
== 0) return false;
4130 foreach (auto row
; stat
.st
.bind(":uid", uid
).range
) return true;
4136 returns first treepane uid.
4138 public uint chiroGetTreePaneTableFirstUid () {
4139 static auto stmt
= LazyStatement
!"View"(`SELECT uid AS uid FROM treepane WHERE iid=1 LIMIT 1;`);
4140 foreach (auto row
; stmt
.st
.range
) return row
.uid
!uint;
4146 returns last treepane uid.
4148 public uint chiroGetTreePaneTableLastUid () {
4149 static auto stmt
= LazyStatement
!"View"(`SELECT MAX(iid), uid AS uid FROM treepane LIMIT 1;`);
4150 foreach (auto row
; stmt
.st
.range
) return row
.uid
!uint;
4156 returns index of the given uid in the treepane.
4158 public int chiroGetTreePaneTableUid2Index (uint uid
) {
4159 static auto stmt
= LazyStatement
!"View"(`SELECT iid-1 AS idx FROM treepane WHERE uid=:uid LIMIT 1;`);
4160 if (uid
== 0) return -1;
4161 foreach (auto row
; stmt
.st
.bind(":uid", uid
).range
) return row
.idx
!int;
4167 returns uid of the given index in the treepane.
4169 public uint chiroGetTreePaneTableIndex2Uid (int index
) {
4170 static auto stmt
= LazyStatement
!"View"(`SELECT uid AS uid FROM treepane WHERE iid=:idx+1 LIMIT 1;`);
4171 if (index
< 0 || index
== int.max
) return 0;
4172 foreach (auto row
; stmt
.st
.bind(":idx", index
).range
) return row
.uid
!uint;
4178 returns previous uid in the treepane.
4180 public uint chiroGetTreePaneTablePrevUid (uint uid
) {
4181 static auto stmt
= LazyStatement
!"View"(`
4182 SELECT uid AS uid FROM treepane
4183 WHERE iid IN (SELECT iid-1 FROM treepane WHERE uid=:uid LIMIT 1)
4186 if (uid
== 0) return chiroGetTreePaneTableFirstUid();
4187 foreach (auto row
; stmt
.st
.bind(":uid", uid
).range
) return row
.uid
!uint;
4193 returns uid of the given index in the treepane.
4195 public uint chiroGetTreePaneTableNextUid (uint uid
) {
4196 static auto stmt
= LazyStatement
!"View"(`
4197 SELECT uid AS uid FROM treepane
4198 WHERE iid IN (SELECT iid+1 FROM treepane WHERE uid=:uid LIMIT 1)
4201 if (uid
== 0) return chiroGetTreePaneTableFirstUid();
4202 foreach (auto row
; stmt
.st
.bind(":uid", uid
).range
) return row
.uid
!uint;
4208 releases (drops) "treepane" table.
4210 can be called several times, but usually you don't need to call this at all.
4212 public void chiroClearTreePaneTable () {
4213 //dbView.execute(`DROP TABLE IF EXISTS treepane;`);
4214 dbView
.execute(`DELETE FROM treepane;`);
4219 return next unread message uid in treepane, or 0.
4221 public uint chiroGetPaneNextUnread (uint curruid
) {
4222 static auto stmtNext
= LazyStatement
!"View"(`
4223 SELECT treepane.uid AS uid FROM treepane
4224 INNER JOIN threads USING(uid, tagid)
4225 WHERE treepane.iid-1 > :cidx AND threads.appearance=:appr
4229 immutable int cidx
= chiroGetTreePaneTableUid2Index(curruid
);
4230 foreach (auto row
; stmtNext
.st
.bind(":cidx", cidx
).bind(":appr", Appearance
.Unread
).range
) return row
.uid
!uint;
4232 // try from the beginning
4233 foreach (auto row
; stmtNext
.st
.bind(":cidx", -1).bind(":appr", Appearance
.Unread
).range
) return row
.uid
!uint;
4240 selects given number of items starting with the given item id.
4242 returns numer of selected items.
4244 `stiid` counts from zero
4246 WARNING! "treepane" table must be prepared with `chiroCreateTreePaneTable()`!
4248 WARNING! [i]dup `SQ3Text` arguments if necessary, they won't survive the `cb` return!
4250 public int chiroGetPaneTablePage (int stiid
, int limit
,
4251 void delegate (int pgofs
, /* offset from the page start, from zero and up to `limit` */
4252 int iid
, /* item id, counts from zero*/
4253 uint uid
, /* msguid, never zero */
4254 uint parentuid
, /* parent msguid, may be zero */
4255 uint level
, /* threading level, from zero */
4256 Appearance appearance
, /* see above */
4257 Mute mute
, /* see above */
4258 SQ3Text date
, /* string representation of receiving date and time */
4259 SQ3Text subj
, /* message subject, can be empty string */
4260 SQ3Text fromName
, /* message sender name, can be empty string */
4261 SQ3Text fromMail
, /* message sender email, can be empty string */
4262 SQ3Text title
) cb
/* title from twiting */
4264 static auto stat
= LazyStatement
!"View"(`
4267 , treepane.uid AS uid
4268 , treepane.level AS level
4269 , threads.parent AS parent
4270 , threads.appearance AS appearance
4271 , threads.mute AS mute
4272 , datetime(threads.time, 'unixepoch') AS time
4274 , info.from_name AS from_name
4275 , info.from_mail AS from_mail
4276 , threads.title AS title
4278 INNER JOIN info USING(uid)
4279 INNER JOIN threads USING(uid, tagid)
4280 WHERE treepane.iid >= :stiid
4281 ORDER BY treepane.iid
4285 if (limit
<= 0) return 0;
4287 if (stiid
== int.min
) return 0;
4289 if (limit
<= 0) return 0;
4293 foreach (auto row
; stat
.st
.bind(":stiid", stiid
+1).bind(":limit", limit
).range
)
4296 cb(total
, row
.iid
!int, row
.uid
!uint, row
.parent
!uint, row
.level
!uint,
4297 cast(Appearance
)row
.appearance
!int, cast(Mute
)row
.mute
!int,
4298 row
.time
!SQ3Text
, row
.subj
!SQ3Text
, row
.from_name
!SQ3Text
, row
.from_mail
!SQ3Text
, row
.title
!SQ3Text
);
4306 // ////////////////////////////////////////////////////////////////////////// //
4307 /** returns full content of the messare or `null` if no message found (or it was deleted).
4309 public DynStr
chiroGetFullMessageContent (uint uid
) {
4311 if (uid
== 0) return res
;
4312 foreach (auto row
; dbStore
.statement(`SELECT ChiroUnpack(data) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid
).range
) {
4313 res
= row
.result
!SQ3Text
;
4320 /** returns full content of the messare or `null` if no message found (or it was deleted).
4322 public DynStr
chiroMessageHeaders (uint uid
) {
4324 if (uid
== 0) return res
;
4325 foreach (auto row
; dbStore
.statement(`SELECT ChiroExtractHeaders(ChiroUnpack(data)) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid
).range
) {
4326 res
= row
.result
!SQ3Text
;
4333 /** returns full content of the messare or `null` if no message found (or it was deleted).
4335 public DynStr
chiroMessageBody (uint uid
) {
4337 if (uid
== 0) return res
;
4338 foreach (auto row
; dbStore
.statement(`SELECT ChiroExtractBody(ChiroUnpack(data)) AS result FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid
).range
) {
4339 res
= row
.result
!SQ3Text
;
4346 // ////////////////////////////////////////////////////////////////////////// //
4348 Error
, // some error occured
4354 public Bogo
messageBogoCheck (uint uid
) {
4355 if (uid
== 0) return Bogo
.Error
;
4356 DynStr content
= chiroGetFullMessageContent(uid
);
4357 if (content
.length
== 0) return Bogo
.Error
;
4361 //{ auto fo = VFile("/tmp/zzzz", "w"); fo.rawWriteExact(art.data); }
4362 auto pipes
= pipeProcess(["/usr/bin/bogofilter", "-T"]);
4363 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4364 //pipes.stdin.writeln();
4365 //foreach (string s; art.text) pipes.stdin.writeln(s);
4366 pipes
.stdin
.writeln(content
.getData
.xstripright
);
4367 pipes
.stdin
.flush();
4368 pipes
.stdin
.close();
4369 auto res
= pipes
.stdout
.readln();
4371 //conwriteln("RESULT: [", res, "]");
4372 if (res
.length
== 0) {
4373 //conwriteln("ERROR: bogofilter returned nothing");
4376 if (res
[0] == 'H') return Bogo
.Ham
;
4377 if (res
[0] == 'U') return Bogo
.Unsure
;
4378 if (res
[0] == 'S') return Bogo
.Spam
;
4379 //while (res.length && res[$-1] <= ' ') res = res[0..$-1];
4380 //conwriteln("ERROR: bogofilter returned some shit: [", res, "]");
4381 } catch (Exception e
) { // sorry
4382 //conwriteln("ERROR bogofiltering: ", e.msg);
4389 // ////////////////////////////////////////////////////////////////////////// //
4390 private void messageBogoMarkSpamHam(bool spam
) (uint uid
) {
4391 if (uid
== 0) return;
4392 DynStr content
= chiroGetFullMessageContent(uid
);
4393 if (content
.length
== 0) return;
4395 static if (spam
) enum arg
= "-s"; else enum arg
= "-n";
4398 auto pipes
= pipeProcess(["/usr/bin/bogofilter", arg
]);
4399 //foreach (string s; art.headers) pipes.stdin.writeln(s);
4400 //pipes.stdin.writeln();
4401 //foreach (string s; art.text) pipes.stdin.writeln(s);
4402 pipes
.stdin
.writeln(content
.getData
.xstripright
);
4403 pipes
.stdin
.flush();
4404 pipes
.stdin
.close();
4406 } catch (Exception e
) { // sorry
4407 //conwriteln("ERROR bogofiltering: ", e.msg);
4412 public void messageBogoMarkHam (uint uid
) { messageBogoMarkSpamHam
!false(uid
); }
4413 public void messageBogoMarkSpam (uint uid
) { messageBogoMarkSpamHam
!true(uid
); }
4416 // ////////////////////////////////////////////////////////////////////////// //
4417 public alias TwitProcessCallback
= void delegate (const(char)[] msg
, uint curr
, uint total
);
4419 void processEmailTwits (TwitProcessCallback cb
) {
4420 enum Message
= "processing email twits";
4422 auto stFindTwitNameEmail
= LazyStatement
!"View"(`
4425 , threads.tagid AS tagid
4427 INNER JOIN info AS ii ON
4428 ii.uid=threads.uid AND
4429 ii.from_mail=:email AND
4434 auto stFindTwitEmail
= LazyStatement
!"View"(`
4437 , threads.tagid AS tagid
4439 INNER JOIN info AS ii ON
4440 ii.uid=threads.uid AND
4445 auto stFindTwitName
= LazyStatement
!"View"(`
4448 , threads.tagid AS tagid
4450 INNER JOIN info AS ii ON
4451 ii.uid=threads.uid AND
4457 auto stFindTwitNameEmailMasked
= LazyStatement
!"View"(`
4460 , threads.tagid AS tagid
4462 INNER JOIN info AS ii ON
4463 ii.uid=threads.uid AND
4464 ii.from_name=:name AND
4465 ii.from_mail GLOB :email
4469 auto stFindTwitEmailMasked
= LazyStatement
!"View"(`
4472 , threads.tagid AS tagid
4474 INNER JOIN info AS ii ON
4475 ii.uid=threads.uid AND
4476 ii.from_mail GLOB :email
4481 auto stUpdateMute
= LazyStatement
!"View"(`
4483 SET mute=:mute, title=:title
4484 WHERE uid=:uid AND tagid=:tagid AND mute=0
4487 static struct UidTag
{
4493 foreach (auto trow
; dbConf
.statement(`SELECT COUNT(*) AS twitcount FROM emailtwits;`).range
) twitcount
= trow
.twitcount
!uint;
4495 if (cb
!is null) cb(Message
, 0, twitcount
);
4498 CREATE TEMP TABLE IF NOT EXISTS disemails(
4499 email TEXT NOT NULL UNIQUE
4503 if (cb
!is null) cb("dropping temp tables", twitcount
, twitcount
);
4504 dbView
.execute(`DROP TABLE IF EXISTS disemails;`);
4509 foreach (auto trow
; dbConf
.statement(`
4516 WHERE email NOT LIKE '%*%'
4520 auto title
= trow
.title
!SQ3Text
;
4521 if (title
.length
== 0) continue;
4522 auto email
= trow
.email
!SQ3Text
;
4523 auto name
= trow
.name
!SQ3Text
;
4524 assert(email
.indexOf('*') < 0);
4526 if (email
.length
&& name
.length
) {
4527 st
= stFindTwitNameEmail
.st
;
4528 st
.bindConstText(":email", email
).bindConstText(":name", name
);
4529 } else if (email
.length
) {
4530 st
= stFindTwitEmail
.st
;
4531 st
.bindConstText(":email", email
);
4532 } else if (name
.length
) {
4533 st
= stFindTwitName
.st
;
4534 st
.bindConstText(":name", name
);
4539 msguids
.reserve(128);
4540 scope(exit
) delete msguids
;
4541 //writeln("::: ", email, " : ", name);
4542 foreach (auto mrow
; st
.range
) {
4543 auto tname
= chiroGetTagName(mrow
.tagid
!uint);
4544 if (tname
.length
== 0 ||
!globmatch(tname
, trow
.tagglob
!SQ3Text
)) continue;
4545 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4546 msguids
~= UidTag(mrow
.uid
!uint, mrow
.tagid
!uint);
4548 if (msguids
.length
== 0) continue;
4549 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4550 immutable bool muteAllow
= title
.startsWith("!"); // allow this
4551 //transacted!"View"{
4552 foreach (immutable pair
; msguids
) {
4554 .bind(":uid", pair
.uid
)
4555 .bind(":tagid", pair
.tagid
)
4556 .bind(":mute", (muteAllow ? Mute
.Never
: Mute
.ThreadStart
))
4557 .bindConstText(":title", title
)
4561 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4564 if (cb
!is null) cb("selecting distinct emails", twitdone
, twitcount
);
4566 DELETE FROM disemails;
4567 INSERT INTO disemails
4568 SELECT DISTINCT(from_mail) FROM info;
4570 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4573 foreach (auto trow
; dbConf
.statement(`
4580 WHERE email LIKE '%*%'
4584 auto title
= trow
.title
!SQ3Text
;
4585 if (title
.length
== 0) continue;
4586 auto email
= trow
.email
!SQ3Text
;
4587 auto name
= trow
.name
!SQ3Text
;
4588 assert(email
.indexOf('*') >= 0);
4589 assert(email
.length
);
4591 foreach (auto drow
; dbView
.statement(`SELECT email AS demail FROM disemails WHERE email GLOB :email;`)
4592 .bindConstText(":email", email
).range
)
4596 st
= stFindTwitNameEmail
.st
;
4597 st
.bindConstText(":email", drow
.demail
!SQ3Text
).bindConstText(":name", name
);
4599 st
= stFindTwitEmail
.st
;
4600 st
.bindConstText(":email", drow
.demail
!SQ3Text
);
4603 msguids
.reserve(128);
4604 scope(exit
) delete msguids
;
4605 //writeln("::: ", email, " : ", name);
4606 foreach (auto mrow
; st
.range
) {
4607 auto tname
= chiroGetTagName(mrow
.tagid
!uint);
4608 if (tname
.length
== 0 ||
!globmatch(tname
, trow
.tagglob
!SQ3Text
)) continue;
4609 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4610 msguids
~= UidTag(mrow
.uid
!uint, mrow
.tagid
!uint);
4612 if (msguids
.length
== 0) continue;
4613 //conwriteln("updating ", msguids.length, " messages for email=<", email, ">; name=<", name, ">; title={", trow.title!SQ3Text.recodeToKOI8, ">");
4614 immutable bool muteAllow
= title
.startsWith("!"); // allow this
4615 //transacted!"View"{
4616 foreach (immutable pair
; msguids
) {
4618 .bind(":uid", pair
.uid
)
4619 .bind(":tagid", pair
.tagid
)
4620 .bind(":mute", (muteAllow ? Mute
.Never
: Mute
.ThreadStart
))
4621 .bindConstText(":title", title
)
4627 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4630 foreach (auto trow
; dbConf
.statement(`
4637 WHERE email LIKE '%*%'
4641 auto title
= trow
.title
!SQ3Text
;
4642 if (title
.length
== 0) continue;
4643 auto email
= trow
.email
!SQ3Text
;
4644 auto name
= trow
.name
!SQ3Text
;
4645 assert(email
.indexOf('*') >= 0);
4646 assert(email
.length
);
4648 if (email
.length
&& name
.length
) {
4649 st
= stFindTwitNameEmailMasked
.st
;
4650 st
.bindConstText(":email", email
).bindConstText(":name", name
);
4652 st
= stFindTwitEmailMasked
.st
;
4653 st
.bindConstText(":email", email
);
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
)
4678 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4683 //if (cb !is null) cb(Message, twitcount, twitcount);
4687 void processMsgidTwits (TwitProcessCallback cb
) {
4688 enum Message
= "processing msgid twits";
4690 auto stUpdateMute
= LazyStatement
!"View"(`
4692 SET mute=:mute, title=NULL
4693 WHERE uid=:uid AND tagid=:tagid AND mute=0
4696 static struct UidTag
{
4702 foreach (auto trow
; dbConf
.statement(`SELECT COUNT(*) AS twitcount FROM msgidtwits;`).range
) twitcount
= trow
.twitcount
!uint;
4704 if (cb
!is null) cb(Message
, 0, twitcount
);
4708 foreach (auto trow
; dbConf
.statement(`SELECT msgid AS msgid, tagglob AS tagglob FROM msgidtwits;`).range
) {
4711 msguids
.reserve(128);
4712 scope(exit
) delete msguids
;
4714 foreach (auto mrow
; dbView
.statement(`
4715 SELECT threads.uid AS uid, threads.tagid AS tagid
4717 INNER JOIN msgids AS mm
4718 ON mm.msgid=:msgid AND mm.uid=threads.uid
4720 ;`).bindConstText(":msgid", trow
.msgid
!SQ3Text
).range
)
4722 auto tname
= chiroGetTagName(mrow
.tagid
!uint);
4723 if (tname
.length
== 0 ||
!globmatch(tname
, trow
.tagglob
!SQ3Text
)) continue;
4724 //writeln("tag ", mrow.tagid!uint, " (", tname.getData, "); uid=", mrow.uid!uint);
4725 msguids
~= UidTag(mrow
.uid
!uint, mrow
.tagid
!uint);
4727 if (msguids
.length
== 0) continue;
4728 //conwriteln("updating ", msguids.length, " messages for msgid <", trow.msgid!SQ3Text, ">");
4729 //transacted!"View"{
4730 foreach (immutable pair
; msguids
) {
4732 .bind(":uid", pair
.uid
)
4733 .bind(":tagid", pair
.tagid
)
4734 .bind(":mute", Mute
.ThreadStart
)
4738 if (cb
!is null) cb(Message
, twitdone
, twitcount
);
4742 if (cb
!is null) cb(Message
, twitcount
, twitcount
);
4746 void processThreadMutes (TwitProcessCallback cb
) {
4747 enum Message
= "processing thread mutes";
4749 if (cb
!is null) cb(Message
, 0, 0);
4752 ATTACH DATABASE '`~MailDBPath
~`chiview.db' AS chiview;
4756 --------------------------------------------------------------------------------
4757 -- create temp table with mute pairs
4758 SELECT ChiroTimerStart('creating mute pairs');
4759 CREATE TEMP TABLE mutepairs AS
4760 WITH RECURSIVE children(muid, paruid, mtagid) AS (
4761 SELECT 0, chiview.threads.uid, chiview.threads.tagid
4762 FROM chiview.threads
4763 WHERE chiview.threads.parent=0 AND chiview.threads.mute=2
4764 AND EXISTS (SELECT uid FROM chiview.threads AS tx WHERE tx.tagid=chiview.threads.tagid AND tx.parent=chiview.threads.uid)
4767 tt.uid, tt.uid, mtagid
4769 INNER JOIN chiview.threads AS tt
4771 tt.tagid=cc.mtagid AND
4772 tt.parent=cc.paruid AND
4782 SELECT ChiroTimerStop();
4785 SELECT 'nested mute pairs to skip:', COUNT(uid)
4786 FROM chiview.threads
4787 INNER JOIN mutepairs AS tt
4795 SELECT ChiroTimerStart('updating thread mutes');
4796 UPDATE chiview.threads
4799 , appearance=(SELECT CASE WHEN appearance=0 THEN 1 ELSE appearance END)
4800 FROM (SELECT muid, mtagid FROM mutepairs) AS cc
4801 WHERE uid=cc.muid AND tagid=cc.mtagid AND mute=0
4803 SELECT ChiroTimerStop();
4805 DROP TABLE mutepairs;
4808 --SELECT 'secondary mutes:', COUNT(mute) FROM threads WHERE mute=3;
4813 DETACH DATABASE chiview;
4818 public void chiroRecalcAllTwits (TwitProcessCallback cb
) {
4820 conwriteln("clearing all mutes...");
4821 if (cb
!is null) cb("clearing mutes", 0, 0);
4824 SET mute=0, title=NULL
4826 conwriteln("processing email twits...");
4827 processEmailTwits(cb
);
4828 conwriteln("processing msgid twits...");
4829 processMsgidTwits(cb
);
4830 conwriteln("propagating thread twits...");
4831 processThreadMutes(cb
);
4832 conwriteln("twit recalculation complete.");