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