+388 -57 +/-34 browse
1 | diff --git a/README.md b/README.md |
2 | index 4878d78..e0f7fca 100644 |
3 | --- a/README.md |
4 | +++ b/README.md |
5 | @@ -228,6 +228,7 @@ let list_pk = db.create_list(MailingList { |
6 | id: "foo-chat".into(), |
7 | address: "foo-chat@example.com".into(), |
8 | description: None, |
9 | + topics: vec![], |
10 | archive_url: None, |
11 | })?.pk; |
12 | |
13 | diff --git a/archive-http/src/utils.rs b/archive-http/src/utils.rs |
14 | index 7e2597b..71905b5 100644 |
15 | --- a/archive-http/src/utils.rs |
16 | +++ b/archive-http/src/utils.rs |
17 | @@ -57,6 +57,7 @@ pub struct MailingList { |
18 | pub id: String, |
19 | pub address: String, |
20 | pub description: Option<String>, |
21 | + pub topics: Vec<String>, |
22 | pub archive_url: Option<String>, |
23 | pub inner: DbVal<mailpot::models::MailingList>, |
24 | } |
25 | @@ -70,6 +71,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList { |
26 | id, |
27 | address, |
28 | description, |
29 | + topics, |
30 | archive_url, |
31 | }, |
32 | _, |
33 | @@ -81,6 +83,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList { |
34 | id, |
35 | address, |
36 | description, |
37 | + topics, |
38 | archive_url, |
39 | inner: val, |
40 | } |
41 | @@ -127,13 +130,24 @@ impl minijinja::value::StructObject for MailingList { |
42 | "id" => Some(Value::from_serializable(&self.id)), |
43 | "address" => Some(Value::from_serializable(&self.address)), |
44 | "description" => Some(Value::from_serializable(&self.description)), |
45 | + "topics" => Some(Value::from_serializable(&self.topics)), |
46 | "archive_url" => Some(Value::from_serializable(&self.archive_url)), |
47 | _ => None, |
48 | } |
49 | } |
50 | |
51 | fn static_fields(&self) -> Option<&'static [&'static str]> { |
52 | - Some(&["pk", "name", "id", "address", "description", "archive_url"][..]) |
53 | + Some( |
54 | + &[ |
55 | + "pk", |
56 | + "name", |
57 | + "id", |
58 | + "address", |
59 | + "description", |
60 | + "topics", |
61 | + "archive_url", |
62 | + ][..], |
63 | + ) |
64 | } |
65 | } |
66 | |
67 | diff --git a/cli/Cargo.toml b/cli/Cargo.toml |
68 | index 54a3638..49224df 100644 |
69 | --- a/cli/Cargo.toml |
70 | +++ b/cli/Cargo.toml |
71 | @@ -34,4 +34,5 @@ tempfile = "3.3" |
72 | [build-dependencies] |
73 | clap = { version = "^4.2", default-features = false, features = ["derive", "cargo", "unicode", "wrap_help", "help", "usage", "error-context", "suggestions"] } |
74 | clap_mangen = "0.2.10" |
75 | + mailpot = { version = "^0.1", path = "../core" } |
76 | stderrlog = "^0.5" |
77 | diff --git a/cli/src/args.rs b/cli/src/args.rs |
78 | index d3f79d9..5cc26e8 100644 |
79 | --- a/cli/src/args.rs |
80 | +++ b/cli/src/args.rs |
81 | @@ -19,7 +19,7 @@ |
82 | |
83 | pub use std::path::PathBuf; |
84 | |
85 | - pub use clap::{Args, CommandFactory, Parser, Subcommand}; |
86 | + pub use clap::{builder::TypedValueParser, Args, CommandFactory, Parser, Subcommand}; |
87 | |
88 | #[derive(Debug, Parser)] |
89 | #[command( |
90 | @@ -105,7 +105,14 @@ pub enum Command { |
91 | /// Mail that has not been handled properly end up in the error queue. |
92 | ErrorQueue { |
93 | #[command(subcommand)] |
94 | - cmd: ErrorQueueCommand, |
95 | + cmd: QueueCommand, |
96 | + }, |
97 | + /// Mail that has not been handled properly end up in the error queue. |
98 | + Queue { |
99 | + #[arg(long, value_parser = QueueValueParser)] |
100 | + queue: mailpot::queue::Queue, |
101 | + #[command(subcommand)] |
102 | + cmd: QueueCommand, |
103 | }, |
104 | /// Import a maildir folder into an existing list. |
105 | ImportMaildir { |
106 | @@ -254,7 +261,7 @@ pub struct PostfixConfig { |
107 | } |
108 | |
109 | #[derive(Debug, Subcommand)] |
110 | - pub enum ErrorQueueCommand { |
111 | + pub enum QueueCommand { |
112 | /// List. |
113 | List, |
114 | /// Print entry in RFC5322 or JSON format. |
115 | @@ -344,7 +351,7 @@ pub enum ListCommand { |
116 | subscription_options: SubscriptionOptions, |
117 | }, |
118 | /// Add a new post policy. |
119 | - AddPolicy { |
120 | + AddPostPolicy { |
121 | #[arg(long)] |
122 | /// Only list owners can post. |
123 | announce_only: bool, |
124 | @@ -363,13 +370,13 @@ pub enum ListCommand { |
125 | custom: bool, |
126 | }, |
127 | // Remove post policy. |
128 | - RemovePolicy { |
129 | + RemovePostPolicy { |
130 | #[arg(long)] |
131 | /// Post policy primary key. |
132 | pk: i64, |
133 | }, |
134 | /// Add subscription policy to list. |
135 | - AddSubscribePolicy { |
136 | + AddSubscriptionPolicy { |
137 | #[arg(long)] |
138 | /// Send confirmation e-mail when subscription is finalized. |
139 | send_confirmation: bool, |
140 | @@ -386,9 +393,9 @@ pub enum ListCommand { |
141 | /// Allow subscriptions, but handle it manually. |
142 | custom: bool, |
143 | }, |
144 | - RemoveSubscribePolicy { |
145 | + RemoveSubscriptionPolicy { |
146 | #[arg(long)] |
147 | - /// Subscribe policy primary key. |
148 | + /// Subscription policy primary key. |
149 | pk: i64, |
150 | }, |
151 | /// Add list owner to list. |
152 | @@ -494,3 +501,57 @@ pub enum ListCommand { |
153 | skip_owners: bool, |
154 | }, |
155 | } |
156 | + |
157 | + #[derive(Clone, Copy, Debug)] |
158 | + pub struct QueueValueParser; |
159 | + |
160 | + impl QueueValueParser { |
161 | + /// Implementation for [`ValueParser::path_buf`] |
162 | + pub fn new() -> Self { |
163 | + Self |
164 | + } |
165 | + } |
166 | + |
167 | + impl TypedValueParser for QueueValueParser { |
168 | + type Value = mailpot::queue::Queue; |
169 | + |
170 | + fn parse_ref( |
171 | + &self, |
172 | + cmd: &clap::Command, |
173 | + arg: Option<&clap::Arg>, |
174 | + value: &std::ffi::OsStr, |
175 | + ) -> std::result::Result<Self::Value, clap::Error> { |
176 | + TypedValueParser::parse(self, cmd, arg, value.to_owned()) |
177 | + } |
178 | + |
179 | + fn parse( |
180 | + &self, |
181 | + cmd: &clap::Command, |
182 | + _arg: Option<&clap::Arg>, |
183 | + value: std::ffi::OsString, |
184 | + ) -> std::result::Result<Self::Value, clap::Error> { |
185 | + use std::str::FromStr; |
186 | + |
187 | + use clap::error::ErrorKind; |
188 | + |
189 | + if value.is_empty() { |
190 | + return Err(cmd.clone().error( |
191 | + ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand, |
192 | + "queue value required", |
193 | + )); |
194 | + } |
195 | + Self::Value::from_str(value.to_str().ok_or_else(|| { |
196 | + cmd.clone().error( |
197 | + ErrorKind::InvalidValue, |
198 | + "Queue value is not an UTF-8 string", |
199 | + ) |
200 | + })?) |
201 | + .map_err(|err| cmd.clone().error(ErrorKind::InvalidValue, err)) |
202 | + } |
203 | + } |
204 | + |
205 | + impl Default for QueueValueParser { |
206 | + fn default() -> Self { |
207 | + Self::new() |
208 | + } |
209 | + } |
210 | diff --git a/cli/src/lints.rs b/cli/src/lints.rs |
211 | index 68b118f..f4771ba 100644 |
212 | --- a/cli/src/lints.rs |
213 | +++ b/cli/src/lints.rs |
214 | @@ -228,6 +228,7 @@ pub fn warn_list_no_owner_lint(db: &mut Connection, _: bool) -> Result<()> { |
215 | id: row.get("id")?, |
216 | address: row.get("address")?, |
217 | description: row.get("description")?, |
218 | + topics: vec![], |
219 | archive_url: row.get("archive_url")?, |
220 | }, |
221 | pk, |
222 | diff --git a/cli/src/main.rs b/cli/src/main.rs |
223 | index 26d0c3a..e3e71fe 100644 |
224 | --- a/cli/src/main.rs |
225 | +++ b/cli/src/main.rs |
226 | @@ -29,7 +29,7 @@ use lints::*; |
227 | use mailpot::{ |
228 | melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash}, |
229 | models::{changesets::*, *}, |
230 | - queue::{Queue, QueueEntry}, |
231 | + queue::QueueEntry, |
232 | transaction::TransactionBehavior, |
233 | Configuration, Connection, Error, ErrorKind, Result, *, |
234 | }; |
235 | @@ -127,9 +127,14 @@ fn run_app(opt: Opt) -> Result<()> { |
236 | } |
237 | } |
238 | if let Some(s) = db.list_post_policy(l.pk)? { |
239 | - println!("\tList policy: {}", s); |
240 | + println!("\tPost policy: {}", s); |
241 | } else { |
242 | - println!("\tList policy: None"); |
243 | + println!("\tPost policy: None"); |
244 | + } |
245 | + if let Some(s) = db.list_subscription_policy(l.pk)? { |
246 | + println!("\tSubscription policy: {}", s); |
247 | + } else { |
248 | + println!("\tSubscription policy: None"); |
249 | } |
250 | println!(); |
251 | } |
252 | @@ -299,7 +304,7 @@ fn run_app(opt: Opt) -> Result<()> { |
253 | }; |
254 | db.update_subscription(changeset)?; |
255 | } |
256 | - AddPolicy { |
257 | + AddPostPolicy { |
258 | announce_only, |
259 | subscription_only, |
260 | approval_needed, |
261 | @@ -318,11 +323,11 @@ fn run_app(opt: Opt) -> Result<()> { |
262 | let new_val = db.set_list_post_policy(policy)?; |
263 | println!("Added new policy with pk = {}", new_val.pk()); |
264 | } |
265 | - RemovePolicy { pk } => { |
266 | + RemovePostPolicy { pk } => { |
267 | db.remove_list_post_policy(list.pk, pk)?; |
268 | println!("Removed policy with pk = {}", pk); |
269 | } |
270 | - AddSubscribePolicy { |
271 | + AddSubscriptionPolicy { |
272 | send_confirmation, |
273 | open, |
274 | manual, |
275 | @@ -341,7 +346,7 @@ fn run_app(opt: Opt) -> Result<()> { |
276 | let new_val = db.set_list_subscription_policy(policy)?; |
277 | println!("Added new subscribe policy with pk = {}", new_val.pk()); |
278 | } |
279 | - RemoveSubscribePolicy { pk } => { |
280 | + RemoveSubscriptionPolicy { pk } => { |
281 | db.remove_list_subscription_policy(list.pk, pk)?; |
282 | println!("Removed subscribe policy with pk = {}", pk); |
283 | } |
284 | @@ -491,6 +496,7 @@ fn run_app(opt: Opt) -> Result<()> { |
285 | name, |
286 | id, |
287 | description, |
288 | + topics: vec![], |
289 | address, |
290 | archive_url, |
291 | })?; |
292 | @@ -535,17 +541,17 @@ fn run_app(opt: Opt) -> Result<()> { |
293 | let tx = db.transaction(TransactionBehavior::Exclusive).unwrap(); |
294 | let messages = if opt.debug { |
295 | println!("flush-queue dry_run {:?}", dry_run); |
296 | - tx.queue(Queue::Out)? |
297 | + tx.queue(mailpot::queue::Queue::Out)? |
298 | .into_iter() |
299 | .map(DbVal::into_inner) |
300 | .chain( |
301 | - tx.queue(Queue::Deferred)? |
302 | + tx.queue(mailpot::queue::Queue::Deferred)? |
303 | .into_iter() |
304 | .map(DbVal::into_inner), |
305 | ) |
306 | .collect() |
307 | } else { |
308 | - tx.delete_from_queue(Queue::Out, vec![])? |
309 | + tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])? |
310 | }; |
311 | if opt.verbose > 0 || opt.debug { |
312 | println!("Queue out has {} messages.", messages.len()); |
313 | @@ -614,15 +620,15 @@ fn run_app(opt: Opt) -> Result<()> { |
314 | for (err, mut msg) in failures { |
315 | log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue."); |
316 | |
317 | - msg.queue = Queue::Deferred; |
318 | + msg.queue = mailpot::queue::Queue::Deferred; |
319 | tx.insert_to_queue(msg)?; |
320 | } |
321 | |
322 | tx.commit()?; |
323 | } |
324 | ErrorQueue { cmd } => match cmd { |
325 | - ErrorQueueCommand::List => { |
326 | - let errors = db.queue(Queue::Error)?; |
327 | + QueueCommand::List => { |
328 | + let errors = db.queue(mailpot::queue::Queue::Error)?; |
329 | if errors.is_empty() { |
330 | println!("Error queue is empty."); |
331 | } else { |
332 | @@ -634,8 +640,8 @@ fn run_app(opt: Opt) -> Result<()> { |
333 | } |
334 | } |
335 | } |
336 | - ErrorQueueCommand::Print { index } => { |
337 | - let mut errors = db.queue(Queue::Error)?; |
338 | + QueueCommand::Print { index } => { |
339 | + let mut errors = db.queue(mailpot::queue::Queue::Error)?; |
340 | if !index.is_empty() { |
341 | errors.retain(|el| index.contains(&el.pk())); |
342 | } |
343 | @@ -647,8 +653,8 @@ fn run_app(opt: Opt) -> Result<()> { |
344 | } |
345 | } |
346 | } |
347 | - ErrorQueueCommand::Delete { index, quiet } => { |
348 | - let mut errors = db.queue(Queue::Error)?; |
349 | + QueueCommand::Delete { index, quiet } => { |
350 | + let mut errors = db.queue(mailpot::queue::Queue::Error)?; |
351 | if !index.is_empty() { |
352 | errors.retain(|el| index.contains(&el.pk())); |
353 | } |
354 | @@ -660,7 +666,7 @@ fn run_app(opt: Opt) -> Result<()> { |
355 | if !quiet { |
356 | println!("Deleting error queue elements {:?}", &index); |
357 | } |
358 | - db.delete_from_queue(Queue::Error, index)?; |
359 | + db.delete_from_queue(mailpot::queue::Queue::Error, index)?; |
360 | if !quiet { |
361 | for e in errors { |
362 | println!("{e:?}"); |
363 | @@ -669,6 +675,55 @@ fn run_app(opt: Opt) -> Result<()> { |
364 | } |
365 | } |
366 | }, |
367 | + Queue { queue, cmd } => match cmd { |
368 | + QueueCommand::List => { |
369 | + let entries = db.queue(queue)?; |
370 | + if entries.is_empty() { |
371 | + println!("Queue {queue} is empty."); |
372 | + } else { |
373 | + for e in entries { |
374 | + println!( |
375 | + "- {} {} {} {} {}", |
376 | + e.pk, e.datetime, e.from_address, e.to_addresses, e.subject |
377 | + ); |
378 | + } |
379 | + } |
380 | + } |
381 | + QueueCommand::Print { index } => { |
382 | + let mut entries = db.queue(queue)?; |
383 | + if !index.is_empty() { |
384 | + entries.retain(|el| index.contains(&el.pk())); |
385 | + } |
386 | + if entries.is_empty() { |
387 | + println!("Queue {queue} is empty."); |
388 | + } else { |
389 | + for e in entries { |
390 | + println!("{e:?}"); |
391 | + } |
392 | + } |
393 | + } |
394 | + QueueCommand::Delete { index, quiet } => { |
395 | + let mut entries = db.queue(queue)?; |
396 | + if !index.is_empty() { |
397 | + entries.retain(|el| index.contains(&el.pk())); |
398 | + } |
399 | + if entries.is_empty() { |
400 | + if !quiet { |
401 | + println!("Queue {queue} is empty."); |
402 | + } |
403 | + } else { |
404 | + if !quiet { |
405 | + println!("Deleting queue {queue} elements {:?}", &index); |
406 | + } |
407 | + db.delete_from_queue(queue, index)?; |
408 | + if !quiet { |
409 | + for e in entries { |
410 | + println!("{e:?}"); |
411 | + } |
412 | + } |
413 | + } |
414 | + } |
415 | + }, |
416 | ImportMaildir { |
417 | list_id, |
418 | mut maildir_path, |
419 | diff --git a/cli/tests/basic_interfaces.rs b/cli/tests/basic_interfaces.rs |
420 | index 16a83c0..903d502 100644 |
421 | --- a/cli/tests/basic_interfaces.rs |
422 | +++ b/cli/tests/basic_interfaces.rs |
423 | @@ -132,6 +132,7 @@ For more information, try '--help'."#, |
424 | name: "foobar chat".into(), |
425 | id: "foo-chat".into(), |
426 | address: "foo-chat@example.com".into(), |
427 | + topics: vec![], |
428 | description: None, |
429 | archive_url: None, |
430 | }) |
431 | @@ -142,8 +143,8 @@ For more information, try '--help'."#, |
432 | list_lists( |
433 | &conf_path, |
434 | "- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \ |
435 | - \"foo-chat@example.com\", description: None, archive_url: None }, 1)\n\tList owners: \ |
436 | - None\n\tList policy: None", |
437 | + \"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \ |
438 | + owners: None\n\tPost policy: None\n\tSubscription policy: None", |
439 | ); |
440 | |
441 | fn create_list(conf: &Path) { |
442 | @@ -171,9 +172,10 @@ For more information, try '--help'."#, |
443 | list_lists( |
444 | &conf_path, |
445 | "- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \ |
446 | - \"foo-chat@example.com\", description: None, archive_url: None }, 1)\n\tList owners: \ |
447 | - None\n\tList policy: None\n\n- twobar-chat DbVal(MailingList { pk: 2, name: \"twobar\", \ |
448 | - id: \"twobar-chat\", address: \"twobar-chat@example.com\", description: None, \ |
449 | - archive_url: None }, 2)\n\tList owners: None\n\tList policy: None", |
450 | + \"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \ |
451 | + owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \ |
452 | + DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \ |
453 | + \"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \ |
454 | + 2)\n\tList owners: None\n\tPost policy: None\n\tSubscription policy: None", |
455 | ); |
456 | } |
457 | diff --git a/cli/tests/out_queue_flush.rs b/cli/tests/out_queue_flush.rs |
458 | index e962fd2..87a381b 100644 |
459 | --- a/cli/tests/out_queue_flush.rs |
460 | +++ b/cli/tests/out_queue_flush.rs |
461 | @@ -85,6 +85,7 @@ fn test_out_queue_flush() { |
462 | id: "foo-chat".into(), |
463 | address: "foo-chat@example.com".into(), |
464 | description: None, |
465 | + topics: vec![], |
466 | archive_url: None, |
467 | }) |
468 | .unwrap(); |
469 | @@ -282,6 +283,7 @@ fn test_list_requests_submission() { |
470 | id: "foo-chat".into(), |
471 | address: "foo-chat@example.com".into(), |
472 | description: None, |
473 | + topics: vec![], |
474 | archive_url: None, |
475 | }) |
476 | .unwrap(); |
477 | diff --git a/core/migrations/001.sql b/core/migrations/001.sql |
478 | index a62617c..345a376 100644 |
479 | --- a/core/migrations/001.sql |
480 | +++ b/core/migrations/001.sql |
481 | @@ -1,4 +1,2 @@ |
482 | PRAGMA foreign_keys=ON; |
483 | - BEGIN; |
484 | ALTER TABLE templates RENAME TO template; |
485 | - COMMIT; |
486 | diff --git a/core/migrations/001.undo.sql b/core/migrations/001.undo.sql |
487 | index 86fe8ac..e0e03fb 100644 |
488 | --- a/core/migrations/001.undo.sql |
489 | +++ b/core/migrations/001.undo.sql |
490 | @@ -1,4 +1,2 @@ |
491 | PRAGMA foreign_keys=ON; |
492 | - BEGIN; |
493 | ALTER TABLE template RENAME TO templates; |
494 | - COMMIT; |
495 | diff --git a/core/migrations/002.sql b/core/migrations/002.sql |
496 | new file mode 100644 |
497 | index 0000000..7dbb83a |
498 | --- /dev/null |
499 | +++ b/core/migrations/002.sql |
500 | @@ -0,0 +1,2 @@ |
501 | + PRAGMA foreign_keys=ON; |
502 | + ALTER TABLE list ADD COLUMN topics JSON NOT NULL CHECK (json_type(topics) == 'array') DEFAULT '[]'; |
503 | diff --git a/core/migrations/002.undo.sql b/core/migrations/002.undo.sql |
504 | new file mode 100644 |
505 | index 0000000..9a18755 |
506 | --- /dev/null |
507 | +++ b/core/migrations/002.undo.sql |
508 | @@ -0,0 +1,2 @@ |
509 | + PRAGMA foreign_keys=ON; |
510 | + ALTER TABLE list DROP COLUMN topics; |
511 | diff --git a/core/src/connection.rs b/core/src/connection.rs |
512 | index aa1866e..05ad84c 100644 |
513 | --- a/core/src/connection.rs |
514 | +++ b/core/src/connection.rs |
515 | @@ -195,7 +195,7 @@ impl Connection { |
516 | conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_FKEY, true)?; |
517 | conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, true)?; |
518 | conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?; |
519 | - conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, false)?; |
520 | + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, true)?; |
521 | conn.busy_timeout(core::time::Duration::from_millis(500))?; |
522 | conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?; |
523 | |
524 | @@ -246,14 +246,14 @@ impl Connection { |
525 | if undo { "un " } else { "re" } |
526 | ); |
527 | if undo { |
528 | - trace!("{}", Self::MIGRATIONS[from as usize].2); |
529 | + trace!("{}", Self::MIGRATIONS[from as usize - 1].2); |
530 | tx.connection |
531 | - .execute(Self::MIGRATIONS[from as usize].2, [])?; |
532 | + .execute_batch(Self::MIGRATIONS[from as usize - 1].2)?; |
533 | from -= 1; |
534 | } else { |
535 | trace!("{}", Self::MIGRATIONS[from as usize].1); |
536 | tx.connection |
537 | - .execute(Self::MIGRATIONS[from as usize].1, [])?; |
538 | + .execute_batch(Self::MIGRATIONS[from as usize].1)?; |
539 | from += 1; |
540 | } |
541 | } |
542 | @@ -384,6 +384,8 @@ impl Connection { |
543 | let mut stmt = self.connection.prepare("SELECT * FROM list;")?; |
544 | let list_iter = stmt.query_map([], |row| { |
545 | let pk = row.get("pk")?; |
546 | + let topics: serde_json::Value = row.get("topics")?; |
547 | + let topics = MailingList::topics_from_json_value(topics)?; |
548 | Ok(DbVal( |
549 | MailingList { |
550 | pk, |
551 | @@ -391,6 +393,7 @@ impl Connection { |
552 | id: row.get("id")?, |
553 | address: row.get("address")?, |
554 | description: row.get("description")?, |
555 | + topics, |
556 | archive_url: row.get("archive_url")?, |
557 | }, |
558 | pk, |
559 | @@ -413,6 +416,8 @@ impl Connection { |
560 | let ret = stmt |
561 | .query_row([&pk], |row| { |
562 | let pk = row.get("pk")?; |
563 | + let topics: serde_json::Value = row.get("topics")?; |
564 | + let topics = MailingList::topics_from_json_value(topics)?; |
565 | Ok(DbVal( |
566 | MailingList { |
567 | pk, |
568 | @@ -420,6 +425,7 @@ impl Connection { |
569 | id: row.get("id")?, |
570 | address: row.get("address")?, |
571 | description: row.get("description")?, |
572 | + topics, |
573 | archive_url: row.get("archive_url")?, |
574 | }, |
575 | pk, |
576 | @@ -438,6 +444,8 @@ impl Connection { |
577 | let ret = stmt |
578 | .query_row([&id], |row| { |
579 | let pk = row.get("pk")?; |
580 | + let topics: serde_json::Value = row.get("topics")?; |
581 | + let topics = MailingList::topics_from_json_value(topics)?; |
582 | Ok(DbVal( |
583 | MailingList { |
584 | pk, |
585 | @@ -445,6 +453,7 @@ impl Connection { |
586 | id: row.get("id")?, |
587 | address: row.get("address")?, |
588 | description: row.get("description")?, |
589 | + topics, |
590 | archive_url: row.get("archive_url")?, |
591 | }, |
592 | pk, |
593 | @@ -458,8 +467,8 @@ impl Connection { |
594 | /// Create a new list. |
595 | pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> { |
596 | let mut stmt = self.connection.prepare( |
597 | - "INSERT INTO list(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) \ |
598 | - RETURNING *;", |
599 | + "INSERT INTO list(name, id, address, description, archive_url, topics) VALUES(?, ?, \ |
600 | + ?, ?, ?, ?) RETURNING *;", |
601 | )?; |
602 | let ret = stmt.query_row( |
603 | rusqlite::params![ |
604 | @@ -468,9 +477,12 @@ impl Connection { |
605 | &new_val.address, |
606 | new_val.description.as_ref(), |
607 | new_val.archive_url.as_ref(), |
608 | + serde_json::json!(new_val.topics.as_slice()), |
609 | ], |
610 | |row| { |
611 | let pk = row.get("pk")?; |
612 | + let topics: serde_json::Value = row.get("topics")?; |
613 | + let topics = MailingList::topics_from_json_value(topics)?; |
614 | Ok(DbVal( |
615 | MailingList { |
616 | pk, |
617 | @@ -478,6 +490,7 @@ impl Connection { |
618 | id: row.get("id")?, |
619 | address: row.get("address")?, |
620 | description: row.get("description")?, |
621 | + topics, |
622 | archive_url: row.get("archive_url")?, |
623 | }, |
624 | pk, |
625 | @@ -999,6 +1012,7 @@ mod tests { |
626 | name: "".into(), |
627 | id: "".into(), |
628 | description: None, |
629 | + topics: vec![], |
630 | address: "".into(), |
631 | archive_url: None, |
632 | }; |
633 | diff --git a/core/src/doctests/db_setup.rs.inc b/core/src/doctests/db_setup.rs.inc |
634 | index bb38811..46b82ca 100644 |
635 | --- a/core/src/doctests/db_setup.rs.inc |
636 | +++ b/core/src/doctests/db_setup.rs.inc |
637 | @@ -29,6 +29,7 @@ |
638 | # id: "foo-chat".into(), |
639 | # address: "foo-chat@example.com".into(), |
640 | # description: Some("Hello world, from foo-chat list".into()), |
641 | + # topics: vec![], |
642 | # archive_url: Some("https://lists.example.com".into()), |
643 | # }) |
644 | # .unwrap(); |
645 | diff --git a/core/src/errors.rs b/core/src/errors.rs |
646 | index 113bb70..8aebb2a 100644 |
647 | --- a/core/src/errors.rs |
648 | +++ b/core/src/errors.rs |
649 | @@ -66,3 +66,11 @@ error_chain! { |
650 | Template(minijinja::Error) #[doc="Error returned from minijinja template engine."]; |
651 | } |
652 | } |
653 | + |
654 | + impl Error { |
655 | + /// Helper function to create a new generic error message. |
656 | + pub fn new_external<S: Into<String>>(msg: S) -> Self { |
657 | + let msg = msg.into(); |
658 | + Self::from(ErrorKind::External(anyhow::Error::msg(msg))) |
659 | + } |
660 | + } |
661 | diff --git a/core/src/lib.rs b/core/src/lib.rs |
662 | index 9c3fdbb..f6520c3 100644 |
663 | --- a/core/src/lib.rs |
664 | +++ b/core/src/lib.rs |
665 | @@ -83,6 +83,7 @@ |
666 | //! name: "foobar chat".into(), |
667 | //! id: "foo-chat".into(), |
668 | //! address: "foo-chat@example.com".into(), |
669 | + //! topics: vec![], |
670 | //! description: None, |
671 | //! archive_url: None, |
672 | //! })? |
673 | diff --git a/core/src/migrations.rs.inc b/core/src/migrations.rs.inc |
674 | index b6ad33e..ffdaa44 100644 |
675 | --- a/core/src/migrations.rs.inc |
676 | +++ b/core/src/migrations.rs.inc |
677 | @@ -1,11 +1,11 @@ |
678 | |
679 | //(user_version, redo sql, undo sql |
680 | &[(1,"PRAGMA foreign_keys=ON; |
681 | - BEGIN; |
682 | ALTER TABLE templates RENAME TO template; |
683 | - COMMIT; |
684 | ","PRAGMA foreign_keys=ON; |
685 | - BEGIN; |
686 | ALTER TABLE template RENAME TO templates; |
687 | - COMMIT; |
688 | + "),(2,"PRAGMA foreign_keys=ON; |
689 | + ALTER TABLE list ADD COLUMN topics JSON NOT NULL CHECK (json_type(topics) == 'array') DEFAULT '[]'; |
690 | + ","PRAGMA foreign_keys=ON; |
691 | + ALTER TABLE list DROP COLUMN topics; |
692 | "),] |
693 | \ No newline at end of file |
694 | diff --git a/core/src/models.rs b/core/src/models.rs |
695 | index fbac235..e4fe69a 100644 |
696 | --- a/core/src/models.rs |
697 | +++ b/core/src/models.rs |
698 | @@ -92,6 +92,8 @@ pub struct MailingList { |
699 | pub id: String, |
700 | /// Mailing list e-mail address. |
701 | pub address: String, |
702 | + /// Discussion topics. |
703 | + pub topics: Vec<String>, |
704 | /// Mailing list description. |
705 | pub description: Option<String>, |
706 | /// Mailing list archive URL. |
707 | @@ -417,6 +419,52 @@ impl MailingList { |
708 | contact = self.owner_mailto().address, |
709 | ) |
710 | } |
711 | + |
712 | + /// Utility function to get a `Vec<String>` -which is the expected type of |
713 | + /// the `topics` field- from a `serde_json::Value`, which is the value |
714 | + /// stored in the `topics` column in `sqlite3`. |
715 | + /// |
716 | + /// # Example |
717 | + /// |
718 | + /// ```rust |
719 | + /// # use mailpot::models::MailingList; |
720 | + /// use serde_json::Value; |
721 | + /// |
722 | + /// # fn main() -> Result<(), serde_json::Error> { |
723 | + /// let value: Value = serde_json::from_str(r#"["fruits","vegetables"]"#)?; |
724 | + /// assert_eq!( |
725 | + /// MailingList::topics_from_json_value(value), |
726 | + /// Ok(vec!["fruits".to_string(), "vegetables".to_string()]) |
727 | + /// ); |
728 | + /// |
729 | + /// let value: Value = serde_json::from_str(r#"{"invalid":"value"}"#)?; |
730 | + /// assert!(MailingList::topics_from_json_value(value).is_err()); |
731 | + /// # Ok(()) |
732 | + /// # } |
733 | + /// ``` |
734 | + pub fn topics_from_json_value( |
735 | + v: serde_json::Value, |
736 | + ) -> std::result::Result<Vec<String>, rusqlite::Error> { |
737 | + let err_fn = || { |
738 | + rusqlite::Error::FromSqlConversionFailure( |
739 | + 8, |
740 | + rusqlite::types::Type::Text, |
741 | + anyhow::Error::msg( |
742 | + "topics column must be a json array of strings serialized as a string, e.g. \ |
743 | + \"[]\" or \"['topicA', 'topicB']\"", |
744 | + ) |
745 | + .into(), |
746 | + ) |
747 | + }; |
748 | + v.as_array() |
749 | + .map(|arr| { |
750 | + arr.iter() |
751 | + .map(|v| v.as_str().map(str::to_string)) |
752 | + .collect::<Option<Vec<String>>>() |
753 | + }) |
754 | + .ok_or_else(err_fn)? |
755 | + .ok_or_else(err_fn) |
756 | + } |
757 | } |
758 | |
759 | /// A mailing list subscription entry. |
760 | diff --git a/core/src/policies.rs b/core/src/policies.rs |
761 | index 902404a..12e2df3 100644 |
762 | --- a/core/src/policies.rs |
763 | +++ b/core/src/policies.rs |
764 | @@ -84,6 +84,7 @@ mod post_policy { |
765 | /// id: "foo-chat".into(), |
766 | /// address: "foo-chat@example.com".into(), |
767 | /// description: None, |
768 | + /// topics: vec![], |
769 | /// archive_url: None, |
770 | /// }) |
771 | /// .unwrap(); |
772 | @@ -278,6 +279,7 @@ mod subscription_policy { |
773 | /// id: "foo-chat".into(), |
774 | /// address: "foo-chat@example.com".into(), |
775 | /// description: None, |
776 | + /// topics: vec![], |
777 | /// archive_url: None, |
778 | /// }) |
779 | /// .unwrap(); |
780 | diff --git a/core/src/postfix.rs b/core/src/postfix.rs |
781 | index f3446c1..4673d74 100644 |
782 | --- a/core/src/postfix.rs |
783 | +++ b/core/src/postfix.rs |
784 | @@ -430,6 +430,7 @@ fn test_postfix_generation() -> Result<()> { |
785 | id: "first".into(), |
786 | address: "first@example.com".into(), |
787 | description: None, |
788 | + topics: vec![], |
789 | archive_url: None, |
790 | })?; |
791 | assert_eq!(first.pk(), 1); |
792 | @@ -439,6 +440,7 @@ fn test_postfix_generation() -> Result<()> { |
793 | id: "second".into(), |
794 | address: "second@example.com".into(), |
795 | description: None, |
796 | + topics: vec![], |
797 | archive_url: None, |
798 | })?; |
799 | assert_eq!(second.pk(), 2); |
800 | @@ -459,6 +461,7 @@ fn test_postfix_generation() -> Result<()> { |
801 | id: "third".into(), |
802 | address: "third@example.com".into(), |
803 | description: None, |
804 | + topics: vec![], |
805 | archive_url: None, |
806 | })?; |
807 | assert_eq!(third.pk(), 3); |
808 | diff --git a/core/src/queue.rs b/core/src/queue.rs |
809 | index 8cb311e..45761ad 100644 |
810 | --- a/core/src/queue.rs |
811 | +++ b/core/src/queue.rs |
812 | @@ -51,6 +51,22 @@ pub enum Queue { |
813 | Error, |
814 | } |
815 | |
816 | + impl std::str::FromStr for Queue { |
817 | + type Err = Error; |
818 | + |
819 | + fn from_str(s: &str) -> Result<Self> { |
820 | + Ok(match s.trim() { |
821 | + s if s.eq_ignore_ascii_case(stringify!(Maildrop)) => Self::Maildrop, |
822 | + s if s.eq_ignore_ascii_case(stringify!(Hold)) => Self::Hold, |
823 | + s if s.eq_ignore_ascii_case(stringify!(Deferred)) => Self::Deferred, |
824 | + s if s.eq_ignore_ascii_case(stringify!(Corrupt)) => Self::Corrupt, |
825 | + s if s.eq_ignore_ascii_case(stringify!(Out)) => Self::Out, |
826 | + s if s.eq_ignore_ascii_case(stringify!(Error)) => Self::Error, |
827 | + other => return Err(Error::new_external(format!("Invalid Queue name: {other}."))), |
828 | + }) |
829 | + } |
830 | + } |
831 | + |
832 | impl Queue { |
833 | /// Returns the name of the queue used in the database schema. |
834 | pub fn as_str(&self) -> &'static str { |
835 | @@ -65,6 +81,12 @@ impl Queue { |
836 | } |
837 | } |
838 | |
839 | + impl std::fmt::Display for Queue { |
840 | + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { |
841 | + write!(fmt, "{}", self.as_str()) |
842 | + } |
843 | + } |
844 | + |
845 | /// A queue entry. |
846 | #[derive(Clone, Deserialize, Serialize, PartialEq, Eq)] |
847 | pub struct QueueEntry { |
848 | diff --git a/core/src/schema.sql b/core/src/schema.sql |
849 | index 30654a6..aba82bb 100644 |
850 | --- a/core/src/schema.sql |
851 | +++ b/core/src/schema.sql |
852 | @@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS list ( |
853 | request_local_part TEXT, |
854 | archive_url TEXT, |
855 | description TEXT, |
856 | + topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]', |
857 | created INTEGER NOT NULL DEFAULT (unixepoch()), |
858 | last_modified INTEGER NOT NULL DEFAULT (unixepoch()), |
859 | verify BOOLEAN CHECK (verify IN (0, 1)) NOT NULL DEFAULT 1, |
860 | diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4 |
861 | index 0f1cee6..93324d6 100644 |
862 | --- a/core/src/schema.sql.m4 |
863 | +++ b/core/src/schema.sql.m4 |
864 | @@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS list ( |
865 | request_local_part TEXT, |
866 | archive_url TEXT, |
867 | description TEXT, |
868 | + topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]', |
869 | created INTEGER NOT NULL DEFAULT (unixepoch()), |
870 | last_modified INTEGER NOT NULL DEFAULT (unixepoch()), |
871 | verify BOOLEAN_TYPE(verify) DEFAULT BOOLEAN_TRUE(), |
872 | diff --git a/core/src/subscriptions.rs b/core/src/subscriptions.rs |
873 | index fce5678..f25ffb9 100644 |
874 | --- a/core/src/subscriptions.rs |
875 | +++ b/core/src/subscriptions.rs |
876 | @@ -623,6 +623,7 @@ mod tests { |
877 | name: "foobar chat".into(), |
878 | id: "foo-chat".into(), |
879 | address: "foo-chat@example.com".into(), |
880 | + topics: vec![], |
881 | description: None, |
882 | archive_url: None, |
883 | }) |
884 | @@ -633,6 +634,7 @@ mod tests { |
885 | name: "foobar chat2".into(), |
886 | id: "foo-chat2".into(), |
887 | address: "foo-chat2@example.com".into(), |
888 | + topics: vec![], |
889 | description: None, |
890 | archive_url: None, |
891 | }) |
892 | diff --git a/core/tests/account.rs b/core/tests/account.rs |
893 | index 0a97f20..f02a05f 100644 |
894 | --- a/core/tests/account.rs |
895 | +++ b/core/tests/account.rs |
896 | @@ -46,6 +46,7 @@ fn test_accounts() { |
897 | id: "foo-chat".into(), |
898 | address: "foo-chat@example.com".into(), |
899 | description: None, |
900 | + topics: vec![], |
901 | archive_url: None, |
902 | }) |
903 | .unwrap(); |
904 | diff --git a/core/tests/authorizer.rs b/core/tests/authorizer.rs |
905 | index 12e2349..b5fa1ca 100644 |
906 | --- a/core/tests/authorizer.rs |
907 | +++ b/core/tests/authorizer.rs |
908 | @@ -46,6 +46,7 @@ fn test_authorizer() { |
909 | id: "foo-chat".into(), |
910 | address: "foo-chat@example.com".into(), |
911 | description: None, |
912 | + topics: vec![], |
913 | archive_url: None, |
914 | }) |
915 | .unwrap_err(), |
916 | @@ -84,6 +85,7 @@ fn test_authorizer() { |
917 | id: "foo-chat".into(), |
918 | address: "foo-chat@example.com".into(), |
919 | description: None, |
920 | + topics: vec![], |
921 | archive_url: None, |
922 | }) |
923 | .map(|_| ()), |
924 | diff --git a/core/tests/creation.rs b/core/tests/creation.rs |
925 | index c244181..31aa0cc 100644 |
926 | --- a/core/tests/creation.rs |
927 | +++ b/core/tests/creation.rs |
928 | @@ -61,6 +61,7 @@ fn test_list_creation() { |
929 | id: "foo-chat".into(), |
930 | address: "foo-chat@example.com".into(), |
931 | description: None, |
932 | + topics: vec![], |
933 | archive_url: None, |
934 | }) |
935 | .unwrap(); |
936 | diff --git a/core/tests/error_queue.rs b/core/tests/error_queue.rs |
937 | index fa33b83..ed8a117 100644 |
938 | --- a/core/tests/error_queue.rs |
939 | +++ b/core/tests/error_queue.rs |
940 | @@ -55,6 +55,7 @@ fn test_error_queue() { |
941 | id: "foo-chat".into(), |
942 | address: "foo-chat@example.com".into(), |
943 | description: None, |
944 | + topics: vec![], |
945 | archive_url: None, |
946 | }) |
947 | .unwrap(); |
948 | diff --git a/core/tests/smtp.rs b/core/tests/smtp.rs |
949 | index 63160a9..6fc84d9 100644 |
950 | --- a/core/tests/smtp.rs |
951 | +++ b/core/tests/smtp.rs |
952 | @@ -48,6 +48,7 @@ fn test_smtp() { |
953 | id: "foo-chat".into(), |
954 | address: "foo-chat@example.com".into(), |
955 | description: None, |
956 | + topics: vec![], |
957 | archive_url: None, |
958 | }) |
959 | .unwrap(); |
960 | @@ -202,6 +203,7 @@ fn test_smtp_mailcrab() { |
961 | id: "foo-chat".into(), |
962 | address: "foo-chat@example.com".into(), |
963 | description: None, |
964 | + topics: vec![], |
965 | archive_url: None, |
966 | }) |
967 | .unwrap(); |
968 | diff --git a/core/tests/subscription.rs b/core/tests/subscription.rs |
969 | index c83f201..c92081a 100644 |
970 | --- a/core/tests/subscription.rs |
971 | +++ b/core/tests/subscription.rs |
972 | @@ -44,6 +44,7 @@ fn test_list_subscription() { |
973 | id: "foo-chat".into(), |
974 | address: "foo-chat@example.com".into(), |
975 | description: None, |
976 | + topics: vec![], |
977 | archive_url: None, |
978 | }) |
979 | .unwrap(); |
980 | @@ -178,6 +179,7 @@ fn test_post_rejection() { |
981 | id: "foo-chat".into(), |
982 | address: "foo-chat@example.com".into(), |
983 | description: None, |
984 | + topics: vec![], |
985 | archive_url: None, |
986 | }) |
987 | .unwrap(); |
988 | diff --git a/core/tests/template_replies.rs b/core/tests/template_replies.rs |
989 | index 438c6c2..8648b2e 100644 |
990 | --- a/core/tests/template_replies.rs |
991 | +++ b/core/tests/template_replies.rs |
992 | @@ -67,6 +67,7 @@ MIME-Version: 1.0 |
993 | id: "foo-chat".into(), |
994 | address: "foo-chat@example.com".into(), |
995 | description: None, |
996 | + topics: vec![], |
997 | archive_url: None, |
998 | }) |
999 | .unwrap(); |
1000 | diff --git a/docs/mpot.1 b/docs/mpot.1 |
1001 | index 02a0504..f950b67 100644 |
1002 | --- a/docs/mpot.1 |
1003 | +++ b/docs/mpot.1 |
1004 | @@ -330,13 +330,13 @@ Is subscription enabled. |
1005 | .ie \n(.g .ds Aq \(aq |
1006 | .el .ds Aq ' |
1007 | .\fB |
1008 | - .SS mpot list add-policy |
1009 | + .SS mpot list add-post-policy |
1010 | .\fR |
1011 | .br |
1012 | |
1013 | .br |
1014 | |
1015 | - mpot list add\-policy [\-\-announce\-only \fIANNOUNCE_ONLY\fR] [\-\-subscription\-only \fISUBSCRIPTION_ONLY\fR] [\-\-approval\-needed \fIAPPROVAL_NEEDED\fR] [\-\-open \fIOPEN\fR] [\-\-custom \fICUSTOM\fR] |
1016 | + mpot list add\-post\-policy [\-\-announce\-only \fIANNOUNCE_ONLY\fR] [\-\-subscription\-only \fISUBSCRIPTION_ONLY\fR] [\-\-approval\-needed \fIAPPROVAL_NEEDED\fR] [\-\-open \fIOPEN\fR] [\-\-custom \fICUSTOM\fR] |
1017 | .br |
1018 | |
1019 | Add a new post policy. |
1020 | @@ -358,13 +358,13 @@ Allow posts, but handle it manually. |
1021 | .ie \n(.g .ds Aq \(aq |
1022 | .el .ds Aq ' |
1023 | .\fB |
1024 | - .SS mpot list remove-policy |
1025 | + .SS mpot list remove-post-policy |
1026 | .\fR |
1027 | .br |
1028 | |
1029 | .br |
1030 | |
1031 | - mpot list remove\-policy \-\-pk \fIPK\fR |
1032 | + mpot list remove\-post\-policy \-\-pk \fIPK\fR |
1033 | .br |
1034 | |
1035 | .TP |
1036 | @@ -373,13 +373,13 @@ Post policy primary key. |
1037 | .ie \n(.g .ds Aq \(aq |
1038 | .el .ds Aq ' |
1039 | .\fB |
1040 | - .SS mpot list add-subscribe-policy |
1041 | + .SS mpot list add-subscription-policy |
1042 | .\fR |
1043 | .br |
1044 | |
1045 | .br |
1046 | |
1047 | - mpot list add\-subscribe\-policy [\-\-send\-confirmation \fISEND_CONFIRMATION\fR] [\-\-open \fIOPEN\fR] [\-\-manual \fIMANUAL\fR] [\-\-request \fIREQUEST\fR] [\-\-custom \fICUSTOM\fR] |
1048 | + mpot list add\-subscription\-policy [\-\-send\-confirmation \fISEND_CONFIRMATION\fR] [\-\-open \fIOPEN\fR] [\-\-manual \fIMANUAL\fR] [\-\-request \fIREQUEST\fR] [\-\-custom \fICUSTOM\fR] |
1049 | .br |
1050 | |
1051 | Add subscription policy to list. |
1052 | @@ -401,18 +401,18 @@ Allow subscriptions, but handle it manually. |
1053 | .ie \n(.g .ds Aq \(aq |
1054 | .el .ds Aq ' |
1055 | .\fB |
1056 | - .SS mpot list remove-subscribe-policy |
1057 | + .SS mpot list remove-subscription-policy |
1058 | .\fR |
1059 | .br |
1060 | |
1061 | .br |
1062 | |
1063 | - mpot list remove\-subscribe\-policy \-\-pk \fIPK\fR |
1064 | + mpot list remove\-subscription\-policy \-\-pk \fIPK\fR |
1065 | .br |
1066 | |
1067 | .TP |
1068 | \-\-pk \fIPK\fR |
1069 | - Subscribe policy primary key. |
1070 | + Subscription policy primary key. |
1071 | .ie \n(.g .ds Aq \(aq |
1072 | .el .ds Aq ' |
1073 | .\fB |
1074 | @@ -715,6 +715,67 @@ Do not print in stdout. |
1075 | .ie \n(.g .ds Aq \(aq |
1076 | .el .ds Aq ' |
1077 | .\fB |
1078 | + .SS mpot queue |
1079 | + .\fR |
1080 | + .br |
1081 | + |
1082 | + .br |
1083 | + |
1084 | + mpot queue \-\-queue \fIQUEUE\fR |
1085 | + .br |
1086 | + |
1087 | + Mail that has not been handled properly end up in the error queue. |
1088 | + .TP |
1089 | + \-\-queue \fIQUEUE\fR |
1090 | + |
1091 | + .ie \n(.g .ds Aq \(aq |
1092 | + .el .ds Aq ' |
1093 | + .\fB |
1094 | + .SS mpot queue list |
1095 | + .\fR |
1096 | + .br |
1097 | + |
1098 | + .br |
1099 | + |
1100 | + List. |
1101 | + .ie \n(.g .ds Aq \(aq |
1102 | + .el .ds Aq ' |
1103 | + .\fB |
1104 | + .SS mpot queue print |
1105 | + .\fR |
1106 | + .br |
1107 | + |
1108 | + .br |
1109 | + |
1110 | + mpot queue print [\-\-index \fIINDEX\fR] |
1111 | + .br |
1112 | + |
1113 | + Print entry in RFC5322 or JSON format. |
1114 | + .TP |
1115 | + \-\-index \fIINDEX\fR |
1116 | + index of entry. |
1117 | + .ie \n(.g .ds Aq \(aq |
1118 | + .el .ds Aq ' |
1119 | + .\fB |
1120 | + .SS mpot queue delete |
1121 | + .\fR |
1122 | + .br |
1123 | + |
1124 | + .br |
1125 | + |
1126 | + mpot queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR] |
1127 | + .br |
1128 | + |
1129 | + Delete entry and print it in stdout. |
1130 | + .TP |
1131 | + \-\-index \fIINDEX\fR |
1132 | + index of entry. |
1133 | + .TP |
1134 | + \-\-quiet |
1135 | + Do not print in stdout. |
1136 | + .ie \n(.g .ds Aq \(aq |
1137 | + .el .ds Aq ' |
1138 | + .\fB |
1139 | .SS mpot import-maildir |
1140 | .\fR |
1141 | .br |
1142 | diff --git a/rest-http/src/routes/list.rs b/rest-http/src/routes/list.rs |
1143 | index ebe9910..e7f4211 100644 |
1144 | --- a/rest-http/src/routes/list.rs |
1145 | +++ b/rest-http/src/routes/list.rs |
1146 | @@ -217,6 +217,10 @@ mod tests { |
1147 | |
1148 | let db_path = tmp_dir.path().join("mpot.db"); |
1149 | std::fs::copy("../mailpot-tests/for_testing.db", &db_path).unwrap(); |
1150 | + let mut perms = std::fs::metadata(&db_path).unwrap().permissions(); |
1151 | + #[allow(clippy::permissions_set_readonly_false)] |
1152 | + perms.set_readonly(false); |
1153 | + std::fs::set_permissions(&db_path, perms).unwrap(); |
1154 | let config = Configuration { |
1155 | send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
1156 | db_path, |
1157 | @@ -231,9 +235,11 @@ mod tests { |
1158 | name: "foobar chat".into(), |
1159 | id: "foo-chat".into(), |
1160 | address: "foo-chat@example.com".into(), |
1161 | + topics: vec![], |
1162 | description: None, |
1163 | archive_url: None, |
1164 | }; |
1165 | + assert_eq!(&db.lists().unwrap().remove(0).into_inner(), &foo_chat); |
1166 | drop(db); |
1167 | |
1168 | let config = Arc::new(config); |
1169 | diff --git a/web/src/minijinja_utils.rs b/web/src/minijinja_utils.rs |
1170 | index 04da2d1..b7b0c04 100644 |
1171 | --- a/web/src/minijinja_utils.rs |
1172 | +++ b/web/src/minijinja_utils.rs |
1173 | @@ -92,6 +92,7 @@ pub struct MailingList { |
1174 | pub id: String, |
1175 | pub address: String, |
1176 | pub description: Option<String>, |
1177 | + pub topics: Vec<String>, |
1178 | #[serde(serialize_with = "super::utils::to_safe_string_opt")] |
1179 | pub archive_url: Option<String>, |
1180 | pub inner: DbVal<mailpot::models::MailingList>, |
1181 | @@ -106,6 +107,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList { |
1182 | id, |
1183 | address, |
1184 | description, |
1185 | + topics, |
1186 | archive_url, |
1187 | }, |
1188 | _, |
1189 | @@ -117,6 +119,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList { |
1190 | id, |
1191 | address, |
1192 | description, |
1193 | + topics, |
1194 | archive_url, |
1195 | inner: val, |
1196 | } |
1197 | @@ -163,13 +166,24 @@ impl minijinja::value::StructObject for MailingList { |
1198 | "id" => Some(Value::from_serializable(&self.id)), |
1199 | "address" => Some(Value::from_serializable(&self.address)), |
1200 | "description" => Some(Value::from_serializable(&self.description)), |
1201 | + "topics" => Some(Value::from_serializable(&self.topics)), |
1202 | "archive_url" => Some(Value::from_serializable(&self.archive_url)), |
1203 | _ => None, |
1204 | } |
1205 | } |
1206 | |
1207 | fn static_fields(&self) -> Option<&'static [&'static str]> { |
1208 | - Some(&["pk", "name", "id", "address", "description", "archive_url"][..]) |
1209 | + Some( |
1210 | + &[ |
1211 | + "pk", |
1212 | + "name", |
1213 | + "id", |
1214 | + "address", |
1215 | + "description", |
1216 | + "topics", |
1217 | + "archive_url", |
1218 | + ][..], |
1219 | + ) |
1220 | } |
1221 | } |
1222 |