some code for main UI: it can show list of folders
[chiroptera.git] / chibackend / sqbase.d
blob77fda312b27e8036cb8a4517b88b24038353812f
1 /* E-Mail Client
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module chibackend.sqbase is aliced;
18 private:
20 // do not use, for testing only!
21 // and it seems to generate bigger files, lol
22 //version = use_balz;
24 // use libdeflate instead of zlib
25 // see https://github.com/ebiggers/libdeflate
26 // around 2 times slower on level 9 than zlib, resulting size is 5MB less
27 // around 3 times slower on level 12, resulting size it 10MB less
28 // totally doesn't worth it
29 //version = use_libdeflate;
31 // use libxpack instead of zlib
32 // see https://github.com/ebiggers/xpack
33 // it supports buffers up to 2^19 (524288) bytes (see https://github.com/ebiggers/xpack/issues/1)
34 // therefore it is useless (the resulting file is bigger than with zlib)
35 //version = use_libxpack;
37 // just for fun
38 // see https://github.com/jibsen/brieflz
39 // it has spurious slowdowns, and so is 4 times slower than zlib, with worser compression
40 //version = use_libbrieflz;
42 // apple crap; i just wanted to see how bad it is ;-)
43 // speed is comparable with zlib, compression is shittier by 60MB; crap
44 //version = use_liblzfse;
46 // just for fun
47 //version = use_lzjb;
49 // just for fun, slightly better than lzjb
50 //version = use_lz4;
52 // some compressors from wimlib
53 // see https://wimlib.net/
54 // only one can be selected!
55 // 15 times slower than zlib, much worser compression (~100MB bigger)
56 //version = use_libwim_lzms; // this supports chunks up to our maximum blob size
57 // two times faster than lzms, compression is still awful
58 //version = use_libwim_lzx; // this supports chunks up to 2MB; more-or-less useful
59 // quite fast (because it refuses to compress anything bigger than 64KB); compression is most awful
60 //version = use_libwim_xpress; // this supports chunks up to 64KB; useless
62 // oh, because why not?
63 // surprisingly good (but not as good as zlib), and lightning fast on default compression level
64 // sadly, requires external lib
65 //version = use_zstd;
67 private import std.digest.ripemd;
68 private import iv.strex;
69 private import iv.sq3;
70 private import iv.timer;
71 private import iv.vfs.io;
72 private import iv.vfs.util;
73 private import chibackend.parse;
74 private import chibackend.decode;
75 //private import iv.utfutil;
76 //private import iv.vfs.io;
78 version(use_libdeflate) private import chibackend.pack.libdeflate;
79 else version(use_balz) private import iv.balz;
80 else version(use_libxpack) private import chibackend.pack.libxpack;
81 else version(use_libbrieflz) private import chibackend.pack.libbrieflz;
82 else version(use_liblzfse) private import chibackend.pack.liblzfse;
83 else version(use_lzjb) private import chibackend.pack.lzjb;
84 else version(use_libwim_lzms) private import chibackend.pack.libwim;
85 else version(use_libwim_lzx) private import chibackend.pack.libwim;
86 else version(use_libwim_xpress) private import chibackend.pack.libwim;
87 else version(use_lz4) private import chibackend.pack.liblz4;
88 else version(use_zstd) private import chibackend.pack.libzstd;
91 version(use_zstd) {
92 public enum ChiroDefaultPackLevel = 6;
93 } else {
94 public enum ChiroDefaultPackLevel = 9;
98 // use `MailDBPath()` to get/set it
99 private __gshared string ExpandedMailDBPath = null;
100 public __gshared int ChiroCompressionLevel = ChiroDefaultPackLevel;
102 public __gshared bool ChiroTimerEnabled = false;
103 private __gshared Timer chiTimer = Timer(false);
104 private __gshared char[] chiTimerMsg = null;
106 // opened databases
107 public __gshared Database dbStore; // message store db
108 public __gshared Database dbView; // message view db
109 public __gshared Database dbConf; // config/options db
113 There are several added SQLite functions:
115 ChiroPack(data[, compratio])
116 ===============
118 This tries to compress the given data, and returns a compressed blob.
119 If `compratio` is negative or zero, do not compress anything.
122 ChiroUnpack(data)
123 =================
125 This decompresses the blob compressed with `ChiroPack()`. It is (usually) safe to pass
126 non-compressed data to this function.
129 ChiroNormCRLF(content)
130 ======================
132 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
133 Removes trailing blanks.
136 ChiroNormHeaders(content)
137 =========================
139 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
140 Then replaces 'space, LF' with a single space (joins multiline headers).
141 Removes trailing blanks.
144 ChiroExtractHeaders(content)
145 ============================
147 Can be used to extract headers from the message.
148 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
149 Then replaces 'space, LF' with a single space (joins multiline headers).
150 Removes trailing blanks.
153 ChiroExtractBody(content)
154 =========================
156 Can be used to extract body from the message.
157 Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
158 Then replaces 'space, LF' with a single space (joins multiline headers).
159 Removes trailing blanks and final dot.
163 public enum OptionsDBName = "chiroptera.db";
164 public enum StorageDBName = "chistore.db";
165 public enum SupportDBName = "chiview.db";
168 // ////////////////////////////////////////////////////////////////////////// //
169 private enum CommonPragmas = `
170 PRAGMA case_sensitive_like = OFF;
171 PRAGMA foreign_keys = OFF;
172 PRAGMA locking_mode = NORMAL; /*EXCLUSIVE;*/
173 PRAGMA secure_delete = OFF;
174 PRAGMA threads = 3;
175 PRAGMA trusted_schema = OFF;
176 PRAGMA writable_schema = OFF;
179 enum CommonPragmasRO = CommonPragmas~`
180 PRAGMA temp_store = MEMORY; /*DEFAULT*/ /*FILE*/
183 enum CommonPragmasRW = CommonPragmas~`
184 PRAGMA application_id = 1128810834; /*CHIR*/
185 PRAGMA auto_vacuum = NONE;
186 PRAGMA encoding = "UTF-8";
187 PRAGMA temp_store = DEFAULT;
188 --PRAGMA journal_mode = WAL; /*OFF;*/
189 --PRAGMA journal_mode = DELETE; /*OFF;*/
190 PRAGMA synchronous = NORMAL; /*OFF;*/
193 enum CommonPragmasRecreate = `
194 PRAGMA locking_mode = EXCLUSIVE;
195 PRAGMA journal_mode = OFF;
196 PRAGMA synchronous = OFF;
199 static immutable dbpragmasRO = CommonPragmasRO;
201 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
202 // use the smallest page size, because we don't need to perform alot of selects here
203 static immutable dbpragmasRWStorage = "PRAGMA page_size = 512;"~CommonPragmasRW~"PRAGMA journal_mode = DELETE;";
204 static immutable dbpragmasRWStorageRecreate = dbpragmasRWStorage~CommonPragmasRecreate;
206 // use slightly bigger pages
207 // funny, smaller pages leads to bigger files
208 static immutable dbpragmasRWSupport = "PRAGMA page_size = 4096;"~CommonPragmasRW~"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;";
209 static immutable dbpragmasRWSupportRecreate = dbpragmasRWSupport~CommonPragmasRecreate;
211 // smaller page size is ok
212 // we aren't expecting to change things much, so "DELETE" journal seems to be adequate
213 static immutable dbpragmasRWOptions = "PRAGMA page_size = 512;"~CommonPragmasRW~"PRAGMA journal_mode = DELETE;";
214 static immutable dbpragmasRWOptionsRecreate = dbpragmasRWOptions~CommonPragmasRecreate;
217 enum msgTagNameCheckSQL = `
218 WITH RECURSIVE tagtable(tag, rest) AS (
219 VALUES('', NEW.tags||'|')
220 UNION ALL
221 SELECT
222 SUBSTR(rest, 0, INSTR(rest, '|')),
223 SUBSTR(rest, INSTR(rest, '|')+1)
224 FROM tagtable
225 WHERE rest <> '')
226 SELECT
227 (CASE
228 WHEN tag = '/' THEN RAISE(FAIL, 'tag name violation (root tags are not allowed)')
229 WHEN LENGTH(tag) = 1 THEN RAISE(FAIL, 'tag name violation (too short tag name)')
230 WHEN SUBSTR(tag, LENGTH(tag)) = '/' THEN RAISE(FAIL, 'tag name violation (tag should not end with a slash)')
231 END)
232 FROM tagtable
233 WHERE tag <> '';
236 // main storage and support databases will be in different files
237 static immutable string schemaStorage = `
238 -- deleted messages have empty headers and body
239 -- this is so uids will remain unique on inserting
240 -- tags are used to associate the message with various folders, and stored here for rebuild purposes
241 -- the frontend will use the separate "tags" table to select messages
242 -- deleted messages must not have any tags, and should contain no other data
243 -- (keeping the data is harmless, it simply sits there and takes space)
244 CREATE TABLE IF NOT EXISTS messages (
245 uid INTEGER PRIMARY KEY /* rowid, never zero */
246 , tags TEXT DEFAULT NULL /* associated message tags, '|'-separated; case-sensitive, no extra whitespaces or '||'! */
247 -- article data; MUST contain the ending dot, and be properly dot-stuffed
248 -- basically, this is "what we had received, as is" (*WITH* the ending dot!)
249 -- there is no need to normalize it in any way (and you *SHOULD NOT* do it!)
250 -- it should be compressed with "ChiroPack()", and extracted with "ChiroUnpack()"
251 , data BLOB
254 -- check tag constraints
255 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_insert
256 BEFORE INSERT ON messages
257 FOR EACH ROW
258 BEGIN`~msgTagNameCheckSQL~`
259 END;
261 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_update_tags
262 BEFORE UPDATE OF tags ON messages
263 FOR EACH ROW
264 BEGIN`~msgTagNameCheckSQL~`
265 END;
268 static immutable string schemaStorageIndex = `
272 static immutable string schemaOptions = `
273 -- use "autoincrement" to allow account deletion
274 CREATE TABLE IF NOT EXISTS accounts (
275 accid INTEGER PRIMARY KEY AUTOINCREMENT /* unique, never zero */
276 , checktime INTEGER NOT NULL DEFAULT 15 /* check time, in minutes */
277 , nosendauth INTEGER NOT NULL DEFAULT 0 /* turn off authentication on sending? */
278 , debuglog INTEGER NOT NULL DEFAULT 0 /* do debug logging? */
279 , nocheck INTEGER NOT NULL DEFAULT 0 /* disable checking? */
280 , nntplastindex INTEGER NOT NULL DEFAULT 0 /* last seen article index for NNTP groups */
281 , name TEXT NOT NULL UNIQUE /* account name; lowercase alphanum, '_', '-', '.' */
282 , recvserver TEXT NOT NULL /* server for receiving messages */
283 , sendserver TEXT NOT NULL /* server for sending messages */
284 , user TEXT NOT NULL /* pop3 user name */
285 , pass TEXT NOT NULL /* pop3 password, empty for no authorisation */
286 , realname TEXT NOT NULL /* user name for e-mail headers */
287 , email TEXT NOT NULL /* account e-mail address (full, name@host) */
288 , inbox TEXT NOT NULL /* inbox tag, usually "/accname/inbox", or folder for nntp */
289 , nntpgroup TEXT NOT NULL DEFAULT '' /* nntp group name for NNTP accounts; if empty, this is POP3 account */
293 CREATE TABLE IF NOT EXISTS options (
294 name TEXT NOT NULL UNIQUE
295 , value TEXT
299 CREATE TABLE IF NOT EXISTS addressbook (
300 nick TEXT NOT NULL UNIQUE /* short nick for this address book entry */
301 , name TEXT NOT NULL DEFAULT ''
302 , email TEXT NOT NULL
303 , notes TEXT DEFAULT NULL
307 -- twits by email/name
308 CREATE TABLE IF NOT EXISTS emailtwits (
309 etwitid INTEGER PRIMARY KEY
310 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
311 , email TEXT /* if both name and email present, use only email */
312 , name TEXT /* name to twit by */
313 , title TEXT /* optional title */
314 , notes TEXT /* notes; often URL */
317 -- twits by msgids
318 CREATE TABLE IF NOT EXISTS msgidtwits (
319 mtwitid INTEGER PRIMARY KEY
320 , etwitid INTEGER /* parent mail twit, if any */
321 , automatic INTEGER DEFAULT 1 /* added by message filtering, not from .rc? */
322 , tagglob TEXT NOT NULL /* pattern for "GLOB" */
323 , msgid TEXT /* message used to set twit */
327 static immutable string schemaOptionsIndex = `
328 -- no need to, "UNIQUE" automaptically creates it
329 --CREATE INDEX IF NOT EXISTS accounts_name ON accounts(name);
331 -- this index in implicit
332 --CREATE INDEX IF NOT EXISTS options_name ON options(name);
334 CREATE INDEX IF NOT EXISTS emailtwits_email ON emailtwits(email);
335 CREATE INDEX IF NOT EXISTS emailtwits_name ON emailtwits(name);
337 CREATE INDEX IF NOT EXISTS msgidtwits_msgid ON msgidtwits(msgid);
341 enum schemaSupportTable = `
342 -- tag <-> messageid correspondence
343 -- note that one message can be tagged with more than one tag
344 -- there is always tag with "uid=0", to keep all tags alive
345 -- special tags:
346 -- account:name -- received via this account
347 -- #spam -- this is spam message
348 CREATE TABLE IF NOT EXISTS tagnames (
349 tagid INTEGER PRIMARY KEY
350 , hidden INTEGER NOT NULL DEFAULT 0 /* deleting tags may cause 'tagid' reuse, so it's better to hide them instead */
351 , threading INTEGER NOT NULL DEFAULT 1 /* enable threaded view? */
352 , noattaches INTEGER NOT NULL DEFAULT 0 /* ignore non-text attachments? */
353 , tag TEXT NOT NULL UNIQUE
356 -- it is here, because we don't have a lot of tags, and inserts are slightly faster this way
357 -- it's not required, because "UNIQUE" constraint will create automatic index
358 --CREATE INDEX IF NOT EXISTS tagname_tag ON tagnames(tag);
360 --CREATE INDEX IF NOT EXISTS tagname_tag_uid ON tagnames(tag, tagid);
363 -- each tag has its own unique threads (so uids can be duplicated, but (uid,tagid) paris cannot
364 -- appearance is:
365 -- -1: can be used to ignore messages in thread view
366 -- 0: unread
367 -- 1: read
368 -- 2: soft-delete from filter
369 -- 3: soft-delete by user (will be purged on folder change)
370 -- 4: muted (by filter)
371 -- 5: self-highlight
372 CREATE TABLE IF NOT EXISTS threads (
373 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
374 , tagid INTEGER /* we need separate threads for each tag */
375 , time INTEGER DEFAULT 0 /* unixtime -- creation/send/receive */
376 /* threading info */
377 , parent INTEGER DEFAULT 0 /* uid: parent message in thread, or 0 */
378 /* flags */
379 , appearance INTEGER DEFAULT 0 /* how the message should look */
383 -- WARNING!
384 -- for FTS5 to work, this table must be:
385 -- updated LAST on INSERT
386 -- updated FIRST on DELETE
387 -- this is due to FTS5 triggers
388 -- message texts should NEVER be updated!
389 -- if you want to do update a message:
390 -- first, DELETE the old one from this table
391 -- then, update textx
392 -- then, INSERT here again
393 -- doing it like that will keep FTS5 in sync
394 CREATE TABLE IF NOT EXISTS info (
395 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
396 , from_name TEXT /* can be empty */
397 , from_mail TEXT /* can be empty */
398 , subj TEXT /* can be empty */
399 , to_name TEXT /* can be empty */
400 , to_mail TEXT /* can be empty */
404 -- this holds msgid
405 -- moved to separate table, because this info is used only when inserting new messages
406 CREATE TABLE IF NOT EXISTS msgids (
407 uid INTEGER PRIMARY KEY /* rowid, corresponds to "id" in "messages", never zero */
408 , time INTEGER /* so we can select the most recent message */
409 , msgid TEXT /* message id */
413 -- this holds in-reply-to, and references
414 -- moved to separate table, because this info is used only when inserting new messages
415 CREATE TABLE IF NOT EXISTS refids (
416 uid INTEGER /* rowid, corresponds to "id" in "messages", never zero */
417 , idx INTEGER /* internal index in headers, cannot have gaps, starts from 0 */
418 , msgid TEXT /* message id */
422 -- this ALWAYS contain an entry (yet content may be empty string)
423 CREATE TABLE IF NOT EXISTS content_text (
424 uid INTEGER PRIMARY KEY /* owner message uid */
425 , format TEXT NOT NULL /* optional format, like 'flowed' */
426 , content TEXT NOT NULL /* properly decoded */
430 -- this ALWAYS contain an entry (yet content may be empty string)
431 CREATE TABLE IF NOT EXISTS content_html (
432 uid INTEGER PRIMARY KEY /* owner message uid */
433 , format TEXT NOT NULL /* optional format, like 'flowed' */
434 , content TEXT NOT NULL /* properly decoded */
438 -- this DOES NOT include text and html contents (and may exclude others)
439 CREATE TABLE IF NOT EXISTS attaches (
440 uid INTEGER /* owner message uid */
441 , idx INTEGER /* code should take care of proper autoincrementing this */
442 , mime TEXT NOT NULL /* always lowercased */
443 , name TEXT NOT NULL /* attachment name; always empty for inline content, never empty for non-inline content */
444 , format TEXT NOT NULL /* optional format, like 'flowed' */
445 , content BLOB /* properly decoded; NULL if the attach was dropped */
449 -- this view is used for FTS5 content queries
450 -- it is harmless to keep it here even if FTS5 is not used
451 --DROP VIEW IF EXISTS fts5_msgview;
452 CREATE VIEW IF NOT EXISTS fts5_msgview (uid, sender, subj, text, html)
454 SELECT
455 info.uid AS uid
456 , info.from_name||' '||CHAR(26)||' '||info.from_mail AS sender
457 , info.subj AS subj
458 , ChiroUnpack(content_text.content) AS text
459 , ChiroUnpack(content_html.content) AS html
460 FROM info
461 INNER JOIN content_text USING(uid)
462 INNER JOIN content_html USING(uid)
465 -- old joins
467 LEFT OUTER JOIN content AS content_text ON
468 content_text.uid = info.uid AND content_text.mime = 'text/plain'
469 LEFT OUTER JOIN content AS content_html ON
470 content_html.uid = info.uid AND content_html.mime = 'text/html'
476 enum schemaSupportIndex = `
477 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_tag_uid ON threads(tagid, uid);
478 CREATE UNIQUE INDEX IF NOT EXISTS trd_by_uid_tag ON threads(uid, tagid);
480 -- this is for views where threading is disabled
481 CREATE INDEX IF NOT EXISTS trd_by_tag_time ON threads(tagid, time);
482 --CREATE INDEX IF NOT EXISTS trd_by_tag_time_parent ON threads(tagid, time, parent);
483 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent_time ON threads(tagid, parent, time);
484 --CREATE INDEX IF NOT EXISTS trd_by_tag_parent ON threads(tagid, parent);
485 CREATE INDEX IF NOT EXISTS trd_by_parent_tag ON threads(parent, tagid);
487 -- this is for test if we have any unread articles (we don't mind the exact numbers, tho)
488 CREATE INDEX IF NOT EXISTS trd_by_appearance ON threads(appearance);
489 CREATE INDEX IF NOT EXISTS trd_by_tag_appearance ON threads(tagid, appearance);
490 -- was used in table view creation, not used anymore
491 --CREATE INDEX IF NOT EXISTS trd_by_parent_tag_appearance ON threads(parent, tagid, appearance);
493 -- for theadmsgview
494 -- was used in table view creation, not used anymore
495 --CREATE INDEX IF NOT EXISTS trd_by_tag_appearance_time ON threads(tagid, appearance, time);
497 CREATE INDEX IF NOT EXISTS msgid_by_msgid_time ON msgids(msgid, time DESC);
499 CREATE INDEX IF NOT EXISTS refid_by_refids_idx ON refids(msgid, idx);
500 CREATE INDEX IF NOT EXISTS refid_by_uid_idx ON refids(uid, idx);
502 CREATE INDEX IF NOT EXISTS content_text_by_uid ON content_text(uid);
503 CREATE INDEX IF NOT EXISTS content_html_by_uid ON content_html(uid);
505 CREATE INDEX IF NOT EXISTS attaches_by_uid_name ON attaches(uid, name);
506 CREATE INDEX IF NOT EXISTS attaches_by_uid_idx ON attaches(uid, idx);
509 static immutable string schemaSupport = schemaSupportTable~schemaSupportIndex;
512 static immutable string recreateFTS5 = `
513 DROP TABLE IF EXISTS fts5_messages;
514 CREATE VIRTUAL TABLE fts5_messages USING fts5(
515 sender /* sender name and email, separated by " \x1a " (dec 26) (substitute char) */
516 , subj /* email subject */
517 , text /* email body, text/plain */
518 , html /* email body, text/html */
519 --, uid UNINDEXED /* message uid this comes from (not needed, use "rowid" instead */
520 , tokenize = 'porter unicode61 remove_diacritics 2'
521 , content = 'fts5_msgview'
522 , content_rowid = 'uid'
524 /* sender, subj, text, html */
525 INSERT INTO fts5_messages(fts5_messages, rank) VALUES('rank', 'bm25(1.0, 3.0, 10.0, 6.0)');
528 static immutable string repopulateFTS5 = `
529 SELECT ChiroTimerStart('updating FTS5');
530 BEGIN TRANSACTION;
532 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
533 SELECT uid, sender, subj, text, html
534 FROM fts5_msgview
535 WHERE
536 EXISTS (
537 SELECT threads.tagid FROM threads
538 INNER JOIN tagnames USING(tagid)
539 WHERE
540 threads.uid=fts5_msgview.uid AND
541 tagnames.hidden=0 AND SUBSTR(tagnames.tag, 1, 1)='/'
544 COMMIT TRANSACTION;
545 SELECT ChiroTimerStop();
549 static immutable string recreateFTS5Triggers = `
550 -- triggers to keep the FTS index up to date
551 -- this rely on the proper "info" table update order
552 DROP TRIGGER IF EXISTS fts5xtrig_insert;
553 CREATE TRIGGER fts5xtrig_insert
554 AFTER INSERT ON info
555 BEGIN
556 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
557 SELECT uid, sender, subj, text, html FROM fts5_msgview WHERE uid=NEW.uid LIMIT 1;
558 END;
560 -- not AFTER, because we still need a valid view!
561 DROP TRIGGER IF EXISTS fts5xtrig_delete;
562 CREATE TRIGGER fts5xtrig_delete
563 BEFORE DELETE ON info
564 BEGIN
565 INSERT INTO fts5_messages(fts5_messages, rowid, sender, subj, text, html)
566 SELECT 'delete', uid, sender, subj, text, html FROM fts5_msgview WHERE uid=OLD.uid LIMIT 1;
567 END;
569 -- message texts should NEVER be updated, so no ON UPDATE trigger
573 // ////////////////////////////////////////////////////////////////////////// //
574 private bool isGoodText (const(void)[] buf) pure nothrow @safe @nogc {
575 foreach (immutable ubyte ch; cast(const(ubyte)[])buf) {
576 if (ch < 32) {
577 if (ch != 9 && ch != 10 && ch != 13 && ch != 27) return false;
578 } else {
579 if (ch == 127) return false;
582 return true;
583 //return utf8ValidText(buf);
587 // ////////////////////////////////////////////////////////////////////////// //
588 private bool isBadPrefix (const(char)[] buf) pure nothrow @trusted @nogc {
589 if (buf.length < 5) return false;
590 return
591 buf.ptr[0] == '\x1b' &&
592 buf.ptr[1] >= 'A' && buf.ptr[1] <= 'Z' &&
593 buf.ptr[2] >= 'A' && buf.ptr[2] <= 'Z' &&
594 buf.ptr[3] >= 'A' && buf.ptr[3] <= 'Z' &&
595 buf.ptr[4] >= 'A' && buf.ptr[4] <= 'Z';
599 /* two high bits of the first byte holds the size:
600 00: fit into 6 bits: [0.. 0x3f] (1 byte)
601 01: fit into 14 bits: [0.. 0x3fff] (2 bytes)
602 10: fit into 22 bits: [0.. 0x3f_ffff] (3 bytes)
603 11: fit into 30 bits: [0..0x3fff_ffff] (4 bytes)
605 number is stored as big-endian.
606 will not write anything to `dest` if there is not enough room.
608 returns number of bytes, or 0 if the number is too big.
610 private uint encodeUInt (void[] dest, uint v) nothrow @trusted @nogc {
611 if (v > 0x3fff_ffffU) return 0;
612 ubyte[] d = cast(ubyte[])dest;
613 // 4 bytes?
614 if (v > 0x3f_ffffU) {
615 v |= 0xc000_0000U;
616 if (d.length >= 4) {
617 d.ptr[0] = cast(ubyte)(v>>24);
618 d.ptr[1] = cast(ubyte)(v>>16);
619 d.ptr[2] = cast(ubyte)(v>>8);
620 d.ptr[3] = cast(ubyte)v;
622 return 4;
624 // 3 bytes?
625 if (v > 0x3fffU) {
626 v |= 0x80_0000U;
627 if (d.length >= 3) {
628 d.ptr[0] = cast(ubyte)(v>>16);
629 d.ptr[1] = cast(ubyte)(v>>8);
630 d.ptr[2] = cast(ubyte)v;
632 return 3;
634 // 2 bytes?
635 if (v > 0x3fU) {
636 v |= 0x4000U;
637 if (d.length >= 2) {
638 d.ptr[0] = cast(ubyte)(v>>8);
639 d.ptr[1] = cast(ubyte)v;
641 return 2;
643 // 1 byte
644 if (d.length >= 1) d.ptr[0] = cast(ubyte)v;
645 return 1;
649 private uint decodeUIntLength (const(void)[] dest) pure nothrow @trusted @nogc {
650 const(ubyte)[] d = cast(const(ubyte)[])dest;
651 if (d.length == 0) return 0;
652 switch (d.ptr[0]&0xc0) {
653 case 0x00: return 1;
654 case 0x40: return (d.length >= 2 ? 2 : 0);
655 case 0x80: return (d.length >= 3 ? 3 : 0);
656 default:
658 return (d.length >= 4 ? 4 : 0);
662 // returns uint.max on error (impossible value)
663 private uint decodeUInt (const(void)[] dest) pure nothrow @trusted @nogc {
664 const(ubyte)[] d = cast(const(ubyte)[])dest;
665 if (d.length == 0) return uint.max;
666 uint res = void;
667 switch (d.ptr[0]&0xc0) {
668 case 0x00:
669 res = d.ptr[0];
670 break;
671 case 0x40:
672 if (d.length < 2) return uint.max;
673 res = ((d.ptr[0]&0x3fU)<<8)|d.ptr[1];
674 break;
675 case 0x80:
676 if (d.length < 3) return uint.max;
677 res = ((d.ptr[0]&0x3fU)<<16)|(d.ptr[1]<<8)|d.ptr[2];
678 break;
679 default:
680 if (d.length < 4) return uint.max;
681 res = ((d.ptr[0]&0x3fU)<<24)|(d.ptr[1]<<16)|(d.ptr[2]<<8)|d.ptr[3];
682 break;
684 return res;
688 // returns position AFTER the headers (empty line is skipped too)
689 // returned value is safe for slicing
690 private int sq3Supp_FindHeadersEnd (const(char)* vs, const int sz) {
691 import core.stdc.string : memchr;
692 if (sz <= 0) return 0;
693 const(char)* eptr = cast(const(char)*)memchr(vs, '\n', cast(uint)sz);
694 while (eptr !is null) {
695 ++eptr;
696 int epos = cast(int)cast(usize)(eptr-vs);
697 if (sz-epos < 1) break;
698 if (*eptr == '\r') {
699 if (sz-epos < 2) break;
700 ++epos;
701 ++eptr;
703 if (*eptr == '\n') return epos+1;
704 assert(epos < sz);
705 eptr = cast(const(char)*)memchr(eptr, '\n', cast(uint)(sz-epos));
707 return sz;
711 // hack for some invalid dates
712 uint parseMailDate (const(char)[] s) nothrow {
713 import std.datetime;
714 if (s.length == 0) return 0;
715 try {
716 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
717 } catch (Exception) {}
718 // sometimes this helps
719 usize dcount = 0;
720 foreach_reverse (immutable char ch; s) {
721 if (ch < '0' || ch > '9') break;
722 ++dcount;
724 if (dcount > 4) return 0;
725 s ~= "0000"[0..4-dcount];
726 try {
727 return cast(uint)(parseRFC822DateTime(s).toUTC.toUnixTime);
728 } catch (Exception) {}
729 return 0;
733 // ////////////////////////////////////////////////////////////////////////// //
734 extern(C) {
737 ** ChiroPack(content)
738 ** ChiroPack(content, packflag)
740 ** second form accepts int flag; 0 means "don't pack"
742 private void sq3Fn_ChiroPackCommon (sqlite3_context *ctx, sqlite3_value *val, int packlevel) nothrow @trusted {
743 immutable int sz = sqlite3_value_bytes(val);
744 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
746 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
748 const(char)* vs = cast(const(char) *)sqlite3_value_blob(val);
749 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroPack()`", -1); return; }
751 if (sz >= 0x3fffffff-8) {
752 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_toobig(ctx); return; }
753 sqlite3_result_value(ctx, val);
754 return;
757 import core.stdc.stdlib : malloc, free;
758 import core.stdc.string : memcpy;
760 if (packlevel > 0 && sz > 8) {
761 import core.stdc.stdio : snprintf;
762 char[16] xsz = void;
763 version(use_balz) {
764 xsz[0..5] = "\x1bBALZ";
765 } else version(use_libxpack) {
766 xsz[0..5] = "\x1bXPAK";
767 } else version(use_libbrieflz) {
768 xsz[0..5] = "\x1bBRLZ";
769 } else version(use_liblzfse) {
770 xsz[0..5] = "\x1bLZFS";
771 } else version(use_lzjb) {
772 xsz[0..5] = "\x1bLZJB";
773 } else version(use_libwim_lzms) {
774 xsz[0..5] = "\x1bLZMS";
775 } else version(use_libwim_lzx) {
776 xsz[0..5] = "\x1bLZMX";
777 } else version(use_libwim_xpress) {
778 xsz[0..5] = "\x1bXPRS";
779 } else version(use_lz4) {
780 xsz[0..5] = "\x1bLZ4D";
781 } else version(use_zstd) {
782 xsz[0..5] = "\x1bZSTD";
783 } else {
784 xsz[0..5] = "\x1bZLIB";
786 uint xszlen = encodeUInt(xsz[5..$], cast(uint)sz);
787 if (xszlen) {
788 xszlen += 5;
789 //xsz[xszlen++] = ':';
790 version(use_libbrieflz) {
791 immutable usize bsz = blz_max_packed_size(cast(usize)sz);
792 } else version(use_lzjb) {
793 immutable uint bsz = cast(uint)sz+1024;
794 } else version(use_lz4) {
795 immutable uint bsz = cast(uint)LZ4_compressBound(sz)+1024;
796 } else {
797 immutable uint bsz = cast(uint)sz;
799 char* cbuf = cast(char*)malloc(bsz+xszlen);
800 if (cbuf is null) {
801 if (isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_error_nomem(ctx); return; }
802 } else {
803 cbuf[0..xszlen] = xsz[0..xszlen];
804 version(use_balz) {
805 Balz bz;
806 usize spos = 0;
807 usize dpos = xszlen;
808 try {
809 bz.compress(
810 // reader
811 (buf) {
812 if (spos >= cast(usize)sz) return 0;
813 usize left = cast(usize)sz-spos;
814 if (left > buf.length) left = buf.length;
815 if (left) memcpy(buf.ptr, vs+spos, left);
816 spos += left;
817 return left;
819 // writer
820 (buf) {
821 if (dpos+buf.length >= cast(usize)sz) throw new Exception("uncompressible");
822 memcpy(cbuf+dpos, buf.ptr, buf.length);
823 dpos += buf.length;
825 // maximum compression?
826 true
828 } catch(Exception) {
829 dpos = usize.max;
831 if (dpos < cast(usize)sz) {
832 sqlite3_result_blob(ctx, cbuf, dpos, &free);
833 return;
835 } else version(use_libdeflate) {
836 if (packlevel > 12) packlevel = 12;
837 libdeflate_compressor *cpr = libdeflate_alloc_compressor(packlevel);
838 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
839 usize dsize = libdeflate_zlib_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
840 libdeflate_free_compressor(cpr);
841 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
842 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
843 return;
845 } else version(use_libxpack) {
846 // 2^19 (524288) bytes. This is definitely a big problem and I am planning to address it.
847 // https://github.com/ebiggers/xpack/issues/1
848 if (sz < 524288-64) {
849 if (packlevel > 9) packlevel = 9;
850 xpack_compressor *cpr = xpack_alloc_compressor(cast(usize)sz, packlevel);
851 if (cpr is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
852 usize dsize = xpack_compress(cpr, vs, cast(usize)sz, cbuf+xszlen, bsz);
853 xpack_free_compressor(cpr);
854 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
855 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
856 return;
859 } else version(use_libbrieflz) {
860 if (packlevel > 10) packlevel = 10;
861 immutable usize wbsize = blz_workmem_size_level(cast(usize)sz, packlevel);
862 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
863 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
864 uint dsize = blz_pack_level(vs, cbuf+xszlen, cast(uint)sz, wbuf, packlevel);
865 free(wbuf);
866 if (dsize+xszlen < cast(usize)sz) {
867 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
868 return;
870 } else version(use_liblzfse) {
871 immutable usize wbsize = lzfse_encode_scratch_size();
872 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
873 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
874 usize dsize = lzfse_encode_buffer(cbuf+xszlen, bsz, vs, cast(uint)sz, wbuf);
875 free(wbuf);
876 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
877 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
878 return;
880 } else version(use_lzjb) {
881 usize dsize = lzjb_compress(vs, cast(usize)sz, cbuf+xszlen, bsz);
882 if (dsize == usize.max) dsize = 0;
883 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
884 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
885 return;
887 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "LZJB FAILED!\n"); }
888 } else version(use_libwim_lzms) {
889 wimlib_compressor* cpr;
890 uint clevel = (packlevel < 10 ? 50 : 1000);
891 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZMS, cast(usize)sz, clevel, &cpr);
892 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
893 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
894 wimlib_free_compressor(cpr);
895 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
896 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
897 return;
899 } else version(use_libwim_lzx) {
900 if (sz <= WIMLIB_LZX_MAX_CHUNK) {
901 wimlib_compressor* cpr;
902 uint clevel = (packlevel < 10 ? 50 : 1000);
903 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_LZX, cast(usize)sz, clevel, &cpr);
904 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
905 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
906 wimlib_free_compressor(cpr);
907 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
908 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
909 return;
912 } else version(use_libwim_xpress) {
913 if (sz <= WIMLIB_XPRESS_MAX_CHUNK) {
914 wimlib_compressor* cpr;
915 uint clevel = (packlevel < 10 ? 50 : 1000);
916 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
917 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < cast(uint)sz) csz *= 2U;
918 int rc = wimlib_create_compressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, clevel, &cpr);
919 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
920 usize dsize = wimlib_compress(vs, cast(usize)sz, cbuf+xszlen, bsz, cpr);
921 wimlib_free_compressor(cpr);
922 if (dsize > 0 && dsize+xszlen < cast(usize)sz) {
923 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
924 return;
927 } else version(use_lz4) {
928 int dsize = LZ4_compress_default(vs, cbuf+xszlen, sz, cast(int)bsz);
929 if (dsize > 0 && dsize+xszlen < sz) {
930 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
931 return;
933 } else version(use_zstd) {
934 immutable int clev =
935 packlevel <= 3 ? ZSTD_minCLevel() :
936 packlevel <= 6 ? ZSTD_defaultCLevel() :
937 packlevel < 10 ? 19 :
938 ZSTD_maxCLevel();
939 usize dsize = ZSTD_compress(cbuf+xszlen, cast(int)bsz, vs, sz, clev);
940 if (!ZSTD_isError(dsize) && dsize > 0 && dsize+xszlen < sz) {
941 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
942 return;
944 } else {
945 import etc.c.zlib : /*compressBound,*/ compress2, Z_OK;
946 //uint bsz = cast(uint)compressBound(cast(uint)sz);
947 if (packlevel > 9) packlevel = 9;
948 usize dsize = bsz;
949 int zres = compress2(cast(ubyte *)(cbuf+xszlen), &dsize, cast(const(ubyte) *)vs, sz, packlevel);
950 if (zres == Z_OK && dsize+xszlen < cast(usize)sz) {
951 sqlite3_result_blob(ctx, cbuf, dsize+xszlen, &free);
952 return;
955 free(cbuf);
960 if (isBadPrefix(vs[0..cast(uint)sz])) {
961 char *res = cast(char *)malloc(sz+4);
962 if (res is null) { sqlite3_result_error_nomem(ctx); return; }
963 res[0..5] = "\x1bRAWB";
964 res[5..sz+5] = vs[0..sz];
965 if (isGoodText(vs[0..cast(usize)sz])) {
966 sqlite3_result_text(ctx, res, sz+5, &free);
967 } else {
968 sqlite3_result_blob(ctx, res, sz+5, &free);
970 } else {
971 immutable bool wantBlob = !isGoodText(vs[0..cast(usize)sz]);
972 immutable int tp = sqlite3_value_type(val);
973 if ((wantBlob && tp == SQLITE_BLOB) || (!wantBlob && tp == SQLITE3_TEXT)) {
974 sqlite3_result_value(ctx, val);
975 } else if (wantBlob) {
976 sqlite3_result_blob(ctx, vs, sz, SQLITE_TRANSIENT);
977 } else {
978 sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
985 ** ChiroPack(content)
987 private void sq3Fn_ChiroPack (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
988 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
989 return sq3Fn_ChiroPackCommon(ctx, argv[0], ChiroCompressionLevel);
994 ** ChiroPack(content, packlevel)
996 ** `packlevel` == 0 means "don't pack"
997 ** `packlevel` == 9 means "maximum compression"
999 private void sq3Fn_ChiroPackDPArg (sqlite3_context *ctx, int argc, sqlite3_value **argv) nothrow @trusted {
1000 if (argc != 2) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroPack()`", -1); return; }
1001 return sq3Fn_ChiroPackCommon(ctx, argv[0], sqlite3_value_int(argv[1]));
1006 ** ChiroUnpack(content)
1008 ** it is (almost) safe to pass non-packed content here
1010 private void sq3Fn_ChiroUnpack (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1011 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!000\n"); }
1012 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroUnpack()`", -1); return; }
1014 int sz = sqlite3_value_bytes(argv[0]);
1015 if (sz < 0 || sz > 0x3fffffff-4) { sqlite3_result_error_toobig(ctx); return; }
1017 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1019 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1020 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroUnpack()`", -1); return; }
1022 if (!isBadPrefix(vs[0..cast(uint)sz])) { sqlite3_result_value(ctx, argv[0]); return; }
1023 if (vs[0..5] == "\x1bRAWB") { sqlite3_result_blob(ctx, vs+5, sz-5, SQLITE_TRANSIENT); return; }
1024 if (sz < 6) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1026 enum {
1027 Codec_ZLIB,
1028 Codec_BALZ,
1029 Codec_XPAK,
1030 Codec_BRLZ,
1031 Codec_LZFS,
1032 Codec_LZJB,
1033 Codec_LZMS,
1034 Codec_LZMX,
1035 Codec_XPRS,
1036 Codec_LZ4D,
1037 Codec_ZSTD,
1040 int codec = Codec_ZLIB;
1041 if (vs[0..5] != "\x1bZLIB") {
1042 version(use_balz) {
1043 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBALZ") codec = Codec_BALZ;
1045 version(use_libxpack) {
1046 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1048 version(use_libxpack) {
1049 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPAK") codec = Codec_XPAK;
1051 version(use_libbrieflz) {
1052 if (codec == Codec_ZLIB && vs[0..5] == "\x1bBRLZ") codec = Codec_BRLZ;
1054 version(use_liblzfse) {
1055 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZFS") codec = Codec_LZFS;
1057 version(use_lzjb) {
1058 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZJB") codec = Codec_LZJB;
1060 version(use_libwim_lzms) {
1061 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMS") codec = Codec_LZMS;
1063 version(use_libwim_lzx) {
1064 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZMX") codec = Codec_LZMX;
1066 version(use_libwim_xpress) {
1067 if (codec == Codec_ZLIB && vs[0..5] == "\x1bXPRS") codec = Codec_XPRS;
1069 version(use_lz4) {
1070 if (codec == Codec_ZLIB && vs[0..5] == "\x1bLZ4D") codec = Codec_LZ4D;
1072 version(use_zstd) {
1073 if (codec == Codec_ZLIB && vs[0..5] == "\x1bZSTD") codec = Codec_ZSTD;
1075 if (codec == Codec_ZLIB) { sqlite3_result_error(ctx, "invalid codec in `ChiroUnpack()`", -1); return; }
1078 // skip codec id
1079 // size is guaranteed to be at least 6 here
1080 vs += 5;
1081 sz -= 5;
1083 immutable uint numsz = decodeUIntLength(vs[0..cast(uint)sz]);
1084 //{ 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]); }
1085 //writeln("sq3Fn_ChiroUnpack: nsz=", sz-5);
1086 if (numsz == 0 || numsz > cast(uint)sz) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1087 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!100\n"); }
1088 immutable uint rsize = decodeUInt(vs[0..cast(uint)sz]);
1089 if (rsize == uint.max) { sqlite3_result_error(ctx, "invalid data in `ChiroUnpack()`", -1); return; }
1090 //{ import core.stdc.stdio : fprintf, stderr; fprintf(stderr, "!!!101:rsize=%u\n", rsize); }
1091 if (rsize == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1092 // skip number
1093 vs += numsz;
1094 sz -= cast(int)numsz;
1095 //{ import core.stdc.stdio : printf; printf("sz=%d; rsize=%u\n", sz, rsize, dpos); }
1097 import core.stdc.stdlib : malloc, free;
1098 import core.stdc.string : memcpy;
1100 char* cbuf = cast(char*)malloc(rsize);
1101 if (cbuf is null) { sqlite3_result_error_nomem(ctx); return; }
1102 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz-dpos);
1104 usize dsize = rsize;
1105 final switch (codec) {
1106 case Codec_ZLIB:
1107 version(use_libdeflate) {
1108 libdeflate_decompressor *dcp = libdeflate_alloc_decompressor();
1109 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1110 auto rc = libdeflate_zlib_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1111 if (rc != LIBDEFLATE_SUCCESS) {
1112 free(cbuf);
1113 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1114 return;
1116 } else {
1117 import etc.c.zlib : uncompress, Z_OK;
1118 int zres = uncompress(cast(ubyte *)cbuf, &dsize, cast(const(ubyte) *)vs, sz);
1119 //writeln("sq3Fn_ChiroUnpack: rsize=", rsize, "; left=", sz, "; dsize=", dsize, "; zres=", zres);
1120 if (zres != Z_OK || dsize != rsize) {
1121 free(cbuf);
1122 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1123 return;
1126 break;
1127 case Codec_BALZ:
1128 version(use_balz) {
1129 uint spos = 0;
1130 uint outpos = 0;
1131 try {
1132 Unbalz bz;
1133 auto dc = bz.decompress(
1134 // reader
1135 (buf) {
1136 uint left = cast(uint)sz-spos;
1137 if (left > buf.length) left = cast(uint)buf.length;
1138 if (left != 0) memcpy(buf.ptr, vs, left);
1139 spos += left;
1140 return left;
1142 // writer
1143 (buf) {
1144 uint left = rsize-outpos;
1145 if (left == 0) throw new Exception("broken data");
1146 if (left > buf.length) left = cast(uint)buf.length;
1147 if (left) memcpy(cbuf+outpos, buf.ptr, left);
1148 outpos += left;
1151 if (dc != rsize) throw new Exception("broken data");
1152 } catch (Exception) {
1153 outpos = uint.max;
1155 if (outpos == uint.max) {
1156 free(cbuf);
1157 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1158 return;
1160 dsize = outpos;
1161 } else {
1162 free(cbuf);
1163 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1164 return;
1166 break;
1167 case Codec_XPAK:
1168 version(use_libxpack) {
1169 xpack_decompressor *dcp = xpack_alloc_decompressor();
1170 if (dcp is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1171 auto rc = xpack_decompress(dcp, vs, cast(usize)sz, cbuf, rsize, null);
1172 if (rc != DECOMPRESS_SUCCESS) {
1173 free(cbuf);
1174 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1175 return;
1177 } else {
1178 free(cbuf);
1179 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1180 return;
1182 break;
1183 case Codec_BRLZ:
1184 version(use_libbrieflz) {
1185 dsize = blz_depack_safe(vs, cast(uint)sz, cbuf, rsize);
1186 if (dsize != rsize) {
1187 free(cbuf);
1188 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1189 return;
1191 } else {
1192 free(cbuf);
1193 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1194 return;
1196 break;
1197 case Codec_LZFS:
1198 version(use_liblzfse) {
1199 immutable usize wbsize = lzfse_decode_scratch_size();
1200 void* wbuf = cast(void*)malloc(wbsize+!wbsize);
1201 if (wbuf is null) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1202 dsize = lzfse_decode_buffer(cbuf, cast(usize)rsize, vs, cast(usize)sz, wbuf);
1203 free(wbuf);
1204 if (dsize == 0 || dsize != rsize) {
1205 free(cbuf);
1206 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1207 return;
1209 } else {
1210 free(cbuf);
1211 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1212 return;
1214 break;
1215 case Codec_LZJB:
1216 version(use_lzjb) {
1217 dsize = lzjb_decompress(vs, cast(usize)sz, cbuf, rsize);
1218 if (dsize != rsize) {
1219 free(cbuf);
1220 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1221 return;
1223 } else {
1224 free(cbuf);
1225 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1226 return;
1228 break;
1229 case Codec_LZMS:
1230 version(use_libwim_lzms) {
1231 wimlib_decompressor* dpr;
1232 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZMS, rsize, &dpr);
1233 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1234 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1235 wimlib_free_decompressor(dpr);
1236 if (rc != 0) {
1237 free(cbuf);
1238 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1239 return;
1241 } else {
1242 free(cbuf);
1243 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1244 return;
1246 break;
1247 case Codec_LZMX:
1248 version(use_libwim_lzx) {
1249 wimlib_decompressor* dpr;
1250 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_LZX, rsize, &dpr);
1251 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1252 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1253 wimlib_free_decompressor(dpr);
1254 if (rc != 0) {
1255 free(cbuf);
1256 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1257 return;
1259 } else {
1260 free(cbuf);
1261 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1262 return;
1264 break;
1265 case Codec_XPRS:
1266 version(use_libwim_xpress) {
1267 wimlib_decompressor* dpr;
1268 uint csz = WIMLIB_XPRESS_MIN_CHUNK;
1269 while (csz < WIMLIB_XPRESS_MAX_CHUNK && csz < rsize) csz *= 2U;
1270 int rc = wimlib_create_decompressor(WIMLIB_COMPRESSION_TYPE_XPRESS, csz, &dpr);
1271 if (rc != 0) { free(cbuf); sqlite3_result_error_nomem(ctx); return; }
1272 rc = wimlib_decompress(vs, cast(usize)sz, cbuf, rsize, dpr);
1273 wimlib_free_decompressor(dpr);
1274 if (rc != 0) {
1275 free(cbuf);
1276 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1277 return;
1279 } else {
1280 free(cbuf);
1281 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1282 return;
1284 break;
1285 case Codec_LZ4D:
1286 version(use_lz4) {
1287 dsize = LZ4_decompress_safe(vs, cbuf, sz, rsize);
1288 if (dsize != rsize) {
1289 free(cbuf);
1290 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1291 return;
1293 } else {
1294 free(cbuf);
1295 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1296 return;
1298 break;
1299 case Codec_ZSTD:
1300 version(use_zstd) {
1301 dsize = ZSTD_decompress(cbuf, rsize, vs, sz);
1302 if (ZSTD_isError(dsize) || dsize != rsize) {
1303 free(cbuf);
1304 sqlite3_result_error(ctx, "broken data in `ChiroUnpack()`", -1);
1305 return;
1307 } else {
1308 free(cbuf);
1309 sqlite3_result_error(ctx, "unsupported compression in `ChiroUnpack()`", -1);
1310 return;
1312 break;
1315 if (isGoodText(cbuf[0..dsize])) {
1316 sqlite3_result_text(ctx, cbuf, cast(int)dsize, &free);
1317 } else {
1318 sqlite3_result_blob(ctx, cbuf, cast(int)dsize, &free);
1324 ** ChiroNormCRLF(content)
1326 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1327 ** Removes trailing blanks.
1329 private void sq3Fn_ChiroNormCRLF (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1330 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormCRLF()`", -1); return; }
1332 int sz = sqlite3_value_bytes(argv[0]);
1333 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1335 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1337 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1338 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormCRLF()`", -1); return; }
1340 // check if we have something to do, and calculate new string size
1341 bool needwork = false;
1342 if (vs[cast(uint)sz-1] <= 32) {
1343 needwork = true;
1344 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1345 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1347 uint newsz = cast(uint)sz;
1348 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1349 if (ch == 13) {
1350 needwork = true;
1351 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1352 } else if (!needwork) {
1353 needwork = ((ch < 32 && ch != 9 && ch != 10) || ch == 127);
1357 if (!needwork) {
1358 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1359 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1360 return;
1363 assert(newsz && newsz <= cast(uint)sz);
1365 // need a new string
1366 import core.stdc.stdlib : malloc, free;
1367 char* newstr = cast(char*)malloc(newsz);
1368 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1369 char* dest = newstr;
1370 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1371 if (ch == 13) {
1372 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1373 } else {
1374 if (ch == 127) *dest++ = '~';
1375 else if (ch == 11 || ch == 12) *dest++ = '\n';
1376 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1377 else *dest++ = ch;
1380 assert(dest == newstr+newsz);
1382 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1387 ** ChiroNormHeaders(content)
1389 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1390 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1391 ** Removes trailing blanks.
1393 private void sq3Fn_ChiroNormHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1394 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroNormHeaders()`", -1); return; }
1396 int sz = sqlite3_value_bytes(argv[0]);
1397 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1399 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1401 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1402 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroNormHeaders()`", -1); return; }
1404 // check if we have something to do, and calculate new string size
1405 bool needwork = false;
1406 if (vs[cast(uint)sz-1] <= 32) {
1407 needwork = true;
1408 while (sz > 0 && vs[cast(uint)sz-1] <= 32) --sz;
1409 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1411 uint newsz = cast(uint)sz;
1412 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1413 if (ch == 13) {
1414 needwork = true;
1415 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) --newsz;
1416 } else if (ch == 10) {
1417 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) { needwork = true; --newsz; }
1418 } else if (!needwork) {
1419 needwork = ((ch < 32 && ch != 10) || ch == 127);
1423 if (!needwork) {
1424 if (sqlite3_value_type(argv[0]) == SQLITE3_TEXT) sqlite3_result_value(ctx, argv[0]);
1425 else sqlite3_result_text(ctx, vs, sz, SQLITE_TRANSIENT);
1426 return;
1429 assert(newsz && newsz <= cast(uint)sz);
1431 // need a new string
1432 import core.stdc.stdlib : malloc, free;
1433 char* newstr = cast(char*)malloc(newsz);
1434 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1435 char* dest = newstr;
1436 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1437 if (ch == 13) {
1438 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1439 } else if (ch == 10) {
1440 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1441 } else {
1442 if (ch == 127) *dest++ = '~';
1443 else if (ch < 32 && ch != 10) *dest++ = ' ';
1444 else *dest++ = ch;
1447 assert(dest == newstr+newsz);
1449 sqlite3_result_text(ctx, newstr, cast(int)newsz, &free);
1454 ** ChiroExtractHeaders(content)
1456 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except CR) with spaces.
1457 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1458 ** Removes trailing blanks.
1460 private void sq3Fn_ChiroExtractHeaders (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1461 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1463 int sz = sqlite3_value_bytes(argv[0]);
1464 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1466 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1468 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1469 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1471 // slice headers
1472 sz = sq3Supp_FindHeadersEnd(vs, sz);
1474 // strip trailing blanks
1475 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1476 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1478 // allocate new string (it can be smaller, but will never be bigger)
1479 import core.stdc.stdlib : malloc, free;
1480 char* newstr = cast(char*)malloc(cast(uint)sz);
1481 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1482 char* dest = newstr;
1483 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1484 if (ch == 13) {
1485 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1486 } else if (ch == 10) {
1487 if (idx+1 < cast(uint)sz && vs[idx+1] <= 32) {} else *dest++ = '\n';
1488 } else {
1489 if (ch == 127) *dest++ = '~';
1490 else if (ch < 32 && ch != 10) *dest++ = ' ';
1491 else *dest++ = ch;
1494 assert(dest <= newstr+cast(uint)sz);
1495 sz = cast(int)cast(usize)(dest-newstr);
1496 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1497 sqlite3_result_text(ctx, newstr, sz, &free);
1502 ** ChiroExtractBody(content)
1504 ** Replaces CR/LF with LF, `\x7f` with `~`, control chars (except TAB and CR) with spaces.
1505 ** Then replaces 'space, LF' with a single space (joins multiline headers).
1506 ** Removes trailing blanks and final dot.
1508 private void sq3Fn_ChiroExtractBody (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1509 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroExtractHeaders()`", -1); return; }
1511 int sz = sqlite3_value_bytes(argv[0]);
1512 if (sz < 0 || sz > 0x3fffffff) { sqlite3_result_error_toobig(ctx); return; }
1514 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1516 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1517 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroExtractHeaders()`", -1); return; }
1519 // slice body
1520 immutable int bstart = sq3Supp_FindHeadersEnd(vs, sz);
1521 if (bstart >= sz) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1522 vs += bstart;
1523 sz -= bstart;
1525 // strip trailing dot
1526 if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '\n') sz -= 2;
1527 else if (sz >= 1 && vs[cast(uint)sz-1U] == '\n') --sz;
1528 if (sz == 1 && vs[0] == '.') sz = 0;
1529 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\n' && vs[cast(uint)sz-1U] == '.') --sz;
1530 else if (sz >= 2 && vs[cast(uint)sz-2U] == '\r' && vs[cast(uint)sz-1U] == '.') --sz;
1532 // strip trailing blanks
1533 while (sz > 0 && vs[cast(uint)sz-1U] <= 32) --sz;
1534 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1536 // allocate new string (it can be smaller, but will never be bigger)
1537 import core.stdc.stdlib : malloc, free;
1538 char* newstr = cast(char*)malloc(cast(uint)sz);
1539 if (newstr is null) { sqlite3_result_error_nomem(ctx); return; }
1540 char* dest = newstr;
1541 foreach (immutable idx, immutable char ch; vs[0..cast(uint)sz]) {
1542 if (ch == 13) {
1543 if (idx+1 < cast(uint)sz && vs[idx+1] == 10) {} else *dest++ = ' ';
1544 } else {
1545 if (ch == 127) *dest++ = '~';
1546 else if (ch == 11 || ch == 12) *dest++ = '\n';
1547 else if (ch < 32 && ch != 9 && ch != 10) *dest++ = ' ';
1548 else *dest++ = ch;
1551 assert(dest <= newstr+cast(uint)sz);
1552 sz = cast(int)cast(usize)(dest-newstr);
1553 if (sz == 0) { sqlite3_result_text(ctx, "", 0, SQLITE_STATIC); return; }
1554 sqlite3_result_text(ctx, newstr, sz, &free);
1559 ** ChiroRIPEMD160(content)
1561 ** Calculates RIPEMD160 hash over the given content.
1563 ** Returns BINARY BLOB! You can use `tolower(hex(ChiroRIPEMD160(contents)))`
1564 ** to get lowercased hex hash string.
1566 private void sq3Fn_ChiroRIPEMD160 (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1567 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to `ChiroRIPEMD160()`", -1); return; }
1569 immutable int sz = sqlite3_value_bytes(argv[0]);
1570 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1572 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1573 if (!vs && sz == 0) vs = "";
1574 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in `ChiroRIPEMD160()`", -1); return; }
1576 ubyte[20] hash = ripemd160Of(vs[0..cast(uint)sz]);
1577 sqlite3_result_blob(ctx, cast(const(char)*)hash.ptr, cast(int)hash.length, SQLITE_TRANSIENT);
1581 enum HeaderProcStartTpl(string fnname) = `
1582 if (argc != 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"`~fnname~`()\"", -1); return; }
1584 immutable int sz = sqlite3_value_bytes(argv[0]);
1585 if (sz < 0) { sqlite3_result_error_toobig(ctx); return; }
1587 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1588 if (!vs && sz == 0) vs = "";
1589 if (!vs) { sqlite3_result_error(ctx, "cannot get blob data in \"`~fnname~`()\"", -1); return; }
1591 const(char)[] hdrs = vs[0..cast(usize)sq3Supp_FindHeadersEnd(vs, sz)];
1596 ** ChiroHdr_NNTPIndex(headers)
1598 ** The content must be email with headers (or headers only).
1599 ** Returns "NNTP-Index" field or zero (int).
1601 private void sq3Fn_ChiroHdr_NNTPIndex (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1602 mixin(HeaderProcStartTpl!"ChiroHdr_NNTPIndex");
1604 uint nntpidx = 0;
1606 auto nntpidxfld = findHeaderField(hdrs, "NNTP-Index");
1607 if (nntpidxfld.length) {
1608 auto id = nntpidxfld.getFieldValue;
1609 if (id.length) {
1610 foreach (immutable ch; id) {
1611 if (ch < '0' || ch > '9') { nntpidx = 0; break; }
1612 if (nntpidx == 0 && ch == '0') continue;
1613 immutable uint nn = nntpidx*10u+(ch-'0');
1614 if (nn <= nntpidx) nntpidx = 0x7fffffff; else nntpidx = nn;
1619 // it is safe, it can't overflow
1620 sqlite3_result_int(ctx, cast(int)nntpidx);
1625 ** ChiroHdr_RecvTime(headers)
1627 ** The content must be email with headers (or headers only).
1628 ** Returns unixtime (can be zero).
1630 private void sq3Fn_ChiroHdr_RecvTime (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1631 mixin(HeaderProcStartTpl!"ChiroHdr_RecvTime");
1633 uint msgtime = 0; // message receiving time
1635 auto datefld = findHeaderField(hdrs, "Injection-Date");
1636 if (datefld.length != 0) {
1637 auto v = datefld.getFieldValue;
1638 try {
1639 msgtime = parseMailDate(v);
1640 } catch (Exception) {
1641 //writeln("UID=", uid, ": FUCKED INJECTION-DATE: |", v, "|");
1642 msgtime = 0; // just in case
1646 if (!msgtime) {
1647 // obsolete NNTP date field, because why not?
1648 datefld = findHeaderField(hdrs, "NNTP-Posting-Date");
1649 if (datefld.length != 0) {
1650 auto v = datefld.getFieldValue;
1651 try {
1652 msgtime = parseMailDate(v);
1653 } catch (Exception) {
1654 //writeln("UID=", uid, ": FUCKED NNTP-POSTING-DATE: |", v, "|");
1655 msgtime = 0; // just in case
1660 if (!msgtime) {
1661 datefld = findHeaderField(hdrs, "Date");
1662 if (datefld.length != 0) {
1663 auto v = datefld.getFieldValue;
1664 try {
1665 msgtime = parseMailDate(v);
1666 } catch (Exception) {
1667 //writeln("UID=", uid, ": FUCKED DATE: |", v, "|");
1668 msgtime = 0; // just in case
1673 // finally, try to get time from "Received:"
1674 //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
1675 if (!msgtime) {
1676 //writeln("!!! --- !!!");
1677 uint lowesttime = uint.max;
1678 foreach (uint fidx; 0..uint.max) {
1679 auto recvfld = findHeaderField(hdrs, "Received", fidx);
1680 if (recvfld.length == 0) break;
1681 auto lsemi = recvfld.lastIndexOf(';');
1682 if (lsemi >= 0) recvfld = recvfld[lsemi+1..$].xstrip;
1683 if (recvfld.length != 0) {
1684 auto v = recvfld.getFieldValue;
1685 uint tm = 0;
1686 try {
1687 tm = parseMailDate(v);
1688 } catch (Exception) {
1689 //writeln("UID=", uid, ": FUCKED RECV DATE: |", v, "|");
1690 tm = 0; // just in case
1692 //writeln(tm, " : ", lowesttime);
1693 if (tm && tm < lowesttime) lowesttime = tm;
1696 if (lowesttime != uint.max) msgtime = lowesttime;
1699 sqlite3_result_int64(ctx, msgtime);
1704 ** ChiroHdr_FromEmail(headers)
1706 ** The content must be email with headers (or headers only).
1707 ** Returns email "From" field.
1709 private void sq3Fn_ChiroHdr_FromEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1710 mixin(HeaderProcStartTpl!"ChiroHdr_FromEmail");
1711 auto from = findHeaderField(hdrs, "From").decodeSubj.extractMail;
1712 if (from.length == 0) {
1713 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
1714 } else {
1715 sqlite3_result_text(ctx, from.ptr, cast(int)from.length, SQLITE_TRANSIENT);
1721 ** ChiroHdr_ToEmail(headers)
1723 ** The content must be email with headers (or headers only).
1724 ** Returns email "From" field.
1726 private void sq3Fn_ChiroHdr_ToEmail (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1727 mixin(HeaderProcStartTpl!"ChiroHdr_ToEmail");
1728 //auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
1729 auto to = findHeaderField(hdrs, "To").decodeSubj.extractMail;
1730 if (to.length == 0) {
1731 sqlite3_result_text(ctx, "nobody@nowhere", -1, SQLITE_STATIC);
1732 } else {
1733 sqlite3_result_text(ctx, to.ptr, cast(int)to.length, SQLITE_TRANSIENT);
1739 ** ChiroHdr_Subj(headers)
1741 ** The content must be email with headers (or headers only).
1742 ** Returns email "From" field.
1744 private void sq3Fn_ChiroHdr_Subj (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1745 mixin(HeaderProcStartTpl!"sq3Fn_ChiroHdr_Subj");
1746 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
1747 if (subj.length == 0) {
1748 sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
1749 } else {
1750 sqlite3_result_text(ctx, subj.ptr, cast(int)subj.length, SQLITE_TRANSIENT);
1756 ** ChiroTimerStart([msg])
1758 ** The content must be email with headers (or headers only).
1759 ** Returns email "From" field.
1761 private void sq3Fn_ChiroTimerStart (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1762 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStart()\"", -1); return; }
1764 delete chiTimerMsg;
1766 if (argc == 1) {
1767 immutable int sz = sqlite3_value_bytes(argv[0]);
1768 if (sz > 0) {
1769 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1770 if (vs) {
1771 chiTimerMsg = new char[cast(usize)sz];
1772 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
1773 writeln("started ", chiTimerMsg, "...");
1778 sqlite3_result_int(ctx, 1);
1779 chiTimer.restart();
1784 ** ChiroTimerStop([msg])
1786 ** The content must be email with headers (or headers only).
1787 ** Returns email "From" field.
1789 private void sq3Fn_ChiroTimerStop (sqlite3_context *ctx, int argc, sqlite3_value **argv) {
1790 chiTimer.stop;
1791 if (argc > 1) { sqlite3_result_error(ctx, "invalid number of arguments to \"ChiroTimerStop()\"", -1); return; }
1793 if (ChiroTimerEnabled) {
1794 if (argc == 1) {
1795 delete chiTimerMsg;
1796 immutable int sz = sqlite3_value_bytes(argv[0]);
1797 if (sz > 0) {
1798 const(char)* vs = cast(const(char) *)sqlite3_value_blob(argv[0]);
1799 if (vs) {
1800 chiTimerMsg = new char[cast(usize)sz];
1801 chiTimerMsg[0..cast(usize)sz] = vs[0..cast(usize)sz];
1806 char[128] buf;
1807 auto tstr = chiTimer.toBuffer(buf[]);
1808 if (chiTimerMsg.length) {
1809 writeln("done ", chiTimerMsg, ": ", tstr);
1810 } else {
1811 writeln("time: ", tstr);
1815 delete chiTimerMsg;
1817 sqlite3_result_int(ctx, 1);
1821 // ////////////////////////////////////////////////////////////////////////// //
1825 // ////////////////////////////////////////////////////////////////////////// //
1826 private void registerFunctions (ref Database db) {
1827 immutable int rc = sqlite3_extended_result_codes(db.getHandle, 1);
1828 if (rc != SQLITE_OK) {
1829 import core.stdc.stdio : stderr, fprintf;
1830 fprintf(stderr, "SQLITE WARNING: cannot enable extended result codes (this is harmless).\n");
1832 db.createFunction("ChiroPack", 1, &sq3Fn_ChiroPack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1833 db.createFunction("ChiroPack", 2, &sq3Fn_ChiroPackDPArg, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1834 db.createFunction("ChiroUnpack", 1, &sq3Fn_ChiroUnpack, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1835 db.createFunction("ChiroNormCRLF", 1, &sq3Fn_ChiroNormCRLF, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1836 db.createFunction("ChiroNormHeaders", 1, &sq3Fn_ChiroNormHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1837 db.createFunction("ChiroExtractHeaders", 1, &sq3Fn_ChiroExtractHeaders, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1838 db.createFunction("ChiroExtractBody", 1, &sq3Fn_ChiroExtractBody, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1839 db.createFunction("ChiroRIPEMD160", 1, &sq3Fn_ChiroRIPEMD160, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1840 db.createFunction("ChiroHdr_NNTPIndex", 1, &sq3Fn_ChiroHdr_NNTPIndex, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1841 db.createFunction("ChiroHdr_RecvTime", 1, &sq3Fn_ChiroHdr_RecvTime, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1842 db.createFunction("ChiroHdr_FromEmail", 1, &sq3Fn_ChiroHdr_FromEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1843 db.createFunction("ChiroHdr_ToEmail", 1, &sq3Fn_ChiroHdr_ToEmail, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1844 db.createFunction("ChiroHdr_Subj", 1, &sq3Fn_ChiroHdr_Subj, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1846 db.createFunction("ChiroTimerStart", 0, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1847 db.createFunction("ChiroTimerStart", 1, &sq3Fn_ChiroTimerStart, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1848 db.createFunction("ChiroTimerStop", 0, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1849 db.createFunction("ChiroTimerStop", 1, &sq3Fn_ChiroTimerStop, moreflags:/*SQLITE_DIRECTONLY*/SQLITE_INNOCUOUS);
1853 // ////////////////////////////////////////////////////////////////////////// //
1854 public void chiroRecreateStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName) {
1855 try { import std.file : remove; remove(dbname); } catch (Exception) {}
1856 dbStore = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWStorageRecreate, schemaStorage);
1857 registerFunctions(dbStore);
1858 dbStore.setOnClose(schemaStorageIndex~dbpragmasRWStorage~"ANALYZE;");
1862 // ////////////////////////////////////////////////////////////////////////// //
1863 public void chiroRecreateViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName) {
1864 try { import std.file : remove; remove(dbname); } catch (Exception) {}
1865 dbView = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWSupportRecreate, schemaSupportTable);
1866 registerFunctions(dbView);
1867 dbView.setOnClose(schemaSupportIndex~dbpragmasRWSupport~"ANALYZE;");
1871 public void chiroCreateViewIndiciesDB () {
1872 dbView.setOnClose(dbpragmasRWSupport~"ANALYZE;");
1873 dbView.execute(schemaSupportIndex);
1877 // ////////////////////////////////////////////////////////////////////////// //
1878 public void chiroRecreateConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName) {
1879 try { import std.file : remove; remove(dbname); } catch (Exception) {}
1880 dbConf = Database(dbname, Database.Mode.ReadWriteCreate, dbpragmasRWOptionsRecreate, schemaOptions);
1881 registerFunctions(dbConf);
1882 dbConf.setOnClose(schemaOptionsIndex~dbpragmasRWOptions~"ANALYZE;");
1886 // ////////////////////////////////////////////////////////////////////////// //
1887 public void chiroOpenStorageDB (const(char)[] dbname=ExpandedMailDBPath~StorageDBName, bool readonly=false) {
1888 dbStore = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWStorage), schemaStorage);
1889 registerFunctions(dbStore);
1890 if (!readonly) dbStore.setOnClose("PRAGMA optimize;");
1894 // ////////////////////////////////////////////////////////////////////////// //
1895 public void chiroOpenViewDB (const(char)[] dbname=ExpandedMailDBPath~SupportDBName, bool readonly=false) {
1896 dbView = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWSupport), schemaSupport);
1897 registerFunctions(dbView);
1898 if (!readonly) dbView.setOnClose("PRAGMA optimize;");
1902 // ////////////////////////////////////////////////////////////////////////// //
1903 public void chiroOpenConfDB (const(char)[] dbname=ExpandedMailDBPath~OptionsDBName, bool readonly=false) {
1904 dbConf = Database(dbname, (readonly ? Database.Mode.ReadOnly : Database.Mode.ReadWrite), (readonly ? dbpragmasRO : dbpragmasRWOptions), schemaOptions);
1905 registerFunctions(dbConf);
1906 if (!readonly) dbConf.setOnClose("PRAGMA optimize;");
1910 // ////////////////////////////////////////////////////////////////////////// //
1912 recreates FTS5 (full-text search) info.
1914 public void chiroRecreateFTS5 (bool repopulate=true) {
1915 dbView.execute(recreateFTS5);
1916 if (repopulate) dbView.execute(repopulateFTS5);
1917 dbView.execute(recreateFTS5Triggers);
1921 // ////////////////////////////////////////////////////////////////////////// //
1922 extern(C) {
1923 static void errorLogCallback (void *pArg, int rc, const char *zMsg) {
1924 import core.stdc.stdio : stderr, fprintf;
1925 switch (rc) {
1926 case SQLITE_NOTICE: fprintf(stderr, "***SQLITE NOTICE: %s\n", zMsg); break;
1927 case SQLITE_NOTICE_RECOVER_WAL: fprintf(stderr, "***SQLITE NOTICE (WAL RECOVER): %s\n", zMsg); break;
1928 case SQLITE_NOTICE_RECOVER_ROLLBACK: fprintf(stderr, "***SQLITE NOTICE (ROLLBACK RECOVER): %s\n", zMsg); break;
1929 /* */
1930 case SQLITE_WARNING: fprintf(stderr, "***SQLITE WARNING: %s\n", zMsg); break;
1931 case SQLITE_WARNING_AUTOINDEX: fprintf(stderr, "***SQLITE AUTOINDEX WARNING: %s\n", zMsg); break;
1932 /* */
1933 case SQLITE_CANTOPEN:
1934 case SQLITE_SCHEMA:
1935 break; // ignore those
1936 /* */
1937 default: fprintf(stderr, "***SQLITE LOG(%d) [%s]: %s\n", rc, sqlite3_errstr(rc), zMsg); break;
1943 static string sqerrstr (immutable int rc) nothrow @trusted {
1944 const(char)* msg = sqlite3_errstr(rc);
1945 if (!msg || !msg[0]) return null;
1946 import core.stdc.string : strlen;
1947 return msg[0..strlen(msg)].idup;
1951 static void sqconfigcheck (immutable int rc, string msg, bool fatal) {
1952 if (rc == SQLITE_OK) return;
1953 if (fatal) {
1954 string errmsg = sqerrstr(rc);
1955 throw new Exception("FATAL: "~msg~": "~errmsg);
1956 } else {
1957 if (msg is null) msg = "";
1958 import core.stdc.stdio : stderr, fprintf;
1959 fprintf(stderr, "SQLITE WARNING: %.*s (this is harmless): %s\n", cast(uint)msg.length, msg.ptr, sqlite3_errstr(rc));
1964 // call this BEFORE opening any SQLite database connection!
1965 public void chiroSwitchToSingleThread () {
1966 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SINGLETHREAD), "cannot set single-threaded mode", fatal:false);
1970 public string MailDBPath () nothrow @trusted @nogc { return ExpandedMailDBPath; }
1973 public void MailDBPath(T:const(char)[]) (T mailpath) nothrow @trusted {
1974 while (mailpath.length > 1 && mailpath[$-1] == '/') mailpath = mailpath[0..$-1];
1976 if (mailpath.length == 0 || mailpath == ".") {
1977 ExpandedMailDBPath = "";
1978 return;
1981 if (mailpath[0] == '~') {
1982 char[] dpath = new char[mailpath.length+4096];
1983 dpath = expandTilde(dpath, mailpath);
1985 while (dpath.length > 1 && dpath[$-1] == '/') dpath = dpath[0..$-1];
1986 dpath ~= '/';
1987 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
1988 } else {
1989 char[] dpath = new char[mailpath.length+1];
1990 dpath[0..$-1] = mailpath[];
1991 dpath[$-1] = '/';
1992 ExpandedMailDBPath = cast(string)dpath; // it is safe to cast here
1997 shared static this () {
1998 enum {
1999 SQLITE_CONFIG_STMTJRNL_SPILL = 26, /* int nByte */
2000 SQLITE_CONFIG_SMALL_MALLOC = 27, /* boolean */
2003 if (!sqlite3_threadsafe()) {
2004 throw new Exception("FATAL: SQLite must be compiled with threading support!");
2007 // we are interested in all errors
2008 sqlite3_config(SQLITE_CONFIG_LOG, &errorLogCallback, null);
2010 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SERIALIZED), "cannot set SQLite serialized threading mode", fatal:true);
2011 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_SMALL_MALLOC, 0), "cannot enable SQLite unrestriced malloc mode", fatal:false);
2012 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_URI, 1), "cannot enable SQLite URI handling", fatal:false);
2013 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_COVERING_INDEX_SCAN, 1), "cannot enable SQLite covering index scan", fatal:false);
2014 sqconfigcheck(sqlite3_config(SQLITE_CONFIG_STMTJRNL_SPILL, 512*1024), "cannot set SQLite statement journal spill threshold", fatal:false);
2016 MailDBPath = "~/Mail";
2020 shared static ~this () {
2021 dbConf.close();
2022 dbView.close();
2023 dbStore.close();
2027 // ////////////////////////////////////////////////////////////////////////// //
2028 public void chiroSetOption(T) (const(char)[] name, T value)
2029 if (__traits(isIntegral, T) || is(T:const(char)[]))
2031 assert(name.length != 0);
2032 auto stat = dbConf.statement(`
2033 INSERT INTO options
2034 ( name, value)
2035 VALUES(:name,:value)
2036 ON CONFLICT(name)
2037 DO UPDATE SET value=:value
2038 ;`).bindText(":name", name, transient:false);
2039 static if (is(T == typeof(null))) {
2040 stat.bindText(":value", "", transient:false);
2041 } else static if (__traits(isIntegral, T)) {
2042 stat.bind(":value", value);
2043 } else static if (is(T:const(char)[])) {
2044 stat.bindText(":value", value);
2045 } else {
2046 static assert(0, "oops");
2048 stat.doAll();
2052 public T chiroGetOption(T) (const(char)[] name, T defval=T.init)
2053 if (__traits(isIntegral, T) || is(T:const(char)[]))
2055 assert(name.length != 0);
2056 foreach (auto row; dbConf.statement(`SELECT value AS value FROM options WHERE name=:name;`).bindText(":name", name, transient:false).range) {
2057 return row.value!T;
2059 return defval;
2063 // ////////////////////////////////////////////////////////////////////////// //
2065 inserts the one message from the message storage with the given id into view storage.
2066 parses it and such, and optionally updates threads.
2068 doesn't updates NNTP indicies and such, never relinks anything.
2070 invalid (unknown) tags will be ignored.
2072 returns number of processed messages.
2074 doesn't start/end any transactions, so wrap it yourself.
2076 public bool chiroParseAndInsertOneMessage (uint uid, uint msgtime, int appearance,
2077 const(char)[] hdrs, const(char)[] body, const(char)[] tags)
2079 auto stInsThreads = dbView.statement(`
2080 INSERT INTO threads
2081 ( uid, tagid, time, appearance)
2082 VALUES(:uid,:tagid,:time,:appearance)
2083 ;`);
2085 auto stInsInfo = dbView.statement(`
2086 INSERT INTO info
2087 ( uid, from_name, from_mail, subj, to_name, to_mail)
2088 VALUES(:uid,:from_name,:from_mail,:subj,:to_name,:to_mail)
2089 ;`);
2091 auto stInsMsgId = dbView.statement(`
2092 INSERT INTO msgids
2093 ( uid, msgid, time)
2094 VALUES(:uid,:msgid,:time)
2095 ;`);
2097 auto stInsMsgRefId = dbView.statement(`
2098 INSERT INTO refids
2099 ( uid, idx, msgid)
2100 VALUES(:uid,:idx,:msgid)
2101 ;`);
2103 auto stInsContentText = dbView.statement(`
2104 INSERT INTO content_text
2105 ( uid, format, content)
2106 VALUES(:uid,:format, ChiroPack(:content))
2107 ;`);
2109 auto stInsContentHtml = dbView.statement(`
2110 INSERT INTO content_html
2111 ( uid, format, content)
2112 VALUES(:uid,:format, ChiroPack(:content))
2113 ;`);
2115 auto stInsAttach = dbView.statement(`
2116 INSERT INTO attaches
2117 ( uid, idx, mime, name, format, content)
2118 VALUES(:uid,:idx,:mime,:name,:format, ChiroPack(:content))
2119 ;`);
2121 bool noattaches = false; // do not store attaches?
2123 // create thread record for each tag (and update max nntp index)
2124 int tagCount = 0;
2125 int noAttachCount = 0;
2126 while (tags.length) {
2127 auto eep = tags.indexOf('|');
2128 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
2129 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
2130 if (tagname.length == 0) continue;
2132 immutable uint tuid = chiroGetTagUid(tagname);
2133 if (tuid == 0) continue;
2135 /* nope
2136 if (nntpidx > 0 && tagname.startsWith("account:")) {
2137 auto accname = tagname[8..$];
2138 stInsNNTPIdx
2139 .bindText(":accname", accname, transient:false)
2140 .bind(":nntpidx", nntpidx)
2141 .doAll();
2145 if (!chiroIsTagAllowAttaches(tuid)) ++noAttachCount;
2146 ++tagCount;
2148 stInsThreads
2149 .bind(":uid", uid)
2150 .bind(":tagid", tuid)
2151 .bind(":time", msgtime)
2152 .bind(":appearance", appearance)
2153 .doAll();
2155 if (!tagCount) return false;
2156 noattaches = (noAttachCount && noAttachCount == tagCount);
2158 // insert msgid
2160 bool hasmsgid = false;
2161 auto msgidfield = findHeaderField(hdrs, "Message-Id");
2162 if (msgidfield.length) {
2163 auto id = msgidfield.getFieldValue;
2164 if (id.length) {
2165 hasmsgid = true;
2166 stInsMsgId
2167 .bind(":uid", uid)
2168 .bind("time", msgtime)
2169 .bindText(":msgid", id, transient:false)
2170 .doAll();
2173 // if there is no msgid, create one
2174 if (!hasmsgid) {
2175 RIPEMD160 hash;
2176 hash.start();
2177 hash.put(cast(const(ubyte)[])hdrs);
2178 hash.put(cast(const(ubyte)[])body);
2179 ubyte[20] digest = hash.finish();
2180 char[20*2+2+16] buf;
2181 import core.stdc.stdio : snprintf;
2182 import core.stdc.string : strcat;
2183 foreach (immutable idx, ubyte b; digest[]) snprintf(buf.ptr+idx*2, 3, "%02x", b);
2184 strcat(buf.ptr, "@artificial"); // it is safe, there is enough room for it
2185 stInsMsgId
2186 .bind(":uid", uid)
2187 .bind("time", msgtime)
2188 .bindText(":msgid", buf[0..20*2], transient:false)
2189 .doAll();
2193 // insert references
2195 uint refidx = 0;
2196 auto inreplyfld = findHeaderField(hdrs, "In-Reply-To");
2197 while (inreplyfld.length) {
2198 auto id = getNextFieldValue(inreplyfld);
2199 if (id.length) {
2200 stInsMsgRefId
2201 .bind(":uid", uid)
2202 .bind(":idx", refidx++)
2203 .bind(":msgid", id)
2204 .doAll();
2208 inreplyfld = findHeaderField(hdrs, "References");
2209 while (inreplyfld.length) {
2210 auto id = getNextFieldValue(inreplyfld);
2211 if (id.length) {
2212 stInsMsgRefId
2213 .bind(":uid", uid)
2214 .bind(":idx", refidx++)
2215 .bind(":msgid", id)
2216 .doAll();
2221 // insert base content and attaches
2223 Content[] content;
2224 parseContent(ref content, hdrs, body, noattaches);
2225 // insert text and html
2226 bool wasText = false, wasHtml = false;
2227 foreach (const ref Content cc; content) {
2228 if (cc.name.length) continue;
2229 if (noattaches && !cc.mime.startsWith("text/")) continue;
2230 if (!wasText && cc.mime == "text/plain") {
2231 wasText = true;
2232 stInsContentText
2233 .bind(":uid", uid)
2234 .bindText(":format", cc.format, transient:false)
2235 .bindBlob(":content", cc.data, transient:false)
2236 .doAll();
2237 } else if (!wasHtml && cc.mime == "text/html") {
2238 wasHtml = true;
2239 stInsContentHtml
2240 .bind(":uid", uid)
2241 .bindText(":format", cc.format, transient:false)
2242 .bindBlob(":content", cc.data, transient:false)
2243 .doAll();
2246 if (!wasText) {
2247 stInsContentText
2248 .bind(":uid", uid)
2249 .bindText(":format", "", transient:false)
2250 .bindBlob(":content", "", transient:false)
2251 .doAll();
2253 if (!wasHtml) {
2254 stInsContentHtml
2255 .bind(":uid", uid)
2256 .bindText(":format", "", transient:false)
2257 .bindBlob(":content", "", transient:false)
2258 .doAll();
2260 // insert everything
2261 uint cidx = 0;
2262 foreach (const ref Content cc; content) {
2263 if (cc.name.length == 0 && cc.mime.startsWith("text/")) continue;
2264 // for "no attaches" mode, still record the attach, but ignore its contents
2265 stInsAttach
2266 .bind(":uid", uid)
2267 .bind(":idx", cidx++)
2268 .bindText(":mime", cc.mime, transient:false)
2269 .bindText(":name", cc.name, transient:false)
2270 .bindText(":format", cc.name, transient:false)
2271 .bindBlob(":content", (noattaches ? null : cc.data), transient:false, allowNull:true)
2272 .doAll();
2276 // insert from/to/subj info
2277 // this must be done last to keep FTS5 in sync
2279 auto subj = findHeaderField(hdrs, "Subject").decodeSubj.subjRemoveRe;
2280 auto from = findHeaderField(hdrs, "From").decodeSubj;
2281 auto to = findHeaderField(hdrs, "To").decodeSubj;
2282 stInsInfo
2283 .bind(":uid", uid)
2284 .bind(":from_name", from.extractName)
2285 .bind(":from_mail", from.extractMail)
2286 .bind(":subj", subj)
2287 .bind(":to_name", to.extractName)
2288 .bind(":to_mail", to.extractMail)
2289 .doAll();
2292 return true;
2297 inserts the messages from the message storage with the given id into view storage.
2298 parses it and such, and optionally updates threads.
2300 WARNING! DOESN'T UPDATE NNTP INDICIES! this should be done by the downloader.
2302 invalid (unknown) tags will be ignored.
2304 returns number of processed messages.
2306 public uint chiroParseAndInsertMessages (uint stmsgid,
2307 void delegate (uint count, uint total, uint nntpidx, const(char)[] tags) progresscb=null,
2308 uint emsgid=uint.max, bool relink=true, bool asread=false)
2310 if (emsgid < stmsgid) return 0; // nothing to do
2312 dbView.execute("BEGIN TRANSACTION;");
2313 scope(success) dbView.execute("COMMIT TRANSACTION;");
2314 scope(failure) dbView.execute("ROLLBACK TRANSACTION;");
2316 uint total = 0;
2317 if (progresscb !is null) {
2318 // find total number of messages to process
2319 foreach (auto row; dbStore.statement(`
2320 SELECT count(uid) AS total FROM messages WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
2321 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
2323 total = row.total!uint;
2324 break;
2326 if (total == 0) return 0; // why not?
2329 uint[] uptagids;
2330 if (relink) uptagids.reserve(128);
2331 scope(exit) delete uptagids;
2333 uint count = 0;
2335 foreach (auto mrow; dbStore.statement(`
2336 -- this should cache unpack results
2337 WITH msgunpacked(msguid, msgdata, msgtags) AS (
2338 SELECT uid AS msguid, ChiroUnpack(data) AS msgdata, tags AS msgtags
2339 FROM messages
2340 WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
2341 ORDER BY uid
2343 SELECT
2344 msguid AS uid
2345 , msgtags AS tags
2346 , ChiroExtractHeaders(msgdata) AS headers
2347 , ChiroExtractBody(msgdata) AS body
2348 , ChiroHdr_NNTPIndex(msgdata) AS nntpidx
2349 , ChiroHdr_RecvTime(msgdata) AS msgtime
2350 FROM msgunpacked
2351 ;`).bind(":msglo", stmsgid).bind(":msghi", emsgid).range)
2353 ++count;
2354 auto hdrs = mrow.headers!SQ3Text;
2355 auto body = mrow.body!SQ3Text;
2356 auto tags = mrow.tags!SQ3Text;
2357 uint uid = mrow.uid!uint;
2358 uint nntpidx = mrow.nntpidx!uint;
2359 uint msgtime = mrow.msgtime!uint;
2360 assert(tags.length);
2362 chiroParseAndInsertOneMessage(uid, msgtime, (asread ? 1 : 0), hdrs, body, tags);
2364 if (progresscb !is null) progresscb(count, total, nntpidx, tags);
2366 if (relink) {
2367 while (tags.length) {
2368 auto eep = tags.indexOf('|');
2369 auto tagname = (eep >= 0 ? tags[0..eep] : tags[0..$]);
2370 tags = (eep >= 0 ? tags[eep+1..$] : tags[0..0]);
2371 if (tagname.length == 0) continue;
2373 immutable uint tuid = chiroGetTagUid(tagname);
2374 if (tuid == 0) continue;
2376 bool found = false;
2377 foreach (immutable n; uptagids) if (n == tuid) { found = true; break; }
2378 if (!found) uptagids ~= tuid;
2383 if (relink && uptagids.length) {
2384 foreach (immutable tagid; uptagids) chiroSupportRelinkTagThreads(tagid);
2387 return count;
2392 returns accouint uid (accid) or 0.
2394 public uint chiroGetAccountUid (const(char)[] accname) {
2395 foreach (auto row; dbConf.statement(`SELECT accid AS accid FROM accounts WHERE name=:accname LIMIT 1;`)
2396 .bindText(":accname", accname, transient:false).range)
2398 return row.accid!uint;
2400 return 0;
2405 returns list of known tags, sorted by name.
2407 public string[] chiroGetTagList () {
2408 string[] res;
2409 foreach (auto row; dbView.statement(`SELECT tag AS tagname FROM tagnames WHERE hidden=0 ORDER BY tag;`).range) {
2410 res ~= row.tagname!string;
2412 return res;
2417 returns tag uid (tagid) or 0.
2419 public uint chiroGetTagUid (const(char)[] tagname) {
2420 foreach (auto row; dbView.statement(`SELECT tagid AS tagid FROM tagnames WHERE tag=:tagname LIMIT 1;`)
2421 .bindText(":tagname", tagname, transient:false).range)
2423 return row.tagid!uint;
2425 return 0;
2430 returns `true` if the given tag supports threads.
2432 this is used only when adding new messages, to set all parents to 0.
2434 public bool chiroIsTagThreaded(T) (T tagnameid)
2435 if (is(T:const(char)[]) || is(T:uint))
2437 static if (is(T:const(char)[])) {
2438 foreach (auto row; dbView.statement(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`)
2439 .bindText(":tagname", tagnameid, transient:false).range)
2441 return (row.threading!uint == 1);
2443 } else {
2444 foreach (auto row; dbView.statement(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`)
2445 .bind(":tagid", tagnameid).range)
2447 return (row.threading!uint == 1);
2450 return false;
2455 returns `true` if the given tag allows attaches.
2457 this is used only when adding new messages, to set all parents to 0.
2459 public bool chiroIsTagAllowAttaches(T) (T tagnameid)
2460 if (is(T:const(char)[]) || is(T:uint))
2462 static if (is(T:const(char)[])) {
2463 foreach (auto row; dbView.statement(`SELECT threading AS threading FROM tagnames WHERE tag=:tagname LIMIT 1;`)
2464 .bindText(":tagname", tagnameid, transient:false).range)
2466 return (row.threading!uint == 1);
2468 } else {
2469 foreach (auto row; dbView.statement(`SELECT threading AS threading FROM tagnames WHERE tagid=:tagid LIMIT 1;`)
2470 .bind(":tagid", tagnameid).range)
2472 return (row.threading!uint == 1);
2475 return false;
2480 relinks all messages in all threads suitable for relinking, and
2481 sets parents to zero otherwise.
2483 public void chiroSupportRelinkAllThreads () {
2484 // yeah, that's it: a single SQL statement
2485 dbView.execute(`
2486 -- clear parents where threading is disabled
2487 SELECT ChiroTimerStart('clearing parents');
2488 UPDATE threads
2490 parent = 0
2491 WHERE
2492 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=0)
2493 AND parent <> 0
2495 SELECT ChiroTimerStop();
2497 SELECT ChiroTimerStart('relinking threads');
2498 UPDATE threads
2500 parent=ifnull(
2502 SELECT uid FROM msgids
2503 WHERE
2504 -- find MSGID for any of our current references
2505 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
2506 -- check if UID for that MSGID has the valid tag
2507 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=threads.tagid)
2508 ORDER BY time DESC
2509 LIMIT 1
2511 , 0)
2512 WHERE
2513 -- do not process messages with non-threading tags
2514 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=1)
2516 SELECT ChiroTimerStop();
2522 relinks all messages for the given tag, or sets parents to zero if
2523 threading for that tag is disabled.
2525 public void chiroSupportRelinkTagThreads(T) (T tagnameid)
2526 if (is(T:const(char)[]) || is(T:uint))
2528 static if (is(T:const(char)[])) {
2529 immutable uint tid = chiroGetTagUid(tagnameid);
2530 if (!tid) return;
2531 } else {
2532 alias tid = tagnameid;
2535 if (!chiroIsTagThreaded(tid)) {
2536 // clear parents (just in case)
2537 dbView.statement(`
2538 UPDATE threads
2540 parent = 0
2541 WHERE
2542 tagid = :tagid AND parent <> 0
2543 ;`).bind(":tagid", tid).doAll();
2544 } else {
2545 // yeah, that's it: a single SQL statement
2546 dbView.statement(`
2547 UPDATE threads
2549 parent=ifnull(
2551 SELECT uid FROM msgids
2552 WHERE
2553 -- find MSGID for any of our current references
2554 msgids.msgid IN (SELECT msgid FROM refids WHERE refids.uid=threads.uid ORDER BY idx) AND
2555 -- check if UID for that MSGID has the valid tag
2556 EXISTS (SELECT uid FROM threads AS tt WHERE tt.uid=msgids.uid AND tt.tagid=:tagid)
2557 ORDER BY time DESC
2558 LIMIT 1
2560 , 0)
2561 WHERE
2562 threads.tagid = :tagid
2563 ;`).bind(":tagid", tid).doAll();
2569 creates "treepane" table for the given tag. that table can be used to
2570 render threaded listview.
2572 returns max id of the existing item. can be used for pagination.
2573 item ids are guaranteed to be sequential, and without any holes.
2574 the first id is `1`.
2576 returned table has "rowid", and two integer fields: "uid" (message uid), and
2577 "level" (message depth, starting from 0).
2579 public uint chiroCreateTreePaneTable(T) (T tagidname, int lastmonthes=12, bool allowThreading=true)
2580 if (is(T:const(char)[]) || is(T:uint))
2582 // shrink temp table to the bare minimum, because each field costs several msecs
2583 // we don't need parent and time here, because we can easily select them with inner joins
2584 dbView.execute(`
2585 DROP TABLE IF EXISTS treepane;
2586 CREATE TEMP TABLE treepane (
2587 iid INTEGER PRIMARY KEY
2588 , uid INTEGER
2589 , level INTEGER
2590 -- to make joins easier
2591 , tagid INTEGER
2593 ;`);
2595 static if (is(T:const(char)[])) {
2596 immutable uint tid = chiroGetTagUid(tagidname);
2597 if (!tid) return 0;
2598 enum selHdr = ``;
2599 } else {
2600 alias tid = tagidname;
2603 uint startTime = 0;
2605 if (lastmonthes > 0) {
2606 if (lastmonthes > 12*100) {
2607 startTime = 0;
2608 } else {
2609 // show last `lastmonthes` (full monthes)
2610 import std.datetime;
2611 import core.time : Duration;
2613 SysTime now = Clock.currTime().toUTC();
2614 int year = now.year;
2615 int month = now.month; // from 1
2616 // yes, i am THAT lazy
2617 while (lastmonthes > 1) {
2618 if (month > lastmonthes) { month -= lastmonthes; break; }
2619 lastmonthes -= month;
2620 month = 12;
2621 --year;
2623 // construct unix time
2624 now.fracSecs = Duration.zero;
2625 now.second = 0;
2626 now.hour = 0;
2627 now.minute = 0;
2628 now.day = 1;
2629 now.month = cast(Month)month;
2630 now.year = year;
2631 startTime = cast(uint)now.toUnixTime();
2635 // this "%08X" will do up to 2038; i'm fine with it
2636 if (allowThreading) {
2637 dbView.statement(`
2638 INSERT INTO treepane
2639 (uid, level, tagid)
2640 WITH tree(uid, parent, level, time, path) AS (
2641 WITH RECURSIVE fulltree(uid, parent, level, time, path) AS (
2642 SELECT t.uid AS uid, t.parent AS parent, 1 AS level, t.time AS time, printf('%08X', t.time) AS path
2643 FROM threads t
2644 WHERE t.time>=:starttime AND parent=0 AND t.tagid=:tagidname AND t.appearance <> -1
2645 UNION ALL
2646 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
2647 FROM threads t, fulltree ft
2648 WHERE t.time>=:starttime AND t.parent=ft.uid AND t.tagid=:tagidname AND t.appearance <> -1
2650 SELECT * FROM fulltree
2652 SELECT
2653 tree.uid AS uid
2654 , tree.level-1 AS level
2655 , :tagidname AS tagid
2656 FROM tree
2657 ORDER BY path
2658 ;`).bind(":tagidname", tid).bind(":starttime", startTime).doAll();
2659 } else {
2660 dbView.statement(`
2661 INSERT INTO treepane
2662 (uid, level, tagid)
2663 SELECT
2664 threads.uid AS uid
2665 , 0 AS level
2666 , :tagidname AS tagid
2667 FROM threads
2668 WHERE
2669 threads.time>=:starttime AND threads.tagid=:tagidname AND threads.appearance <> -1
2670 ORDER BY
2671 threads.time
2672 ;`).bind(":tagidname", tid).bind(":starttime", startTime).doAll();
2675 return cast(uint)dbView.lastRowId;
2680 releases (drops) "treepane" table.
2682 can be called several times, but usually you don't need to call this at all.
2684 public void chiroReleaseTreePaneTable () {
2685 dbView.execute(`DROP TABLE IF EXISTS treepane;`);
2690 selects given number of items starting with the given item id.
2692 returns numer of selected items.
2694 WARNING! "treepane" table must be prepared with `chiroCreateTreePaneTable()`!
2696 WARNING! [i]dup `SQ3Text` arguments if necessary, they won't survive the `cb` return!
2698 public uint chiroGetPaneTablePage (uint stiid, uint limit,
2699 void delegate (uint pgofs, /* offset from the page start, from zero and up to `limit` */
2700 uint iid, /* item id, never zero */
2701 uint uid, /* msguid, never zero */
2702 uint parentuid, /* parent msguid, may be zero */
2703 uint level, /* threading level, from zero */
2704 int appearance, /* flags, etc. */
2705 SQ3Text date, /* string representation of receiving date and time */
2706 SQ3Text subj, /* message subject, can be empty string */
2707 SQ3Text fromName, /* message sender name, can be empty string */
2708 SQ3Text fromMail) cb /* message sender email, can be empty string */
2710 if (stiid < 1) stiid = 1;
2711 if (limit == 0) return 0;
2712 uint total = 0;
2713 foreach (auto row; dbView.statement(`
2714 SELECT
2715 treepane.iid AS iid
2716 , treepane.uid AS uid
2717 , treepane.level AS level
2718 , threads.parent AS parent
2719 , threads.appearance AS appearance
2720 , datetime(threads.time, 'unixepoch') AS time
2721 , info.subj AS subj
2722 , info.from_name AS from_name
2723 , info.from_mail AS from_mail
2724 FROM treepane
2725 INNER JOIN info
2726 USING(uid)
2727 INNER JOIN threads
2728 USING(uid, tagid)
2729 WHERE
2730 treepane.iid >= :stiid
2731 ORDER BY treepane.iid
2732 LIMIT :limit
2733 `).bind(":stiid", stiid).bind(":limit", limit).range)
2735 if (cb !is null) {
2736 cb(total, row.iid!uint, row.uid!uint, row.parent!uint, row.level!uint, row.appearance!int,
2737 row.time!SQ3Text, row.subj!SQ3Text, row.from_name!SQ3Text, row.from_mail!SQ3Text);
2739 ++total;
2741 return total;
2745 // ////////////////////////////////////////////////////////////////////////// //
2746 /** returns full content of the messare or `null` if no message found (or it was deleted).
2748 * you can safely `delete` the result
2750 public char[] GetFullMessageContent (uint uid) {
2751 if (uid == 0) return null;
2752 foreach (auto row; dbStore.statement(`SELECT ChiroUnpack(data) AS content FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", uid).range) {
2753 auto ct = row.content!SQ3Text;
2754 if (!ct.length) return null;
2755 char[] content = new char[ct.length];
2756 content[] = ct[];
2757 return content;
2759 return null;
2763 // ////////////////////////////////////////////////////////////////////////// //
2764 public enum Bogo {
2765 Error, // some error occured
2766 Ham,
2767 Unsure,
2768 Spam,
2771 public Bogo messageBogoCheck (uint uid) {
2772 if (uid == 0) return Bogo.Error;
2773 char[] content = GetFullMessageContent(uid);
2774 scope(exit) delete content;
2775 if (content is null) return Bogo.Error;
2777 try {
2778 import std.process;
2779 //{ auto fo = VFile("/tmp/zzzz", "w"); fo.rawWriteExact(art.data); }
2780 auto pipes = pipeProcess(["/usr/bin/bogofilter", "-T"]);
2781 //foreach (string s; art.headers) pipes.stdin.writeln(s);
2782 //pipes.stdin.writeln();
2783 //foreach (string s; art.text) pipes.stdin.writeln(s);
2784 pipes.stdin.writeln(content.xstripright);
2785 pipes.stdin.flush();
2786 pipes.stdin.close();
2787 auto res = pipes.stdout.readln();
2788 wait(pipes.pid);
2789 //conwriteln("RESULT: [", res, "]");
2790 if (res.length == 0) {
2791 //conwriteln("ERROR: bogofilter returned nothing");
2792 return Bogo.Error;
2794 if (res[0] == 'H') return Bogo.Ham;
2795 if (res[0] == 'U') return Bogo.Unsure;
2796 if (res[0] == 'S') return Bogo.Spam;
2797 //while (res.length && res[$-1] <= ' ') res = res[0..$-1];
2798 //conwriteln("ERROR: bogofilter returned some shit: [", res, "]");
2799 } catch (Exception e) { // sorry
2800 //conwriteln("ERROR bogofiltering: ", e.msg);
2803 return Bogo.Error;
2807 // ////////////////////////////////////////////////////////////////////////// //
2808 private void messageBogoMarkSpamHam(bool spam) (uint uid) {
2809 if (uid == 0) return;
2810 char[] content = GetFullMessageContent(uid);
2811 scope(exit) delete content;
2812 if (content is null) return;
2814 static if (spam) enum arg = "-s"; else enum arg = "-n";
2815 try {
2816 import std.process;
2817 auto pipes = pipeProcess(["/usr/bin/bogofilter", arg]);
2818 //foreach (string s; art.headers) pipes.stdin.writeln(s);
2819 //pipes.stdin.writeln();
2820 //foreach (string s; art.text) pipes.stdin.writeln(s);
2821 pipes.stdin.writeln(content.xstripright);
2822 pipes.stdin.flush();
2823 pipes.stdin.close();
2824 wait(pipes.pid);
2825 } catch (Exception e) { // sorry
2826 //conwriteln("ERROR bogofiltering: ", e.msg);
2831 public void messageBogoMarkHam (uint uid) { messageBogoMarkSpamHam!false(uid); }
2832 public void messageBogoMarkSpam (uint uid) { messageBogoMarkSpamHam!true(uid); }