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 let answer = match answer {
786 Answer::Key(Key::Char(c)) if !matches!(what, AskKind::Sort) => {
787 Answer::Key(Key::Char(c.to_ascii_lowercase()))
788 }
789 other => other,
790 };
791 match (what, answer) {
792 (AskKind::Limit, Answer::Line(input)) => {
793 self.set_limit(input);
794 None
795 }
796 (AskKind::Search { back }, Answer::Line(input)) => {
797 self.search_rev = back;
798 if !input.is_empty() {
799 match self.compile_search(input) {
800 Ok(patterns) => {
801 let redo = input.to_string();
802 if !self.body_terms_ready(
803 &patterns,
804 Box::new(move |session| {
805 session.answer(AskKind::Search { back }, Answer::Line(&redo));
806 }),
807 ) {
808 return None;
809 }
810 self.last_search = Some(patterns);
811 }
812 Err(err) => {
813 self.error(format!("bad pattern: {err}"));
814 return None;
815 }
816 }
817 }
818 self.search_next();
819 None
820 }
821 (AskKind::Pattern { op }, Answer::Line(input)) => {
822 self.apply_pattern(input, op);
823 None
824 }
825 (
826 AskKind::CopyTo {
827 delete,
828 tagged,
829 decode,
830 },
831 Answer::Line(input),
832 ) => {
833 let input = self.expand_folder(input);
834 if self.config.mail.confirmappend && mailbox_exists(&input) {
837 return Some(Ask::Key {
838 label: format!("Append messages to {input}? (y/n): "),
839 what: AskKind::AppendConfirm {
840 input,
841 delete,
842 tagged,
843 decode,
844 },
845 });
846 }
847 self.copy_message(&input, delete, tagged, decode);
848 None
849 }
850 (
851 AskKind::AppendConfirm {
852 input,
853 delete,
854 tagged,
855 decode,
856 },
857 Answer::Key(key),
858 ) => {
859 if matches!(key, Key::Char('y') | Key::Enter) {
861 self.copy_message(&input, delete, tagged, decode);
862 }
863 None
864 }
865 (AskKind::QuitConfirm, Answer::Key(key)) => {
866 let yes = match key {
867 Key::Char('y') => true,
868 Key::Char('n') => false,
869 Key::Enter => self.quit_default,
870 _ => false,
871 };
872 match yes {
873 true => self.leave_now(),
874 false => None,
875 }
876 }
877 (AskKind::Pipe { tagged }, Answer::Line(command)) => {
878 self.pipe_message(command, tagged);
879 None
880 }
881 (AskKind::AliasNick { addr }, Answer::Line(nick)) => {
882 self.create_alias(nick, &addr);
883 None
884 }
885 (AskKind::EditLabel { tagged }, Answer::Line(input)) => {
886 self.edit_label(input, tagged);
887 None
888 }
889 (AskKind::ReplyTo, Answer::Key(key)) => match key {
891 Key::Char('y') | Key::Enter => self.answer_reply_to(true),
892 Key::Char('n') => self.answer_reply_to(false),
893 _ => {
894 self.cancel_setup();
895 self.note("reply cancelled");
896 None
897 }
898 },
899 (AskKind::EditHeader { name }, Answer::Line(input)) => {
900 let value = match name.as_str() {
901 "Subject" => input.to_string(),
902 _ => rmut_core::alias::expand(
903 input,
904 &rmut_core::alias::load(self.config.mail.alias_file.as_deref()),
905 ),
906 };
907 self.set_draft_header(&name, &value);
908 None
909 }
910 (AskKind::Shell, Answer::Line(command)) => {
911 {
913 self.requests.push(Request::Shell(command.to_string()));
914 }
915 None
916 }
917 (AskKind::EditFcc, Answer::Line(input)) => {
918 if let Some(draft) = self.draft_mut() {
919 draft.fcc = Some(input.trim().to_string());
920 }
921 None
922 }
923 (AskKind::AttachFile, Answer::Line(input)) => {
924 self.attach_file(input);
925 None
926 }
927 (AskKind::AttachField { k, is_type }, Answer::Line(input)) => {
928 self.set_attach_field(k, input, is_type);
929 None
930 }
931 (AskKind::RenameAttachment { k }, Answer::Line(input)) => {
932 let name = input.trim().to_string();
933 self.edit_attachment(k, |a| {
934 let own = a.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
936 a.name = (!name.is_empty() && name != own).then(|| name.clone());
937 });
938 self.requests.push(Request::ShowDraft);
939 None
940 }
941 (AskKind::NewMimeFile, Answer::Line(input)) => {
942 let path = input.trim().to_string();
943 if path.is_empty() {
944 self.requests.push(Request::ShowDraft);
945 return None;
946 }
947 Some(Ask::Line {
948 label: "Content-Type: ".into(),
949 prefill: String::new(),
950 wants: Wants::Other,
951 what: AskKind::NewMimeType { path },
952 })
953 }
954 (AskKind::NewMimeType { path }, Answer::Line(input)) => {
955 self.new_mime(&path, input.trim());
956 None
957 }
958 (AskKind::WriteFcc, Answer::Line(input)) => {
959 let mailbox = input.trim().to_string();
960 if !mailbox.is_empty() {
961 self.write_draft_to(&mailbox);
962 }
963 self.requests.push(Request::ShowDraft);
964 None
965 }
966 (AskKind::Security, Answer::Key(key)) => {
967 if let Some(draft) = self.draft_mut() {
968 draft.security = match key {
969 Key::Char('e') => Security::Encrypt,
970 Key::Char('s') => Security::Sign,
971 Key::Char('b') => Security::Both,
972 Key::Char('c') => Security::None,
973 _ => draft.security,
974 };
975 }
976 None
977 }
978 (AskKind::PostponeAsk { default_yes }, Answer::Key(key)) => {
979 let postpone = match key {
980 Key::Char('y') => true,
981 Key::Char('n') => false,
982 Key::Enter => default_yes,
983 _ => {
985 self.requests.push(Request::ShowDraft);
986 return None;
987 }
988 };
989 if let Some(draft) = self.take_draft() {
990 if postpone {
991 self.postpone_draft(draft);
992 } else {
993 let _ = std::fs::remove_file(&draft.path);
994 self.note("message discarded");
995 }
996 }
997 None
998 }
999 (AskKind::FccAttach { default_yes }, Answer::Key(key)) => {
1000 let keep = match key {
1001 Key::Char('y') => true,
1002 Key::Char('n') => false,
1003 Key::Enter => default_yes,
1004 _ => {
1005 self.note("not sent");
1006 self.requests.push(Request::ShowDraft);
1007 return None;
1008 }
1009 };
1010 self.fcc_attach_answer = Some(keep);
1011 self.send_draft()
1012 }
1013 (AskKind::NoAttach, Answer::Key(key)) => match key {
1014 Key::Char('y') => {
1017 self.confirm_attachment();
1018 self.send_draft()
1019 }
1020 _ => {
1021 self.note("not sent; a attaches a file");
1022 self.requests.push(Request::ShowDraft);
1023 None
1024 }
1025 },
1026 (AskKind::ComposeTo, Answer::Line(input)) => self.answer_to(input),
1027 (AskKind::ComposeCc, Answer::Line(input)) => self.answer_cc(input),
1028 (AskKind::ComposeBcc, Answer::Line(input)) => self.answer_bcc(input),
1029 (AskKind::ComposeSubject, Answer::Line(input)) => self.answer_subject(input),
1030 (AskKind::NoSubject { default_yes }, Answer::Key(key)) => match key {
1031 Key::Char('n') => self.answer_subject_kept(),
1032 Key::Char('y') => {
1033 self.cancel_setup();
1034 self.error("aborted (no subject)");
1035 None
1036 }
1037 Key::Enter if !default_yes => self.answer_subject_kept(),
1039 _ => {
1040 self.cancel_setup();
1041 self.error("aborted (no subject)");
1042 None
1043 }
1044 },
1045 (AskKind::IncludeReply { default_yes }, Answer::Key(key)) => match key {
1046 Key::Char('n') => self.answer_include(false),
1047 Key::Char('y') => self.answer_include(true),
1048 Key::Enter => self.answer_include(default_yes),
1049 _ => {
1050 self.cancel_setup();
1051 self.note("reply cancelled");
1052 None
1053 }
1054 },
1055 (AskKind::ForwardAttach, Answer::Key(key)) => match key {
1056 Key::Char('n') => self.answer_forward_attach(false),
1058 Key::Char('y') | Key::Enter => self.answer_forward_attach(true),
1059 _ => {
1060 self.cancel_setup();
1061 self.note("forward cancelled");
1062 None
1063 }
1064 },
1065 (AskKind::ForwardEdit { default_yes }, Answer::Key(key)) => {
1066 self.answer_forward_edit(match key {
1067 Key::Char('y') => Some(true),
1068 Key::Char('n') => Some(false),
1069 Key::Enter => Some(default_yes),
1070 _ => None,
1071 })
1072 }
1073 (AskKind::BounceTo { tagged }, Answer::Line(input)) => {
1074 let to = rmut_core::alias::expand(
1075 input,
1076 &rmut_core::alias::load(self.config.mail.alias_file.as_deref()),
1077 );
1078 if to.trim().is_empty() {
1079 self.note("no recipients, bounce cancelled");
1080 return None;
1081 }
1082 let n = self.op_targets(tagged).len();
1083 Some(Ask::Key {
1084 label: match n {
1085 1 => format!("Bounce message to {to}? (y/n): "),
1086 _ => format!("Bounce {n} messages to {to}? (y/n): "),
1087 },
1088 what: AskKind::BounceConfirm { to, tagged },
1089 })
1090 }
1091 (AskKind::BounceConfirm { to, tagged }, Answer::Key(key)) => {
1092 if key == Key::Char('y') {
1093 self.bounce_current(&to, tagged);
1094 }
1095 None
1096 }
1097 (
1098 AskKind::PrintConfirm {
1099 tagged,
1100 default_yes,
1101 },
1102 Answer::Key(key),
1103 ) => {
1104 if key == Key::Char('y') || (default_yes && key == Key::Enter) {
1105 self.print_current(tagged);
1106 }
1107 None
1108 }
1109 (AskKind::Purge { quit }, Answer::Key(key)) => {
1110 if matches!(key, Key::Char('y') | Key::Char('n') | Key::Enter) {
1115 self.sync(key != Key::Char('n'));
1116 if quit {
1117 self.requests.push(Request::Quit);
1118 }
1119 }
1120 None
1121 }
1122 (AskKind::MarkMessage { msg_id }, Answer::Line(input)) => {
1123 let stroke = input.trim();
1124 if stroke.is_empty() {
1125 return None;
1126 }
1127 let id: String = msg_id
1130 .trim_matches(|c| c == '<' || c == '>')
1131 .chars()
1132 .flat_map(|c| match c {
1133 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^'
1134 | '$' | '\\' => vec!['\\', c],
1135 c => vec![c],
1136 })
1137 .collect();
1138 self.requests
1139 .push(Request::Command(rmut_core::command::Command::Macro {
1140 menu: rmut_core::command::Menu::Index,
1141 key: stroke.to_string(),
1142 seq: format!("/~i {id}<enter>"),
1143 }));
1144 self.note(format!("Message bound to {stroke}."));
1145 None
1146 }
1147 (AskKind::ListAction { actions }, Answer::Key(key)) => {
1148 let Key::Char(c) = key else {
1149 return None;
1150 };
1151 let (name, url) = actions
1152 .iter()
1153 .find(|(name, _)| name.starts_with(c.to_ascii_uppercase()))?;
1154 match url {
1155 None => self.error(format!("No list action available for {name}.")),
1156 Some(url) if !url.to_ascii_lowercase().starts_with("mailto:") => {
1157 self.error("List actions only support mailto: URIs. (Try a browser?)");
1158 }
1159 Some(url) => match rmut_core::mailto::parse(url) {
1160 Some(mailto) => self.requests.push(Request::Mailto(mailto)),
1161 None => self.error("Could not parse mailto: URI."),
1162 },
1163 }
1164 None
1165 }
1166 (AskKind::Sort, Answer::Key(key)) => {
1167 let (sort, rev) = match key {
1168 Key::Char('d') => (SortKey::Date, false),
1169 Key::Char('D') => (SortKey::Date, true),
1170 Key::Char('f') => (SortKey::From, false),
1171 Key::Char('F') => (SortKey::From, true),
1172 Key::Char('s') => (SortKey::Subject, false),
1173 Key::Char('S') => (SortKey::Subject, true),
1174 Key::Char('z') => (SortKey::Size, false),
1175 Key::Char('Z') => (SortKey::Size, true),
1176 Key::Char('t') | Key::Char('T') => (SortKey::Threads, false),
1177 Key::Char('o') => (SortKey::To, false),
1178 Key::Char('O') => (SortKey::To, true),
1179 Key::Char('y') => (SortKey::Label, false),
1180 Key::Char('Y') => (SortKey::Label, true),
1181 Key::Char('u') => (SortKey::Unsorted, false),
1182 Key::Char('U') => (SortKey::Unsorted, true),
1183 _ => return None,
1184 };
1185 self.sort = sort;
1186 self.sort_rev = rev;
1187 self.apply_sort();
1188 self.note(format!(
1189 "sorted by {}{}",
1190 sort.name(),
1191 if rev { " (reverse)" } else { "" }
1192 ));
1193 None
1194 }
1195 _ => None,
1198 }
1199 }
1200
1201 pub fn take_request(&mut self) -> Option<Request> {
1203 match self.requests.is_empty() {
1204 true => None,
1205 false => Some(self.requests.remove(0)),
1206 }
1207 }
1208
1209 fn expand_folder(&self, input: &str) -> String {
1211 rmut_core::config::expand_folder(input, self.config.mail.folder.as_deref())
1212 }
1213
1214 fn set_limit(&mut self, input: &str) {
1217 let keep = self.selected_path();
1218 if input.is_empty() || input == "all" {
1219 self.limit = None;
1220 } else {
1221 match self.compile_search(input) {
1222 Ok(patterns) => {
1223 let redo = input.to_string();
1224 if !self.body_terms_ready(
1225 &patterns,
1226 Box::new(move |session| session.set_limit(&redo)),
1227 ) {
1228 return;
1229 }
1230 self.limit = Some((input.to_string(), patterns));
1231 }
1232 Err(err) => {
1233 self.error(format!("bad pattern: {err}"));
1234 return;
1235 }
1236 }
1237 }
1238 self.rebuild_visible(keep);
1239 if self.visible.is_empty() {
1240 self.note("no messages match the limit");
1241 }
1242 }
1243}