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