+445 -103 +/-17 browse
1 | diff --git a/core/build.rs b/core/build.rs |
2 | index fd9dc55..f48cd5b 100644 |
3 | --- a/core/build.rs |
4 | +++ b/core/build.rs |
5 | @@ -18,31 +18,30 @@ |
6 | */ |
7 | |
8 | use std::{ |
9 | - fs::{metadata, OpenOptions}, |
10 | - io, |
11 | + fs::OpenOptions, |
12 | process::{Command, Stdio}, |
13 | }; |
14 | |
15 | - // Source: https://stackoverflow.com/a/64535181 |
16 | - fn is_output_file_outdated<P1, P2>(input: P1, output: P2) -> io::Result<bool> |
17 | - where |
18 | - P1: AsRef<Path>, |
19 | - P2: AsRef<Path>, |
20 | - { |
21 | - let out_meta = metadata(output); |
22 | - if let Ok(meta) = out_meta { |
23 | - let output_mtime = meta.modified()?; |
24 | - |
25 | - // if input file is more recent than our output, we are outdated |
26 | - let input_meta = metadata(input)?; |
27 | - let input_mtime = input_meta.modified()?; |
28 | - |
29 | - Ok(input_mtime > output_mtime) |
30 | - } else { |
31 | - // output file not found, we are outdated |
32 | - Ok(true) |
33 | - } |
34 | - } |
35 | + // // Source: https://stackoverflow.com/a/64535181 |
36 | + // fn is_output_file_outdated<P1, P2>(input: P1, output: P2) -> io::Result<bool> |
37 | + // where |
38 | + // P1: AsRef<Path>, |
39 | + // P2: AsRef<Path>, |
40 | + // { |
41 | + // let out_meta = metadata(output); |
42 | + // if let Ok(meta) = out_meta { |
43 | + // let output_mtime = meta.modified()?; |
44 | + // |
45 | + // // if input file is more recent than our output, we are outdated |
46 | + // let input_meta = metadata(input)?; |
47 | + // let input_mtime = input_meta.modified()?; |
48 | + // |
49 | + // Ok(input_mtime > output_mtime) |
50 | + // } else { |
51 | + // // output file not found, we are outdated |
52 | + // Ok(true) |
53 | + // } |
54 | + // } |
55 | |
56 | include!("make_migrations.rs"); |
57 | |
58 | diff --git a/core/make_migrations.rs b/core/make_migrations.rs |
59 | index fa21a77..011970a 100644 |
60 | --- a/core/make_migrations.rs |
61 | +++ b/core/make_migrations.rs |
62 | @@ -33,7 +33,6 @@ pub fn make_migrations<M: AsRef<Path>, O: AsRef<Path>>( |
63 | let migrations_folder_path = migrations_path.as_ref(); |
64 | let output_file_path = output_file.as_ref(); |
65 | |
66 | - let mut regen = false; |
67 | let mut paths = vec![]; |
68 | let mut undo_paths = vec![]; |
69 | for entry in read_dir(migrations_folder_path).unwrap() { |
70 | @@ -42,9 +41,6 @@ pub fn make_migrations<M: AsRef<Path>, O: AsRef<Path>>( |
71 | if path.is_dir() || path.extension().map(|os| os.to_str().unwrap()) != Some("sql") { |
72 | continue; |
73 | } |
74 | - if is_output_file_outdated(&path, output_file_path).unwrap() { |
75 | - regen = true; |
76 | - } |
77 | if path |
78 | .file_name() |
79 | .unwrap() |
80 | @@ -58,56 +54,54 @@ pub fn make_migrations<M: AsRef<Path>, O: AsRef<Path>>( |
81 | } |
82 | } |
83 | |
84 | - if regen { |
85 | - paths.sort(); |
86 | - undo_paths.sort(); |
87 | - let mut migr_rs = OpenOptions::new() |
88 | - .write(true) |
89 | - .create(true) |
90 | - .truncate(true) |
91 | - .open(output_file_path) |
92 | - .unwrap(); |
93 | - migr_rs |
94 | - .write_all(b"\n//(user_version, redo sql, undo sql\n&[") |
95 | - .unwrap(); |
96 | - for (i, (p, u)) in paths.iter().zip(undo_paths.iter()).enumerate() { |
97 | - // This should be a number string, padded with 2 zeros if it's less than 3 |
98 | - // digits. e.g. 001, \d{3} |
99 | - let mut num = p.file_stem().unwrap().to_str().unwrap(); |
100 | - let is_data = num.ends_with(".data"); |
101 | - if is_data { |
102 | - num = num.strip_suffix(".data").unwrap(); |
103 | - } |
104 | + paths.sort(); |
105 | + undo_paths.sort(); |
106 | + let mut migr_rs = OpenOptions::new() |
107 | + .write(true) |
108 | + .create(true) |
109 | + .truncate(true) |
110 | + .open(output_file_path) |
111 | + .unwrap(); |
112 | + migr_rs |
113 | + .write_all(b"\n//(user_version, redo sql, undo sql\n&[") |
114 | + .unwrap(); |
115 | + for (i, (p, u)) in paths.iter().zip(undo_paths.iter()).enumerate() { |
116 | + // This should be a number string, padded with 2 zeros if it's less than 3 |
117 | + // digits. e.g. 001, \d{3} |
118 | + let mut num = p.file_stem().unwrap().to_str().unwrap(); |
119 | + let is_data = num.ends_with(".data"); |
120 | + if is_data { |
121 | + num = num.strip_suffix(".data").unwrap(); |
122 | + } |
123 | |
124 | - if !u.file_name().unwrap().to_str().unwrap().starts_with(num) { |
125 | - panic!("Undo file {u:?} should match with {p:?}"); |
126 | - } |
127 | + if !u.file_name().unwrap().to_str().unwrap().starts_with(num) { |
128 | + panic!("Undo file {u:?} should match with {p:?}"); |
129 | + } |
130 | |
131 | - if num.parse::<u32>().is_err() { |
132 | - panic!("Migration file {p:?} should start with a number"); |
133 | - } |
134 | - assert_eq!(num.parse::<usize>().unwrap(), i + 1, "migration sql files should start with 1, not zero, and no intermediate numbers should be missing. Panicked on file: {}", p.display()); |
135 | - migr_rs.write_all(b"(").unwrap(); |
136 | - migr_rs |
137 | - .write_all(num.trim_start_matches('0').as_bytes()) |
138 | - .unwrap(); |
139 | - migr_rs.write_all(b",r###\"").unwrap(); |
140 | + if num.parse::<u32>().is_err() { |
141 | + panic!("Migration file {p:?} should start with a number"); |
142 | + } |
143 | + assert_eq!(num.parse::<usize>().unwrap(), i + 1, "migration sql files should start with 1, not zero, and no intermediate numbers should be missing. Panicked on file: {}", p.display()); |
144 | + migr_rs.write_all(b"(").unwrap(); |
145 | + migr_rs |
146 | + .write_all(num.trim_start_matches('0').as_bytes()) |
147 | + .unwrap(); |
148 | + migr_rs.write_all(b",r###\"").unwrap(); |
149 | |
150 | - let redo = std::fs::read_to_string(p).unwrap(); |
151 | - migr_rs.write_all(redo.trim().as_bytes()).unwrap(); |
152 | - migr_rs.write_all(b"\"###,r###\"").unwrap(); |
153 | - migr_rs |
154 | - .write_all(std::fs::read_to_string(u).unwrap().trim().as_bytes()) |
155 | - .unwrap(); |
156 | - migr_rs.write_all(b"\"###),").unwrap(); |
157 | - if is_data { |
158 | - schema_file.extend(b"\n\n-- ".iter()); |
159 | - schema_file.extend(num.as_bytes().iter()); |
160 | - schema_file.extend(b".data.sql\n\n".iter()); |
161 | - schema_file.extend(redo.into_bytes().into_iter()); |
162 | - } |
163 | + let redo = std::fs::read_to_string(p).unwrap(); |
164 | + migr_rs.write_all(redo.trim().as_bytes()).unwrap(); |
165 | + migr_rs.write_all(b"\"###,r###\"").unwrap(); |
166 | + migr_rs |
167 | + .write_all(std::fs::read_to_string(u).unwrap().trim().as_bytes()) |
168 | + .unwrap(); |
169 | + migr_rs.write_all(b"\"###),").unwrap(); |
170 | + if is_data { |
171 | + schema_file.extend(b"\n\n-- ".iter()); |
172 | + schema_file.extend(num.as_bytes().iter()); |
173 | + schema_file.extend(b".data.sql\n\n".iter()); |
174 | + schema_file.extend(redo.into_bytes().into_iter()); |
175 | } |
176 | - migr_rs.write_all(b"]").unwrap(); |
177 | - migr_rs.flush().unwrap(); |
178 | } |
179 | + migr_rs.write_all(b"]").unwrap(); |
180 | + migr_rs.flush().unwrap(); |
181 | } |
182 | diff --git a/core/migrations/006.data.sql b/core/migrations/006.data.sql |
183 | new file mode 100644 |
184 | index 0000000..a5741e0 |
185 | --- /dev/null |
186 | +++ b/core/migrations/006.data.sql |
187 | @@ -0,0 +1,20 @@ |
188 | + INSERT OR REPLACE INTO settings_json_schema(id, value) VALUES('AddSubjectTagPrefixSettings', '{ |
189 | + "$schema": "http://json-schema.org/draft-07/schema", |
190 | + "$ref": "#/$defs/AddSubjectTagPrefixSettings", |
191 | + "$defs": { |
192 | + "AddSubjectTagPrefixSettings": { |
193 | + "title": "AddSubjectTagPrefixSettings", |
194 | + "description": "Settings for AddSubjectTagPrefix message filter", |
195 | + "type": "object", |
196 | + "properties": { |
197 | + "enabled": { |
198 | + "title": "If true, the list subject prefix is added to post subjects.", |
199 | + "type": "boolean" |
200 | + } |
201 | + }, |
202 | + "required": [ |
203 | + "enabled" |
204 | + ] |
205 | + } |
206 | + } |
207 | + }'); |
208 | diff --git a/core/migrations/006.data.undo.sql b/core/migrations/006.data.undo.sql |
209 | new file mode 100644 |
210 | index 0000000..a805e53 |
211 | --- /dev/null |
212 | +++ b/core/migrations/006.data.undo.sql |
213 | @@ -0,0 +1 @@ |
214 | + DELETE FROM settings_json_schema WHERE id = 'AddSubjectTagPrefixSettings'; |
215 | diff --git a/core/settings_json_schemas/addsubjecttagprefix.json b/core/settings_json_schemas/addsubjecttagprefix.json |
216 | new file mode 100644 |
217 | index 0000000..4556b2b |
218 | --- /dev/null |
219 | +++ b/core/settings_json_schemas/addsubjecttagprefix.json |
220 | @@ -0,0 +1,20 @@ |
221 | + { |
222 | + "$schema": "http://json-schema.org/draft-07/schema", |
223 | + "$ref": "#/$defs/AddSubjectTagPrefixSettings", |
224 | + "$defs": { |
225 | + "AddSubjectTagPrefixSettings": { |
226 | + "title": "AddSubjectTagPrefixSettings", |
227 | + "description": "Settings for AddSubjectTagPrefix message filter", |
228 | + "type": "object", |
229 | + "properties": { |
230 | + "enabled": { |
231 | + "title": "If true, the list subject prefix is added to post subjects.", |
232 | + "type": "boolean" |
233 | + } |
234 | + }, |
235 | + "required": [ |
236 | + "enabled" |
237 | + ] |
238 | + } |
239 | + } |
240 | + } |
241 | diff --git a/core/src/mail.rs b/core/src/mail.rs |
242 | index c482c38..b33e715 100644 |
243 | --- a/core/src/mail.rs |
244 | +++ b/core/src/mail.rs |
245 | @@ -21,8 +21,10 @@ |
246 | //! [`PostFilter`](crate::message_filters::PostFilter), [`ListContext`], |
247 | //! [`MailJob`] and [`PostAction`]. |
248 | |
249 | + use std::collections::HashMap; |
250 | + |
251 | use log::trace; |
252 | - use melib::Address; |
253 | + use melib::{Address, MessageID}; |
254 | |
255 | use crate::{ |
256 | models::{ListOwner, ListSubscription, MailingList, PostPolicy, SubscriptionPolicy}, |
257 | @@ -65,6 +67,9 @@ pub struct ListContext<'list> { |
258 | /// The scheduled jobs added by each filter in a list's |
259 | /// [`PostFilter`](crate::message_filters::PostFilter) stack. |
260 | pub scheduled_jobs: Vec<MailJob>, |
261 | + /// Saved settings for message filters, which process a |
262 | + /// received e-mail before taking a final decision/action. |
263 | + pub filter_settings: HashMap<String, DbVal<serde_json::Value>>, |
264 | } |
265 | |
266 | /// Post to be considered by the list's |
267 | @@ -79,12 +84,15 @@ pub struct PostEntry { |
268 | /// Final action set by each filter in a list's |
269 | /// [`PostFilter`](crate::message_filters::PostFilter) stack. |
270 | pub action: PostAction, |
271 | + /// Post's Message-ID |
272 | + pub message_id: MessageID, |
273 | } |
274 | |
275 | impl core::fmt::Debug for PostEntry { |
276 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { |
277 | fmt.debug_struct(stringify!(PostEntry)) |
278 | .field("from", &self.from) |
279 | + .field("message_id", &self.message_id) |
280 | .field("bytes", &format_args!("{} bytes", self.bytes.len())) |
281 | .field("to", &self.to.as_slice()) |
282 | .field("action", &self.action) |
283 | diff --git a/core/src/message_filters.rs b/core/src/message_filters.rs |
284 | index 7da07c7..f30ce42 100644 |
285 | --- a/core/src/message_filters.rs |
286 | +++ b/core/src/message_filters.rs |
287 | @@ -38,13 +38,16 @@ |
288 | //! |
289 | //! so the processing stops at the first returned error. |
290 | |
291 | + mod settings; |
292 | use log::trace; |
293 | use melib::Address; |
294 | + use percent_encoding::utf8_percent_encode; |
295 | + pub use settings::*; |
296 | |
297 | use crate::{ |
298 | mail::{ListContext, MailJob, PostAction, PostEntry}, |
299 | models::{DbVal, MailingList}, |
300 | - Connection, |
301 | + Connection, StripCarets, PATH_SEGMENT, |
302 | }; |
303 | |
304 | impl Connection { |
305 | @@ -54,6 +57,7 @@ impl Connection { |
306 | Box::new(PostRightsCheck), |
307 | Box::new(FixCRLF), |
308 | Box::new(AddListHeaders), |
309 | + Box::new(ArchivedAtLink), |
310 | Box::new(AddSubjectTagPrefix), |
311 | Box::new(FinalizeRecipients), |
312 | ] |
313 | @@ -219,6 +223,18 @@ impl PostFilter for AddSubjectTagPrefix { |
314 | post: &'p mut PostEntry, |
315 | ctx: &'p mut ListContext<'list>, |
316 | ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> { |
317 | + if let Some(mut settings) = ctx.filter_settings.remove("AddSubjectTagPrefixSettings") { |
318 | + let map = settings.as_object_mut().unwrap(); |
319 | + let enabled = serde_json::from_value::<bool>(map.remove("enabled").unwrap()).unwrap(); |
320 | + if !enabled { |
321 | + trace!( |
322 | + "AddSubjectTagPrefix is disabled from settings found for list.pk = {} \ |
323 | + skipping filter", |
324 | + ctx.list.pk |
325 | + ); |
326 | + return Ok((post, ctx)); |
327 | + } |
328 | + } |
329 | trace!("Running AddSubjectTagPrefix filter"); |
330 | let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap(); |
331 | let mut subject; |
332 | @@ -264,7 +280,58 @@ impl PostFilter for ArchivedAtLink { |
333 | post: &'p mut PostEntry, |
334 | ctx: &'p mut ListContext<'list>, |
335 | ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> { |
336 | + let Some(mut settings) = ctx.filter_settings.remove("ArchivedAtLinkSettings") else { |
337 | + trace!("No ArchivedAtLink settings found for list.pk = {} skipping filter", ctx.list.pk); |
338 | + return Ok((post, ctx)); |
339 | + }; |
340 | trace!("Running ArchivedAtLink filter"); |
341 | + |
342 | + let map = settings.as_object_mut().unwrap(); |
343 | + let template = serde_json::from_value::<String>(map.remove("template").unwrap()).unwrap(); |
344 | + let preserve_carets = |
345 | + serde_json::from_value::<bool>(map.remove("preserve_carets").unwrap()).unwrap(); |
346 | + |
347 | + let env = minijinja::Environment::new(); |
348 | + let message_id = post.message_id.to_string(); |
349 | + let header_val = env |
350 | + .render_named_str( |
351 | + "ArchivedAtLinkSettings.template", |
352 | + &template, |
353 | + &if preserve_carets { |
354 | + minijinja::context! { |
355 | + msg_id => utf8_percent_encode(message_id.as_str(), PATH_SEGMENT).to_string() |
356 | + } |
357 | + } else { |
358 | + minijinja::context! { |
359 | + msg_id => utf8_percent_encode(message_id.as_str().strip_carets(), PATH_SEGMENT).to_string() |
360 | + } |
361 | + }, |
362 | + ) |
363 | + .map_err(|err| { |
364 | + log::error!("ArchivedAtLink: {}", err); |
365 | + })?; |
366 | + let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap(); |
367 | + headers.push((&b"Archived-At"[..], header_val.as_bytes())); |
368 | + |
369 | + let mut new_vec = Vec::with_capacity( |
370 | + headers |
371 | + .iter() |
372 | + .map(|(h, v)| h.len() + v.len() + ": \r\n".len()) |
373 | + .sum::<usize>() |
374 | + + "\r\n\r\n".len() |
375 | + + body.len(), |
376 | + ); |
377 | + for (h, v) in headers { |
378 | + new_vec.extend_from_slice(h); |
379 | + new_vec.extend_from_slice(b": "); |
380 | + new_vec.extend_from_slice(v); |
381 | + new_vec.extend_from_slice(b"\r\n"); |
382 | + } |
383 | + new_vec.extend_from_slice(b"\r\n\r\n"); |
384 | + new_vec.extend_from_slice(body); |
385 | + |
386 | + post.bytes = new_vec; |
387 | + |
388 | Ok((post, ctx)) |
389 | } |
390 | } |
391 | diff --git a/core/src/message_filters/settings.rs b/core/src/message_filters/settings.rs |
392 | new file mode 100644 |
393 | index 0000000..b28be1b |
394 | --- /dev/null |
395 | +++ b/core/src/message_filters/settings.rs |
396 | @@ -0,0 +1,44 @@ |
397 | + /* |
398 | + * This file is part of mailpot |
399 | + * |
400 | + * Copyright 2023 - Manos Pitsidianakis |
401 | + * |
402 | + * This program is free software: you can redistribute it and/or modify |
403 | + * it under the terms of the GNU Affero General Public License as |
404 | + * published by the Free Software Foundation, either version 3 of the |
405 | + * License, or (at your option) any later version. |
406 | + * |
407 | + * This program is distributed in the hope that it will be useful, |
408 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
409 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
410 | + * GNU Affero General Public License for more details. |
411 | + * |
412 | + * You should have received a copy of the GNU Affero General Public License |
413 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
414 | + */ |
415 | + |
416 | + //! Named templates, for generated e-mail like confirmations, alerts etc. |
417 | + //! |
418 | + //! Template database model: [`Template`]. |
419 | + |
420 | + use std::collections::HashMap; |
421 | + |
422 | + use serde_json::Value; |
423 | + |
424 | + use crate::{errors::*, Connection, DbVal}; |
425 | + |
426 | + impl Connection { |
427 | + /// Get json settings. |
428 | + pub fn get_settings(&self, list_pk: i64) -> Result<HashMap<String, DbVal<Value>>> { |
429 | + let mut stmt = self.connection.prepare( |
430 | + "SELECT pk, name, value FROM list_settings_json WHERE list = ? AND is_valid = 1;", |
431 | + )?; |
432 | + let iter = stmt.query_map(rusqlite::params![&list_pk], |row| { |
433 | + let pk: i64 = row.get("pk")?; |
434 | + let name: String = row.get("name")?; |
435 | + let value: Value = row.get("value")?; |
436 | + Ok((name, DbVal(value, pk))) |
437 | + })?; |
438 | + Ok(iter.collect::<std::result::Result<HashMap<String, DbVal<Value>>, rusqlite::Error>>()?) |
439 | + } |
440 | + } |
441 | diff --git a/core/src/migrations.rs.inc b/core/src/migrations.rs.inc |
442 | index adfccf3..d7e5d8a 100644 |
443 | --- a/core/src/migrations.rs.inc |
444 | +++ b/core/src/migrations.rs.inc |
445 | @@ -223,4 +223,23 @@ DROP TABLE list_settings_json;"###),(5,r###"INSERT OR REPLACE INTO settings_json |
446 | ] |
447 | } |
448 | } |
449 | - }');"###,r###"DELETE FROM settings_json_schema WHERE id = 'ArchivedAtLinkSettings';"###),] |
450 | \ No newline at end of file |
451 | + }');"###,r###"DELETE FROM settings_json_schema WHERE id = 'ArchivedAtLinkSettings';"###),(6,r###"INSERT OR REPLACE INTO settings_json_schema(id, value) VALUES('AddSubjectTagPrefixSettings', '{ |
452 | + "$schema": "http://json-schema.org/draft-07/schema", |
453 | + "$ref": "#/$defs/AddSubjectTagPrefixSettings", |
454 | + "$defs": { |
455 | + "AddSubjectTagPrefixSettings": { |
456 | + "title": "AddSubjectTagPrefixSettings", |
457 | + "description": "Settings for AddSubjectTagPrefix message filter", |
458 | + "type": "object", |
459 | + "properties": { |
460 | + "enabled": { |
461 | + "title": "If true, the list subject prefix is added to post subjects.", |
462 | + "type": "boolean" |
463 | + } |
464 | + }, |
465 | + "required": [ |
466 | + "enabled" |
467 | + ] |
468 | + } |
469 | + } |
470 | + }');"###,r###"DELETE FROM settings_json_schema WHERE id = 'AddSubjectTagPrefixSettings';"###),] |
471 | \ No newline at end of file |
472 | diff --git a/core/src/models.rs b/core/src/models.rs |
473 | index f3c1e1d..daa4575 100644 |
474 | --- a/core/src/models.rs |
475 | +++ b/core/src/models.rs |
476 | @@ -613,7 +613,7 @@ impl ListOwner { |
477 | } |
478 | |
479 | /// A mailing list post entry. |
480 | - #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] |
481 | + #[derive(Clone, Deserialize, Serialize, PartialEq, Eq)] |
482 | pub struct Post { |
483 | /// Database primary key. |
484 | pub pk: i64, |
485 | @@ -635,6 +635,22 @@ pub struct Post { |
486 | pub month_year: String, |
487 | } |
488 | |
489 | + impl std::fmt::Debug for Post { |
490 | + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { |
491 | + fmt.debug_struct(stringify!(Post)) |
492 | + .field("pk", &self.pk) |
493 | + .field("list", &self.list) |
494 | + .field("envelope_from", &self.envelope_from) |
495 | + .field("address", &self.address) |
496 | + .field("message_id", &self.message_id) |
497 | + .field("message", &String::from_utf8_lossy(&self.message)) |
498 | + .field("timestamp", &self.timestamp) |
499 | + .field("datetime", &self.datetime) |
500 | + .field("month_year", &self.month_year) |
501 | + .finish() |
502 | + } |
503 | + } |
504 | + |
505 | impl std::fmt::Display for Post { |
506 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { |
507 | write!(fmt, "{:?}", self) |
508 | diff --git a/core/src/posts.rs b/core/src/posts.rs |
509 | index 70eb7e1..fa00cfc 100644 |
510 | --- a/core/src/posts.rs |
511 | +++ b/core/src/posts.rs |
512 | @@ -181,11 +181,13 @@ impl Connection { |
513 | post_policy: self.list_post_policy(list.pk)?, |
514 | subscription_policy: self.list_subscription_policy(list.pk)?, |
515 | list_owners: &owners, |
516 | - list: &mut list, |
517 | subscriptions: &subscriptions, |
518 | scheduled_jobs: vec![], |
519 | + filter_settings: self.get_settings(list.pk)?, |
520 | + list: &mut list, |
521 | }; |
522 | let mut post = PostEntry { |
523 | + message_id: env.message_id().clone(), |
524 | from: env.from()[0].clone(), |
525 | bytes: raw.to_vec(), |
526 | to: env.to().to_vec(), |
527 | diff --git a/core/src/schema.sql b/core/src/schema.sql |
528 | index a983117..713d615 100644 |
529 | --- a/core/src/schema.sql |
530 | +++ b/core/src/schema.sql |
531 | @@ -554,3 +554,62 @@ FOR EACH ROW |
532 | BEGIN |
533 | UPDATE list SET topics = arr FROM (SELECT json_group_array(ord.val) AS arr, ord.pk AS pk FROM (SELECT json_each.value AS val, list.pk AS pk FROM list, json_each(list.topics) ORDER BY val ASC) AS ord GROUP BY pk) AS ord WHERE ord.pk = list.pk AND list.pk = NEW.pk; |
534 | END; |
535 | + |
536 | + |
537 | + -- 005.data.sql |
538 | + |
539 | + INSERT OR REPLACE INTO settings_json_schema(id, value) VALUES('ArchivedAtLinkSettings', '{ |
540 | + "$schema": "http://json-schema.org/draft-07/schema", |
541 | + "$ref": "#/$defs/ArchivedAtLinkSettings", |
542 | + "$defs": { |
543 | + "ArchivedAtLinkSettings": { |
544 | + "title": "ArchivedAtLinkSettings", |
545 | + "description": "Settings for ArchivedAtLink message filter", |
546 | + "type": "object", |
547 | + "properties": { |
548 | + "template": { |
549 | + "title": "Jinja template for header value", |
550 | + "description": "Template for\n `Archived-At` header value, as described in RFC 5064 \"The Archived-At\n Message Header Field\". The template receives only one string variable\n with the value of the mailing list post `Message-ID` header.\n\n For 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\n The full header will be generated as:\n\n `Archived-At: <http://www.example.com/mid/0C2U00F01DFGCR@mailsj-v3.example.com>\n\n Note: Surrounding carets in the `Message-ID` value are not required. If\n you wish to preserve them in the URL, set option `preserve-carets` to\n true.\n ", |
551 | + "examples": [ |
552 | + "https://www.example.com/{{msg_id}}", |
553 | + "https://www.example.com/{{msg_id}}.html" |
554 | + ], |
555 | + "type": "string", |
556 | + "pattern": ".+[{][{]msg_id[}][}].*" |
557 | + }, |
558 | + "preserve_carets": { |
559 | + "title": "Preserve carets of `Message-ID` in generated value", |
560 | + "type": "boolean", |
561 | + "default": false |
562 | + } |
563 | + }, |
564 | + "required": [ |
565 | + "template" |
566 | + ] |
567 | + } |
568 | + } |
569 | + }'); |
570 | + |
571 | + |
572 | + -- 006.data.sql |
573 | + |
574 | + INSERT OR REPLACE INTO settings_json_schema(id, value) VALUES('AddSubjectTagPrefixSettings', '{ |
575 | + "$schema": "http://json-schema.org/draft-07/schema", |
576 | + "$ref": "#/$defs/AddSubjectTagPrefixSettings", |
577 | + "$defs": { |
578 | + "AddSubjectTagPrefixSettings": { |
579 | + "title": "AddSubjectTagPrefixSettings", |
580 | + "description": "Settings for AddSubjectTagPrefix message filter", |
581 | + "type": "object", |
582 | + "properties": { |
583 | + "enabled": { |
584 | + "title": "If true, the list subject prefix is added to post subjects.", |
585 | + "type": "boolean" |
586 | + } |
587 | + }, |
588 | + "required": [ |
589 | + "enabled" |
590 | + ] |
591 | + } |
592 | + } |
593 | + }'); |
594 | diff --git a/core/tests/migrations.rs b/core/tests/migrations.rs |
595 | index 0c8fa9a..25fa17e 100644 |
596 | --- a/core/tests/migrations.rs |
597 | +++ b/core/tests/migrations.rs |
598 | @@ -17,32 +17,12 @@ |
599 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
600 | */ |
601 | |
602 | - use std::fs::{metadata, File, OpenOptions}; |
603 | + use std::fs::{File, OpenOptions}; |
604 | |
605 | use mailpot::{Configuration, Connection, SendMail}; |
606 | use mailpot_tests::init_stderr_logging; |
607 | use tempfile::TempDir; |
608 | |
609 | - // Source: https://stackoverflow.com/a/64535181 |
610 | - fn is_output_file_outdated<P1, P2>(input: P1, output: P2) -> std::io::Result<bool> |
611 | - where |
612 | - P1: AsRef<Path>, |
613 | - P2: AsRef<Path>, |
614 | - { |
615 | - let out_meta = metadata(output); |
616 | - if let Ok(meta) = out_meta { |
617 | - let output_mtime = meta.modified()?; |
618 | - |
619 | - // if input file is more recent than our output, we are outdated |
620 | - let input_meta = metadata(input)?; |
621 | - let input_mtime = input_meta.modified()?; |
622 | - |
623 | - Ok(input_mtime > output_mtime) |
624 | - } else { |
625 | - // output file not found, we are outdated |
626 | - Ok(true) |
627 | - } |
628 | - } |
629 | include!("../make_migrations.rs"); |
630 | |
631 | #[test] |
632 | diff --git a/core/tests/settings_json.rs b/core/tests/settings_json.rs |
633 | index e1600b0..ee79842 100644 |
634 | --- a/core/tests/settings_json.rs |
635 | +++ b/core/tests/settings_json.rs |
636 | @@ -60,7 +60,8 @@ fn test_settings_json() { |
637 | rusqlite::params![ |
638 | &list.pk(), |
639 | &json!({ |
640 | - "template": "https://www.example.com/{msg-id}.html" |
641 | + "template": "https://www.example.com/{{msg_id}}.html", |
642 | + "preserve_carets": false |
643 | }), |
644 | ], |
645 | |row| { |
646 | @@ -103,7 +104,7 @@ fn test_settings_json() { |
647 | rusqlite::params![ |
648 | &list.pk(), |
649 | &json!({ |
650 | - "template": "https://www.example.com/msg-id}.html" |
651 | + "template": "https://www.example.com/msg-id}.html" // should be msg_id |
652 | }), |
653 | ], |
654 | |row| { |
655 | @@ -132,7 +133,7 @@ fn test_settings_json() { |
656 | &stmt |
657 | .query_row( |
658 | rusqlite::params![&json!({ |
659 | - "template": "https://www.example.com/msg-id}.html" |
660 | + "template": "https://www.example.com/msg-id}.html" // should be msg_id |
661 | }),], |
662 | |row| { |
663 | let pk: i64 = row.get("pk")?; |
664 | diff --git a/core/tests/subscription.rs b/core/tests/subscription.rs |
665 | index c92081a..1f5468c 100644 |
666 | --- a/core/tests/subscription.rs |
667 | +++ b/core/tests/subscription.rs |
668 | @@ -19,6 +19,7 @@ |
669 | |
670 | use mailpot::{models::*, queue::Queue, Configuration, Connection, SendMail}; |
671 | use mailpot_tests::init_stderr_logging; |
672 | + use serde_json::json; |
673 | use tempfile::TempDir; |
674 | |
675 | #[test] |
676 | @@ -227,3 +228,103 @@ eT48L2h0bWw+ |
677 | } |
678 | } |
679 | } |
680 | + |
681 | + #[test] |
682 | + fn test_post_filters() { |
683 | + init_stderr_logging(); |
684 | + let tmp_dir = TempDir::new().unwrap(); |
685 | + |
686 | + let mut post_policy = PostPolicy { |
687 | + pk: -1, |
688 | + list: -1, |
689 | + announce_only: false, |
690 | + subscription_only: false, |
691 | + approval_needed: false, |
692 | + open: true, |
693 | + custom: false, |
694 | + }; |
695 | + let db_path = tmp_dir.path().join("mpot.db"); |
696 | + let config = Configuration { |
697 | + send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
698 | + db_path, |
699 | + data_path: tmp_dir.path().to_path_buf(), |
700 | + administrators: vec![], |
701 | + }; |
702 | + |
703 | + let db = Connection::open_or_create_db(config).unwrap().trusted(); |
704 | + let foo_chat = db |
705 | + .create_list(MailingList { |
706 | + pk: 0, |
707 | + name: "foobar chat".into(), |
708 | + id: "foo-chat".into(), |
709 | + address: "foo-chat@example.com".into(), |
710 | + description: None, |
711 | + topics: vec![], |
712 | + archive_url: None, |
713 | + }) |
714 | + .unwrap(); |
715 | + post_policy.list = foo_chat.pk(); |
716 | + db.add_subscription( |
717 | + foo_chat.pk(), |
718 | + ListSubscription { |
719 | + pk: -1, |
720 | + list: foo_chat.pk(), |
721 | + address: "user@example.com".into(), |
722 | + name: None, |
723 | + account: None, |
724 | + digest: false, |
725 | + enabled: true, |
726 | + verified: true, |
727 | + hide_address: false, |
728 | + receive_duplicates: true, |
729 | + receive_own_posts: true, |
730 | + receive_confirmation: false, |
731 | + }, |
732 | + ) |
733 | + .unwrap(); |
734 | + db.set_list_post_policy(post_policy).unwrap(); |
735 | + |
736 | + let post_bytes = b"From: Name <user@example.com> |
737 | + To: <foo-chat@example.com> |
738 | + Subject: This is a post |
739 | + Date: Thu, 29 Oct 2020 13:58:16 +0000 |
740 | + Message-ID: <abcdefgh@sator.example.com> |
741 | + Content-Language: en-US |
742 | + Content-Type: text/html |
743 | + Content-Transfer-Encoding: base64 |
744 | + MIME-Version: 1.0 |
745 | + |
746 | + PCFET0NUWVBFPjxodG1sPjxoZWFkPjx0aXRsZT5mb288L3RpdGxlPjwvaGVhZD48Ym9k |
747 | + eT48dGFibGUgY2xhc3M9ImZvbyI+PHRoZWFkPjx0cj48dGQ+Zm9vPC90ZD48L3RoZWFk |
748 | + Pjx0Ym9keT48dHI+PHRkPmZvbzE8L3RkPjwvdHI+PC90Ym9keT48L3RhYmxlPjwvYm9k |
749 | + eT48L2h0bWw+ |
750 | + "; |
751 | + let envelope = melib::Envelope::from_bytes(post_bytes, None).expect("Could not parse message"); |
752 | + db.post(&envelope, post_bytes, /* dry_run */ false).unwrap(); |
753 | + let q = db.queue(Queue::Out).unwrap(); |
754 | + assert_eq!(&q[0].subject, "[foo-chat] This is a post"); |
755 | + |
756 | + db.delete_from_queue(Queue::Out, vec![]).unwrap(); |
757 | + { |
758 | + let mut stmt = db |
759 | + .connection |
760 | + .prepare( |
761 | + "INSERT INTO list_settings_json(name, list, value) \ |
762 | + VALUES('AddSubjectTagPrefixSettings', ?, ?) RETURNING *;", |
763 | + ) |
764 | + .unwrap(); |
765 | + stmt.query_row( |
766 | + rusqlite::params![ |
767 | + &foo_chat.pk(), |
768 | + &json!({ |
769 | + "enabled": false |
770 | + }), |
771 | + ], |
772 | + |_| Ok(()), |
773 | + ) |
774 | + .unwrap(); |
775 | + } |
776 | + db.post(&envelope, post_bytes, /* dry_run */ false).unwrap(); |
777 | + let q = db.queue(Queue::Out).unwrap(); |
778 | + assert_eq!(&q[0].subject, "This is a post"); |
779 | + } |
780 | diff --git a/web/src/main.rs b/web/src/main.rs |
781 | index 6cdb656..45520a4 100644 |
782 | --- a/web/src/main.rs |
783 | +++ b/web/src/main.rs |
784 | @@ -359,7 +359,7 @@ mod tests { |
785 | assert_eq!( |
786 | res.headers().get(http::header::CONTENT_DISPOSITION), |
787 | Some(&http::HeaderValue::from_static( |
788 | - "attachment; filename=\"<abcdefgh@sator.example.com>.eml\"" |
789 | + "attachment; filename=\"abcdefgh@sator.example.com.eml\"" |
790 | )), |
791 | ); |
792 | } |
793 | diff --git a/web/src/templates/css.html b/web/src/templates/css.html |
794 | index af92ddc..f3bd033 100644 |
795 | --- a/web/src/templates/css.html |
796 | +++ b/web/src/templates/css.html |
797 | @@ -1050,4 +1050,15 @@ |
798 | text-decoration: none; |
799 | color: inherit; |
800 | } |
801 | + |
802 | + blockquote { |
803 | + margin-inline: 0 var(--gap); |
804 | + padding-inline: var(--gap) 0; |
805 | + margin-block: var(--gap); |
806 | + font-size: 1.1em; |
807 | + line-height: var(--rhythm); |
808 | + font-style: italic; |
809 | + border-inline-start: 1px solid var(--graphical-fg); |
810 | + color: var(--muted-fg); |
811 | + } |
812 | </style> |