1use crate::{Msg, Security, Session, SortKey, mailbox_exists};
17
18pub enum Ask {
20 Line {
22 label: String,
24 prefill: String,
26 wants: Wants,
29 what: AskKind,
30 },
31 Key { label: String, what: AskKind },
33}
34
35#[derive(Clone, Copy, PartialEq, Eq)]
37pub enum Wants {
38 Mailbox,
39 Pattern,
40 Address,
41 Command,
42 Other,
43}
44
45#[derive(Clone, Copy, PartialEq, Eq)]
48pub enum Key {
49 Char(char),
50 Enter,
51 Other,
52}
53
54pub enum Answer<'a> {
56 Line(&'a str),
57 Key(Key),
58}
59
60#[derive(Clone)]
65pub enum AskKind {
66 Limit,
68 Search {
70 back: bool,
71 },
72 Pattern {
74 op: PatternOp,
75 },
76 CopyTo {
79 delete: bool,
80 tagged: bool,
81 decode: bool,
84 },
85 Pipe {
86 tagged: bool,
87 },
88 PrintConfirm {
91 tagged: bool,
92 default_yes: bool,
93 },
94 BounceTo {
95 tagged: bool,
96 },
97 BounceConfirm {
98 to: String,
99 tagged: bool,
100 },
101 Purge {
103 quit: bool,
104 },
105 Sort,
107 AliasNick {
109 addr: String,
110 },
111 EditLabel {
114 tagged: bool,
115 },
116 ReplyTo,
118 ComposeTo,
120 ComposeCc,
122 ComposeBcc,
123 ComposeSubject,
125 NoSubject {
128 default_yes: bool,
129 },
130 IncludeReply {
133 default_yes: bool,
134 },
135 ForwardAttach,
137 NoAttach,
140 EditHeader {
142 name: String,
143 },
144 EditFcc,
146 AttachFile,
148 AttachField {
150 k: usize,
151 is_type: bool,
152 },
153 Security,
155 PostponeAsk {
157 default_yes: bool,
158 },
159 Shell,
161 QuitConfirm,
163 AppendConfirm {
165 input: String,
166 delete: bool,
167 tagged: bool,
168 decode: bool,
169 },
170}
171
172#[derive(Clone, Copy, PartialEq, Eq)]
174pub enum PatternOp {
175 Delete,
176 Undelete,
177 Tag,
178 Untag,
179}
180
181impl PatternOp {
182 fn verb(self) -> &'static str {
184 match self {
185 PatternOp::Delete => "deleted",
186 PatternOp::Undelete => "undeleted",
187 PatternOp::Tag => "tagged",
188 PatternOp::Untag => "untagged",
189 }
190 }
191
192 fn label(self) -> &'static str {
194 match self {
195 PatternOp::Delete => "Delete",
196 PatternOp::Undelete => "Undelete",
197 PatternOp::Tag => "Tag",
198 PatternOp::Untag => "Untag",
199 }
200 }
201
202 fn apply(self, m: &mut Msg) {
203 match self {
204 PatternOp::Delete => {
205 if !m.env.file.flags.deleted {
206 m.env.file.flags.deleted = true;
207 m.dirty = true;
208 }
209 }
210 PatternOp::Undelete => {
211 if m.env.file.flags.deleted {
212 m.env.file.flags.deleted = false;
213 m.dirty = true;
214 }
215 }
216 PatternOp::Tag => m.env.tagged = true,
217 PatternOp::Untag => m.env.tagged = false,
218 }
219 }
220}
221
222pub enum Request {
225 Quit,
227 ConfigChanged,
230 Command(rmut_core::command::Command),
233 MailboxesChanged,
236 ShowMessage(Box<rmut_core::message::MessageView>),
239 ShowDraft,
242 Shell(String),
244 Suspend,
246 Editor(crate::Compose),
250}
251
252impl Session {
253 pub fn ask_limit(&self) -> Ask {
255 Ask::Line {
256 label: "Limit (~f/~s/~b/~t/~c/~d/flags, ! | (), empty=all): ".into(),
257 prefill: self
258 .limit
259 .as_ref()
260 .map(|(raw, _)| raw.clone())
261 .unwrap_or_default(),
262 wants: Wants::Pattern,
263 what: AskKind::Limit,
264 }
265 }
266
267 pub fn ask_search(&self, back: bool) -> Ask {
268 Ask::Line {
269 label: match back {
270 true => "Reverse search: ".into(),
271 false => "Search: ".into(),
272 },
273 prefill: String::new(),
274 wants: Wants::Pattern,
275 what: AskKind::Search { back },
276 }
277 }
278
279 pub fn ask_pattern(&mut self, op: PatternOp) -> Option<Ask> {
282 if matches!(op, PatternOp::Delete | PatternOp::Undelete) && self.deny_readonly() {
283 return None;
284 }
285 Some(Ask::Line {
286 label: format!("{} messages matching: ", op.label()),
287 prefill: String::new(),
288 wants: Wants::Pattern,
289 what: AskKind::Pattern { op },
290 })
291 }
292
293 pub fn ask_copy(&mut self, delete: bool, tagged: bool) -> Option<Ask> {
296 self.ask_copy_decode(delete, tagged, false)
297 }
298
299 pub fn ask_copy_decode(&mut self, delete: bool, tagged: bool, decode: bool) -> Option<Ask> {
302 self.visible.get(self.sel)?;
303 if delete && self.deny_readonly() {
305 return None;
306 }
307 Some(Ask::Line {
308 label: match (delete, decode) {
309 (true, false) => "Save to mailbox: ".into(),
310 (false, false) => "Copy to mailbox: ".into(),
311 (true, true) => "Decode-save to mailbox: ".into(),
312 (false, true) => "Decode-copy to mailbox: ".into(),
313 },
314 prefill: self
315 .save_name_target()
316 .or_else(|| self.config.mail.save.clone())
317 .unwrap_or_default(),
318 wants: Wants::Mailbox,
319 what: AskKind::CopyTo {
320 delete,
321 tagged,
322 decode,
323 },
324 })
325 }
326
327 fn save_name_target(&self) -> Option<String> {
331 if !self.config.mail.save_name && !self.config.mail.force_name {
332 return None;
333 }
334 let &i = self.visible.get(self.sel)?;
335 let from = rmut_core::message::first_header(&self.msgs[i].env.file.path, "From")?;
336 let address = rmut_core::compose::bare_address(&from)?;
337 let local = address.split('@').next()?.to_lowercase();
338 if local.is_empty() {
339 return None;
340 }
341 let spec = format!("={local}");
342 let expanded = self.expand_folder(&spec);
343 match self.config.mail.force_name || mailbox_exists(&expanded) {
344 true => Some(spec),
345 false => None,
346 }
347 }
348
349 pub fn ask_pipe(&self, tagged: bool) -> Option<Ask> {
350 self.visible.get(self.sel)?;
351 Some(Ask::Line {
352 label: "Pipe to command: ".into(),
353 prefill: String::new(),
354 wants: Wants::Command,
355 what: AskKind::Pipe { tagged },
356 })
357 }
358
359 pub fn ask_bounce(&self, tagged: bool) -> Option<Ask> {
360 self.visible.get(self.sel)?;
361 Some(Ask::Line {
362 label: "Bounce message to: ".into(),
363 prefill: String::new(),
364 wants: Wants::Address,
365 what: AskKind::BounceTo { tagged },
366 })
367 }
368
369 pub fn ask_print(&mut self, tagged: bool) -> Option<Ask> {
373 self.visible.get(self.sel)?;
374 let quad = self
375 .config
376 .mail
377 .print_confirm
378 .clone()
379 .unwrap_or_else(|| "ask-no".into());
380 match quad.as_str() {
381 "no" => {
382 self.error("printing is off ([mail] print_confirm)");
383 return None;
384 }
385 "yes" => {
386 self.print_current(tagged);
387 return None;
388 }
389 _ => {}
390 }
391 let n = self.op_targets(tagged).len();
392 Some(Ask::Key {
393 label: match n {
394 1 => "Print message? (y/n): ".to_string(),
395 _ => format!("Print {n} messages? (y/n): "),
396 },
397 what: AskKind::PrintConfirm {
398 tagged,
399 default_yes: quad == "ask-yes",
400 },
401 })
402 }
403
404 pub fn ask_alias(&mut self) -> Option<Ask> {
406 let &i = self.visible.get(self.sel)?;
407 let path = self.msgs[i].env.file.path.clone();
408 let Some(from) = rmut_core::message::first_header(&path, "From") else {
409 self.error("the message has no From header");
410 return None;
411 };
412 let nick = rmut_core::compose::bare_address(&from)
413 .and_then(|a| a.split('@').next().map(|l| l.to_lowercase()))
414 .unwrap_or_default();
415 Some(Ask::Line {
416 label: "Alias as (nick): ".into(),
417 prefill: nick,
418 wants: Wants::Other,
419 what: AskKind::AliasNick {
420 addr: from.trim().to_string(),
421 },
422 })
423 }
424
425 pub fn ask_edit_label(&self, tagged: bool) -> Option<Ask> {
428 let targets = self.op_targets(tagged);
429 if targets.is_empty() {
430 return None;
431 }
432 let prefill = if !tagged && targets.len() == 1 {
433 self.msgs[targets[0]].env.label.clone().unwrap_or_default()
434 } else {
435 String::new()
436 };
437 Some(Ask::Line {
438 label: "Label: ".into(),
439 prefill,
440 wants: Wants::Other,
441 what: AskKind::EditLabel { tagged },
442 })
443 }
444
445 pub fn ask_sort(&self) -> Ask {
446 Ask::Key {
447 label: "Sort: (d)ate (f)rom (s)ubject si(z)e (t)hreads (y) label, uppercase reverses: "
448 .into(),
449 what: AskKind::Sort,
450 }
451 }
452
453 pub fn leave(&mut self) -> Option<Ask> {
456 match self.config.mail.quit.as_deref().unwrap_or("yes") {
457 "no" => {
458 self.error("quitting is off ($quit = no)");
459 None
460 }
461 "yes" => self.leave_now(),
462 quit => {
463 self.quit_default = quit != "ask-no";
466 Some(Ask::Key {
467 label: "Quit rmut? (y/n): ".into(),
468 what: AskKind::QuitConfirm,
469 })
470 }
471 }
472 }
473
474 fn leave_now(&mut self) -> Option<Ask> {
477 self.mark_old_unread();
478 if self.deleted_count() > 0 {
481 return self.ask_purge(true);
482 }
483 if self.pending_count() > 0 {
484 self.sync(true);
485 }
486 self.requests.push(Request::Quit);
487 None
488 }
489
490 pub fn ask_purge(&mut self, quit: bool) -> Option<Ask> {
496 match self.config.mail.delete.as_deref() {
497 Some("yes") | Some("no") => {
498 let purge = self.config.mail.delete.as_deref() == Some("yes");
499 self.sync(purge);
500 if quit {
501 self.requests.push(Request::Quit);
502 }
503 None
504 }
505 _ => Some(Ask::Key {
506 label: format!("Purge {} deleted message(s)? (y/n): ", self.deleted_count()),
507 what: AskKind::Purge { quit },
508 }),
509 }
510 }
511
512 pub fn ask_header(&self, name: &str) -> Option<Ask> {
514 self.draft()?;
515 Some(Ask::Line {
516 label: format!("{name}: "),
517 prefill: self.draft_header(name),
518 wants: match name {
519 "Subject" => Wants::Other,
520 _ => Wants::Address,
521 },
522 what: AskKind::EditHeader {
523 name: name.to_string(),
524 },
525 })
526 }
527
528 pub fn ask_fcc(&self) -> Option<Ask> {
530 let draft = self.draft()?;
531 Some(Ask::Line {
532 label: "Fcc: ".into(),
533 prefill: match draft.fcc.clone() {
534 Some(fcc) => fcc,
535 None => self.default_fcc(Some(draft)),
536 },
537 wants: Wants::Mailbox,
538 what: AskKind::EditFcc,
539 })
540 }
541
542 pub fn ask_attach_file(&self) -> Option<Ask> {
543 self.draft()?;
544 Some(Ask::Line {
545 label: "Attach file: ".into(),
546 prefill: String::new(),
547 wants: Wants::Other,
548 what: AskKind::AttachFile,
549 })
550 }
551
552 pub fn ask_attach_field(&mut self, sel: usize, is_type: bool) -> Option<Ask> {
555 let draft = self.draft()?;
556 let fixed = 1 + usize::from(draft.attach.is_some());
557 if sel < fixed {
558 self.error("only Attach: files can be edited");
559 return None;
560 }
561 let k = sel - fixed;
562 let attachment = crate::draft_full(draft)
563 .ok()
564 .map(|full| rmut_core::compose::extract_attachments(&full).1)
565 .and_then(|mut atts| (k < atts.len()).then(|| atts.swap_remove(k)))?;
566 let prefill = match is_type {
567 true => attachment
568 .mime
569 .clone()
570 .unwrap_or_else(|| rmut_core::compose::content_type(&attachment.path).to_string()),
571 false => attachment.description.clone().unwrap_or_default(),
572 };
573 Some(Ask::Line {
574 label: match is_type {
575 true => "Content-Type: ".into(),
576 false => "Description: ".into(),
577 },
578 prefill,
579 wants: Wants::Other,
580 what: AskKind::AttachField { k, is_type },
581 })
582 }
583
584 pub fn ask_security(&self) -> Option<Ask> {
585 self.draft()?;
586 Some(Ask::Key {
587 label: "Security: (e)ncrypt (s)ign (b)oth (c)lear: ".into(),
588 what: AskKind::Security,
589 })
590 }
591
592 pub fn ask_postpone(&mut self) -> Option<Ask> {
593 self.draft()?;
594 match self
597 .config
598 .mail
599 .postpone
600 .as_deref()
601 .unwrap_or("ask-yes")
602 .trim()
603 .to_lowercase()
604 .as_str()
605 {
606 "yes" => {
607 if let Some(draft) = self.take_draft() {
608 self.postpone_draft(draft);
609 }
610 None
611 }
612 "no" => {
613 if let Some(draft) = self.take_draft() {
614 let _ = std::fs::remove_file(&draft.path);
615 self.note("message discarded");
616 }
617 None
618 }
619 other => Some(Ask::Key {
620 label: "Postpone this message? (y/n): ".into(),
621 what: AskKind::PostponeAsk {
622 default_yes: other != "ask-no",
623 },
624 }),
625 }
626 }
627
628 pub fn ask_shell(&self) -> Ask {
630 Ask::Line {
631 label: "Shell command: ".into(),
632 prefill: String::new(),
633 wants: Wants::Command,
634 what: AskKind::Shell,
635 }
636 }
637
638 pub fn request_suspend(&mut self) {
641 self.requests.push(Request::Suspend);
642 }
643
644 pub fn answer(&mut self, what: AskKind, answer: Answer<'_>) -> Option<Ask> {
646 match (what, answer) {
647 (AskKind::Limit, Answer::Line(input)) => {
648 self.set_limit(input);
649 None
650 }
651 (AskKind::Search { back }, Answer::Line(input)) => {
652 self.search_rev = back;
653 if !input.is_empty() {
654 match self.compile_search(input) {
655 Ok(patterns) => {
656 self.resolve_body_terms(&patterns);
657 self.last_search = Some(patterns);
658 }
659 Err(err) => {
660 self.error(format!("bad pattern: {err}"));
661 return None;
662 }
663 }
664 }
665 self.search_next();
666 None
667 }
668 (AskKind::Pattern { op }, Answer::Line(input)) => {
669 self.apply_pattern(input, op.verb(), |m| op.apply(m));
670 None
671 }
672 (
673 AskKind::CopyTo {
674 delete,
675 tagged,
676 decode,
677 },
678 Answer::Line(input),
679 ) => {
680 let input = self.expand_folder(input);
681 if self.config.mail.confirmappend && mailbox_exists(&input) {
684 return Some(Ask::Key {
685 label: format!("Append messages to {input}? (y/n): "),
686 what: AskKind::AppendConfirm {
687 input,
688 delete,
689 tagged,
690 decode,
691 },
692 });
693 }
694 self.copy_message(&input, delete, tagged, decode);
695 None
696 }
697 (
698 AskKind::AppendConfirm {
699 input,
700 delete,
701 tagged,
702 decode,
703 },
704 Answer::Key(key),
705 ) => {
706 if matches!(key, Key::Char('y') | Key::Enter) {
708 self.copy_message(&input, delete, tagged, decode);
709 }
710 None
711 }
712 (AskKind::QuitConfirm, Answer::Key(key)) => {
713 let yes = match key {
714 Key::Char('y') => true,
715 Key::Char('n') => false,
716 Key::Enter => self.quit_default,
717 _ => false,
718 };
719 match yes {
720 true => self.leave_now(),
721 false => None,
722 }
723 }
724 (AskKind::Pipe { tagged }, Answer::Line(command)) => {
725 self.pipe_message(command, tagged);
726 None
727 }
728 (AskKind::AliasNick { addr }, Answer::Line(nick)) => {
729 self.create_alias(nick, &addr);
730 None
731 }
732 (AskKind::EditLabel { tagged }, Answer::Line(input)) => {
733 self.edit_label(input, tagged);
734 None
735 }
736 (AskKind::ReplyTo, Answer::Key(key)) => match key {
738 Key::Char('y') | Key::Enter => self.answer_reply_to(true),
739 Key::Char('n') => self.answer_reply_to(false),
740 _ => {
741 self.cancel_setup();
742 self.note("reply cancelled");
743 None
744 }
745 },
746 (AskKind::EditHeader { name }, Answer::Line(input)) => {
747 let value = match name.as_str() {
748 "Subject" => input.to_string(),
749 _ => rmut_core::alias::expand(
750 input,
751 &rmut_core::alias::load(self.config.mail.alias_file.as_deref()),
752 ),
753 };
754 self.set_draft_header(&name, &value);
755 None
756 }
757 (AskKind::Shell, Answer::Line(command)) => {
758 if !command.is_empty() {
759 self.requests.push(Request::Shell(command.to_string()));
760 }
761 None
762 }
763 (AskKind::EditFcc, Answer::Line(input)) => {
764 if let Some(draft) = self.draft_mut() {
765 draft.fcc = Some(input.trim().to_string());
766 }
767 None
768 }
769 (AskKind::AttachFile, Answer::Line(input)) => {
770 self.attach_file(input);
771 None
772 }
773 (AskKind::AttachField { k, is_type }, Answer::Line(input)) => {
774 self.set_attach_field(k, input, is_type);
775 None
776 }
777 (AskKind::Security, Answer::Key(key)) => {
778 if let Some(draft) = self.draft_mut() {
779 draft.security = match key {
780 Key::Char('e') => Security::Encrypt,
781 Key::Char('s') => Security::Sign,
782 Key::Char('b') => Security::Both,
783 Key::Char('c') => Security::None,
784 _ => draft.security,
785 };
786 }
787 None
788 }
789 (AskKind::PostponeAsk { default_yes }, Answer::Key(key)) => {
790 let postpone = match key {
791 Key::Char('y') => true,
792 Key::Char('n') => false,
793 Key::Enter => default_yes,
794 _ => {
796 self.requests.push(Request::ShowDraft);
797 return None;
798 }
799 };
800 if let Some(draft) = self.take_draft() {
801 if postpone {
802 self.postpone_draft(draft);
803 } else {
804 let _ = std::fs::remove_file(&draft.path);
805 self.note("message discarded");
806 }
807 }
808 None
809 }
810 (AskKind::NoAttach, Answer::Key(key)) => match key {
811 Key::Char('y') => {
814 self.confirm_attachment();
815 self.send_draft()
816 }
817 _ => {
818 self.note("not sent; a attaches a file");
819 self.requests.push(Request::ShowDraft);
820 None
821 }
822 },
823 (AskKind::ComposeTo, Answer::Line(input)) => self.answer_to(input),
824 (AskKind::ComposeCc, Answer::Line(input)) => self.answer_cc(input),
825 (AskKind::ComposeBcc, Answer::Line(input)) => self.answer_bcc(input),
826 (AskKind::ComposeSubject, Answer::Line(input)) => self.answer_subject(input),
827 (AskKind::NoSubject { default_yes }, Answer::Key(key)) => match key {
828 Key::Char('n') => self.answer_subject_kept(),
829 Key::Char('y') => {
830 self.cancel_setup();
831 self.error("aborted (no subject)");
832 None
833 }
834 Key::Enter if !default_yes => self.answer_subject_kept(),
836 _ => {
837 self.cancel_setup();
838 self.error("aborted (no subject)");
839 None
840 }
841 },
842 (AskKind::IncludeReply { default_yes }, Answer::Key(key)) => match key {
843 Key::Char('n') => self.answer_include(false),
844 Key::Char('y') => self.answer_include(true),
845 Key::Enter => self.answer_include(default_yes),
846 _ => {
847 self.cancel_setup();
848 self.note("reply cancelled");
849 None
850 }
851 },
852 (AskKind::ForwardAttach, Answer::Key(key)) => match key {
853 Key::Char('n') => self.answer_forward_attach(false),
855 Key::Char('y') | Key::Enter => self.answer_forward_attach(true),
856 _ => {
857 self.cancel_setup();
858 self.note("forward cancelled");
859 None
860 }
861 },
862 (AskKind::BounceTo { tagged }, Answer::Line(input)) => {
863 let to = rmut_core::alias::expand(
864 input,
865 &rmut_core::alias::load(self.config.mail.alias_file.as_deref()),
866 );
867 if to.trim().is_empty() {
868 self.note("no recipients, bounce cancelled");
869 return None;
870 }
871 let n = self.op_targets(tagged).len();
872 Some(Ask::Key {
873 label: match n {
874 1 => format!("Bounce message to {to}? (y/n): "),
875 _ => format!("Bounce {n} messages to {to}? (y/n): "),
876 },
877 what: AskKind::BounceConfirm { to, tagged },
878 })
879 }
880 (AskKind::BounceConfirm { to, tagged }, Answer::Key(key)) => {
881 if key == Key::Char('y') {
882 self.bounce_current(&to, tagged);
883 }
884 None
885 }
886 (
887 AskKind::PrintConfirm {
888 tagged,
889 default_yes,
890 },
891 Answer::Key(key),
892 ) => {
893 if key == Key::Char('y') || (default_yes && key == Key::Enter) {
894 self.print_current(tagged);
895 }
896 None
897 }
898 (AskKind::Purge { quit }, Answer::Key(key)) => {
899 if matches!(key, Key::Char('y') | Key::Char('n') | Key::Enter) {
904 self.sync(key != Key::Char('n'));
905 if quit {
906 self.requests.push(Request::Quit);
907 }
908 }
909 None
910 }
911 (AskKind::Sort, Answer::Key(key)) => {
912 let (sort, rev) = match key {
913 Key::Char('d') => (SortKey::Date, false),
914 Key::Char('D') => (SortKey::Date, true),
915 Key::Char('f') => (SortKey::From, false),
916 Key::Char('F') => (SortKey::From, true),
917 Key::Char('s') => (SortKey::Subject, false),
918 Key::Char('S') => (SortKey::Subject, true),
919 Key::Char('z') => (SortKey::Size, false),
920 Key::Char('Z') => (SortKey::Size, true),
921 Key::Char('t') | Key::Char('T') => (SortKey::Threads, false),
922 Key::Char('o') => (SortKey::To, false),
923 Key::Char('O') => (SortKey::To, true),
924 Key::Char('y') => (SortKey::Label, false),
925 Key::Char('Y') => (SortKey::Label, true),
926 Key::Char('u') => (SortKey::Unsorted, false),
927 Key::Char('U') => (SortKey::Unsorted, true),
928 _ => return None,
929 };
930 self.sort = sort;
931 self.sort_rev = rev;
932 self.apply_sort();
933 self.note(format!(
934 "sorted by {}{}",
935 sort.name(),
936 if rev { " (reverse)" } else { "" }
937 ));
938 None
939 }
940 _ => None,
943 }
944 }
945
946 pub fn take_request(&mut self) -> Option<Request> {
948 match self.requests.is_empty() {
949 true => None,
950 false => Some(self.requests.remove(0)),
951 }
952 }
953
954 fn expand_folder(&self, input: &str) -> String {
956 rmut_core::config::expand_folder(input, self.config.mail.folder.as_deref())
957 }
958
959 fn set_limit(&mut self, input: &str) {
962 let keep = self.selected_path();
963 if input.is_empty() || input == "all" {
964 self.limit = None;
965 } else {
966 match self.compile_search(input) {
967 Ok(patterns) => {
968 self.resolve_body_terms(&patterns);
969 self.limit = Some((input.to_string(), patterns));
970 }
971 Err(err) => {
972 self.error(format!("bad pattern: {err}"));
973 return;
974 }
975 }
976 }
977 self.rebuild_visible(keep);
978 if self.visible.is_empty() {
979 self.note("no messages match the limit");
980 }
981 }
982}