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 MarkMessage {
71 msg_id: String,
72 },
73 ListAction {
76 actions: Vec<(&'static str, Option<String>)>,
77 },
78 Search {
80 back: bool,
81 },
82 Pattern {
84 op: PatternOp,
85 },
86 CopyTo {
89 delete: bool,
90 tagged: bool,
91 decode: bool,
94 },
95 Pipe {
96 tagged: bool,
97 },
98 PrintConfirm {
101 tagged: bool,
102 default_yes: bool,
103 },
104 BounceTo {
105 tagged: bool,
106 },
107 BounceConfirm {
108 to: String,
109 tagged: bool,
110 },
111 Purge {
113 quit: bool,
114 },
115 Sort,
117 AliasNick {
119 addr: String,
120 },
121 EditLabel {
124 tagged: bool,
125 },
126 ReplyTo,
128 ComposeTo,
130 ComposeCc,
132 ComposeBcc,
133 ComposeSubject,
135 NoSubject {
138 default_yes: bool,
139 },
140 IncludeReply {
143 default_yes: bool,
144 },
145 ForwardAttach,
147 ForwardEdit {
150 default_yes: bool,
151 },
152 FccAttach {
155 default_yes: bool,
156 },
157 NoAttach,
160 EditHeader {
162 name: String,
163 },
164 EditFcc,
166 AttachFile,
168 AttachField {
170 k: usize,
171 is_type: bool,
172 },
173 RenameAttachment {
175 k: usize,
176 },
177 NewMimeFile,
179 NewMimeType {
181 path: String,
182 },
183 WriteFcc,
186 Security,
188 PostponeAsk {
190 default_yes: bool,
191 },
192 Shell,
194 QuitConfirm,
196 AppendConfirm {
198 input: String,
199 delete: bool,
200 tagged: bool,
201 decode: bool,
202 },
203}
204
205#[derive(Clone, Copy, PartialEq, Eq)]
207pub enum PatternOp {
208 Delete,
209 Undelete,
210 Tag,
211 Untag,
212}
213
214impl PatternOp {
215 pub(crate) fn verb(self) -> &'static str {
217 match self {
218 PatternOp::Delete => "deleted",
219 PatternOp::Undelete => "undeleted",
220 PatternOp::Tag => "tagged",
221 PatternOp::Untag => "untagged",
222 }
223 }
224
225 fn label(self) -> &'static str {
227 match self {
228 PatternOp::Delete => "Delete",
229 PatternOp::Undelete => "Undelete",
230 PatternOp::Tag => "Tag",
231 PatternOp::Untag => "Untag",
232 }
233 }
234
235 pub(crate) fn apply(self, m: &mut Msg, flag_safe: bool) {
236 match self {
237 PatternOp::Delete => {
238 let safe = flag_safe && m.env.file.flags.flagged;
241 if !m.env.file.flags.deleted && !safe {
242 m.env.file.flags.deleted = true;
243 m.dirty = true;
244 }
245 }
246 PatternOp::Undelete => {
247 if m.env.file.flags.deleted {
248 m.env.file.flags.deleted = false;
249 m.dirty = true;
250 }
251 }
252 PatternOp::Tag => m.env.tagged = true,
253 PatternOp::Untag => m.env.tagged = false,
254 }
255 }
256}
257
258pub enum Request {
261 Quit,
263 ConfigChanged,
266 Command(rmut_core::command::Command),
269 MailboxesChanged,
272 FoldersChanged,
275 Opened(Vec<String>),
279 ShowMessage(Box<rmut_core::message::MessageView>),
282 ShowDraft,
285 Mailto(rmut_core::mailto::Mailto),
288 EditFile(std::path::PathBuf),
291 Shell(String),
293 Suspend,
295 Editor(crate::Compose),
299}
300
301impl Session {
302 pub fn ask_limit(&self) -> Ask {
304 Ask::Line {
305 label: "Limit (~f/~s/~b/~t/~c/~d/flags, ! | (), empty=all): ".into(),
306 prefill: self
307 .limit
308 .as_ref()
309 .map(|(raw, _)| raw.clone())
310 .unwrap_or_default(),
311 wants: Wants::Pattern,
312 what: AskKind::Limit,
313 }
314 }
315
316 pub fn ask_search(&self, back: bool) -> Ask {
317 Ask::Line {
318 label: match back {
319 true => "Reverse search: ".into(),
320 false => "Search: ".into(),
321 },
322 prefill: String::new(),
323 wants: Wants::Pattern,
324 what: AskKind::Search { back },
325 }
326 }
327
328 pub fn ask_pattern(&mut self, op: PatternOp) -> Option<Ask> {
331 if matches!(op, PatternOp::Delete | PatternOp::Undelete) && self.deny_readonly() {
332 return None;
333 }
334 Some(Ask::Line {
335 label: format!("{} messages matching: ", op.label()),
336 prefill: String::new(),
337 wants: Wants::Pattern,
338 what: AskKind::Pattern { op },
339 })
340 }
341
342 pub fn ask_mark_message(&mut self) -> Option<Ask> {
346 let Some(msg_id) = self
347 .visible
348 .get(self.sel)
349 .and_then(|&i| self.msgs[i].env.msg_id.clone())
350 else {
351 self.error("No message ID to macro.");
352 return None;
353 };
354 Some(Ask::Line {
355 label: "Enter macro stroke: ".into(),
356 prefill: String::new(),
357 wants: Wants::Other,
358 what: AskKind::MarkMessage { msg_id },
359 })
360 }
361
362 pub fn ask_list_action(&mut self) -> Option<Ask> {
365 let &i = self.visible.get(self.sel)?;
366 let raw = self.message_bytes(i)?;
367 let actions = rmut_core::message::list_actions(&raw);
368 if actions.iter().all(|(_, url)| url.is_none()) {
369 self.error("No list actions available for this message.");
370 return None;
371 }
372 let label = actions
373 .iter()
374 .map(|(name, url)| {
375 let (head, tail) = name.split_at(1);
376 match url {
377 Some(_) => format!("({}){tail}", head.to_lowercase()),
378 None => format!("-{}{tail}-", head.to_lowercase()),
379 }
380 })
381 .collect::<Vec<_>>()
382 .join(" ");
383 Some(Ask::Key {
384 label: format!("List action: {label}: "),
385 what: AskKind::ListAction { actions },
386 })
387 }
388
389 pub fn ask_copy(&mut self, delete: bool, tagged: bool) -> Option<Ask> {
392 self.ask_copy_decode(delete, tagged, false)
393 }
394
395 pub fn ask_copy_decode(&mut self, delete: bool, tagged: bool, decode: bool) -> Option<Ask> {
398 self.visible.get(self.sel)?;
399 if delete && self.deny_readonly() {
401 return None;
402 }
403 Some(Ask::Line {
404 label: match (delete, decode) {
405 (true, false) => "Save to mailbox: ".into(),
406 (false, false) => "Copy to mailbox: ".into(),
407 (true, true) => "Decode-save to mailbox: ".into(),
408 (false, true) => "Decode-copy to mailbox: ".into(),
409 },
410 prefill: self
411 .save_name_target()
412 .or_else(|| self.config.mail.save.clone())
413 .unwrap_or_default(),
414 wants: Wants::Mailbox,
415 what: AskKind::CopyTo {
416 delete,
417 tagged,
418 decode,
419 },
420 })
421 }
422
423 fn save_name_target(&self) -> Option<String> {
427 if !self.config.mail.save_name && !self.config.mail.force_name {
428 return None;
429 }
430 let &i = self.visible.get(self.sel)?;
431 let from = rmut_core::message::first_header(&self.msgs[i].env.file.path, "From")?;
432 let address = rmut_core::compose::bare_address(&from)?;
433 let local = address.split('@').next()?.to_lowercase();
434 if local.is_empty() {
435 return None;
436 }
437 let spec = format!("={local}");
438 let expanded = self.expand_folder(&spec);
439 match self.config.mail.force_name || mailbox_exists(&expanded) {
440 true => Some(spec),
441 false => None,
442 }
443 }
444
445 pub fn ask_pipe(&self, tagged: bool) -> Option<Ask> {
446 self.visible.get(self.sel)?;
447 Some(Ask::Line {
448 label: "Pipe to command: ".into(),
449 prefill: String::new(),
450 wants: Wants::Command,
451 what: AskKind::Pipe { tagged },
452 })
453 }
454
455 pub fn ask_bounce(&self, tagged: bool) -> Option<Ask> {
456 self.visible.get(self.sel)?;
457 Some(Ask::Line {
458 label: "Bounce message to: ".into(),
459 prefill: String::new(),
460 wants: Wants::Address,
461 what: AskKind::BounceTo { tagged },
462 })
463 }
464
465 pub fn ask_print(&mut self, tagged: bool) -> Option<Ask> {
469 self.visible.get(self.sel)?;
470 let quad = self
471 .config
472 .mail
473 .print_confirm
474 .clone()
475 .unwrap_or_else(|| "ask-no".into());
476 match quad.as_str() {
477 "no" => {
478 self.error("printing is off ([mail] print_confirm)");
479 return None;
480 }
481 "yes" => {
482 self.print_current(tagged);
483 return None;
484 }
485 _ => {}
486 }
487 let n = self.op_targets(tagged).len();
488 Some(Ask::Key {
489 label: match n {
490 1 => "Print message? (y/n): ".to_string(),
491 _ => format!("Print {n} messages? (y/n): "),
492 },
493 what: AskKind::PrintConfirm {
494 tagged,
495 default_yes: quad == "ask-yes",
496 },
497 })
498 }
499
500 pub fn ask_alias(&mut self) -> Option<Ask> {
502 let &i = self.visible.get(self.sel)?;
503 let path = self.msgs[i].env.file.path.clone();
504 let Some(from) = rmut_core::message::first_header(&path, "From") else {
505 self.error("the message has no From header");
506 return None;
507 };
508 let nick = rmut_core::compose::bare_address(&from)
509 .and_then(|a| a.split('@').next().map(|l| l.to_lowercase()))
510 .unwrap_or_default();
511 Some(Ask::Line {
512 label: "Alias as (nick): ".into(),
513 prefill: nick,
514 wants: Wants::Other,
515 what: AskKind::AliasNick {
516 addr: from.trim().to_string(),
517 },
518 })
519 }
520
521 pub fn ask_edit_label(&self, tagged: bool) -> Option<Ask> {
524 let targets = self.op_targets(tagged);
525 if targets.is_empty() {
526 return None;
527 }
528 let prefill = if !tagged && targets.len() == 1 {
529 self.msgs[targets[0]].env.label.clone().unwrap_or_default()
530 } else {
531 String::new()
532 };
533 Some(Ask::Line {
534 label: "Label: ".into(),
535 prefill,
536 wants: Wants::Other,
537 what: AskKind::EditLabel { tagged },
538 })
539 }
540
541 pub fn ask_sort(&self) -> Ask {
542 Ask::Key {
543 label: "Sort: (d)ate (f)rom (s)ubject si(z)e (t)hreads (y) label, uppercase reverses: "
544 .into(),
545 what: AskKind::Sort,
546 }
547 }
548
549 pub fn leave(&mut self) -> Option<Ask> {
552 match self.config.mail.quit.as_deref().unwrap_or("yes") {
553 "no" => {
554 self.error("quitting is off ($quit = no)");
555 None
556 }
557 "yes" => self.leave_now(),
558 quit => {
559 self.quit_default = quit != "ask-no";
562 Some(Ask::Key {
563 label: "Quit rmut? (y/n): ".into(),
564 what: AskKind::QuitConfirm,
565 })
566 }
567 }
568 }
569
570 fn leave_now(&mut self) -> Option<Ask> {
573 self.mark_old_unread();
574 if self.deleted_count() > 0 {
577 return self.ask_purge(true);
578 }
579 if self.pending_count() > 0 {
580 self.sync(true);
581 }
582 self.requests.push(Request::Quit);
583 None
584 }
585
586 pub fn ask_purge(&mut self, quit: bool) -> Option<Ask> {
592 match self.config.mail.delete.as_deref() {
593 Some("yes") | Some("no") => {
594 let purge = self.config.mail.delete.as_deref() == Some("yes");
595 self.sync(purge);
596 if quit {
597 self.requests.push(Request::Quit);
598 }
599 None
600 }
601 _ => Some(Ask::Key {
602 label: format!("Purge {} deleted message(s)? (y/n): ", self.deleted_count()),
603 what: AskKind::Purge { quit },
604 }),
605 }
606 }
607
608 pub fn ask_header(&self, name: &str) -> Option<Ask> {
610 self.draft()?;
611 Some(Ask::Line {
612 label: format!("{name}: "),
613 prefill: self.draft_header(name),
614 wants: match name {
615 "Subject" => Wants::Other,
616 _ => Wants::Address,
617 },
618 what: AskKind::EditHeader {
619 name: name.to_string(),
620 },
621 })
622 }
623
624 pub fn ask_fcc(&self) -> Option<Ask> {
626 let draft = self.draft()?;
627 Some(Ask::Line {
628 label: "Fcc: ".into(),
629 prefill: match draft.fcc.clone() {
630 Some(fcc) => fcc,
631 None => self.default_fcc(Some(draft)),
632 },
633 wants: Wants::Mailbox,
634 what: AskKind::EditFcc,
635 })
636 }
637
638 pub fn ask_attach_file(&self) -> Option<Ask> {
639 self.draft()?;
640 Some(Ask::Line {
641 label: "Attach file: ".into(),
642 prefill: String::new(),
643 wants: Wants::Other,
644 what: AskKind::AttachFile,
645 })
646 }
647
648 pub fn ask_attach_field(&mut self, sel: usize, is_type: bool) -> Option<Ask> {
651 let draft = self.draft()?;
652 let fixed = 1 + usize::from(draft.attach.is_some());
653 if sel < fixed {
654 self.error("only Attach: files can be edited");
655 return None;
656 }
657 let k = sel - fixed;
658 let attachment = crate::draft_full(draft)
659 .ok()
660 .map(|full| rmut_core::compose::extract_attachments(&full).1)
661 .and_then(|mut atts| (k < atts.len()).then(|| atts.swap_remove(k)))?;
662 let prefill = match is_type {
663 true => attachment
664 .mime
665 .clone()
666 .unwrap_or_else(|| rmut_core::compose::content_type(&attachment.path).to_string()),
667 false => attachment.description.clone().unwrap_or_default(),
668 };
669 Some(Ask::Line {
670 label: match is_type {
671 true => "Content-Type: ".into(),
672 false => "Description: ".into(),
673 },
674 prefill,
675 wants: Wants::Other,
676 what: AskKind::AttachField { k, is_type },
677 })
678 }
679
680 pub fn ask_rename_attachment(&mut self, sel: usize) -> Option<Ask> {
683 let k = self.attach_index(sel)?;
684 let name = self
685 .attachments()
686 .get(k)
687 .map(|a| a.send_name().to_string())
688 .unwrap_or_default();
689 Some(Ask::Line {
690 label: "Send attachment with name: ".into(),
691 prefill: name,
692 wants: Wants::Other,
693 what: AskKind::RenameAttachment { k },
694 })
695 }
696
697 pub fn ask_new_mime(&self) -> Option<Ask> {
699 self.draft()?;
700 Some(Ask::Line {
701 label: "New file: ".into(),
702 prefill: String::new(),
703 wants: Wants::Other,
704 what: AskKind::NewMimeFile,
705 })
706 }
707
708 pub fn ask_write_fcc(&self) -> Option<Ask> {
711 self.draft()?;
712 Some(Ask::Line {
713 label: "Write message to mailbox: ".into(),
714 prefill: self.title.clone(),
715 wants: Wants::Mailbox,
716 what: AskKind::WriteFcc,
717 })
718 }
719
720 pub fn ask_security(&self) -> Option<Ask> {
721 self.draft()?;
722 Some(Ask::Key {
723 label: "Security: (e)ncrypt (s)ign (b)oth (c)lear: ".into(),
724 what: AskKind::Security,
725 })
726 }
727
728 pub fn ask_postpone(&mut self) -> Option<Ask> {
729 self.draft()?;
730 match self
733 .config
734 .mail
735 .postpone
736 .as_deref()
737 .unwrap_or("ask-yes")
738 .trim()
739 .to_lowercase()
740 .as_str()
741 {
742 "yes" => {
743 if let Some(draft) = self.take_draft() {
744 self.postpone_draft(draft);
745 }
746 None
747 }
748 "no" => {
749 if let Some(draft) = self.take_draft() {
750 let _ = std::fs::remove_file(&draft.path);
751 self.note("message discarded");
752 }
753 None
754 }
755 other => Some(Ask::Key {
756 label: "Postpone this message? (y/n): ".into(),
757 what: AskKind::PostponeAsk {
758 default_yes: other != "ask-no",
759 },
760 }),
761 }
762 }
763
764 pub fn ask_shell(&self) -> Ask {
766 Ask::Line {
767 label: "Shell command: ".into(),
768 prefill: String::new(),
769 wants: Wants::Command,
770 what: AskKind::Shell,
771 }
772 }
773
774 pub fn request_suspend(&mut self) {
777 self.requests.push(Request::Suspend);
778 }
779
780 pub fn answer(&mut self, what: AskKind, answer: Answer<'_>) -> Option<Ask> {
782 match (what, answer) {
783 (AskKind::Limit, Answer::Line(input)) => {
784 self.set_limit(input);
785 None
786 }
787 (AskKind::Search { back }, Answer::Line(input)) => {
788 self.search_rev = back;
789 if !input.is_empty() {
790 match self.compile_search(input) {
791 Ok(patterns) => {
792 let redo = input.to_string();
793 if !self.body_terms_ready(
794 &patterns,
795 Box::new(move |session| {
796 session.answer(AskKind::Search { back }, Answer::Line(&redo));
797 }),
798 ) {
799 return None;
800 }
801 self.last_search = Some(patterns);
802 }
803 Err(err) => {
804 self.error(format!("bad pattern: {err}"));
805 return None;
806 }
807 }
808 }
809 self.search_next();
810 None
811 }
812 (AskKind::Pattern { op }, Answer::Line(input)) => {
813 self.apply_pattern(input, op);
814 None
815 }
816 (
817 AskKind::CopyTo {
818 delete,
819 tagged,
820 decode,
821 },
822 Answer::Line(input),
823 ) => {
824 let input = self.expand_folder(input);
825 if self.config.mail.confirmappend && mailbox_exists(&input) {
828 return Some(Ask::Key {
829 label: format!("Append messages to {input}? (y/n): "),
830 what: AskKind::AppendConfirm {
831 input,
832 delete,
833 tagged,
834 decode,
835 },
836 });
837 }
838 self.copy_message(&input, delete, tagged, decode);
839 None
840 }
841 (
842 AskKind::AppendConfirm {
843 input,
844 delete,
845 tagged,
846 decode,
847 },
848 Answer::Key(key),
849 ) => {
850 if matches!(key, Key::Char('y') | Key::Enter) {
852 self.copy_message(&input, delete, tagged, decode);
853 }
854 None
855 }
856 (AskKind::QuitConfirm, Answer::Key(key)) => {
857 let yes = match key {
858 Key::Char('y') => true,
859 Key::Char('n') => false,
860 Key::Enter => self.quit_default,
861 _ => false,
862 };
863 match yes {
864 true => self.leave_now(),
865 false => None,
866 }
867 }
868 (AskKind::Pipe { tagged }, Answer::Line(command)) => {
869 self.pipe_message(command, tagged);
870 None
871 }
872 (AskKind::AliasNick { addr }, Answer::Line(nick)) => {
873 self.create_alias(nick, &addr);
874 None
875 }
876 (AskKind::EditLabel { tagged }, Answer::Line(input)) => {
877 self.edit_label(input, tagged);
878 None
879 }
880 (AskKind::ReplyTo, Answer::Key(key)) => match key {
882 Key::Char('y') | Key::Enter => self.answer_reply_to(true),
883 Key::Char('n') => self.answer_reply_to(false),
884 _ => {
885 self.cancel_setup();
886 self.note("reply cancelled");
887 None
888 }
889 },
890 (AskKind::EditHeader { name }, Answer::Line(input)) => {
891 let value = match name.as_str() {
892 "Subject" => input.to_string(),
893 _ => rmut_core::alias::expand(
894 input,
895 &rmut_core::alias::load(self.config.mail.alias_file.as_deref()),
896 ),
897 };
898 self.set_draft_header(&name, &value);
899 None
900 }
901 (AskKind::Shell, Answer::Line(command)) => {
902 {
904 self.requests.push(Request::Shell(command.to_string()));
905 }
906 None
907 }
908 (AskKind::EditFcc, Answer::Line(input)) => {
909 if let Some(draft) = self.draft_mut() {
910 draft.fcc = Some(input.trim().to_string());
911 }
912 None
913 }
914 (AskKind::AttachFile, Answer::Line(input)) => {
915 self.attach_file(input);
916 None
917 }
918 (AskKind::AttachField { k, is_type }, Answer::Line(input)) => {
919 self.set_attach_field(k, input, is_type);
920 None
921 }
922 (AskKind::RenameAttachment { k }, Answer::Line(input)) => {
923 let name = input.trim().to_string();
924 self.edit_attachment(k, |a| {
925 let own = a.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
927 a.name = (!name.is_empty() && name != own).then(|| name.clone());
928 });
929 self.requests.push(Request::ShowDraft);
930 None
931 }
932 (AskKind::NewMimeFile, Answer::Line(input)) => {
933 let path = input.trim().to_string();
934 if path.is_empty() {
935 self.requests.push(Request::ShowDraft);
936 return None;
937 }
938 Some(Ask::Line {
939 label: "Content-Type: ".into(),
940 prefill: String::new(),
941 wants: Wants::Other,
942 what: AskKind::NewMimeType { path },
943 })
944 }
945 (AskKind::NewMimeType { path }, Answer::Line(input)) => {
946 self.new_mime(&path, input.trim());
947 None
948 }
949 (AskKind::WriteFcc, Answer::Line(input)) => {
950 let mailbox = input.trim().to_string();
951 if !mailbox.is_empty() {
952 self.write_draft_to(&mailbox);
953 }
954 self.requests.push(Request::ShowDraft);
955 None
956 }
957 (AskKind::Security, Answer::Key(key)) => {
958 if let Some(draft) = self.draft_mut() {
959 draft.security = match key {
960 Key::Char('e') => Security::Encrypt,
961 Key::Char('s') => Security::Sign,
962 Key::Char('b') => Security::Both,
963 Key::Char('c') => Security::None,
964 _ => draft.security,
965 };
966 }
967 None
968 }
969 (AskKind::PostponeAsk { default_yes }, Answer::Key(key)) => {
970 let postpone = match key {
971 Key::Char('y') => true,
972 Key::Char('n') => false,
973 Key::Enter => default_yes,
974 _ => {
976 self.requests.push(Request::ShowDraft);
977 return None;
978 }
979 };
980 if let Some(draft) = self.take_draft() {
981 if postpone {
982 self.postpone_draft(draft);
983 } else {
984 let _ = std::fs::remove_file(&draft.path);
985 self.note("message discarded");
986 }
987 }
988 None
989 }
990 (AskKind::FccAttach { default_yes }, Answer::Key(key)) => {
991 let keep = match key {
992 Key::Char('y') => true,
993 Key::Char('n') => false,
994 Key::Enter => default_yes,
995 _ => {
996 self.note("not sent");
997 self.requests.push(Request::ShowDraft);
998 return None;
999 }
1000 };
1001 self.fcc_attach_answer = Some(keep);
1002 self.send_draft()
1003 }
1004 (AskKind::NoAttach, Answer::Key(key)) => match key {
1005 Key::Char('y') => {
1008 self.confirm_attachment();
1009 self.send_draft()
1010 }
1011 _ => {
1012 self.note("not sent; a attaches a file");
1013 self.requests.push(Request::ShowDraft);
1014 None
1015 }
1016 },
1017 (AskKind::ComposeTo, Answer::Line(input)) => self.answer_to(input),
1018 (AskKind::ComposeCc, Answer::Line(input)) => self.answer_cc(input),
1019 (AskKind::ComposeBcc, Answer::Line(input)) => self.answer_bcc(input),
1020 (AskKind::ComposeSubject, Answer::Line(input)) => self.answer_subject(input),
1021 (AskKind::NoSubject { default_yes }, Answer::Key(key)) => match key {
1022 Key::Char('n') => self.answer_subject_kept(),
1023 Key::Char('y') => {
1024 self.cancel_setup();
1025 self.error("aborted (no subject)");
1026 None
1027 }
1028 Key::Enter if !default_yes => self.answer_subject_kept(),
1030 _ => {
1031 self.cancel_setup();
1032 self.error("aborted (no subject)");
1033 None
1034 }
1035 },
1036 (AskKind::IncludeReply { default_yes }, Answer::Key(key)) => match key {
1037 Key::Char('n') => self.answer_include(false),
1038 Key::Char('y') => self.answer_include(true),
1039 Key::Enter => self.answer_include(default_yes),
1040 _ => {
1041 self.cancel_setup();
1042 self.note("reply cancelled");
1043 None
1044 }
1045 },
1046 (AskKind::ForwardAttach, Answer::Key(key)) => match key {
1047 Key::Char('n') => self.answer_forward_attach(false),
1049 Key::Char('y') | Key::Enter => self.answer_forward_attach(true),
1050 _ => {
1051 self.cancel_setup();
1052 self.note("forward cancelled");
1053 None
1054 }
1055 },
1056 (AskKind::ForwardEdit { default_yes }, Answer::Key(key)) => {
1057 self.answer_forward_edit(match key {
1058 Key::Char('y') => Some(true),
1059 Key::Char('n') => Some(false),
1060 Key::Enter => Some(default_yes),
1061 _ => None,
1062 })
1063 }
1064 (AskKind::BounceTo { tagged }, Answer::Line(input)) => {
1065 let to = rmut_core::alias::expand(
1066 input,
1067 &rmut_core::alias::load(self.config.mail.alias_file.as_deref()),
1068 );
1069 if to.trim().is_empty() {
1070 self.note("no recipients, bounce cancelled");
1071 return None;
1072 }
1073 let n = self.op_targets(tagged).len();
1074 Some(Ask::Key {
1075 label: match n {
1076 1 => format!("Bounce message to {to}? (y/n): "),
1077 _ => format!("Bounce {n} messages to {to}? (y/n): "),
1078 },
1079 what: AskKind::BounceConfirm { to, tagged },
1080 })
1081 }
1082 (AskKind::BounceConfirm { to, tagged }, Answer::Key(key)) => {
1083 if key == Key::Char('y') {
1084 self.bounce_current(&to, tagged);
1085 }
1086 None
1087 }
1088 (
1089 AskKind::PrintConfirm {
1090 tagged,
1091 default_yes,
1092 },
1093 Answer::Key(key),
1094 ) => {
1095 if key == Key::Char('y') || (default_yes && key == Key::Enter) {
1096 self.print_current(tagged);
1097 }
1098 None
1099 }
1100 (AskKind::Purge { quit }, Answer::Key(key)) => {
1101 if matches!(key, Key::Char('y') | Key::Char('n') | Key::Enter) {
1106 self.sync(key != Key::Char('n'));
1107 if quit {
1108 self.requests.push(Request::Quit);
1109 }
1110 }
1111 None
1112 }
1113 (AskKind::MarkMessage { msg_id }, Answer::Line(input)) => {
1114 let stroke = input.trim();
1115 if stroke.is_empty() {
1116 return None;
1117 }
1118 let id: String = msg_id
1121 .trim_matches(|c| c == '<' || c == '>')
1122 .chars()
1123 .flat_map(|c| match c {
1124 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^'
1125 | '$' | '\\' => vec!['\\', c],
1126 c => vec![c],
1127 })
1128 .collect();
1129 self.requests
1130 .push(Request::Command(rmut_core::command::Command::Macro {
1131 menu: rmut_core::command::Menu::Index,
1132 key: stroke.to_string(),
1133 seq: format!("/~i {id}<enter>"),
1134 }));
1135 self.note(format!("Message bound to {stroke}."));
1136 None
1137 }
1138 (AskKind::ListAction { actions }, Answer::Key(key)) => {
1139 let Key::Char(c) = key else {
1140 return None;
1141 };
1142 let (name, url) = actions
1143 .iter()
1144 .find(|(name, _)| name.starts_with(c.to_ascii_uppercase()))?;
1145 match url {
1146 None => self.error(format!("No list action available for {name}.")),
1147 Some(url) if !url.to_ascii_lowercase().starts_with("mailto:") => {
1148 self.error("List actions only support mailto: URIs. (Try a browser?)");
1149 }
1150 Some(url) => match rmut_core::mailto::parse(url) {
1151 Some(mailto) => self.requests.push(Request::Mailto(mailto)),
1152 None => self.error("Could not parse mailto: URI."),
1153 },
1154 }
1155 None
1156 }
1157 (AskKind::Sort, Answer::Key(key)) => {
1158 let (sort, rev) = match key {
1159 Key::Char('d') => (SortKey::Date, false),
1160 Key::Char('D') => (SortKey::Date, true),
1161 Key::Char('f') => (SortKey::From, false),
1162 Key::Char('F') => (SortKey::From, true),
1163 Key::Char('s') => (SortKey::Subject, false),
1164 Key::Char('S') => (SortKey::Subject, true),
1165 Key::Char('z') => (SortKey::Size, false),
1166 Key::Char('Z') => (SortKey::Size, true),
1167 Key::Char('t') | Key::Char('T') => (SortKey::Threads, false),
1168 Key::Char('o') => (SortKey::To, false),
1169 Key::Char('O') => (SortKey::To, true),
1170 Key::Char('y') => (SortKey::Label, false),
1171 Key::Char('Y') => (SortKey::Label, true),
1172 Key::Char('u') => (SortKey::Unsorted, false),
1173 Key::Char('U') => (SortKey::Unsorted, true),
1174 _ => return None,
1175 };
1176 self.sort = sort;
1177 self.sort_rev = rev;
1178 self.apply_sort();
1179 self.note(format!(
1180 "sorted by {}{}",
1181 sort.name(),
1182 if rev { " (reverse)" } else { "" }
1183 ));
1184 None
1185 }
1186 _ => None,
1189 }
1190 }
1191
1192 pub fn take_request(&mut self) -> Option<Request> {
1194 match self.requests.is_empty() {
1195 true => None,
1196 false => Some(self.requests.remove(0)),
1197 }
1198 }
1199
1200 fn expand_folder(&self, input: &str) -> String {
1202 rmut_core::config::expand_folder(input, self.config.mail.folder.as_deref())
1203 }
1204
1205 fn set_limit(&mut self, input: &str) {
1208 let keep = self.selected_path();
1209 if input.is_empty() || input == "all" {
1210 self.limit = None;
1211 } else {
1212 match self.compile_search(input) {
1213 Ok(patterns) => {
1214 let redo = input.to_string();
1215 if !self.body_terms_ready(
1216 &patterns,
1217 Box::new(move |session| session.set_limit(&redo)),
1218 ) {
1219 return;
1220 }
1221 self.limit = Some((input.to_string(), patterns));
1222 }
1223 Err(err) => {
1224 self.error(format!("bad pattern: {err}"));
1225 return;
1226 }
1227 }
1228 }
1229 self.rebuild_visible(keep);
1230 if self.visible.is_empty() {
1231 self.note("no messages match the limit");
1232 }
1233 }
1234}