2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module chibackend
.sqbase
is aliced
;
20 // do not use, for testing only!
21 // and it seems to generate bigger files, lol
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;
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;
49 // just for fun, slightly better than lzjb
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
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
;
92 public enum ChiroDefaultPackLevel
= 6;
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;
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])
118 This tries to compress the given data, and returns a compressed blob.
119 If `compratio` is negative or zero, do not compress anything.
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;
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||'|')
222 SUBSTR(rest, 0, INSTR(rest, '|')),
223 SUBSTR(rest, INSTR(rest, '|')+1)
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)')
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()"
254 -- check tag constraints
255 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_insert
256 BEFORE INSERT ON messages
258 BEGIN`~msgTagNameCheckSQL
~`
261 CREATE TRIGGER IF NOT EXISTS fix_message_hashes_update_tags
262 BEFORE UPDATE OF tags ON messages
264 BEGIN`~msgTagNameCheckSQL
~`
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
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 */
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
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
365 -- -1: can be used to ignore messages in thread view
368 -- 2: soft-delete from filter
369 -- 3: soft-delete by user (will be purged on folder change)
370 -- 4: muted (by filter)
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 */
377 , parent INTEGER DEFAULT 0 /* uid: parent message in thread, or 0 */
379 , appearance INTEGER DEFAULT 0 /* how the message should look */
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 */
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)
456 , info.from_name||' '||CHAR(26)||' '||info.from_mail AS sender
458 , ChiroUnpack(content_text.content) AS text
459 , ChiroUnpack(content_html.content) AS html
461 INNER JOIN content_text USING(uid)
462 INNER JOIN content_html USING(uid)
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);
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');
532 INSERT INTO fts5_messages(rowid, sender, subj, text, html)
533 SELECT uid, sender, subj, text, html
537 SELECT threads.tagid FROM threads
538 INNER JOIN tagnames USING(tagid)
540 threads.uid=fts5_msgview.uid AND
541 tagnames.hidden=0 AND SUBSTR(tagnames.tag, 1, 1)='/'
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
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;
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
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;
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
) {
577 if (ch
!= 9 && ch
!= 10 && ch
!= 13 && ch
!= 27) return false;
579 if (ch
== 127) return false;
583 //return utf8ValidText(buf);
587 // ////////////////////////////////////////////////////////////////////////// //
588 private bool isBadPrefix (const(char)[] buf
) pure nothrow @trusted @nogc {
589 if (buf
.length
< 5) return false;
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
;
614 if (v
> 0x3f_ffffU
) {
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
;
628 d
.ptr
[0] = cast(ubyte)(v
>>16);
629 d
.ptr
[1] = cast(ubyte)(v
>>8);
630 d
.ptr
[2] = cast(ubyte)v
;
638 d
.ptr
[0] = cast(ubyte)(v
>>8);
639 d
.ptr
[1] = cast(ubyte)v
;
644 if (d
.length
>= 1) d
.ptr
[0] = cast(ubyte)v
;
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) {
654 case 0x40: return (d
.length
>= 2 ?
2 : 0);
655 case 0x80: return (d
.length
>= 3 ?
3 : 0);
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
;
667 switch (d
.ptr
[0]&0xc0) {
672 if (d
.length
< 2) return uint.max
;
673 res
= ((d
.ptr
[0]&0x3fU
)<<8)|d
.ptr
[1];
676 if (d
.length
< 3) return uint.max
;
677 res
= ((d
.ptr
[0]&0x3fU
)<<16)|
(d
.ptr
[1]<<8)|d
.ptr
[2];
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];
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) {
696 int epos
= cast(int)cast(usize
)(eptr
-vs
);
697 if (sz
-epos
< 1) break;
699 if (sz
-epos
< 2) break;
703 if (*eptr
== '\n') return epos
+1;
705 eptr
= cast(const(char)*)memchr(eptr
, '\n', cast(uint)(sz
-epos
));
711 // hack for some invalid dates
712 uint parseMailDate (const(char)[] s
) nothrow {
714 if (s
.length
== 0) return 0;
716 return cast(uint)(parseRFC822DateTime(s
).toUTC
.toUnixTime
);
717 } catch (Exception
) {}
718 // sometimes this helps
720 foreach_reverse (immutable char ch
; s
) {
721 if (ch
< '0' || ch
> '9') break;
724 if (dcount
> 4) return 0;
725 s
~= "0000"[0..4-dcount
];
727 return cast(uint)(parseRFC822DateTime(s
).toUTC
.toUnixTime
);
728 } catch (Exception
) {}
733 // ////////////////////////////////////////////////////////////////////////// //
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
);
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
;
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";
784 xsz
[0..5] = "\x1bZLIB";
786 uint xszlen
= encodeUInt(xsz
[5..$], cast(uint)sz
);
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;
797 immutable uint bsz
= cast(uint)sz
;
799 char* cbuf
= cast(char*)malloc(bsz
+xszlen
);
801 if (isBadPrefix(vs
[0..cast(uint)sz
])) { sqlite3_result_error_nomem(ctx
); return; }
803 cbuf
[0..xszlen
] = xsz
[0..xszlen
];
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
);
821 if (dpos
+buf
.length
>= cast(usize
)sz
) throw new Exception("uncompressible");
822 memcpy(cbuf
+dpos
, buf
.ptr
, buf
.length
);
825 // maximum compression?
831 if (dpos
< cast(usize
)sz
) {
832 sqlite3_result_blob(ctx
, cbuf
, dpos
, &free
);
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
);
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
);
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
);
866 if (dsize
+xszlen
< cast(usize
)sz
) {
867 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
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
);
876 if (dsize
> 0 && dsize
+xszlen
< cast(usize
)sz
) {
877 sqlite3_result_blob(ctx
, cbuf
, dsize
+xszlen
, &free
);
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
);
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
);
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
);
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
);
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
);
933 } else version(use_zstd
) {
935 packlevel
<= 3 ?
ZSTD_minCLevel() :
936 packlevel
<= 6 ?
ZSTD_defaultCLevel() :
937 packlevel
< 10 ?
19 :
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
);
945 import etc
.c
.zlib
: /*compressBound,*/ compress2
, Z_OK
;
946 //uint bsz = cast(uint)compressBound(cast(uint)sz);
947 if (packlevel
> 9) packlevel
= 9;
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
);
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
);
968 sqlite3_result_blob(ctx
, res
, sz
+5, &free
);
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
);
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; }
1040 int codec
= Codec_ZLIB
;
1041 if (vs
[0..5] != "\x1bZLIB") {
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
;
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
;
1070 if (codec
== Codec_ZLIB
&& vs
[0..5] == "\x1bLZ4D") codec
= Codec_LZ4D
;
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; }
1079 // size is guaranteed to be at least 6 here
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; }
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
) {
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
) {
1113 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
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
) {
1122 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1133 auto dc
= bz
.decompress(
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
);
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
);
1151 if (dc
!= rsize
) throw new Exception("broken data");
1152 } catch (Exception
) {
1155 if (outpos
== uint.max
) {
1157 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1163 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
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
) {
1174 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1179 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1184 version(use_libbrieflz
) {
1185 dsize
= blz_depack_safe(vs
, cast(uint)sz
, cbuf
, rsize
);
1186 if (dsize
!= rsize
) {
1188 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1193 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
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
);
1204 if (dsize
== 0 || dsize
!= rsize
) {
1206 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1211 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1217 dsize
= lzjb_decompress(vs
, cast(usize
)sz
, cbuf
, rsize
);
1218 if (dsize
!= rsize
) {
1220 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1225 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
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
);
1238 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1243 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
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
);
1256 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1261 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
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
);
1276 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1281 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1287 dsize
= LZ4_decompress_safe(vs
, cbuf
, sz
, rsize
);
1288 if (dsize
!= rsize
) {
1290 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1295 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1301 dsize
= ZSTD_decompress(cbuf
, rsize
, vs
, sz
);
1302 if (ZSTD_isError(dsize
) || dsize
!= rsize
) {
1304 sqlite3_result_error(ctx
, "broken data in `ChiroUnpack()`", -1);
1309 sqlite3_result_error(ctx
, "unsupported compression in `ChiroUnpack()`", -1);
1315 if (isGoodText(cbuf
[0..dsize
])) {
1316 sqlite3_result_text(ctx
, cbuf
, cast(int)dsize
, &free
);
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) {
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
]) {
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);
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
);
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
]) {
1372 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1374 if (ch
== 127) *dest
++ = '~';
1375 else if (ch
== 11 || ch
== 12) *dest
++ = '\n';
1376 else if (ch
< 32 && ch
!= 9 && ch
!= 10) *dest
++ = ' ';
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) {
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
]) {
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);
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
);
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
]) {
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';
1442 if (ch
== 127) *dest
++ = '~';
1443 else if (ch
< 32 && ch
!= 10) *dest
++ = ' ';
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; }
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
]) {
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';
1489 if (ch
== 127) *dest
++ = '~';
1490 else if (ch
< 32 && ch
!= 10) *dest
++ = ' ';
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; }
1520 immutable int bstart
= sq3Supp_FindHeadersEnd(vs
, sz
);
1521 if (bstart
>= sz
) { sqlite3_result_text(ctx
, "", 0, SQLITE_STATIC
); return; }
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
]) {
1543 if (idx
+1 < cast(uint)sz
&& vs
[idx
+1] == 10) {} else *dest
++ = ' ';
1545 if (ch
== 127) *dest
++ = '~';
1546 else if (ch
== 11 || ch
== 12) *dest
++ = '\n';
1547 else if (ch
< 32 && ch
!= 9 && ch
!= 10) *dest
++ = ' ';
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");
1606 auto nntpidxfld
= findHeaderField(hdrs
, "NNTP-Index");
1607 if (nntpidxfld
.length
) {
1608 auto id
= nntpidxfld
.getFieldValue
;
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
;
1639 msgtime
= parseMailDate(v
);
1640 } catch (Exception
) {
1641 //writeln("UID=", uid, ": FUCKED INJECTION-DATE: |", v, "|");
1642 msgtime
= 0; // just in case
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
;
1652 msgtime
= parseMailDate(v
);
1653 } catch (Exception
) {
1654 //writeln("UID=", uid, ": FUCKED NNTP-POSTING-DATE: |", v, "|");
1655 msgtime
= 0; // just in case
1661 datefld
= findHeaderField(hdrs
, "Date");
1662 if (datefld
.length
!= 0) {
1663 auto v
= datefld
.getFieldValue
;
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
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
;
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
);
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
);
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
);
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; }
1767 immutable int sz
= sqlite3_value_bytes(argv
[0]);
1769 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
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);
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
) {
1791 if (argc
> 1) { sqlite3_result_error(ctx
, "invalid number of arguments to \"ChiroTimerStop()\"", -1); return; }
1793 if (ChiroTimerEnabled
) {
1796 immutable int sz
= sqlite3_value_bytes(argv
[0]);
1798 const(char)* vs
= cast(const(char) *)sqlite3_value_blob(argv
[0]);
1800 chiTimerMsg
= new char[cast(usize
)sz
];
1801 chiTimerMsg
[0..cast(usize
)sz
] = vs
[0..cast(usize
)sz
];
1807 auto tstr
= chiTimer
.toBuffer(buf
[]);
1808 if (chiTimerMsg
.length
) {
1809 writeln("done ", chiTimerMsg
, ": ", tstr
);
1811 writeln("time: ", tstr
);
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 // ////////////////////////////////////////////////////////////////////////// //
1923 static void errorLogCallback (void *pArg
, int rc
, const char *zMsg
) {
1924 import core
.stdc
.stdio
: stderr
, fprintf
;
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;
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;
1933 case SQLITE_CANTOPEN
:
1935 break; // ignore those
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;
1954 string errmsg
= sqerrstr(rc
);
1955 throw new Exception("FATAL: "~msg
~": "~errmsg
);
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
= "";
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];
1987 ExpandedMailDBPath
= cast(string
)dpath
; // it is safe to cast here
1989 char[] dpath
= new char[mailpath
.length
+1];
1990 dpath
[0..$-1] = mailpath
[];
1992 ExpandedMailDBPath
= cast(string
)dpath
; // it is safe to cast here
1997 shared static this () {
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 () {
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(`
2035 VALUES(:name,:value)
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
);
2046 static assert(0, "oops");
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
) {
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(`
2081 ( uid, tagid, time, appearance)
2082 VALUES(:uid,:tagid,:time,:appearance)
2085 auto stInsInfo
= dbView
.statement(`
2087 ( uid, from_name, from_mail, subj, to_name, to_mail)
2088 VALUES(:uid,:from_name,:from_mail,:subj,:to_name,:to_mail)
2091 auto stInsMsgId
= dbView
.statement(`
2094 VALUES(:uid,:msgid,:time)
2097 auto stInsMsgRefId
= dbView
.statement(`
2100 VALUES(:uid,:idx,:msgid)
2103 auto stInsContentText
= dbView
.statement(`
2104 INSERT INTO content_text
2105 ( uid, format, content)
2106 VALUES(:uid,:format, ChiroPack(:content))
2109 auto stInsContentHtml
= dbView
.statement(`
2110 INSERT INTO content_html
2111 ( uid, format, content)
2112 VALUES(:uid,:format, ChiroPack(:content))
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))
2121 bool noattaches
= false; // do not store attaches?
2123 // create thread record for each tag (and update max nntp index)
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;
2136 if (nntpidx > 0 && tagname.startsWith("account:")) {
2137 auto accname = tagname[8..$];
2139 .bindText(":accname", accname, transient:false)
2140 .bind(":nntpidx", nntpidx)
2145 if (!chiroIsTagAllowAttaches(tuid
)) ++noAttachCount
;
2150 .bind(":tagid", tuid
)
2151 .bind(":time", msgtime
)
2152 .bind(":appearance", appearance
)
2155 if (!tagCount
) return false;
2156 noattaches
= (noAttachCount
&& noAttachCount
== tagCount
);
2160 bool hasmsgid
= false;
2161 auto msgidfield
= findHeaderField(hdrs
, "Message-Id");
2162 if (msgidfield
.length
) {
2163 auto id
= msgidfield
.getFieldValue
;
2168 .bind("time", msgtime
)
2169 .bindText(":msgid", id
, transient
:false)
2173 // if there is no msgid, create one
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
2187 .bind("time", msgtime
)
2188 .bindText(":msgid", buf
[0..20*2], transient
:false)
2193 // insert references
2196 auto inreplyfld
= findHeaderField(hdrs
, "In-Reply-To");
2197 while (inreplyfld
.length
) {
2198 auto id
= getNextFieldValue(inreplyfld
);
2202 .bind(":idx", refidx
++)
2208 inreplyfld
= findHeaderField(hdrs
, "References");
2209 while (inreplyfld
.length
) {
2210 auto id
= getNextFieldValue(inreplyfld
);
2214 .bind(":idx", refidx
++)
2221 // insert base content and attaches
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") {
2234 .bindText(":format", cc
.format
, transient
:false)
2235 .bindBlob(":content", cc
.data
, transient
:false)
2237 } else if (!wasHtml
&& cc
.mime
== "text/html") {
2241 .bindText(":format", cc
.format
, transient
:false)
2242 .bindBlob(":content", cc
.data
, transient
:false)
2249 .bindText(":format", "", transient
:false)
2250 .bindBlob(":content", "", transient
:false)
2256 .bindText(":format", "", transient
:false)
2257 .bindBlob(":content", "", transient
:false)
2260 // insert everything
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
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)
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
;
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
)
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;");
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;
2326 if (total
== 0) return 0; // why not?
2330 if (relink
) uptagids
.reserve(128);
2331 scope(exit
) delete uptagids
;
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
2340 WHERE uid BETWEEN :msglo AND :msghi AND tags <> ''
2346 , ChiroExtractHeaders(msgdata) AS headers
2347 , ChiroExtractBody(msgdata) AS body
2348 , ChiroHdr_NNTPIndex(msgdata) AS nntpidx
2349 , ChiroHdr_RecvTime(msgdata) AS msgtime
2351 ;`).bind(":msglo", stmsgid
).bind(":msghi", emsgid
).range
)
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
);
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;
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
);
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;
2405 returns list of known tags, sorted by name.
2407 public string
[] chiroGetTagList () {
2409 foreach (auto row
; dbView
.statement(`SELECT tag AS tagname FROM tagnames WHERE hidden=0 ORDER BY tag;`).range
) {
2410 res
~= row
.tagname
!string
;
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;
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);
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);
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);
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);
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
2486 -- clear parents where threading is disabled
2487 SELECT ChiroTimerStart('clearing parents');
2492 EXISTS (SELECT threading FROM tagnames WHERE tagnames.tagid=threads.tagid AND threading=0)
2495 SELECT ChiroTimerStop();
2497 SELECT ChiroTimerStart('relinking threads');
2502 SELECT uid FROM msgids
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)
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
);
2532 alias tid
= tagnameid
;
2535 if (!chiroIsTagThreaded(tid
)) {
2536 // clear parents (just in case)
2542 tagid = :tagid AND parent <> 0
2543 ;`).bind(":tagid", tid
).doAll();
2545 // yeah, that's it: a single SQL statement
2551 SELECT uid FROM msgids
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)
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
2585 DROP TABLE IF EXISTS treepane;
2586 CREATE TEMP TABLE treepane (
2587 iid INTEGER PRIMARY KEY
2590 -- to make joins easier
2595 static if (is(T
:const(char)[])) {
2596 immutable uint tid
= chiroGetTagUid(tagidname
);
2600 alias tid
= tagidname
;
2605 if (lastmonthes
> 0) {
2606 if (lastmonthes
> 12*100) {
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
;
2623 // construct unix time
2624 now
.fracSecs
= Duration
.zero
;
2629 now
.month
= cast(Month
)month
;
2631 startTime
= cast(uint)now
.toUnixTime();
2635 // this "%08X" will do up to 2038; i'm fine with it
2636 if (allowThreading
) {
2638 INSERT INTO treepane
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
2644 WHERE t.time>=:starttime AND parent=0 AND t.tagid=:tagidname AND t.appearance <> -1
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
2654 , tree.level-1 AS level
2655 , :tagidname AS tagid
2658 ;`).bind(":tagidname", tid
).bind(":starttime", startTime
).doAll();
2661 INSERT INTO treepane
2666 , :tagidname AS tagid
2669 threads.time>=:starttime AND threads.tagid=:tagidname AND threads.appearance <> -1
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;
2713 foreach (auto row
; dbView
.statement(`
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
2722 , info.from_name AS from_name
2723 , info.from_mail AS from_mail
2730 treepane.iid >= :stiid
2731 ORDER BY treepane.iid
2733 `).bind(":stiid", stiid
).bind(":limit", limit
).range
)
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
);
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
];
2763 // ////////////////////////////////////////////////////////////////////////// //
2765 Error
, // some error occured
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
;
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();
2789 //conwriteln("RESULT: [", res, "]");
2790 if (res
.length
== 0) {
2791 //conwriteln("ERROR: bogofilter returned nothing");
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);
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";
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();
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
); }