+975 -64 +/-13 browse
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index 95f65b9..4d1ed8e 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -14,6 +14,19 @@ dependencies = [ |
6 | ] |
7 | |
8 | [[package]] |
9 | + name = "ahash" |
10 | + version = "0.8.3" |
11 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
12 | + checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" |
13 | + dependencies = [ |
14 | + "cfg-if 1.0.0", |
15 | + "getrandom", |
16 | + "once_cell", |
17 | + "serde", |
18 | + "version_check", |
19 | + ] |
20 | + |
21 | + [[package]] |
22 | name = "aho-corasick" |
23 | version = "1.0.1" |
24 | source = "registry+https://github.com/rust-lang/crates.io-index" |
25 | @@ -459,6 +472,21 @@ dependencies = [ |
26 | ] |
27 | |
28 | [[package]] |
29 | + name = "bit-set" |
30 | + version = "0.5.3" |
31 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
32 | + checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" |
33 | + dependencies = [ |
34 | + "bit-vec", |
35 | + ] |
36 | + |
37 | + [[package]] |
38 | + name = "bit-vec" |
39 | + version = "0.6.3" |
40 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
41 | + checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" |
42 | + |
43 | + [[package]] |
44 | name = "bitflags" |
45 | version = "1.3.2" |
46 | source = "registry+https://github.com/rust-lang/crates.io-index" |
47 | @@ -633,6 +661,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
48 | checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" |
49 | |
50 | [[package]] |
51 | + name = "bytecount" |
52 | + version = "0.6.3" |
53 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
54 | + checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" |
55 | + |
56 | + [[package]] |
57 | name = "byteorder" |
58 | version = "1.4.3" |
59 | source = "registry+https://github.com/rust-lang/crates.io-index" |
60 | @@ -1169,6 +1203,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
61 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" |
62 | |
63 | [[package]] |
64 | + name = "fancy-regex" |
65 | + version = "0.11.0" |
66 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
67 | + checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" |
68 | + dependencies = [ |
69 | + "bit-set", |
70 | + "regex", |
71 | + ] |
72 | + |
73 | + [[package]] |
74 | name = "fastrand" |
75 | version = "1.9.0" |
76 | source = "registry+https://github.com/rust-lang/crates.io-index" |
77 | @@ -1229,6 +1273,16 @@ dependencies = [ |
78 | ] |
79 | |
80 | [[package]] |
81 | + name = "fraction" |
82 | + version = "0.13.1" |
83 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
84 | + checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" |
85 | + dependencies = [ |
86 | + "lazy_static", |
87 | + "num", |
88 | + ] |
89 | + |
90 | + [[package]] |
91 | name = "fsevent" |
92 | version = "0.4.0" |
93 | source = "registry+https://github.com/rust-lang/crates.io-index" |
94 | @@ -1384,8 +1438,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
95 | checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" |
96 | dependencies = [ |
97 | "cfg-if 1.0.0", |
98 | + "js-sys", |
99 | "libc", |
100 | "wasi 0.11.0+wasi-snapshot-preview1", |
101 | + "wasm-bindgen", |
102 | ] |
103 | |
104 | [[package]] |
105 | @@ -1432,7 +1488,7 @@ version = "0.12.3" |
106 | source = "registry+https://github.com/rust-lang/crates.io-index" |
107 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" |
108 | dependencies = [ |
109 | - "ahash", |
110 | + "ahash 0.7.6", |
111 | ] |
112 | |
113 | [[package]] |
114 | @@ -1720,6 +1776,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
115 | checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" |
116 | |
117 | [[package]] |
118 | + name = "iso8601" |
119 | + version = "0.6.1" |
120 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
121 | + checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" |
122 | + dependencies = [ |
123 | + "nom", |
124 | + ] |
125 | + |
126 | + [[package]] |
127 | name = "itertools" |
128 | version = "0.10.5" |
129 | source = "registry+https://github.com/rust-lang/crates.io-index" |
130 | @@ -1764,6 +1829,34 @@ dependencies = [ |
131 | ] |
132 | |
133 | [[package]] |
134 | + name = "jsonschema" |
135 | + version = "0.17.0" |
136 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
137 | + checksum = "e48354c4c4f088714424ddf090de1ff84acc82b2f08c192d46d226ae2529a465" |
138 | + dependencies = [ |
139 | + "ahash 0.8.3", |
140 | + "anyhow", |
141 | + "base64 0.21.0", |
142 | + "bytecount", |
143 | + "fancy-regex", |
144 | + "fraction", |
145 | + "getrandom", |
146 | + "iso8601", |
147 | + "itoa", |
148 | + "memchr", |
149 | + "num-cmp", |
150 | + "once_cell", |
151 | + "parking_lot", |
152 | + "percent-encoding", |
153 | + "regex", |
154 | + "serde", |
155 | + "serde_json", |
156 | + "time 0.3.21", |
157 | + "url", |
158 | + "uuid", |
159 | + ] |
160 | + |
161 | + [[package]] |
162 | name = "kernel32-sys" |
163 | version = "0.2.2" |
164 | source = "registry+https://github.com/rust-lang/crates.io-index" |
165 | @@ -1922,6 +2015,7 @@ dependencies = [ |
166 | "anyhow", |
167 | "chrono", |
168 | "error-chain", |
169 | + "jsonschema", |
170 | "log", |
171 | "mailpot-tests", |
172 | "melib", |
173 | @@ -2255,6 +2349,20 @@ dependencies = [ |
174 | ] |
175 | |
176 | [[package]] |
177 | + name = "num" |
178 | + version = "0.4.0" |
179 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
180 | + checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" |
181 | + dependencies = [ |
182 | + "num-bigint", |
183 | + "num-complex", |
184 | + "num-integer", |
185 | + "num-iter", |
186 | + "num-rational", |
187 | + "num-traits", |
188 | + ] |
189 | + |
190 | + [[package]] |
191 | name = "num-bigint" |
192 | version = "0.4.3" |
193 | source = "registry+https://github.com/rust-lang/crates.io-index" |
194 | @@ -2266,6 +2374,21 @@ dependencies = [ |
195 | ] |
196 | |
197 | [[package]] |
198 | + name = "num-cmp" |
199 | + version = "0.1.0" |
200 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
201 | + checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" |
202 | + |
203 | + [[package]] |
204 | + name = "num-complex" |
205 | + version = "0.4.3" |
206 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
207 | + checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" |
208 | + dependencies = [ |
209 | + "num-traits", |
210 | + ] |
211 | + |
212 | + [[package]] |
213 | name = "num-integer" |
214 | version = "0.1.45" |
215 | source = "registry+https://github.com/rust-lang/crates.io-index" |
216 | @@ -2276,6 +2399,29 @@ dependencies = [ |
217 | ] |
218 | |
219 | [[package]] |
220 | + name = "num-iter" |
221 | + version = "0.1.43" |
222 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
223 | + checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" |
224 | + dependencies = [ |
225 | + "autocfg", |
226 | + "num-integer", |
227 | + "num-traits", |
228 | + ] |
229 | + |
230 | + [[package]] |
231 | + name = "num-rational" |
232 | + version = "0.4.1" |
233 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
234 | + checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" |
235 | + dependencies = [ |
236 | + "autocfg", |
237 | + "num-bigint", |
238 | + "num-integer", |
239 | + "num-traits", |
240 | + ] |
241 | + |
242 | + [[package]] |
243 | name = "num-traits" |
244 | version = "0.2.15" |
245 | source = "registry+https://github.com/rust-lang/crates.io-index" |
246 | diff --git a/Makefile b/Makefile |
247 | index d8135c9..b58141c 100644 |
248 | --- a/Makefile |
249 | +++ b/Makefile |
250 | @@ -1,31 +1,38 @@ |
251 | .POSIX: |
252 | .SUFFIXES: |
253 | + CARGOBIN = cargo |
254 | + CARGOSORTBIN = cargo-sort |
255 | + DJHTMLBIN = djhtml |
256 | + BLACKBIN = black |
257 | + PRINTF = /usr/bin/printf |
258 | |
259 | - HTML_FILES := $(shell find web/src/templates -type f -print0 | tr '\0' ' ') |
260 | + HTML_FILES := $(shell find web/src/templates -type f -print0 | tr '\0' ' ') |
261 | + PY_FILES := $(shell find . -type f -name '*.py' -print0 | tr '\0' ' ') |
262 | |
263 | .PHONY: check |
264 | check: |
265 | - @cargo check --all-features --all --tests --examples --benches --bins |
266 | + @$(CARGOBIN) check --all-features --all --tests --examples --benches --bins |
267 | |
268 | .PHONY: fmt |
269 | fmt: |
270 | - @cargo +nightly fmt --all || cargo fmt --all |
271 | - @cargo sort -w || printf "cargo-sort binary not found in PATH.\n" |
272 | - @djhtml $(HTML_FILES) || printf "djhtml binary not found in PATH.\n" |
273 | + @$(CARGOBIN) +nightly fmt --all || $(CARGOBIN) fmt --all |
274 | + @OUT=$$($(CARGOSORTBIN) -w 2>&1) || $(PRINTF) "ERROR: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT" |
275 | + @OUT=$$($(DJHTMLBIN) $(HTML_FILES) 2>&1) || $(PRINTF) "ERROR: %s djhtml failed or binary not found in PATH.\n" "$$OUT" |
276 | + @OUT=$$($(BLACKBIN) -q $(PY_FILES) 2>&1) || $(PRINTF) "ERROR: %s black failed or binary not found in PATH.\n" "$$OUT" |
277 | |
278 | .PHONY: lint |
279 | lint: |
280 | - @cargo clippy --no-deps --all-features --all --tests --examples --benches --bins |
281 | + @$(CARGOBIN) clippy --no-deps --all-features --all --tests --examples --benches --bins |
282 | |
283 | |
284 | .PHONY: test |
285 | test: check lint |
286 | - @cargo test --all --no-fail-fast --all-features |
287 | + @$(CARGOBIN) test --all --no-fail-fast --all-features |
288 | |
289 | .PHONY: rustdoc |
290 | rustdoc: |
291 | - @RUSTDOCFLAGS="--html-before-content ./.github/doc_extra.html" cargo doc --workspace --all-features --no-deps --document-private-items |
292 | + @RUSTDOCFLAGS="--html-before-content ./.github/doc_extra.html" $(CARGOBIN) doc --workspace --all-features --no-deps --document-private-items |
293 | |
294 | .PHONY: rustdoc-open |
295 | rustdoc-open: |
296 | - @RUSTDOCFLAGS="--html-before-content ./.github/doc_extra.html" cargo doc --workspace --all-features --no-deps --document-private-items --open |
297 | + @RUSTDOCFLAGS="--html-before-content ./.github/doc_extra.html" $(CARGOBIN) doc --workspace --all-features --no-deps --document-private-items --open |
298 | diff --git a/core/Cargo.toml b/core/Cargo.toml |
299 | index ba0c794..92bd33d 100644 |
300 | --- a/core/Cargo.toml |
301 | +++ b/core/Cargo.toml |
302 | @@ -14,10 +14,11 @@ categories = ["email"] |
303 | anyhow = "1.0.58" |
304 | chrono = { version = "^0.4", features = ["serde", ] } |
305 | error-chain = { version = "0.12.4", default-features = false } |
306 | + jsonschema = { version = "0.17", default-features = false } |
307 | log = "0.4" |
308 | melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms", "maildir_backend"], git = "https://github.com/meli/meli", rev = "2447a2c" } |
309 | minijinja = { version = "0.31.0", features = ["source", ] } |
310 | - rusqlite = { version = "^0.28", features = ["bundled", "trace", "hooks", "serde_json", "array", "chrono", "unlock_notify"] } |
311 | + rusqlite = { version = "^0.28", features = ["bundled", "functions", "trace", "hooks", "serde_json", "array", "chrono", "unlock_notify"] } |
312 | serde = { version = "^1", features = ["derive", ] } |
313 | serde_json = "^1" |
314 | toml = "^0.5" |
315 | diff --git a/core/migrations/004.sql b/core/migrations/004.sql |
316 | new file mode 100644 |
317 | index 0000000..95aff47 |
318 | --- /dev/null |
319 | +++ b/core/migrations/004.sql |
320 | @@ -0,0 +1,167 @@ |
321 | + CREATE TABLE IF NOT EXISTS settings_json_schema ( |
322 | + pk INTEGER PRIMARY KEY NOT NULL, |
323 | + id TEXT NOT NULL UNIQUE, |
324 | + value JSON NOT NULL CHECK (json_type(value) = 'object'), |
325 | + created INTEGER NOT NULL DEFAULT (unixepoch()), |
326 | + last_modified INTEGER NOT NULL DEFAULT (unixepoch()) |
327 | + ); |
328 | + |
329 | + CREATE TABLE IF NOT EXISTS list_settings_json ( |
330 | + pk INTEGER PRIMARY KEY NOT NULL, |
331 | + name TEXT NOT NULL, |
332 | + list INTEGER, |
333 | + value JSON NOT NULL CHECK (json_type(value) = 'object'), |
334 | + is_valid BOOLEAN CHECK (is_valid IN (0, 1)) NOT NULL DEFAULT 0, -- BOOLEAN_FALSE-> 0, BOOLEAN_TRUE-> 1 |
335 | + created INTEGER NOT NULL DEFAULT (unixepoch()), |
336 | + last_modified INTEGER NOT NULL DEFAULT (unixepoch()), |
337 | + FOREIGN KEY (list) REFERENCES list(pk) ON DELETE CASCADE, |
338 | + FOREIGN KEY (name) REFERENCES settings_json_schema(id) ON DELETE CASCADE, |
339 | + UNIQUE (list, name) ON CONFLICT ROLLBACK |
340 | + ); |
341 | + |
342 | + CREATE TRIGGER |
343 | + IF NOT EXISTS is_valid_settings_json_on_update |
344 | + AFTER UPDATE OF value, name, is_valid ON list_settings_json |
345 | + FOR EACH ROW |
346 | + BEGIN |
347 | + SELECT RAISE(ROLLBACK, 'new settings value is not valid according to the json schema. Rolling back transaction.') FROM settings_json_schema AS schema WHERE schema.id = NEW.name AND NOT validate_json_schema(schema.value, NEW.value); |
348 | + UPDATE list_settings_json SET is_valid = 1 WHERE pk = NEW.pk; |
349 | + END; |
350 | + |
351 | + CREATE TRIGGER |
352 | + IF NOT EXISTS is_valid_settings_json_on_insert |
353 | + AFTER INSERT ON list_settings_json |
354 | + FOR EACH ROW |
355 | + BEGIN |
356 | + SELECT RAISE(ROLLBACK, 'new settings value is not valid according to the json schema. Rolling back transaction.') FROM settings_json_schema AS schema WHERE schema.id = NEW.name AND NOT validate_json_schema(schema.value, NEW.value); |
357 | + UPDATE list_settings_json SET is_valid = 1 WHERE pk = NEW.pk; |
358 | + END; |
359 | + |
360 | + CREATE TRIGGER |
361 | + IF NOT EXISTS invalidate_settings_json_on_schema_update |
362 | + AFTER UPDATE OF value, id ON settings_json_schema |
363 | + FOR EACH ROW |
364 | + BEGIN |
365 | + UPDATE list_settings_json SET name = NEW.id, is_valid = 0 WHERE name = OLD.id; |
366 | + END; |
367 | + |
368 | + DROP TRIGGER IF EXISTS last_modified_list; |
369 | + DROP TRIGGER IF EXISTS last_modified_owner; |
370 | + DROP TRIGGER IF EXISTS last_modified_post_policy; |
371 | + DROP TRIGGER IF EXISTS last_modified_subscription_policy; |
372 | + DROP TRIGGER IF EXISTS last_modified_subscription; |
373 | + DROP TRIGGER IF EXISTS last_modified_account; |
374 | + DROP TRIGGER IF EXISTS last_modified_candidate_subscription; |
375 | + DROP TRIGGER IF EXISTS last_modified_template; |
376 | + DROP TRIGGER IF EXISTS last_modified_settings_json_schema; |
377 | + DROP TRIGGER IF EXISTS last_modified_list_settings_json; |
378 | + |
379 | + -- [tag:last_modified_list]: update last_modified on every change. |
380 | + CREATE TRIGGER |
381 | + IF NOT EXISTS last_modified_list |
382 | + AFTER UPDATE ON list |
383 | + FOR EACH ROW |
384 | + WHEN NEW.last_modified == OLD.last_modified |
385 | + BEGIN |
386 | + UPDATE list SET last_modified = unixepoch() |
387 | + WHERE pk = NEW.pk; |
388 | + END; |
389 | + |
390 | + -- [tag:last_modified_owner]: update last_modified on every change. |
391 | + CREATE TRIGGER |
392 | + IF NOT EXISTS last_modified_owner |
393 | + AFTER UPDATE ON owner |
394 | + FOR EACH ROW |
395 | + WHEN NEW.last_modified == OLD.last_modified |
396 | + BEGIN |
397 | + UPDATE owner SET last_modified = unixepoch() |
398 | + WHERE pk = NEW.pk; |
399 | + END; |
400 | + |
401 | + -- [tag:last_modified_post_policy]: update last_modified on every change. |
402 | + CREATE TRIGGER |
403 | + IF NOT EXISTS last_modified_post_policy |
404 | + AFTER UPDATE ON post_policy |
405 | + FOR EACH ROW |
406 | + WHEN NEW.last_modified == OLD.last_modified |
407 | + BEGIN |
408 | + UPDATE post_policy SET last_modified = unixepoch() |
409 | + WHERE pk = NEW.pk; |
410 | + END; |
411 | + |
412 | + -- [tag:last_modified_subscription_policy]: update last_modified on every change. |
413 | + CREATE TRIGGER |
414 | + IF NOT EXISTS last_modified_subscription_policy |
415 | + AFTER UPDATE ON subscription_policy |
416 | + FOR EACH ROW |
417 | + WHEN NEW.last_modified == OLD.last_modified |
418 | + BEGIN |
419 | + UPDATE subscription_policy SET last_modified = unixepoch() |
420 | + WHERE pk = NEW.pk; |
421 | + END; |
422 | + |
423 | + -- [tag:last_modified_subscription]: update last_modified on every change. |
424 | + CREATE TRIGGER |
425 | + IF NOT EXISTS last_modified_subscription |
426 | + AFTER UPDATE ON subscription |
427 | + FOR EACH ROW |
428 | + WHEN NEW.last_modified == OLD.last_modified |
429 | + BEGIN |
430 | + UPDATE subscription SET last_modified = unixepoch() |
431 | + WHERE pk = NEW.pk; |
432 | + END; |
433 | + |
434 | + -- [tag:last_modified_account]: update last_modified on every change. |
435 | + CREATE TRIGGER |
436 | + IF NOT EXISTS last_modified_account |
437 | + AFTER UPDATE ON account |
438 | + FOR EACH ROW |
439 | + WHEN NEW.last_modified == OLD.last_modified |
440 | + BEGIN |
441 | + UPDATE account SET last_modified = unixepoch() |
442 | + WHERE pk = NEW.pk; |
443 | + END; |
444 | + |
445 | + -- [tag:last_modified_candidate_subscription]: update last_modified on every change. |
446 | + CREATE TRIGGER |
447 | + IF NOT EXISTS last_modified_candidate_subscription |
448 | + AFTER UPDATE ON candidate_subscription |
449 | + FOR EACH ROW |
450 | + WHEN NEW.last_modified == OLD.last_modified |
451 | + BEGIN |
452 | + UPDATE candidate_subscription SET last_modified = unixepoch() |
453 | + WHERE pk = NEW.pk; |
454 | + END; |
455 | + |
456 | + -- [tag:last_modified_template]: update last_modified on every change. |
457 | + CREATE TRIGGER |
458 | + IF NOT EXISTS last_modified_template |
459 | + AFTER UPDATE ON template |
460 | + FOR EACH ROW |
461 | + WHEN NEW.last_modified == OLD.last_modified |
462 | + BEGIN |
463 | + UPDATE template SET last_modified = unixepoch() |
464 | + WHERE pk = NEW.pk; |
465 | + END; |
466 | + |
467 | + -- [tag:last_modified_settings_json_schema]: update last_modified on every change. |
468 | + CREATE TRIGGER |
469 | + IF NOT EXISTS last_modified_settings_json_schema |
470 | + AFTER UPDATE ON settings_json_schema |
471 | + FOR EACH ROW |
472 | + WHEN NEW.last_modified == OLD.last_modified |
473 | + BEGIN |
474 | + UPDATE settings_json_schema SET last_modified = unixepoch() |
475 | + WHERE pk = NEW.pk; |
476 | + END; |
477 | + |
478 | + -- [tag:last_modified_list_settings_json]: update last_modified on every change. |
479 | + CREATE TRIGGER |
480 | + IF NOT EXISTS last_modified_list_settings_json |
481 | + AFTER UPDATE ON list_settings_json |
482 | + FOR EACH ROW |
483 | + WHEN NEW.last_modified == OLD.last_modified |
484 | + BEGIN |
485 | + UPDATE list_settings_json SET last_modified = unixepoch() |
486 | + WHERE pk = NEW.pk; |
487 | + END; |
488 | diff --git a/core/migrations/004.undo.sql b/core/migrations/004.undo.sql |
489 | new file mode 100644 |
490 | index 0000000..b780b5c |
491 | --- /dev/null |
492 | +++ b/core/migrations/004.undo.sql |
493 | @@ -0,0 +1,2 @@ |
494 | + DROP TABLE settings_json_schema; |
495 | + DROP TABLE list_settings_json; |
496 | diff --git a/core/settings_json_schemas/archivedatlink.json b/core/settings_json_schemas/archivedatlink.json |
497 | new file mode 100644 |
498 | index 0000000..2b832fe |
499 | --- /dev/null |
500 | +++ b/core/settings_json_schemas/archivedatlink.json |
501 | @@ -0,0 +1,31 @@ |
502 | + { |
503 | + "$schema": "http://json-schema.org/draft-07/schema", |
504 | + "$ref": "#/$defs/ArchivedAtLinkSettings", |
505 | + "$defs": { |
506 | + "ArchivedAtLinkSettings": { |
507 | + "title": "ArchivedAtLinkSettings", |
508 | + "description": "Settings for ArchivedAtLink message filter", |
509 | + "type": "object", |
510 | + "properties": { |
511 | + "template": { |
512 | + "title": "Jinja template for header value", |
513 | + "description": "Template for `Archived-At` header value, as described in RFC 5064 \"The Archived-At Message Header Field\". The template receives only one string variable with the value of the mailing list post `Message-ID` header.\n\nFor example, if:\n\n- the template is `http://www.example.com/mid/{{msg_id}}`\n- the `Message-ID` is `<0C2U00F01DFGCR@mailsj-v3.example.com>`\n\nThe full header will be generated as:\n\n`Archived-At: <http://www.example.com/mid/0C2U00F01DFGCR@mailsj-v3.example.com>\n\nNote: Surrounding carets in the `Message-ID` value are not required. If you wish to preserve them in the URL, set option `preserve-carets` to true.", |
514 | + "examples": [ |
515 | + "https://www.example.com/{{msg_id}}", |
516 | + "https://www.example.com/{{msg_id}}.html" |
517 | + ], |
518 | + "type": "string", |
519 | + "pattern": ".+[{][{]msg_id[}][}].*" |
520 | + }, |
521 | + "preserve_carets": { |
522 | + "title": "Preserve carets of `Message-ID` in generated value", |
523 | + "type": "boolean", |
524 | + "default": false |
525 | + } |
526 | + }, |
527 | + "required": [ |
528 | + "template" |
529 | + ] |
530 | + } |
531 | + } |
532 | + } |
533 | diff --git a/core/src/connection.rs b/core/src/connection.rs |
534 | index e6d6907..f875fb0 100644 |
535 | --- a/core/src/connection.rs |
536 | +++ b/core/src/connection.rs |
537 | @@ -24,8 +24,9 @@ use std::{ |
538 | process::{Command, Stdio}, |
539 | }; |
540 | |
541 | + use jsonschema::JSONSchema; |
542 | use log::{info, trace}; |
543 | - use rusqlite::{Connection as DbConnection, OptionalExtension}; |
544 | + use rusqlite::{functions::FunctionFlags, Connection as DbConnection, OptionalExtension}; |
545 | |
546 | use crate::{ |
547 | config::Configuration, |
548 | @@ -65,9 +66,9 @@ impl Drop for Connection { |
549 | |
550 | fn log_callback(error_code: std::ffi::c_int, message: &str) { |
551 | match error_code { |
552 | + rusqlite::ffi::SQLITE_NOTICE => log::trace!("{}", message), |
553 | rusqlite::ffi::SQLITE_OK |
554 | | rusqlite::ffi::SQLITE_DONE |
555 | - | rusqlite::ffi::SQLITE_NOTICE |
556 | | rusqlite::ffi::SQLITE_NOTICE_RECOVER_WAL |
557 | | rusqlite::ffi::SQLITE_NOTICE_RECOVER_ROLLBACK => log::info!("{}", message), |
558 | rusqlite::ffi::SQLITE_WARNING | rusqlite::ffi::SQLITE_WARNING_AUTOINDEX => { |
559 | @@ -198,6 +199,44 @@ impl Connection { |
560 | conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, true)?; |
561 | conn.busy_timeout(core::time::Duration::from_millis(500))?; |
562 | conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?; |
563 | + conn.create_scalar_function( |
564 | + "validate_json_schema", |
565 | + 2, |
566 | + FunctionFlags::SQLITE_INNOCUOUS |
567 | + | FunctionFlags::SQLITE_UTF8 |
568 | + | FunctionFlags::SQLITE_DETERMINISTIC, |
569 | + |ctx| { |
570 | + if log::log_enabled!(log::Level::Trace) { |
571 | + rusqlite::trace::log( |
572 | + rusqlite::ffi::SQLITE_NOTICE, |
573 | + "validate_json_schema RUNNING", |
574 | + ); |
575 | + } |
576 | + let map_err = rusqlite::Error::UserFunctionError; |
577 | + let schema = ctx.get::<String>(0)?; |
578 | + let value = ctx.get::<String>(1)?; |
579 | + let schema_val: serde_json::Value = serde_json::from_str(&schema) |
580 | + .map_err(Into::into) |
581 | + .map_err(map_err)?; |
582 | + let value: serde_json::Value = serde_json::from_str(&value) |
583 | + .map_err(Into::into) |
584 | + .map_err(map_err)?; |
585 | + let compiled = JSONSchema::compile(&schema_val) |
586 | + .map_err(|err| err.to_string()) |
587 | + .map_err(Into::into) |
588 | + .map_err(map_err)?; |
589 | + let x = if let Err(errors) = compiled.validate(&value) { |
590 | + for err in errors { |
591 | + rusqlite::trace::log(rusqlite::ffi::SQLITE_WARNING, &err.to_string()); |
592 | + drop(err); |
593 | + } |
594 | + Ok(false) |
595 | + } else { |
596 | + Ok(true) |
597 | + }; |
598 | + x |
599 | + }, |
600 | + )?; |
601 | |
602 | let ret = Self { |
603 | conf, |
604 | diff --git a/core/src/migrations.rs.inc b/core/src/migrations.rs.inc |
605 | index cca184e..c70fe37 100644 |
606 | --- a/core/src/migrations.rs.inc |
607 | +++ b/core/src/migrations.rs.inc |
608 | @@ -32,4 +32,173 @@ END; |
609 | |
610 | DROP TRIGGER sort_topics_update_trigger; |
611 | DROP TRIGGER sort_topics_new_trigger; |
612 | + "),(4,"CREATE TABLE IF NOT EXISTS settings_json_schema ( |
613 | + pk INTEGER PRIMARY KEY NOT NULL, |
614 | + id TEXT NOT NULL UNIQUE, |
615 | + value JSON NOT NULL CHECK (json_type(value) = 'object'), |
616 | + created INTEGER NOT NULL DEFAULT (unixepoch()), |
617 | + last_modified INTEGER NOT NULL DEFAULT (unixepoch()) |
618 | + ); |
619 | + |
620 | + CREATE TABLE IF NOT EXISTS list_settings_json ( |
621 | + pk INTEGER PRIMARY KEY NOT NULL, |
622 | + name TEXT NOT NULL, |
623 | + list INTEGER, |
624 | + value JSON NOT NULL CHECK (json_type(value) = 'object'), |
625 | + is_valid BOOLEAN CHECK (is_valid IN (0, 1)) NOT NULL DEFAULT 0, -- BOOLEAN_FALSE-> 0, BOOLEAN_TRUE-> 1 |
626 | + created INTEGER NOT NULL DEFAULT (unixepoch()), |
627 | + last_modified INTEGER NOT NULL DEFAULT (unixepoch()), |
628 | + FOREIGN KEY (list) REFERENCES list(pk) ON DELETE CASCADE, |
629 | + FOREIGN KEY (name) REFERENCES settings_json_schema(id) ON DELETE CASCADE, |
630 | + UNIQUE (list, name) ON CONFLICT ROLLBACK |
631 | + ); |
632 | + |
633 | + CREATE TRIGGER |
634 | + IF NOT EXISTS is_valid_settings_json_on_update |
635 | + AFTER UPDATE OF value, name, is_valid ON list_settings_json |
636 | + FOR EACH ROW |
637 | + BEGIN |
638 | + SELECT RAISE(ROLLBACK, 'new settings value is not valid according to the json schema. Rolling back transaction.') FROM settings_json_schema AS schema WHERE schema.id = NEW.name AND NOT validate_json_schema(schema.value, NEW.value); |
639 | + UPDATE list_settings_json SET is_valid = 1 WHERE pk = NEW.pk; |
640 | + END; |
641 | + |
642 | + CREATE TRIGGER |
643 | + IF NOT EXISTS is_valid_settings_json_on_insert |
644 | + AFTER INSERT ON list_settings_json |
645 | + FOR EACH ROW |
646 | + BEGIN |
647 | + SELECT RAISE(ROLLBACK, 'new settings value is not valid according to the json schema. Rolling back transaction.') FROM settings_json_schema AS schema WHERE schema.id = NEW.name AND NOT validate_json_schema(schema.value, NEW.value); |
648 | + UPDATE list_settings_json SET is_valid = 1 WHERE pk = NEW.pk; |
649 | + END; |
650 | + |
651 | + CREATE TRIGGER |
652 | + IF NOT EXISTS invalidate_settings_json_on_schema_update |
653 | + AFTER UPDATE OF value, id ON settings_json_schema |
654 | + FOR EACH ROW |
655 | + BEGIN |
656 | + UPDATE list_settings_json SET name = NEW.id, is_valid = 0 WHERE name = OLD.id; |
657 | + END; |
658 | + |
659 | + DROP TRIGGER IF EXISTS last_modified_list; |
660 | + DROP TRIGGER IF EXISTS last_modified_owner; |
661 | + DROP TRIGGER IF EXISTS last_modified_post_policy; |
662 | + DROP TRIGGER IF EXISTS last_modified_subscription_policy; |
663 | + DROP TRIGGER IF EXISTS last_modified_subscription; |
664 | + DROP TRIGGER IF EXISTS last_modified_account; |
665 | + DROP TRIGGER IF EXISTS last_modified_candidate_subscription; |
666 | + DROP TRIGGER IF EXISTS last_modified_template; |
667 | + DROP TRIGGER IF EXISTS last_modified_settings_json_schema; |
668 | + DROP TRIGGER IF EXISTS last_modified_list_settings_json; |
669 | + |
670 | + -- [tag:last_modified_list]: update last_modified on every change. |
671 | + CREATE TRIGGER |
672 | + IF NOT EXISTS last_modified_list |
673 | + AFTER UPDATE ON list |
674 | + FOR EACH ROW |
675 | + WHEN NEW.last_modified == OLD.last_modified |
676 | + BEGIN |
677 | + UPDATE list SET last_modified = unixepoch() |
678 | + WHERE pk = NEW.pk; |
679 | + END; |
680 | + |
681 | + -- [tag:last_modified_owner]: update last_modified on every change. |
682 | + CREATE TRIGGER |
683 | + IF NOT EXISTS last_modified_owner |
684 | + AFTER UPDATE ON owner |
685 | + FOR EACH ROW |
686 | + WHEN NEW.last_modified == OLD.last_modified |
687 | + BEGIN |
688 | + UPDATE owner SET last_modified = unixepoch() |
689 | + WHERE pk = NEW.pk; |
690 | + END; |
691 | + |
692 | + -- [tag:last_modified_post_policy]: update last_modified on every change. |
693 | + CREATE TRIGGER |
694 | + IF NOT EXISTS last_modified_post_policy |
695 | + AFTER UPDATE ON post_policy |
696 | + FOR EACH ROW |
697 | + WHEN NEW.last_modified == OLD.last_modified |
698 | + BEGIN |
699 | + UPDATE post_policy SET last_modified = unixepoch() |
700 | + WHERE pk = NEW.pk; |
701 | + END; |
702 | + |
703 | + -- [tag:last_modified_subscription_policy]: update last_modified on every change. |
704 | + CREATE TRIGGER |
705 | + IF NOT EXISTS last_modified_subscription_policy |
706 | + AFTER UPDATE ON subscription_policy |
707 | + FOR EACH ROW |
708 | + WHEN NEW.last_modified == OLD.last_modified |
709 | + BEGIN |
710 | + UPDATE subscription_policy SET last_modified = unixepoch() |
711 | + WHERE pk = NEW.pk; |
712 | + END; |
713 | + |
714 | + -- [tag:last_modified_subscription]: update last_modified on every change. |
715 | + CREATE TRIGGER |
716 | + IF NOT EXISTS last_modified_subscription |
717 | + AFTER UPDATE ON subscription |
718 | + FOR EACH ROW |
719 | + WHEN NEW.last_modified == OLD.last_modified |
720 | + BEGIN |
721 | + UPDATE subscription SET last_modified = unixepoch() |
722 | + WHERE pk = NEW.pk; |
723 | + END; |
724 | + |
725 | + -- [tag:last_modified_account]: update last_modified on every change. |
726 | + CREATE TRIGGER |
727 | + IF NOT EXISTS last_modified_account |
728 | + AFTER UPDATE ON account |
729 | + FOR EACH ROW |
730 | + WHEN NEW.last_modified == OLD.last_modified |
731 | + BEGIN |
732 | + UPDATE account SET last_modified = unixepoch() |
733 | + WHERE pk = NEW.pk; |
734 | + END; |
735 | + |
736 | + -- [tag:last_modified_candidate_subscription]: update last_modified on every change. |
737 | + CREATE TRIGGER |
738 | + IF NOT EXISTS last_modified_candidate_subscription |
739 | + AFTER UPDATE ON candidate_subscription |
740 | + FOR EACH ROW |
741 | + WHEN NEW.last_modified == OLD.last_modified |
742 | + BEGIN |
743 | + UPDATE candidate_subscription SET last_modified = unixepoch() |
744 | + WHERE pk = NEW.pk; |
745 | + END; |
746 | + |
747 | + -- [tag:last_modified_template]: update last_modified on every change. |
748 | + CREATE TRIGGER |
749 | + IF NOT EXISTS last_modified_template |
750 | + AFTER UPDATE ON template |
751 | + FOR EACH ROW |
752 | + WHEN NEW.last_modified == OLD.last_modified |
753 | + BEGIN |
754 | + UPDATE template SET last_modified = unixepoch() |
755 | + WHERE pk = NEW.pk; |
756 | + END; |
757 | + |
758 | + -- [tag:last_modified_settings_json_schema]: update last_modified on every change. |
759 | + CREATE TRIGGER |
760 | + IF NOT EXISTS last_modified_settings_json_schema |
761 | + AFTER UPDATE ON settings_json_schema |
762 | + FOR EACH ROW |
763 | + WHEN NEW.last_modified == OLD.last_modified |
764 | + BEGIN |
765 | + UPDATE settings_json_schema SET last_modified = unixepoch() |
766 | + WHERE pk = NEW.pk; |
767 | + END; |
768 | + |
769 | + -- [tag:last_modified_list_settings_json]: update last_modified on every change. |
770 | + CREATE TRIGGER |
771 | + IF NOT EXISTS last_modified_list_settings_json |
772 | + AFTER UPDATE ON list_settings_json |
773 | + FOR EACH ROW |
774 | + WHEN NEW.last_modified == OLD.last_modified |
775 | + BEGIN |
776 | + UPDATE list_settings_json SET last_modified = unixepoch() |
777 | + WHERE pk = NEW.pk; |
778 | + END; |
779 | + ","DROP TABLE settings_json_schema; |
780 | + DROP TABLE list_settings_json; |
781 | "),] |
782 | \ No newline at end of file |
783 | diff --git a/core/src/schema.sql b/core/src/schema.sql |
784 | index 4db9f82..a983117 100644 |
785 | --- a/core/src/schema.sql |
786 | +++ b/core/src/schema.sql |
787 | @@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS list ( |
788 | topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]', |
789 | created INTEGER NOT NULL DEFAULT (unixepoch()), |
790 | last_modified INTEGER NOT NULL DEFAULT (unixepoch()), |
791 | - verify BOOLEAN CHECK (verify IN (0, 1)) NOT NULL DEFAULT 1, |
792 | + verify BOOLEAN CHECK (verify IN (0, 1)) NOT NULL DEFAULT 1, -- BOOLEAN FALSE == 0, BOOLEAN TRUE == 1 |
793 | hidden BOOLEAN CHECK (hidden IN (0, 1)) NOT NULL DEFAULT 0, |
794 | enabled BOOLEAN CHECK (enabled IN (0, 1)) NOT NULL DEFAULT 1 |
795 | ); |
796 | @@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS post_policy ( |
797 | pk INTEGER PRIMARY KEY NOT NULL, |
798 | list INTEGER NOT NULL UNIQUE, |
799 | announce_only BOOLEAN CHECK (announce_only IN (0, 1)) NOT NULL |
800 | - DEFAULT 0, |
801 | + DEFAULT 0, -- BOOLEAN FALSE == 0, BOOLEAN TRUE == 1 |
802 | subscription_only BOOLEAN CHECK (subscription_only IN (0, 1)) NOT NULL |
803 | DEFAULT 0, |
804 | approval_needed BOOLEAN CHECK (approval_needed IN (0, 1)) NOT NULL |
805 | @@ -139,7 +139,7 @@ CREATE TABLE IF NOT EXISTS subscription_policy ( |
806 | pk INTEGER PRIMARY KEY NOT NULL, |
807 | list INTEGER NOT NULL UNIQUE, |
808 | send_confirmation BOOLEAN CHECK (send_confirmation IN (0, 1)) NOT NULL |
809 | - DEFAULT 1, |
810 | + DEFAULT 1, -- BOOLEAN FALSE == 0, BOOLEAN TRUE == 1 |
811 | open BOOLEAN CHECK (open IN (0, 1)) NOT NULL DEFAULT 0, |
812 | manual BOOLEAN CHECK (manual IN (0, 1)) NOT NULL DEFAULT 0, |
813 | request BOOLEAN CHECK (request IN (0, 1)) NOT NULL DEFAULT 0, |
814 | @@ -199,7 +199,7 @@ CREATE TABLE IF NOT EXISTS subscription ( |
815 | name TEXT, |
816 | account INTEGER, |
817 | enabled BOOLEAN CHECK (enabled IN (0, 1)) NOT NULL |
818 | - DEFAULT 1, |
819 | + DEFAULT 1, -- BOOLEAN FALSE == 0, BOOLEAN TRUE == 1 |
820 | verified BOOLEAN CHECK (verified IN (0, 1)) NOT NULL |
821 | DEFAULT 1, |
822 | digest BOOLEAN CHECK (digest IN (0, 1)) NOT NULL |
823 | @@ -226,7 +226,7 @@ CREATE TABLE IF NOT EXISTS account ( |
824 | address TEXT NOT NULL UNIQUE, |
825 | public_key TEXT, |
826 | password TEXT NOT NULL, |
827 | - enabled BOOLEAN CHECK (enabled IN (0, 1)) NOT NULL DEFAULT 1, |
828 | + enabled BOOLEAN CHECK (enabled IN (0, 1)) NOT NULL DEFAULT 1, -- BOOLEAN FALSE == 0, BOOLEAN TRUE == 1 |
829 | created INTEGER NOT NULL DEFAULT (unixepoch()), |
830 | last_modified INTEGER NOT NULL DEFAULT (unixepoch()) |
831 | ); |
832 | @@ -270,6 +270,53 @@ CREATE TABLE IF NOT EXISTS template ( |
833 | UNIQUE (list, name) ON CONFLICT ROLLBACK |
834 | ); |
835 | |
836 | + CREATE TABLE IF NOT EXISTS settings_json_schema ( |
837 | + pk INTEGER PRIMARY KEY NOT NULL, |
838 | + id TEXT NOT NULL UNIQUE, |
839 | + value JSON NOT NULL CHECK (json_type(value) = 'object'), |
840 | + created INTEGER NOT NULL DEFAULT (unixepoch()), |
841 | + last_modified INTEGER NOT NULL DEFAULT (unixepoch()) |
842 | + ); |
843 | + |
844 | + CREATE TABLE IF NOT EXISTS list_settings_json ( |
845 | + pk INTEGER PRIMARY KEY NOT NULL, |
846 | + name TEXT NOT NULL, |
847 | + list INTEGER, |
848 | + value JSON NOT NULL CHECK (json_type(value) = 'object'), |
849 | + is_valid BOOLEAN CHECK (is_valid IN (0, 1)) NOT NULL DEFAULT 0, -- BOOLEAN FALSE == 0, BOOLEAN TRUE == 1 |
850 | + created INTEGER NOT NULL DEFAULT (unixepoch()), |
851 | + last_modified INTEGER NOT NULL DEFAULT (unixepoch()), |
852 | + FOREIGN KEY (list) REFERENCES list(pk) ON DELETE CASCADE, |
853 | + FOREIGN KEY (name) REFERENCES settings_json_schema(id) ON DELETE CASCADE, |
854 | + UNIQUE (list, name) ON CONFLICT ROLLBACK |
855 | + ); |
856 | + |
857 | + CREATE TRIGGER |
858 | + IF NOT EXISTS is_valid_settings_json_on_update |
859 | + AFTER UPDATE OF value, name, is_valid ON list_settings_json |
860 | + FOR EACH ROW |
861 | + BEGIN |
862 | + SELECT RAISE(ROLLBACK, 'new settings value is not valid according to the json schema. Rolling back transaction.') FROM settings_json_schema AS schema WHERE schema.id = NEW.name AND NOT validate_json_schema(schema.value, NEW.value); |
863 | + UPDATE list_settings_json SET is_valid = 1 WHERE pk = NEW.pk; |
864 | + END; |
865 | + |
866 | + CREATE TRIGGER |
867 | + IF NOT EXISTS is_valid_settings_json_on_insert |
868 | + AFTER INSERT ON list_settings_json |
869 | + FOR EACH ROW |
870 | + BEGIN |
871 | + SELECT RAISE(ROLLBACK, 'new settings value is not valid according to the json schema. Rolling back transaction.') FROM settings_json_schema AS schema WHERE schema.id = NEW.name AND NOT validate_json_schema(schema.value, NEW.value); |
872 | + UPDATE list_settings_json SET is_valid = 1 WHERE pk = NEW.pk; |
873 | + END; |
874 | + |
875 | + CREATE TRIGGER |
876 | + IF NOT EXISTS invalidate_settings_json_on_schema_update |
877 | + AFTER UPDATE OF value, id ON settings_json_schema |
878 | + FOR EACH ROW |
879 | + BEGIN |
880 | + UPDATE list_settings_json SET name = NEW.id, is_valid = 0 WHERE name = OLD.id; |
881 | + END; |
882 | + |
883 | -- # Queues |
884 | -- |
885 | -- ## The "maildrop" queue |
886 | @@ -386,7 +433,7 @@ CREATE TRIGGER |
887 | IF NOT EXISTS last_modified_list |
888 | AFTER UPDATE ON list |
889 | FOR EACH ROW |
890 | - WHEN NEW.last_modified != OLD.last_modified |
891 | + WHEN NEW.last_modified == OLD.last_modified |
892 | BEGIN |
893 | UPDATE list SET last_modified = unixepoch() |
894 | WHERE pk = NEW.pk; |
895 | @@ -397,7 +444,7 @@ CREATE TRIGGER |
896 | IF NOT EXISTS last_modified_owner |
897 | AFTER UPDATE ON owner |
898 | FOR EACH ROW |
899 | - WHEN NEW.last_modified != OLD.last_modified |
900 | + WHEN NEW.last_modified == OLD.last_modified |
901 | BEGIN |
902 | UPDATE owner SET last_modified = unixepoch() |
903 | WHERE pk = NEW.pk; |
904 | @@ -408,7 +455,7 @@ CREATE TRIGGER |
905 | IF NOT EXISTS last_modified_post_policy |
906 | AFTER UPDATE ON post_policy |
907 | FOR EACH ROW |
908 | - WHEN NEW.last_modified != OLD.last_modified |
909 | + WHEN NEW.last_modified == OLD.last_modified |
910 | BEGIN |
911 | UPDATE post_policy SET last_modified = unixepoch() |
912 | WHERE pk = NEW.pk; |
913 | @@ -419,7 +466,7 @@ CREATE TRIGGER |
914 | IF NOT EXISTS last_modified_subscription_policy |
915 | AFTER UPDATE ON subscription_policy |
916 | FOR EACH ROW |
917 | - WHEN NEW.last_modified != OLD.last_modified |
918 | + WHEN NEW.last_modified == OLD.last_modified |
919 | BEGIN |
920 | UPDATE subscription_policy SET last_modified = unixepoch() |
921 | WHERE pk = NEW.pk; |
922 | @@ -430,7 +477,7 @@ CREATE TRIGGER |
923 | IF NOT EXISTS last_modified_subscription |
924 | AFTER UPDATE ON subscription |
925 | FOR EACH ROW |
926 | - WHEN NEW.last_modified != OLD.last_modified |
927 | + WHEN NEW.last_modified == OLD.last_modified |
928 | BEGIN |
929 | UPDATE subscription SET last_modified = unixepoch() |
930 | WHERE pk = NEW.pk; |
931 | @@ -441,7 +488,7 @@ CREATE TRIGGER |
932 | IF NOT EXISTS last_modified_account |
933 | AFTER UPDATE ON account |
934 | FOR EACH ROW |
935 | - WHEN NEW.last_modified != OLD.last_modified |
936 | + WHEN NEW.last_modified == OLD.last_modified |
937 | BEGIN |
938 | UPDATE account SET last_modified = unixepoch() |
939 | WHERE pk = NEW.pk; |
940 | @@ -452,7 +499,7 @@ CREATE TRIGGER |
941 | IF NOT EXISTS last_modified_candidate_subscription |
942 | AFTER UPDATE ON candidate_subscription |
943 | FOR EACH ROW |
944 | - WHEN NEW.last_modified != OLD.last_modified |
945 | + WHEN NEW.last_modified == OLD.last_modified |
946 | BEGIN |
947 | UPDATE candidate_subscription SET last_modified = unixepoch() |
948 | WHERE pk = NEW.pk; |
949 | @@ -463,12 +510,34 @@ CREATE TRIGGER |
950 | IF NOT EXISTS last_modified_template |
951 | AFTER UPDATE ON template |
952 | FOR EACH ROW |
953 | - WHEN NEW.last_modified != OLD.last_modified |
954 | + WHEN NEW.last_modified == OLD.last_modified |
955 | BEGIN |
956 | UPDATE template SET last_modified = unixepoch() |
957 | WHERE pk = NEW.pk; |
958 | END; |
959 | |
960 | + -- [tag:last_modified_settings_json_schema]: update last_modified on every change. |
961 | + CREATE TRIGGER |
962 | + IF NOT EXISTS last_modified_settings_json_schema |
963 | + AFTER UPDATE ON settings_json_schema |
964 | + FOR EACH ROW |
965 | + WHEN NEW.last_modified == OLD.last_modified |
966 | + BEGIN |
967 | + UPDATE settings_json_schema SET last_modified = unixepoch() |
968 | + WHERE pk = NEW.pk; |
969 | + END; |
970 | + |
971 | + -- [tag:last_modified_list_settings_json]: update last_modified on every change. |
972 | + CREATE TRIGGER |
973 | + IF NOT EXISTS last_modified_list_settings_json |
974 | + AFTER UPDATE ON list_settings_json |
975 | + FOR EACH ROW |
976 | + WHEN NEW.last_modified == OLD.last_modified |
977 | + BEGIN |
978 | + UPDATE list_settings_json SET last_modified = unixepoch() |
979 | + WHERE pk = NEW.pk; |
980 | + END; |
981 | + |
982 | CREATE TRIGGER |
983 | IF NOT EXISTS sort_topics_update_trigger |
984 | AFTER UPDATE ON list |
985 | diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4 |
986 | index e09b8f7..c89fa8f 100644 |
987 | --- a/core/src/schema.sql.m4 |
988 | +++ b/core/src/schema.sql.m4 |
989 | @@ -11,6 +11,7 @@ dnl # Define boolean column types and defaults |
990 | define(BOOLEAN_TYPE, `BOOLEAN CHECK ($1 IN (0, 1)) NOT NULL')dnl |
991 | define(BOOLEAN_FALSE, `0')dnl |
992 | define(BOOLEAN_TRUE, `1')dnl |
993 | + define(BOOLEAN_DOCS, ` -- BOOLEAN FALSE == 0, BOOLEAN TRUE == 1')dnl |
994 | dnl |
995 | dnl # defile comment functions |
996 | dnl |
997 | @@ -26,7 +27,7 @@ CREATE TRIGGER |
998 | IF NOT EXISTS last_modified_$1 |
999 | AFTER UPDATE ON $1 |
1000 | FOR EACH ROW |
1001 | - WHEN NEW.last_modified != OLD.last_modified |
1002 | + WHEN NEW.last_modified == OLD.last_modified |
1003 | BEGIN |
1004 | UPDATE $1 SET last_modified = unixepoch() |
1005 | WHERE pk = NEW.pk; |
1006 | @@ -47,7 +48,7 @@ CREATE TABLE IF NOT EXISTS list ( |
1007 | topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]', |
1008 | created INTEGER NOT NULL DEFAULT (unixepoch()), |
1009 | last_modified INTEGER NOT NULL DEFAULT (unixepoch()), |
1010 | - verify BOOLEAN_TYPE(verify) DEFAULT BOOLEAN_TRUE(), |
1011 | + verify BOOLEAN_TYPE(verify) DEFAULT BOOLEAN_TRUE(),BOOLEAN_DOCS() |
1012 | hidden BOOLEAN_TYPE(hidden) DEFAULT BOOLEAN_FALSE(), |
1013 | enabled BOOLEAN_TYPE(enabled) DEFAULT BOOLEAN_TRUE() |
1014 | ); |
1015 | @@ -66,7 +67,7 @@ CREATE TABLE IF NOT EXISTS post_policy ( |
1016 | pk INTEGER PRIMARY KEY NOT NULL, |
1017 | list INTEGER NOT NULL UNIQUE, |
1018 | announce_only BOOLEAN_TYPE(announce_only) |
1019 | - DEFAULT BOOLEAN_FALSE(), |
1020 | + DEFAULT BOOLEAN_FALSE(),BOOLEAN_DOCS() |
1021 | subscription_only BOOLEAN_TYPE(subscription_only) |
1022 | DEFAULT BOOLEAN_FALSE(), |
1023 | approval_needed BOOLEAN_TYPE(approval_needed) |
1024 | @@ -83,7 +84,7 @@ CREATE TABLE IF NOT EXISTS subscription_policy ( |
1025 | pk INTEGER PRIMARY KEY NOT NULL, |
1026 | list INTEGER NOT NULL UNIQUE, |
1027 | send_confirmation BOOLEAN_TYPE(send_confirmation) |
1028 | - DEFAULT BOOLEAN_TRUE(), |
1029 | + DEFAULT BOOLEAN_TRUE(),BOOLEAN_DOCS() |
1030 | open BOOLEAN_TYPE(open) DEFAULT BOOLEAN_FALSE(), |
1031 | manual BOOLEAN_TYPE(manual) DEFAULT BOOLEAN_FALSE(), |
1032 | request BOOLEAN_TYPE(request) DEFAULT BOOLEAN_FALSE(), |
1033 | @@ -101,7 +102,7 @@ CREATE TABLE IF NOT EXISTS subscription ( |
1034 | name TEXT, |
1035 | account INTEGER, |
1036 | enabled BOOLEAN_TYPE(enabled) |
1037 | - DEFAULT BOOLEAN_TRUE(), |
1038 | + DEFAULT BOOLEAN_TRUE(),BOOLEAN_DOCS() |
1039 | verified BOOLEAN_TYPE(verified) |
1040 | DEFAULT BOOLEAN_TRUE(), |
1041 | digest BOOLEAN_TYPE(digest) |
1042 | @@ -128,7 +129,7 @@ CREATE TABLE IF NOT EXISTS account ( |
1043 | address TEXT NOT NULL UNIQUE, |
1044 | public_key TEXT, |
1045 | password TEXT NOT NULL, |
1046 | - enabled BOOLEAN_TYPE(enabled) DEFAULT BOOLEAN_TRUE(), |
1047 | + enabled BOOLEAN_TYPE(enabled) DEFAULT BOOLEAN_TRUE(),BOOLEAN_DOCS() |
1048 | created INTEGER NOT NULL DEFAULT (unixepoch()), |
1049 | last_modified INTEGER NOT NULL DEFAULT (unixepoch()) |
1050 | ); |
1051 | @@ -172,6 +173,53 @@ CREATE TABLE IF NOT EXISTS template ( |
1052 | UNIQUE (list, name) ON CONFLICT ROLLBACK |
1053 | ); |
1054 | |
1055 | + CREATE TABLE IF NOT EXISTS settings_json_schema ( |
1056 | + pk INTEGER PRIMARY KEY NOT NULL, |
1057 | + id TEXT NOT NULL UNIQUE, |
1058 | + value JSON NOT NULL CHECK (json_type(value) = 'object'), |
1059 | + created INTEGER NOT NULL DEFAULT (unixepoch()), |
1060 | + last_modified INTEGER NOT NULL DEFAULT (unixepoch()) |
1061 | + ); |
1062 | + |
1063 | + CREATE TABLE IF NOT EXISTS list_settings_json ( |
1064 | + pk INTEGER PRIMARY KEY NOT NULL, |
1065 | + name TEXT NOT NULL, |
1066 | + list INTEGER, |
1067 | + value JSON NOT NULL CHECK (json_type(value) = 'object'), |
1068 | + is_valid BOOLEAN_TYPE(is_valid) DEFAULT BOOLEAN_FALSE(),BOOLEAN_DOCS() |
1069 | + created INTEGER NOT NULL DEFAULT (unixepoch()), |
1070 | + last_modified INTEGER NOT NULL DEFAULT (unixepoch()), |
1071 | + FOREIGN KEY (list) REFERENCES list(pk) ON DELETE CASCADE, |
1072 | + FOREIGN KEY (name) REFERENCES settings_json_schema(id) ON DELETE CASCADE, |
1073 | + UNIQUE (list, name) ON CONFLICT ROLLBACK |
1074 | + ); |
1075 | + |
1076 | + CREATE TRIGGER |
1077 | + IF NOT EXISTS is_valid_settings_json_on_update |
1078 | + AFTER UPDATE OF value, name, is_valid ON list_settings_json |
1079 | + FOR EACH ROW |
1080 | + BEGIN |
1081 | + SELECT RAISE(ROLLBACK, 'new settings value is not valid according to the json schema. Rolling back transaction.') FROM settings_json_schema AS schema WHERE schema.id = NEW.name AND NOT validate_json_schema(schema.value, NEW.value); |
1082 | + UPDATE list_settings_json SET is_valid = BOOLEAN_TRUE() WHERE pk = NEW.pk; |
1083 | + END; |
1084 | + |
1085 | + CREATE TRIGGER |
1086 | + IF NOT EXISTS is_valid_settings_json_on_insert |
1087 | + AFTER INSERT ON list_settings_json |
1088 | + FOR EACH ROW |
1089 | + BEGIN |
1090 | + SELECT RAISE(ROLLBACK, 'new settings value is not valid according to the json schema. Rolling back transaction.') FROM settings_json_schema AS schema WHERE schema.id = NEW.name AND NOT validate_json_schema(schema.value, NEW.value); |
1091 | + UPDATE list_settings_json SET is_valid = BOOLEAN_TRUE() WHERE pk = NEW.pk; |
1092 | + END; |
1093 | + |
1094 | + CREATE TRIGGER |
1095 | + IF NOT EXISTS invalidate_settings_json_on_schema_update |
1096 | + AFTER UPDATE OF value, id ON settings_json_schema |
1097 | + FOR EACH ROW |
1098 | + BEGIN |
1099 | + UPDATE list_settings_json SET name = NEW.id, is_valid = BOOLEAN_FALSE() WHERE name = OLD.id; |
1100 | + END; |
1101 | + |
1102 | -- # Queues |
1103 | -- |
1104 | -- ## The "maildrop" queue |
1105 | @@ -290,6 +338,8 @@ update_last_modified(`subscription') |
1106 | update_last_modified(`account') |
1107 | update_last_modified(`candidate_subscription') |
1108 | update_last_modified(`template') |
1109 | + update_last_modified(`settings_json_schema') |
1110 | + update_last_modified(`list_settings_json') |
1111 | |
1112 | CREATE TRIGGER |
1113 | IF NOT EXISTS sort_topics_update_trigger |
1114 | diff --git a/core/tests/migrations.rs b/core/tests/migrations.rs |
1115 | index 841baf7..e05f9e7 100644 |
1116 | --- a/core/tests/migrations.rs |
1117 | +++ b/core/tests/migrations.rs |
1118 | @@ -144,6 +144,38 @@ impl ConnectionExt for rusqlite::Connection { |
1119 | } |
1120 | } |
1121 | |
1122 | + const FIRST_SCHEMA: &str = r#" |
1123 | + PRAGMA foreign_keys = true; |
1124 | + PRAGMA encoding = 'UTF-8'; |
1125 | + PRAGMA schema_version = 0; |
1126 | + |
1127 | + CREATE TABLE IF NOT EXISTS person ( |
1128 | + pk INTEGER PRIMARY KEY NOT NULL, |
1129 | + name TEXT, |
1130 | + address TEXT NOT NULL, |
1131 | + created INTEGER NOT NULL DEFAULT (unixepoch()), |
1132 | + last_modified INTEGER NOT NULL DEFAULT (unixepoch()) |
1133 | + ); |
1134 | + "#; |
1135 | + |
1136 | + const MIGRATIONS: &[(u32, &str, &str)] = &[ |
1137 | + ( |
1138 | + 1, |
1139 | + "ALTER TABLE PERSON ADD COLUMN interests TEXT;", |
1140 | + "ALTER TABLE PERSON DROP COLUMN interests;", |
1141 | + ), |
1142 | + ( |
1143 | + 2, |
1144 | + "CREATE TABLE hobby ( pk INTEGER PRIMARY KEY NOT NULL,title TEXT NOT NULL);", |
1145 | + "DROP TABLE hobby;", |
1146 | + ), |
1147 | + ( |
1148 | + 3, |
1149 | + "ALTER TABLE PERSON ADD COLUMN main_hobby INTEGER REFERENCES hobby(pk) ON DELETE SET NULL;", |
1150 | + "ALTER TABLE PERSON DROP COLUMN main_hobby;", |
1151 | + ), |
1152 | + ]; |
1153 | + |
1154 | #[test] |
1155 | fn test_migration_gen() { |
1156 | init_stderr_logging(); |
1157 | @@ -329,35 +361,3 @@ CREATE TABLE person ( |
1158 | ) |
1159 | ); |
1160 | } |
1161 | - |
1162 | - const FIRST_SCHEMA: &str = r#" |
1163 | - PRAGMA foreign_keys = true; |
1164 | - PRAGMA encoding = 'UTF-8'; |
1165 | - PRAGMA schema_version = 0; |
1166 | - |
1167 | - CREATE TABLE IF NOT EXISTS person ( |
1168 | - pk INTEGER PRIMARY KEY NOT NULL, |
1169 | - name TEXT, |
1170 | - address TEXT NOT NULL, |
1171 | - created INTEGER NOT NULL DEFAULT (unixepoch()), |
1172 | - last_modified INTEGER NOT NULL DEFAULT (unixepoch()) |
1173 | - ); |
1174 | - "#; |
1175 | - |
1176 | - const MIGRATIONS: &[(u32, &str, &str)] = &[ |
1177 | - ( |
1178 | - 1, |
1179 | - "ALTER TABLE PERSON ADD COLUMN interests TEXT;", |
1180 | - "ALTER TABLE PERSON DROP COLUMN interests;", |
1181 | - ), |
1182 | - ( |
1183 | - 2, |
1184 | - "CREATE TABLE hobby ( pk INTEGER PRIMARY KEY NOT NULL,title TEXT NOT NULL);", |
1185 | - "DROP TABLE hobby;", |
1186 | - ), |
1187 | - ( |
1188 | - 3, |
1189 | - "ALTER TABLE PERSON ADD COLUMN main_hobby INTEGER REFERENCES hobby(pk) ON DELETE SET NULL;", |
1190 | - "ALTER TABLE PERSON DROP COLUMN main_hobby;", |
1191 | - ), |
1192 | - ]; |
1193 | diff --git a/core/tests/settings_json.rs b/core/tests/settings_json.rs |
1194 | new file mode 100644 |
1195 | index 0000000..e1600b0 |
1196 | --- /dev/null |
1197 | +++ b/core/tests/settings_json.rs |
1198 | @@ -0,0 +1,178 @@ |
1199 | + /* |
1200 | + * This file is part of mailpot |
1201 | + * |
1202 | + * Copyright 2023 - Manos Pitsidianakis |
1203 | + * |
1204 | + * This program is free software: you can redistribute it and/or modify |
1205 | + * it under the terms of the GNU Affero General Public License as |
1206 | + * published by the Free Software Foundation, either version 3 of the |
1207 | + * License, or (at your option) any later version. |
1208 | + * |
1209 | + * This program is distributed in the hope that it will be useful, |
1210 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1211 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1212 | + * GNU Affero General Public License for more details. |
1213 | + * |
1214 | + * You should have received a copy of the GNU Affero General Public License |
1215 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
1216 | + */ |
1217 | + |
1218 | + use jsonschema::JSONSchema; |
1219 | + use mailpot::{Configuration, Connection, SendMail}; |
1220 | + use mailpot_tests::init_stderr_logging; |
1221 | + use serde_json::{json, Value}; |
1222 | + use tempfile::TempDir; |
1223 | + |
1224 | + #[test] |
1225 | + fn test_settings_json() { |
1226 | + init_stderr_logging(); |
1227 | + let tmp_dir = TempDir::new().unwrap(); |
1228 | + |
1229 | + let db_path = tmp_dir.path().join("mpot.db"); |
1230 | + std::fs::copy("../mailpot-tests/for_testing.db", &db_path).unwrap(); |
1231 | + let mut perms = std::fs::metadata(&db_path).unwrap().permissions(); |
1232 | + #[allow(clippy::permissions_set_readonly_false)] |
1233 | + perms.set_readonly(false); |
1234 | + std::fs::set_permissions(&db_path, perms).unwrap(); |
1235 | + |
1236 | + let config = Configuration { |
1237 | + send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
1238 | + db_path, |
1239 | + data_path: tmp_dir.path().to_path_buf(), |
1240 | + administrators: vec![], |
1241 | + }; |
1242 | + let db = Connection::open_or_create_db(config).unwrap().trusted(); |
1243 | + let list = db.lists().unwrap().remove(0); |
1244 | + |
1245 | + let archived_at_link_settings_schema = |
1246 | + std::fs::read_to_string("./settings_json_schemas/archivedatlink.json").unwrap(); |
1247 | + |
1248 | + println!("Testing that inserting settings works…"); |
1249 | + let (settings_pk, settings_val, last_modified): (i64, Value, i64) = { |
1250 | + let mut stmt = db |
1251 | + .connection |
1252 | + .prepare( |
1253 | + "INSERT INTO list_settings_json(name, list, value) \ |
1254 | + VALUES('ArchivedAtLinkSettings', ?, ?) RETURNING pk, value, last_modified;", |
1255 | + ) |
1256 | + .unwrap(); |
1257 | + stmt.query_row( |
1258 | + rusqlite::params![ |
1259 | + &list.pk(), |
1260 | + &json!({ |
1261 | + "template": "https://www.example.com/{msg-id}.html" |
1262 | + }), |
1263 | + ], |
1264 | + |row| { |
1265 | + let pk: i64 = row.get("pk")?; |
1266 | + let value: Value = row.get("value")?; |
1267 | + let last_modified: i64 = row.get("last_modified")?; |
1268 | + Ok((pk, value, last_modified)) |
1269 | + }, |
1270 | + ) |
1271 | + .unwrap() |
1272 | + }; |
1273 | + db.connection |
1274 | + .execute_batch("UPDATE list_settings_json SET is_valid = 1;") |
1275 | + .unwrap(); |
1276 | + |
1277 | + println!("Testing that schema is actually valid…"); |
1278 | + let schema: Value = serde_json::from_str(&archived_at_link_settings_schema).unwrap(); |
1279 | + let compiled = JSONSchema::compile(&schema).expect("A valid schema"); |
1280 | + if let Err(errors) = compiled.validate(&settings_val) { |
1281 | + for err in errors { |
1282 | + eprintln!("Error: {err}"); |
1283 | + } |
1284 | + panic!("Could not validate settings."); |
1285 | + }; |
1286 | + |
1287 | + println!("Testing that inserting invalid settings aborts…"); |
1288 | + { |
1289 | + let mut stmt = db |
1290 | + .connection |
1291 | + .prepare( |
1292 | + "INSERT OR REPLACE INTO list_settings_json(name, list, value) \ |
1293 | + VALUES('ArchivedAtLinkSettings', ?, ?) RETURNING pk, value;", |
1294 | + ) |
1295 | + .unwrap(); |
1296 | + assert_eq!( |
1297 | + "new settings value is not valid according to the json schema. Rolling back \ |
1298 | + transaction.", |
1299 | + &stmt |
1300 | + .query_row( |
1301 | + rusqlite::params![ |
1302 | + &list.pk(), |
1303 | + &json!({ |
1304 | + "template": "https://www.example.com/msg-id}.html" |
1305 | + }), |
1306 | + ], |
1307 | + |row| { |
1308 | + let pk: i64 = row.get("pk")?; |
1309 | + let value: Value = row.get("value")?; |
1310 | + Ok((pk, value)) |
1311 | + }, |
1312 | + ) |
1313 | + .unwrap_err() |
1314 | + .to_string() |
1315 | + ); |
1316 | + }; |
1317 | + |
1318 | + println!("Testing that updating settings with invalid value aborts…"); |
1319 | + { |
1320 | + let mut stmt = db |
1321 | + .connection |
1322 | + .prepare( |
1323 | + "UPDATE list_settings_json SET value = ? WHERE name = 'ArchivedAtLinkSettings' \ |
1324 | + RETURNING pk, value;", |
1325 | + ) |
1326 | + .unwrap(); |
1327 | + assert_eq!( |
1328 | + "new settings value is not valid according to the json schema. Rolling back \ |
1329 | + transaction.", |
1330 | + &stmt |
1331 | + .query_row( |
1332 | + rusqlite::params![&json!({ |
1333 | + "template": "https://www.example.com/msg-id}.html" |
1334 | + }),], |
1335 | + |row| { |
1336 | + let pk: i64 = row.get("pk")?; |
1337 | + let value: Value = row.get("value")?; |
1338 | + Ok((pk, value)) |
1339 | + }, |
1340 | + ) |
1341 | + .unwrap_err() |
1342 | + .to_string() |
1343 | + ); |
1344 | + }; |
1345 | + |
1346 | + std::thread::sleep(std::time::Duration::from_millis(1000)); |
1347 | + println!("Finally, testing that updating schema reverifies settings…"); |
1348 | + { |
1349 | + let mut stmt = db |
1350 | + .connection |
1351 | + .prepare( |
1352 | + "UPDATE settings_json_schema SET id = ? WHERE id = 'ArchivedAtLinkSettings' \ |
1353 | + RETURNING pk;", |
1354 | + ) |
1355 | + .unwrap(); |
1356 | + stmt.query_row([&"ArchivedAtLinkSettingsv2"], |_| Ok(())) |
1357 | + .unwrap(); |
1358 | + }; |
1359 | + let (new_name, is_valid, new_last_modified): (String, bool, i64) = { |
1360 | + let mut stmt = db |
1361 | + .connection |
1362 | + .prepare("SELECT name, is_valid, last_modified from list_settings_json WHERE pk = ?;") |
1363 | + .unwrap(); |
1364 | + stmt.query_row([&settings_pk], |row| { |
1365 | + Ok(( |
1366 | + row.get("name")?, |
1367 | + row.get("is_valid")?, |
1368 | + row.get("last_modified")?, |
1369 | + )) |
1370 | + }) |
1371 | + .unwrap() |
1372 | + }; |
1373 | + assert_eq!(&new_name, "ArchivedAtLinkSettingsv2"); |
1374 | + assert!(is_valid); |
1375 | + assert!(new_last_modified != last_modified); |
1376 | + } |
1377 | diff --git a/core/tools/generate_configuration_json_schema.py b/core/tools/generate_configuration_json_schema.py |
1378 | new file mode 100755 |
1379 | index 0000000..e12fae1 |
1380 | --- /dev/null |
1381 | +++ b/core/tools/generate_configuration_json_schema.py |
1382 | @@ -0,0 +1,52 @@ |
1383 | + #!/usr/bin/env python3 |
1384 | + """ |
1385 | + Example taken from https://jcristharif.com/msgspec/jsonschema.html |
1386 | + """ |
1387 | + import msgspec |
1388 | + from msgspec import Struct, Meta |
1389 | + from typing import Annotated, Optional |
1390 | + |
1391 | + Template = Annotated[ |
1392 | + str, |
1393 | + Meta( |
1394 | + pattern=".+[{]msg-id[}].*", |
1395 | + description="""Template for \ |
1396 | + `Archived-At` header value, as described in RFC 5064 "The Archived-At \ |
1397 | + Message Header Field". The template receives only one string variable \ |
1398 | + with the value of the mailing list post `Message-ID` header. |
1399 | + |
1400 | + For example, if: |
1401 | + |
1402 | + - the template is `http://www.example.com/mid/{msg-id}` |
1403 | + - the `Message-ID` is `<0C2U00F01DFGCR@mailsj-v3.example.com>` |
1404 | + |
1405 | + The full header will be generated as: |
1406 | + |
1407 | + `Archived-At: <http://www.example.com/mid/0C2U00F01DFGCR@mailsj-v3.example.com> |
1408 | + |
1409 | + Note: Surrounding carets in the `Message-ID` value are not required. If \ |
1410 | + you wish to preserve them in the URL, set option `preserve-carets` to \ |
1411 | + true.""", |
1412 | + title="Jinja template for header value", |
1413 | + examples=[ |
1414 | + "https://www.example.com/{msg-id}", |
1415 | + "https://www.example.com/{msg-id}.html", |
1416 | + ], |
1417 | + ), |
1418 | + ] |
1419 | + |
1420 | + PreserveCarets = Annotated[ |
1421 | + bool, Meta(title="Preserve carets of `Message-ID` in generated value") |
1422 | + ] |
1423 | + |
1424 | + |
1425 | + class ArchivedAtLinkSettings(Struct): |
1426 | + """Settings for ArchivedAtLink message filter""" |
1427 | + |
1428 | + template: Template |
1429 | + preserve_carets: PreserveCarets = False |
1430 | + |
1431 | + |
1432 | + schema = {"$schema": "http://json-schema.org/draft-07/schema"} |
1433 | + schema.update(msgspec.json.schema(ArchivedAtLinkSettings)) |
1434 | + print(msgspec.json.format(msgspec.json.encode(schema)).decode("utf-8")) |