1use crate::error::{HypermailError, Result};
2use std::path::{Path, PathBuf};
3
4pub const ANTISPAM_AT: &str = "@";
5pub const LANGUAGE: &str = "en";
6pub const HTMLSUFFIX: &str = "html";
7pub const DEFAULTINDEX: &str = "date";
8pub const INLINE_TYPES: &str = "image/gif image/jpeg image/png";
9pub const PROGRESS: i32 = 0;
10pub const MAILCOMMAND: &str = "mailto:$TO?subject=$SUBJECT&in-reply-to=$ID";
11pub const DOMAINADDR: &str = "";
12
13pub const DELETE_REMOVES_FILES: i32 = 0;
14pub const DELETE_LEAVES_STUBS: i32 = 1;
15pub const DELETE_LEAVES_EXPIRED_TEXT: i32 = 2;
16pub const DELETE_LEAVES_TEXT: i32 = 3;
17
18#[derive(Debug, Clone)]
20pub enum ConfigType {
21 String,
22 Switch,
23 Integer,
24 List,
25 StringList,
26 Octal,
27}
28
29#[derive(Debug, Clone)]
31pub struct ConfigEntry {
32 pub label: &'static str,
33 pub flags: ConfigType,
34 pub default_str: Option<&'static str>,
35 pub default_int: i64,
36 pub verbose: &'static str,
37}
38
39#[derive(Debug, Clone)]
41pub struct HmList {
42 pub values: Vec<String>,
43}
44
45impl Default for HmList {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl HmList {
52 pub fn new() -> Self {
54 HmList { values: Vec::new() }
55 }
56
57 pub fn from_whitespace_str(s: &str) -> Self {
59 let values: Vec<String> = s.split_whitespace().map(|s| s.to_string()).collect();
60 HmList { values }
61 }
62
63 pub fn contains(&self, val: &str) -> bool {
65 self.values.iter().any(|v| v == val)
66 }
67
68 pub fn add(&mut self, val: &str) {
70 if !self.contains(val) {
71 self.values.push(val.to_string());
72 }
73 }
74
75 pub fn add_list(&mut self, val: &str) {
77 for v in val.split_whitespace() {
78 self.add(v);
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
88pub struct Config {
89 pub fragment_prefix: String,
91 pub htmlmessage_deleted: Option<String>,
92 pub antispam_at: String,
93 pub antispamdomain: Option<String>,
94 pub language: String,
95 pub htmlsuffix: String,
96 pub mbox: Option<String>,
97 pub archives: Option<String>,
98 pub custom_archives: Option<String>,
99 pub about: Option<String>,
100 pub label: Option<String>,
101 pub dir: Option<String>,
102 pub defaultindex: String,
103 pub default_top_index: String,
104 pub mailcommand: String,
105 pub newmsg_command: String,
106 pub replymsg_command: String,
107 pub inreplyto_command: Option<String>,
108 pub mailto: Option<String>,
109 pub hmail: Option<String>,
110 pub domainaddr: Option<String>,
111 pub css: Option<String>,
112 pub icss_url: Option<String>,
113 pub mcss_url: Option<String>,
114 pub dateformat: Option<String>,
115 pub indexdateformat: Option<String>,
116 pub stripsubject: Option<String>,
117 pub link_to_replies: Option<String>,
118 pub quote_link_string: Option<String>,
119 pub ihtmlheader: Option<String>,
120 pub ihtmlfooter: Option<String>,
121 pub ihtmlhead: Option<String>,
122 pub ihtmlhelpup: Option<String>,
123 pub ihtmlhelplow: Option<String>,
124 pub ihtmlnavbar2up: Option<String>,
125 pub mhtmlheader: Option<String>,
126 pub mhtmlfooter: Option<String>,
127 pub attachmentlink: Option<String>,
128 pub bodyheader: Option<String>,
129 pub bodyheaderend: Option<String>,
130 pub bodyfooter: Option<String>,
131 pub unsafe_chars: Option<String>,
132 pub filename_base: Option<String>,
133 pub folder_by_date: Option<String>,
134 pub latest_folder: Option<String>,
135 pub base_url: Option<String>,
136 pub describe_folder: Option<String>,
137 pub delete_older: Option<String>,
138 pub delete_newer: Option<String>,
139 pub alts_text: Option<String>,
140 pub description: Option<String>,
141 pub theme: Option<String>,
142 pub append_filename: Option<String>,
143 pub txtsuffix: Option<String>,
144
145 pub email_address_obfuscation: bool,
147 pub i18n: bool,
148 pub i18n_body: bool,
149 pub overwrite: bool,
150 pub inlinehtml: bool,
151 pub readone: bool,
152 pub reverse: bool,
153 pub reverse_folders: bool,
154 pub showheaders: bool,
155 pub showbr: bool,
156 pub showreplies: bool,
157 pub indextable: bool,
158 pub iquotes: bool,
159 pub eurodate: bool,
160 pub gmtime: bool,
161 pub isodate: bool,
162 pub require_msgids: bool,
163 pub discard_dup_msgids: bool,
164 pub usemeta: bool,
165 pub uselock: bool,
166 pub ietf_mbox: bool,
167 pub linkquotes: bool,
168 pub monthly_index: bool,
169 pub yearly_index: bool,
170 pub spamprotect: bool,
171 pub spamprotect_id: bool,
172 pub attachmentsindex: bool,
173 pub usegdbm: bool,
174 pub writehaof: bool,
175 pub append: bool,
176 pub nonsequential: bool,
177 pub warn_surpressions: bool,
178 pub files_by_thread: bool,
179 pub href_detection: bool,
180 pub mbox_shortened: bool,
181 pub report_new_file: bool,
182 pub report_new_folder: bool,
183 pub use_sender_date: bool,
184 pub inline_addlink: bool,
185 pub iso2022jp: bool,
186 pub delete_incremental: bool,
187 pub showgenerator: bool,
188 pub show_warnings: bool,
189 pub external_fonts: bool,
191
192 pub increment: i32,
194 pub showhtml: i32,
195 pub show_msg_links: i32,
196 pub show_index_links: i32,
197 pub thrdlevels: i32,
198 pub dirmode: i32,
199 pub filemode: i32,
200 pub locktime: i32,
201 pub searchbackmsgnum: i32,
202 pub quote_hide_threshold: i32,
203 pub thread_file_depth: i32,
204 pub startmsgnum: i32,
205 pub msgsperfolder: i32,
206 pub save_alts: i32,
207 pub delete_level: i32,
208 pub progress: i32,
209 pub max_message_size: usize,
210 pub max_messages: usize,
212
213 pub show_headers: HmList,
215 pub avoid_indices: HmList,
216 pub avoid_top_indices: HmList,
217 pub skip_headers: HmList,
218 pub text_types: HmList,
219 pub inline_types: HmList,
220 pub prefered_types: HmList,
221 pub ignore_types: HmList,
222 pub filter_out: HmList,
223 pub filter_require: HmList,
224 pub filter_out_full_body: HmList,
225 pub filter_require_full_body: HmList,
226 pub deleted: HmList,
227 pub expires: HmList,
228 pub delete_msgnum: HmList,
229}
230
231impl Default for Config {
232 fn default() -> Self {
233 Config {
234 fragment_prefix: "msg".to_string(),
235 htmlmessage_deleted: None,
236 antispam_at: ANTISPAM_AT.to_string(),
237 antispamdomain: None,
238 language: LANGUAGE.to_string(),
239 htmlsuffix: HTMLSUFFIX.to_string(),
240 mbox: None,
241 archives: None,
242 custom_archives: None,
243 about: None,
244 label: None,
245 dir: None,
246 defaultindex: DEFAULTINDEX.to_string(),
247 default_top_index: "folders".to_string(),
248 mailcommand: MAILCOMMAND.to_string(),
249 newmsg_command: "mailto:$TO".to_string(),
250 replymsg_command: MAILCOMMAND.to_string(),
251 inreplyto_command: None,
252 mailto: None,
253 hmail: None,
254 domainaddr: None,
255 css: None,
256 icss_url: None,
257 mcss_url: None,
258 dateformat: None,
259 indexdateformat: None,
260 stripsubject: None,
261 link_to_replies: None,
262 quote_link_string: None,
263 ihtmlheader: None,
264 ihtmlfooter: None,
265 ihtmlhead: None,
266 ihtmlhelpup: None,
267 ihtmlhelplow: None,
268 ihtmlnavbar2up: None,
269 mhtmlheader: None,
270 mhtmlfooter: None,
271 attachmentlink: None,
272 unsafe_chars: None,
273 filename_base: None,
274 folder_by_date: None,
275 latest_folder: None,
276 base_url: None,
277 describe_folder: None,
278 delete_older: None,
279 delete_newer: None,
280 alts_text: None,
281 append_filename: None,
282 txtsuffix: None,
283 description: None,
284 theme: None,
285 bodyheader: None,
286 bodyheaderend: None,
287 bodyfooter: None,
288 email_address_obfuscation: false,
289 i18n: false,
290 i18n_body: false,
291 overwrite: false,
292 inlinehtml: true,
293 readone: false,
294 reverse: false,
295 reverse_folders: false,
296 showheaders: true,
297 showbr: true,
298 showreplies: true,
299 indextable: false,
300 iquotes: true,
301 eurodate: true,
302 gmtime: false,
303 isodate: false,
304 require_msgids: true,
305 discard_dup_msgids: true,
306 usemeta: false,
307 uselock: true,
308 ietf_mbox: false,
309 linkquotes: false,
310 monthly_index: false,
311 yearly_index: false,
312 spamprotect: true,
313 spamprotect_id: true,
314 attachmentsindex: true,
315 usegdbm: false,
316 writehaof: false,
317 append: false,
318 nonsequential: false,
319 warn_surpressions: true,
320 files_by_thread: false,
321 href_detection: true,
322 mbox_shortened: false,
323 report_new_file: false,
324 report_new_folder: false,
325 use_sender_date: false,
326 inline_addlink: true,
327 iso2022jp: false,
328 delete_incremental: true,
329 showgenerator: true,
330 show_warnings: false,
331 external_fonts: true,
332 increment: 0,
333 showhtml: 1,
334 show_msg_links: 1,
335 show_index_links: 1,
336 thrdlevels: 50, dirmode: 0o755,
338 filemode: 0o644,
339 locktime: 3600,
340 searchbackmsgnum: 500,
341 quote_hide_threshold: 100,
342 thread_file_depth: 0,
343 startmsgnum: 0,
344 msgsperfolder: 0,
345 save_alts: 0,
346 delete_level: DELETE_LEAVES_TEXT,
347 progress: PROGRESS,
348 max_message_size: 100 * 1024 * 1024,
349 max_messages: 1_000_000,
350 show_headers: HmList::new(),
351 avoid_indices: HmList::new(),
352 avoid_top_indices: HmList::new(),
353 skip_headers: HmList::new(),
354 text_types: HmList::new(),
355 inline_types: HmList::from_whitespace_str(INLINE_TYPES),
356 deleted: HmList::from_whitespace_str("X-Hypermail-Deleted X-No-Archive"),
357 expires: HmList::from_whitespace_str("Expires"),
358 delete_msgnum: HmList::new(),
359 filter_out: HmList::new(),
360 filter_require: HmList::new(),
361 filter_out_full_body: HmList::new(),
362 filter_require_full_body: HmList::new(),
363 prefered_types: HmList::new(),
364 ignore_types: HmList::new(),
365 }
366 }
367}
368
369impl Config {
370 pub fn set_string(&mut self, key: &str, val: &str) -> Result<()> {
372 match key {
373 "fragment_prefix" => self.fragment_prefix = val.to_string(),
374 "htmlmessage_deleted" => self.htmlmessage_deleted = Some(val.to_string()),
375 "antispam_at" => self.antispam_at = val.to_string(),
376 "antispamdomain" => {
377 if val == "NONE" || val.is_empty() {
378 self.antispamdomain = None;
379 } else {
380 self.antispamdomain = Some(val.to_string());
381 }
382 },
383 "language" => self.language = val.to_string(),
384 "htmlsuffix" => self.htmlsuffix = val.to_string(),
385 "mbox" => {
386 if val == "NONE" {
387 self.mbox = None;
388 } else {
389 self.mbox = Some(val.to_string());
390 }
391 },
392 "archives" => {
393 if val == "NONE" {
394 self.archives = None;
395 } else {
396 self.archives = Some(val.to_string());
397 }
398 },
399 "custom_archives" => {
400 if val == "NONE" {
401 self.custom_archives = None;
402 } else {
403 self.custom_archives = Some(val.to_string());
404 }
405 },
406 "about" => {
407 if val == "NONE" {
408 self.about = None;
409 } else {
410 self.about = Some(val.to_string());
411 }
412 },
413 "label" => {
414 if val == "NONE" {
415 self.label = None;
416 } else {
417 self.label = Some(val.to_string());
418 }
419 },
420 "dir" => {
421 if val == "NONE" {
422 self.dir = None;
423 } else {
424 self.dir = Some(val.to_string());
425 }
426 },
427 "defaultindex" => self.defaultindex = val.to_string(),
428 "default_top_index" => self.default_top_index = val.to_string(),
429 "mailcommand" => self.mailcommand = val.to_string(),
430 "newmsg_command" => self.newmsg_command = val.to_string(),
431 "replymsg_command" => self.replymsg_command = val.to_string(),
432 "inreplyto_command" => self.inreplyto_command = Some(val.to_string()),
433 "mailto" => {
434 if val == "NONE" {
435 self.mailto = None;
436 } else {
437 self.mailto = Some(val.to_string());
438 }
439 },
440 "hmail" => {
441 if val == "NONE" {
442 self.hmail = None;
443 } else {
444 self.hmail = Some(val.to_string());
445 }
446 },
447 "domainaddr" => {
448 if val == "NONE" {
449 self.domainaddr = None;
450 } else {
451 self.domainaddr = Some(val.to_string());
452 }
453 },
454 "css" => self.css = Some(val.to_string()),
455 "icss_url" => self.icss_url = Some(val.to_string()),
456 "mcss_url" => self.mcss_url = Some(val.to_string()),
457 "dateformat" => self.dateformat = Some(val.to_string()),
458 "indexdateformat" => self.indexdateformat = Some(val.to_string()),
459 "stripsubject" => self.stripsubject = Some(val.to_string()),
460 "link_to_replies" => self.link_to_replies = Some(val.to_string()),
461 "quote_link_string" => self.quote_link_string = Some(val.to_string()),
462 "ihtmlheaderfile" => self.ihtmlheader = Some(val.to_string()),
463 "ihtmlfooterfile" => self.ihtmlfooter = Some(val.to_string()),
464 "ihtmlheadfile" => self.ihtmlhead = Some(val.to_string()),
465 "ihtmlhelpupfile" => self.ihtmlhelpup = Some(val.to_string()),
466 "ihtmlhelplowfile" => self.ihtmlhelplow = Some(val.to_string()),
467 "ihtmlnavbar2upfile" => self.ihtmlnavbar2up = Some(val.to_string()),
468 "mhtmlheaderfile" => self.mhtmlheader = Some(val.to_string()),
469 "mhtmlfooterfile" => self.mhtmlfooter = Some(val.to_string()),
470 "htmlheaderfile" => {
472 self.ihtmlheader = Some(val.to_string());
473 self.mhtmlheader = Some(val.to_string());
474 },
475 "htmlfooterfile" => {
476 self.ihtmlfooter = Some(val.to_string());
477 self.mhtmlfooter = Some(val.to_string());
478 },
479 "attachmentlink" => self.attachmentlink = Some(val.to_string()),
480 "unsafe_chars" => self.unsafe_chars = Some(val.to_string()),
481 "description" => self.description = Some(val.to_string()),
482 "theme" => self.theme = Some(val.to_string()),
483 "bodyheader" => self.bodyheader = Some(val.to_string()),
484 "bodyheaderend" => self.bodyheaderend = Some(val.to_string()),
485 "bodyfooter" => self.bodyfooter = Some(val.to_string()),
486 "filename_base" => self.filename_base = Some(val.to_string()),
487 "folder_by_date" => {
488 if val.is_empty() || val == "NONE" {
489 self.folder_by_date = None;
490 } else {
491 self.folder_by_date = Some(val.to_string());
492 }
493 },
494 "latest_folder" => self.latest_folder = Some(val.to_string()),
495 "base_url" => self.base_url = Some(val.to_string()),
496 "describe_folder" => self.describe_folder = Some(val.to_string()),
497 "delete_older" => {
498 self.delete_older = if val.is_empty() {
499 None
500 } else {
501 Some(val.to_string())
502 }
503 },
504 "delete_newer" => {
505 self.delete_newer = if val.is_empty() {
506 None
507 } else {
508 Some(val.to_string())
509 }
510 },
511 "alts_text" => self.alts_text = Some(val.to_string()),
512 "append_filename" => self.append_filename = Some(val.to_string()),
513 "txtsuffix" => self.txtsuffix = Some(val.to_string()),
514 _ => {
515 return Err(HypermailError::InvalidConfigValue {
516 key: key.to_string(),
517 message: format!("unknown string config key: {}", key),
518 })
519 },
520 }
521 Ok(())
522 }
523
524 pub fn set_switch(&mut self, key: &str, val: bool) -> Result<()> {
526 match key {
527 "email_address_obfuscation" => self.email_address_obfuscation = val,
528 "i18n" => self.i18n = val,
529 "i18n_body" => self.i18n_body = val,
530 "overwrite" => self.overwrite = val,
531 "inlinehtml" => self.inlinehtml = val,
532 "readone" => self.readone = val,
533 "reverse" => self.reverse = val,
534 "reverse_folders" => self.reverse_folders = val,
535 "showheaders" => self.showheaders = val,
536 "showbr" => self.showbr = val,
537 "showreplies" => self.showreplies = val,
538 "indextable" => self.indextable = val,
539 "iquotes" => self.iquotes = val,
540 "eurodate" => self.eurodate = val,
541 "gmtime" => self.gmtime = val,
542 "isodate" => self.isodate = val,
543 "require_msgids" => self.require_msgids = val,
544 "discard_dup_msgids" => self.discard_dup_msgids = val,
545 "usemeta" => self.usemeta = val,
546 "uselock" => self.uselock = val,
547 "ietf_mbox" => self.ietf_mbox = val,
548 "linkquotes" => self.linkquotes = val,
549 "monthly_index" => self.monthly_index = val,
550 "yearly_index" => self.yearly_index = val,
551 "spamprotect" => self.spamprotect = val,
552 "spamprotect_id" => self.spamprotect_id = val,
553 "attachmentsindex" => self.attachmentsindex = val,
554 "usegdbm" => self.usegdbm = val,
555 "writehaof" => self.writehaof = val,
556 "append" => self.append = val,
557 "nonsequential" => self.nonsequential = val,
558 "warn_surpressions" => self.warn_surpressions = val,
559 "files_by_thread" => self.files_by_thread = val,
560 "href_detection" => self.href_detection = val,
561 "mbox_shortened" => self.mbox_shortened = val,
562 "report_new_file" => self.report_new_file = val,
563 "report_new_folder" => self.report_new_folder = val,
564 "use_sender_date" => self.use_sender_date = val,
565 "inline_addlink" => self.inline_addlink = val,
566 "iso2022jp" => self.iso2022jp = val,
567 "delete_incremental" => self.delete_incremental = val,
568 "showgenerator" => self.showgenerator = val,
569 "show_warnings" => self.show_warnings = val,
570 "external_fonts" => self.external_fonts = val,
571 _ => {
572 return Err(HypermailError::InvalidConfigValue {
573 key: key.to_string(),
574 message: format!("unknown switch config key: {}", key),
575 })
576 },
577 }
578 Ok(())
579 }
580
581 pub fn set_integer(&mut self, key: &str, val: i64) -> Result<()> {
583 match key {
584 "increment" => self.increment = val as i32,
585 "showhtml" => self.showhtml = val as i32,
586 "show_msg_links" => self.show_msg_links = val as i32,
587 "show_index_links" => self.show_index_links = val as i32,
588 "thrdlevels" => self.thrdlevels = val as i32,
589 "dirmode" => self.dirmode = val as i32,
590 "filemode" => self.filemode = val as i32,
591 "locktime" => self.locktime = val as i32,
592 "searchbackmsgnum" => self.searchbackmsgnum = val as i32,
593 "quote_hide_threshold" => self.quote_hide_threshold = val as i32,
594 "thread_file_depth" => self.thread_file_depth = val as i32,
595 "startmsgnum" => self.startmsgnum = val as i32,
596 "msgsperfolder" => self.msgsperfolder = val as i32,
597 "save_alts" => self.save_alts = val as i32,
598 "delete_level" => self.delete_level = val as i32,
599 "progress" => self.progress = val as i32,
600 "max_message_size" => self.max_message_size = val as usize,
601 "max_messages" => self.max_messages = val as usize,
602 _ => {
603 return Err(HypermailError::InvalidConfigValue {
604 key: key.to_string(),
605 message: format!("unknown integer config key: {}", key),
606 })
607 },
608 }
609 Ok(())
610 }
611
612 pub fn set_list(&mut self, key: &str, val: &str) -> Result<()> {
614 let list = match key {
615 "show_headers" => &mut self.show_headers,
616 "avoid_indices" => &mut self.avoid_indices,
617 "avoid_top_indices" => &mut self.avoid_top_indices,
618 "text_types" => &mut self.text_types,
619 "inline_types" => &mut self.inline_types,
620 "prefered_types" => &mut self.prefered_types,
621 "ignore_types" => &mut self.ignore_types,
622 "filter_out" => &mut self.filter_out,
623 "filter_require" => &mut self.filter_require,
624 "filter_out_full_body" => &mut self.filter_out_full_body,
625 "filter_require_full_body" => &mut self.filter_require_full_body,
626 "deleted" => &mut self.deleted,
627 "expires" => &mut self.expires,
628 "delete_msgnum" => &mut self.delete_msgnum,
629 _ => {
630 return Err(HypermailError::InvalidConfigValue {
631 key: key.to_string(),
632 message: format!("unknown list config key: {}", key),
633 })
634 },
635 };
636 list.add_list(val);
637 Ok(())
638 }
639
640 pub fn apply_cli_arg(&mut self, key: &str, val: &str) -> Result<()> {
644 let (actual_key, actual_val) = if let Some(eq_pos) = key.find('=') {
645 let k = &key[..eq_pos];
646 let v = &key[eq_pos + 1..];
647 (k, v)
648 } else {
649 (key, val)
650 };
651
652 let actual_key = actual_key.strip_prefix("hm_").unwrap_or(actual_key);
653 let actual_key = actual_key.strip_prefix("set_").unwrap_or(actual_key);
654
655 if matches!(actual_key, "showhr" | "usetable" | "body") {
658 return Err(HypermailError::InvalidConfigValue {
659 key: actual_key.to_string(),
660 message: "deprecated config key — has no effect; remove from your config file"
661 .to_string(),
662 });
663 }
664 let actual_val = actual_val.trim();
665
666 if actual_val == "ON"
667 || actual_val == "YES"
668 || actual_val == "On"
669 || actual_val == "Yes"
670 || actual_val == "on"
671 || actual_val == "yes"
672 {
673 if let Ok(()) = self.set_switch(actual_key, true) {
674 return Ok(());
675 }
676 return self
677 .set_integer(actual_key, 1)
678 .or_else(|_| self.set_string(actual_key, actual_val));
679 }
680 if (actual_val == "OFF"
681 || actual_val == "NO"
682 || actual_val == "Off"
683 || actual_val == "No"
684 || actual_val == "off"
685 || actual_val == "no")
686 && self.set_switch(actual_key, false).is_ok()
687 {
688 return Ok(());
689 }
690
691 if actual_key == "dirmode" || actual_key == "filemode" {
694 let octal_str = actual_val.strip_prefix('0').unwrap_or(actual_val);
695 if let Ok(i) = i64::from_str_radix(octal_str, 8) {
696 if self.set_integer(actual_key, i).is_ok() {
697 return Ok(());
698 }
699 }
700 }
701
702 let int_val = actual_val.parse::<i64>();
703 if let Ok(i) = int_val {
704 if self.set_integer(actual_key, i).is_ok() {
705 return Ok(());
706 }
707 }
708 if self.set_string(actual_key, actual_val).is_ok() {
709 return Ok(());
710 }
711 if self.set_list(actual_key, actual_val).is_ok() {
712 return Ok(());
713 }
714
715 Err(HypermailError::InvalidConfigValue {
716 key: actual_key.to_string(),
717 message: format!("unrecognized config key or invalid value: {}", actual_val),
718 })
719 }
720
721 pub fn load_env(&mut self) {
723 self.load_env_from(std::env::vars());
724 }
725
726 fn load_env_from(&mut self, vars: impl Iterator<Item = (String, String)>) {
729 for (key, val) in vars {
730 if let Some(stripped) = key.strip_prefix("HM_") {
731 let config_key = stripped.to_lowercase();
732 let _ = self.apply_cli_arg(&config_key, &val);
733 }
734 }
735 }
736
737 pub fn post_process(&mut self) {
739 self.skip_headers.add("from");
740 self.skip_headers.add("date");
741 self.skip_headers.add("subject");
742 }
743
744 pub fn css_path(&self) -> String {
746 if let Some(ref css) = self.css {
747 if css.starts_with("http") || Path::new(css).is_absolute() {
748 css.clone()
749 } else if let Some(ref dir) = self.dir {
750 PathBuf::from(dir).join(css).to_string_lossy().into_owned()
751 } else {
752 css.clone()
753 }
754 } else {
755 String::new()
756 }
757 }
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 #[test]
765 fn test_default_config() {
766 let cfg = Config::default();
767 assert_eq!(cfg.language, "en");
768 assert_eq!(cfg.htmlsuffix, "html");
769 assert_eq!(cfg.defaultindex, "date");
770 assert!(cfg.inlinehtml);
771 assert!(!cfg.overwrite);
772 assert_eq!(cfg.showhtml, 1);
773 assert_eq!(cfg.thrdlevels, 50); assert_eq!(cfg.dirmode, 0o755);
775 assert_eq!(cfg.filemode, 0o644);
776 assert_eq!(cfg.locktime, 3600);
777 assert_eq!(cfg.searchbackmsgnum, 500);
778 assert_eq!(cfg.quote_hide_threshold, 100);
779 assert_eq!(cfg.delete_level, DELETE_LEAVES_TEXT);
780 assert!(cfg.discard_dup_msgids);
781 assert!(cfg.require_msgids);
782 assert!(cfg.uselock);
783 assert!(cfg.href_detection);
784 assert!(cfg.warn_surpressions);
785 assert!(cfg.attachmentsindex);
786 assert!(cfg.spamprotect);
787 assert!(cfg.spamprotect_id);
788 assert!(cfg.showbr);
789 assert!(cfg.showreplies);
790 assert!(cfg.inline_addlink);
791 assert!(cfg.delete_incremental);
792 assert_eq!(cfg.fragment_prefix, "msg");
793 assert_eq!(cfg.antispam_at, "@");
794 assert_eq!(cfg.progress, 0);
795 assert!(cfg.inline_types.contains("image/gif"));
796 assert!(cfg.deleted.contains("X-Hypermail-Deleted"));
797 assert!(cfg.expires.contains("Expires"));
798 }
799
800 #[test]
801 fn test_set_string() {
802 let mut cfg = Config::default();
803 cfg.set_string("language", "de").unwrap();
804 assert_eq!(cfg.language, "de");
805 cfg.set_string("mbox", "NONE").unwrap();
806 assert!(cfg.mbox.is_none());
807 cfg.set_string("label", "test list").unwrap();
808 assert_eq!(cfg.label.as_deref(), Some("test list"));
809 }
810
811 #[test]
812 fn test_set_switch() {
813 let mut cfg = Config::default();
814 assert!(!cfg.overwrite);
815 cfg.set_switch("overwrite", true).unwrap();
816 assert!(cfg.overwrite);
817 }
818
819 #[test]
820 fn test_set_integer() {
821 let mut cfg = Config::default();
822 cfg.set_integer("showhtml", 2).unwrap();
823 assert_eq!(cfg.showhtml, 2);
824 cfg.set_integer("thrdlevels", 8).unwrap();
825 assert_eq!(cfg.thrdlevels, 8);
826 }
827
828 #[test]
829 fn test_set_list() {
830 let mut cfg = Config::default();
831 cfg.set_list("text_types", "text/html text/plain").unwrap();
832 assert!(cfg.text_types.contains("text/html"));
833 assert!(cfg.text_types.contains("text/plain"));
834 }
835
836 #[test]
837 fn test_apply_cli_arg() {
838 let mut cfg = Config::default();
839 cfg.apply_cli_arg("overwrite", "On").unwrap();
840 assert!(cfg.overwrite);
841 cfg.apply_cli_arg("showhtml", "2").unwrap();
842 assert_eq!(cfg.showhtml, 2);
843 cfg.apply_cli_arg("language=de", "").unwrap();
844 assert_eq!(cfg.language, "de");
845 }
846
847 #[test]
848 fn test_post_process() {
849 let mut cfg = Config::default();
850 cfg.post_process();
851 assert!(cfg.skip_headers.contains("from"));
852 assert!(cfg.skip_headers.contains("date"));
853 assert!(cfg.skip_headers.contains("subject"));
854 }
855
856 #[test]
857 fn test_unknown_key() {
858 let mut cfg = Config::default();
859 assert!(cfg.set_string("nonexistent", "value").is_err());
860 assert!(cfg.set_switch("nonexistent", true).is_err());
861 assert!(cfg.set_integer("nonexistent", 42).is_err());
862 }
863
864 #[test]
865 fn test_none_strings() {
866 let mut cfg = Config::default();
867 cfg.set_string("archives", "NONE").unwrap();
868 assert!(cfg.archives.is_none());
869 cfg.set_string("about", "NONE").unwrap();
870 assert!(cfg.about.is_none());
871 cfg.set_string("custom_archives", "NONE").unwrap();
872 assert!(cfg.custom_archives.is_none());
873 }
874
875 #[test]
876 fn test_apply_cli_hm_prefix() {
877 let mut cfg = Config::default();
878 cfg.apply_cli_arg("hm_overwrite", "On").unwrap();
879 assert!(cfg.overwrite);
880 }
881
882 #[test]
883 fn test_apply_cli_set_prefix() {
884 let mut cfg = Config::default();
885 cfg.apply_cli_arg("set_overwrite", "On").unwrap();
886 assert!(cfg.overwrite);
887 }
888
889 #[test]
890 fn test_apply_cli_bool_yes() {
891 let mut cfg = Config::default();
892 cfg.apply_cli_arg("overwrite", "YES").unwrap();
893 assert!(cfg.overwrite);
894 }
895
896 #[test]
897 fn test_apply_cli_bool_no() {
898 let mut cfg = Config::default();
899 assert!(cfg.inlinehtml);
900 cfg.apply_cli_arg("inlinehtml", "OFF").unwrap();
901 assert!(!cfg.inlinehtml);
902 }
903
904 #[test]
905 fn test_apply_cli_octal_dirmode() {
906 let mut cfg = Config::default();
907 cfg.apply_cli_arg("dirmode", "0755").unwrap();
908 assert_eq!(cfg.dirmode, 0o755);
909 }
910
911 #[test]
912 fn test_apply_cli_octal_filemode() {
913 let mut cfg = Config::default();
914 cfg.apply_cli_arg("filemode", "0644").unwrap();
915 assert_eq!(cfg.filemode, 0o644);
916 }
917
918 #[test]
919 fn test_apply_cli_decimal_dirmode() {
920 let mut cfg = Config::default();
921 cfg.apply_cli_arg("dirmode", "755").unwrap();
923 assert_eq!(cfg.dirmode, 0o755);
924 }
925
926 #[test]
927 fn test_apply_cli_inline_eq() {
928 let mut cfg = Config::default();
929 cfg.apply_cli_arg("language=fr", "").unwrap();
930 assert_eq!(cfg.language, "fr");
931 }
932
933 #[test]
934 fn test_apply_cli_list() {
935 let mut cfg = Config::default();
936 cfg.apply_cli_arg("filter_out", "spam").unwrap();
937 assert!(cfg.filter_out.contains("spam"));
938 }
939
940 #[test]
941 fn test_apply_cli_with_quoted_value() {
942 let mut cfg = Config::default();
943 cfg.apply_cli_arg("label", "\"My Archive\"").unwrap();
944 assert_eq!(cfg.label.as_deref(), Some("\"My Archive\""));
945 }
946
947 #[test]
948 fn test_apply_cli_unknown_key() {
949 let mut cfg = Config::default();
950 assert!(cfg.apply_cli_arg("nonexistent", "value").is_err());
951 }
952
953 #[test]
954 fn test_htmlheaderfile_sets_both_i_and_m() {
955 let mut cfg = Config::default();
956 cfg.apply_cli_arg("htmlheaderfile", "/path/to/header.html").unwrap();
957 assert_eq!(cfg.ihtmlheader.as_deref(), Some("/path/to/header.html"));
958 assert_eq!(cfg.mhtmlheader.as_deref(), Some("/path/to/header.html"));
959 }
960
961 #[test]
962 fn test_htmlfooterfile_sets_both_i_and_m() {
963 let mut cfg = Config::default();
964 cfg.apply_cli_arg("htmlfooterfile", "/path/to/footer.html").unwrap();
965 assert_eq!(cfg.ihtmlfooter.as_deref(), Some("/path/to/footer.html"));
966 assert_eq!(cfg.mhtmlfooter.as_deref(), Some("/path/to/footer.html"));
967 }
968
969 #[test]
970 fn test_htmlheaderfile_does_not_override_specific_variants() {
971 let mut cfg = Config::default();
972 cfg.apply_cli_arg("ihtmlheaderfile", "/index/header.html").unwrap();
973 cfg.apply_cli_arg("htmlheaderfile", "/shared/header.html").unwrap();
974 assert_eq!(cfg.ihtmlheader.as_deref(), Some("/shared/header.html"));
976 assert_eq!(cfg.mhtmlheader.as_deref(), Some("/shared/header.html"));
977 }
978
979 #[test]
980 fn test_deprecated_showhr_returns_deprecation_error() {
981 let mut cfg = Config::default();
982 let err = cfg.apply_cli_arg("showhr", "1").unwrap_err();
983 assert!(
984 err.to_string().contains("deprecated config key"),
985 "expected deprecation message, got: {err}"
986 );
987 }
988
989 #[test]
990 fn test_deprecated_usetable_returns_deprecation_error() {
991 let mut cfg = Config::default();
992 let err = cfg.apply_cli_arg("usetable", "1").unwrap_err();
993 assert!(
994 err.to_string().contains("deprecated config key"),
995 "expected deprecation message, got: {err}"
996 );
997 }
998
999 #[test]
1000 fn test_deprecated_body_returns_deprecation_error() {
1001 let mut cfg = Config::default();
1002 let err = cfg.apply_cli_arg("body", "1").unwrap_err();
1003 assert!(
1004 err.to_string().contains("deprecated config key"),
1005 "expected deprecation message, got: {err}"
1006 );
1007 }
1008
1009 #[test]
1010 fn test_env_loading() {
1011 let mut cfg = Config::default();
1012 let vars = vec![("HM_LANGUAGE".to_string(), "de".to_string())];
1013 cfg.load_env_from(vars.into_iter());
1014 assert_eq!(cfg.language, "de");
1015 }
1016
1017 #[test]
1018 fn test_set_all_switches() {
1019 let mut cfg = Config::default();
1020 for (key, initial) in &[
1021 ("email_address_obfuscation", false),
1022 ("i18n", true),
1023 ("i18n_body", false),
1024 ("overwrite", false),
1025 ("inlinehtml", true),
1026 ("readone", false),
1027 ("reverse", false),
1028 ("reverse_folders", false),
1029 ("showheaders", true),
1030 ("showbr", true),
1031 ("showreplies", true),
1032 ("indextable", false),
1033 ("iquotes", true),
1034 ("eurodate", true),
1035 ("gmtime", false),
1036 ("isodate", false),
1037 ("require_msgids", true),
1038 ("discard_dup_msgids", true),
1039 ("usemeta", false),
1040 ("uselock", true),
1041 ("ietf_mbox", false),
1042 ("linkquotes", false),
1043 ("monthly_index", false),
1044 ("yearly_index", false),
1045 ("spamprotect", true),
1046 ("spamprotect_id", true),
1047 ("attachmentsindex", true),
1048 ("usegdbm", false),
1049 ("writehaof", false),
1050 ("append", false),
1051 ("nonsequential", false),
1052 ("warn_surpressions", true),
1053 ("files_by_thread", false),
1054 ("href_detection", true),
1055 ("mbox_shortened", false),
1056 ("report_new_file", false),
1057 ("report_new_folder", false),
1058 ("use_sender_date", false),
1059 ("inline_addlink", true),
1060 ("iso2022jp", false),
1061 ("delete_incremental", true),
1062 ("showgenerator", true),
1063 ("show_warnings", false),
1064 ("external_fonts", true),
1065 ] {
1066 assert!(cfg.set_switch(key, *initial).is_ok(), "switch {} should exist", key);
1067 }
1068 }
1069
1070 #[test]
1071 fn test_set_all_integers() {
1072 let mut cfg = Config::default();
1073 for key in &[
1074 "increment",
1075 "showhtml",
1076 "show_msg_links",
1077 "show_index_links",
1078 "thrdlevels",
1079 "dirmode",
1080 "filemode",
1081 "locktime",
1082 "searchbackmsgnum",
1083 "quote_hide_threshold",
1084 "thread_file_depth",
1085 "startmsgnum",
1086 "msgsperfolder",
1087 "save_alts",
1088 "delete_level",
1089 "progress",
1090 "max_message_size",
1091 "max_messages",
1092 ] {
1093 assert!(cfg.set_integer(key, 1).is_ok(), "integer {} should exist", key);
1094 }
1095 }
1096
1097 #[test]
1098 fn test_set_all_strings() {
1099 let mut cfg = Config::default();
1100 for key in &[
1101 "fragment_prefix",
1102 "htmlmessage_deleted",
1103 "antispam_at",
1104 "antispamdomain",
1105 "language",
1106 "htmlsuffix",
1107 "mbox",
1108 "archives",
1109 "custom_archives",
1110 "about",
1111 "label",
1112 "dir",
1113 "defaultindex",
1114 "default_top_index",
1115 "mailcommand",
1116 "newmsg_command",
1117 "replymsg_command",
1118 "inreplyto_command",
1119 "mailto",
1120 "hmail",
1121 "domainaddr",
1122 "css",
1123 "icss_url",
1124 "mcss_url",
1125 "dateformat",
1126 "indexdateformat",
1127 "stripsubject",
1128 "link_to_replies",
1129 "quote_link_string",
1130 "ihtmlheaderfile",
1131 "ihtmlfooterfile",
1132 "ihtmlheadfile",
1133 "ihtmlhelpupfile",
1134 "ihtmlhelplowfile",
1135 "ihtmlnavbar2upfile",
1136 "mhtmlheaderfile",
1137 "mhtmlfooterfile",
1138 "htmlheaderfile",
1139 "htmlfooterfile",
1140 "attachmentlink",
1141 "bodyheader",
1142 "bodyheaderend",
1143 "bodyfooter",
1144 "unsafe_chars",
1145 "filename_base",
1146 "folder_by_date",
1147 "latest_folder",
1148 "base_url",
1149 "describe_folder",
1150 "delete_older",
1151 "delete_newer",
1152 "alts_text",
1153 "description",
1154 "theme",
1155 "append_filename",
1156 "txtsuffix",
1157 ] {
1158 assert!(cfg.set_string(key, "test").is_ok(), "string {} should exist", key);
1159 }
1160 }
1161
1162 #[test]
1163 fn test_set_all_lists() {
1164 let mut cfg = Config::default();
1165 for key in &[
1166 "show_headers",
1167 "avoid_indices",
1168 "avoid_top_indices",
1169 "text_types",
1170 "inline_types",
1171 "prefered_types",
1172 "ignore_types",
1173 "filter_out",
1174 "filter_require",
1175 "filter_out_full_body",
1176 "filter_require_full_body",
1177 "deleted",
1178 "expires",
1179 "delete_msgnum",
1180 ] {
1181 assert!(cfg.set_list(key, "test").is_ok(), "list {} should exist", key);
1182 }
1183 }
1184
1185 #[test]
1186 fn test_config_file_content_can_parse() {
1187 let config_content = "\
1188# comment
1189set language=de
1190hm_overwrite=On
1191nonsequential On
1192dirmode 0755
1193mbox mailbox/test
1194label \"My Archive\"
1195";
1196 let mut cfg = Config::default();
1198 for line in config_content.lines() {
1199 let line = line.trim();
1200 if line.is_empty() || line.starts_with('#') {
1201 continue;
1202 }
1203 let line = line.strip_prefix("set ").unwrap_or(line);
1204 let eq_pos = line.find('=').or_else(|| line.find(':'));
1205 if let Some(eq_pos) = eq_pos {
1206 let key = line[..eq_pos].trim();
1207 let val = line[eq_pos + 1..].trim();
1208 let val = val.trim_matches('"');
1209 cfg.apply_cli_arg(key, val).unwrap_or_else(|e| {
1210 panic!("Failed to parse line '{}': {e}", line);
1211 });
1212 }
1213 }
1214 assert_eq!(cfg.language, "de");
1215 assert!(cfg.overwrite);
1216 }
1217
1218 #[test]
1219 fn test_antispamdomain_roundtrip() {
1220 let mut cfg = Config::default();
1221 cfg.set_string("antispamdomain", "nospam.invalid").unwrap();
1222 assert_eq!(cfg.antispamdomain.as_deref(), Some("nospam.invalid"));
1223 }
1224
1225 #[test]
1226 fn test_antispamdomain_none_on_empty() {
1227 let mut cfg = Config::default();
1228 cfg.set_string("antispamdomain", "").unwrap();
1229 assert!(cfg.antispamdomain.is_none());
1230 }
1231
1232 #[test]
1233 fn test_antispamdomain_none_on_keyword() {
1234 let mut cfg = Config::default();
1235 cfg.set_string("antispamdomain", "NONE").unwrap();
1236 assert!(cfg.antispamdomain.is_none());
1237 }
1238}