1use rmut_core::{alias, compose, message};
10
11use crate::{
12 Ask, AskKind, Compose, ComposeBase, ComposeKind, ComposeSetup, Request, Session, Wants,
13};
14
15impl Session {
16 pub fn start_compose(&mut self, kind: ComposeKind) -> Option<Ask> {
19 let base = match kind {
20 ComposeKind::New => None,
21 _ => match self.compose_base() {
22 Some(b) => Some(b),
23 None => {
24 self.error("no message selected");
25 return None;
26 }
27 },
28 };
29 self.continue_setup(kind, base)
30 }
31
32 pub fn start_list_reply(&mut self) -> Option<Ask> {
36 let Some(base) = self.compose_base() else {
37 self.error("no message selected");
38 return None;
39 };
40 if self.list_target(&base).is_none() {
41 self.error(match self.lists.is_empty() {
42 true => "no mailing lists configured (mail.lists / mail.subscribed)",
43 false => "not a message from a known mailing list",
44 });
45 return None;
46 }
47 self.continue_setup(ComposeKind::ListReply, Some(base))
48 }
49
50 fn continue_setup(&mut self, kind: ComposeKind, base: Option<ComposeBase>) -> Option<Ask> {
51 if self.config.mail.autoedit && self.edit_headers() {
55 let to = match (&kind, &base) {
56 (ComposeKind::Reply | ComposeKind::GroupReply, Some(b)) => b.reply_to.clone(),
57 (ComposeKind::ListReply, Some(b)) => self.list_target(b).unwrap_or_default(),
58 _ => String::new(),
59 };
60 let subject = match (&kind, &base) {
61 (
62 ComposeKind::Reply | ComposeKind::GroupReply | ComposeKind::ListReply,
63 Some(b),
64 ) => self.reply_subject(&b.subject),
65 (ComposeKind::Forward, Some(b)) => self.forward_subject(b),
66 _ => String::new(),
67 };
68 self.setup = Some(ComposeSetup {
69 kind,
70 base,
71 to: Some(to),
72 cc: None,
73 bcc: None,
74 subject_prefill: None,
75 subject: None,
76 fwd_attach: None,
77 });
78 self.finish_compose_setup(&subject, true);
79 return None;
80 }
81 let ask_reply_to = matches!(kind, ComposeKind::Reply | ComposeKind::GroupReply)
82 && base.as_ref().is_some_and(|b| b.has_reply_to);
83 self.setup = Some(ComposeSetup {
84 kind,
85 base,
86 to: None,
87 cc: None,
88 bcc: None,
89 subject_prefill: None,
90 subject: None,
91 fwd_attach: None,
92 });
93 if ask_reply_to {
94 let addr = self
96 .setup
97 .as_ref()
98 .and_then(|s| s.base.as_ref())
99 .map(|b| b.reply_to.clone())
100 .unwrap_or_default();
101 return Some(Ask::Key {
102 label: format!("Reply to {addr}? (y/n): "),
103 what: AskKind::ReplyTo,
104 });
105 }
106 self.ask_to(true)
107 }
108
109 fn ask_to(&mut self, use_reply_to: bool) -> Option<Ask> {
112 let setup = self.setup.as_ref()?;
113 let to_prefill = match setup.kind {
114 ComposeKind::Reply | ComposeKind::GroupReply => setup
115 .base
116 .as_ref()
117 .map(|b| {
118 if use_reply_to {
119 b.reply_to.clone()
120 } else {
121 b.from_hdr.clone()
122 }
123 })
124 .unwrap_or_default(),
125 ComposeKind::ListReply => setup
126 .base
127 .as_ref()
128 .and_then(|b| self.list_target(b))
129 .unwrap_or_default(),
130 ComposeKind::New | ComposeKind::Forward => String::new(),
131 };
132 if self.config.mail.fast_reply
135 && matches!(
136 setup.kind,
137 ComposeKind::Reply | ComposeKind::GroupReply | ComposeKind::ListReply
138 )
139 && setup.base.is_some()
140 {
141 let subject = setup
142 .base
143 .as_ref()
144 .map(|b| self.reply_subject(&b.subject))
145 .unwrap_or_default();
146 if let Some(setup) = &mut self.setup {
147 setup.to = Some(to_prefill);
148 }
149 return self.subject_submitted(&subject);
150 }
151 Some(Ask::Line {
152 label: "To: ".into(),
153 prefill: to_prefill,
154 wants: Wants::Address,
155 what: AskKind::ComposeTo,
156 })
157 }
158
159 fn setup_to_submitted(&mut self, input: &str) -> Option<Ask> {
160 let to = alias::expand(input, &alias::load(self.config.mail.alias_file.as_deref()));
161 self.setup.as_mut()?.to = Some(to);
162 let setup = self.setup.as_ref()?;
163 let subject_prefill = match (&setup.kind, &setup.base) {
164 (ComposeKind::Reply | ComposeKind::GroupReply | ComposeKind::ListReply, Some(b)) => {
165 self.reply_subject(&b.subject)
166 }
167 (ComposeKind::Forward, Some(b)) => self.forward_subject(b),
168 _ => String::new(),
169 };
170 self.setup.as_mut()?.subject_prefill = Some(subject_prefill);
174 self.ask_cc_or_on()
175 }
176
177 fn ask_cc_or_on(&mut self) -> Option<Ask> {
180 if !self.config.mail.ask_cc {
181 return self.ask_bcc_or_on();
182 }
183 let prefill = self.group_cc().unwrap_or_default();
184 Some(Ask::Line {
185 label: "Cc: ".into(),
186 prefill,
187 wants: Wants::Address,
188 what: AskKind::ComposeCc,
189 })
190 }
191
192 fn ask_bcc_or_on(&mut self) -> Option<Ask> {
194 if !self.config.mail.ask_bcc {
195 return self.ask_subject();
196 }
197 Some(Ask::Line {
198 label: "Bcc: ".into(),
199 prefill: String::new(),
200 wants: Wants::Address,
201 what: AskKind::ComposeBcc,
202 })
203 }
204
205 fn ask_subject(&mut self) -> Option<Ask> {
208 let prefill = self.setup.as_mut()?.subject_prefill.take()?;
209 if self.config.mail.fast_reply && !prefill.is_empty() {
210 return self.subject_submitted(&prefill);
211 }
212 Some(Ask::Line {
213 label: "Subject: ".into(),
214 prefill,
215 wants: Wants::Other,
216 what: AskKind::ComposeSubject,
217 })
218 }
219
220 pub(crate) fn answer_cc(&mut self, input: &str) -> Option<Ask> {
221 let cc = alias::expand(input, &alias::load(self.config.mail.alias_file.as_deref()));
222 self.setup.as_mut()?.cc = Some(cc);
223 self.ask_bcc_or_on()
224 }
225
226 pub(crate) fn answer_bcc(&mut self, input: &str) -> Option<Ask> {
227 let bcc = alias::expand(input, &alias::load(self.config.mail.alias_file.as_deref()));
228 self.setup.as_mut()?.bcc = Some(bcc);
229 self.ask_subject()
230 }
231
232 fn group_cc(&self) -> Option<String> {
238 let setup = self.setup.as_ref()?;
239 if setup.kind != ComposeKind::GroupReply {
240 return None;
241 }
242 let base = setup.base.as_ref()?;
243 if !base.followup_to.trim().is_empty() {
244 return None;
245 }
246 let joined = compose::group_recipients(
247 &base.orig_to,
248 &base.orig_cc,
249 setup.to.as_deref().unwrap_or_default(),
250 self.me(),
251 self.config.mail.metoo,
252 );
253 (!joined.is_empty()).then_some(joined)
254 }
255
256 fn indent_string(&self) -> &str {
258 self.config
259 .mail
260 .indent_string
261 .as_deref()
262 .unwrap_or(compose::DEFAULT_INDENT)
263 }
264
265 fn signature(&self) -> Option<String> {
269 compose::signature_text(self.config.mail.signature.as_deref()?)
270 }
271
272 fn sig_dashes(&self) -> bool {
274 self.config.mail.sig_dashes.unwrap_or(true)
275 }
276
277 fn subject_submitted(&mut self, input: &str) -> Option<Ask> {
280 if input.trim().is_empty() {
281 match self
284 .config
285 .mail
286 .abort_nosubject
287 .as_deref()
288 .unwrap_or("ask-yes")
289 {
290 "no" => return self.subject_ready(String::new()),
291 "yes" => {
292 self.cancel_setup();
293 self.error("aborted (no subject)");
294 return None;
295 }
296 quad => {
297 return Some(Ask::Key {
298 label: "No subject, abort? (y/n): ".into(),
299 what: AskKind::NoSubject {
300 default_yes: quad != "ask-no",
301 },
302 });
303 }
304 }
305 }
306 self.subject_ready(input.to_string())
307 }
308
309 fn subject_ready(&mut self, subject: String) -> Option<Ask> {
310 let is_reply = self.setup.as_ref().is_some_and(|s| {
311 matches!(
312 s.kind,
313 ComposeKind::Reply | ComposeKind::GroupReply | ComposeKind::ListReply
314 ) && s.base.is_some()
315 });
316 let ask_fwd = self.config.mail.forward.as_deref() == Some("ask")
317 && self
318 .setup
319 .as_ref()
320 .is_some_and(|s| s.kind == ComposeKind::Forward && s.base.is_some());
321 if is_reply {
322 match self.config.mail.include.as_deref().unwrap_or("ask-yes") {
325 "yes" => {
326 self.finish_compose_setup(&subject, true);
327 return None;
328 }
329 "no" => {
330 self.finish_compose_setup(&subject, false);
331 return None;
332 }
333 include => {
334 if let Some(setup) = &mut self.setup {
335 setup.subject = Some(subject);
336 }
337 return Some(Ask::Key {
338 label: "Include message in reply? (y/n): ".into(),
339 what: AskKind::IncludeReply {
340 default_yes: include != "ask-no",
341 },
342 });
343 }
344 }
345 }
346 if ask_fwd {
347 if let Some(setup) = &mut self.setup {
349 setup.subject = Some(subject);
350 }
351 return Some(Ask::Key {
352 label: "Forward as attachment? (y/n): ".into(),
353 what: AskKind::ForwardAttach,
354 });
355 }
356 self.finish_compose_setup(&subject, true);
357 None
358 }
359
360 fn finish_compose_setup(&mut self, subject: &str, include: bool) {
361 let Some(setup) = self.setup.take() else {
362 return;
363 };
364 let reply_hooks = self.apply_reply_hooks(setup.base.as_ref(), setup.kind);
367 self.finish_compose_draft(setup, subject, include);
368 self.restore_after_reply_hooks(reply_hooks);
369 }
370
371 fn finish_compose_draft(&mut self, setup: ComposeSetup, subject: &str, include: bool) {
372 let asked_cc = setup.cc.clone().filter(|cc| !cc.trim().is_empty());
373 let bcc = setup.bcc.clone().filter(|bcc| !bcc.trim().is_empty());
374 let mut to = setup.to.unwrap_or_default();
375 let mut cc = None;
376 let mut in_reply_to = None;
377 let mut references = None;
378 let mut body = String::new();
379 let mut attach = None;
380 if let Some(b) = &setup.base {
381 match setup.kind {
382 ComposeKind::Reply | ComposeKind::GroupReply | ComposeKind::ListReply => {
383 if include {
384 let orig = message::body_text(&b.path).unwrap_or_default();
385 let quoted = self.quoted_of(b);
386 let attribution = compose::attribution(
387 self.config
388 .mail
389 .attribution
390 .as_deref()
391 .unwrap_or(compose::DEFAULT_ATTRIBUTION),
392 "ed,
393 );
394 body = compose::quote(&attribution, self.indent_string(), &orig);
395 }
396 in_reply_to = b.msg_id.clone();
397 let mut refs = b.references.clone();
398 if let Some(id) = &b.msg_id
399 && !refs.contains(id)
400 {
401 refs.push(id.clone());
402 }
403 if !refs.is_empty() {
404 references = Some(refs.join(" "));
405 }
406 if setup.kind == ComposeKind::GroupReply {
407 if !b.followup_to.trim().is_empty() {
411 to = b.followup_to.trim().to_string();
412 } else {
413 let joined = compose::group_recipients(
418 &b.orig_to,
419 &b.orig_cc,
420 &to,
421 self.me(),
422 self.config.mail.metoo,
423 );
424 if !joined.is_empty() {
425 cc = Some(joined);
426 }
427 }
428 }
429 }
430 ComposeKind::Forward
431 if setup.fwd_attach.unwrap_or_else(|| self.forward_attaches()) =>
432 {
433 attach = Some(b.path.clone());
435 }
436 ComposeKind::Forward => {
437 let orig = message::body_text(&b.path).unwrap_or_default();
438 let indent = self.config.mail.forward_quote.then(|| self.indent_string());
441 body =
442 compose::forward_body(&b.from_display, b.date, &b.subject, &orig, indent);
443 }
444 ComposeKind::New => {}
445 }
446 }
447 if let Some(sig) = self.signature() {
450 let on_top = self.config.mail.sig_on_top.unwrap_or(false);
451 body = compose::with_signature_at(&body, &sig, self.sig_dashes(), on_top);
452 }
453 let cc = asked_cc.or(cc);
456 let from = self.compose_from(setup.base.as_ref(), &to);
457 let followup =
458 self.followup_header(&to, cc.as_deref(), from.as_deref().unwrap_or_default());
459 let text = compose::draft_text(
460 &compose::DraftHeaders {
461 from,
462 to,
463 cc,
464 subject: subject.to_string(),
465 in_reply_to,
466 references,
467 },
468 &body,
469 );
470 let mut extra: Vec<String> = Vec::new();
474 if let Some(value) = followup {
475 extra.push(format!("Mail-Followup-To: {value}"));
476 }
477 if let Some(value) = bcc {
478 extra.push(format!("Bcc: {value}"));
479 }
480 let text = match extra.is_empty() {
481 true => text,
482 false => match text.split_once("\n\n") {
483 Some((head, rest)) => format!("{head}\n{}\n\n{rest}", extra.join("\n")),
484 None => text,
485 },
486 };
487 match self.stage_draft(&text) {
488 Ok((path, hidden_head)) => {
489 let staged = std::fs::read_to_string(&path).unwrap_or_default();
492 self.staged = Some((path.clone(), staged));
493 let security = self.security_for(&setup.kind, setup.base.as_ref());
494 self.requests.push(Request::Editor(Compose {
495 path,
496 recall_source: None,
497 security,
498 attach,
499 hidden_head,
500 fcc: None,
501 }));
502 }
503 Err(err) => self.error(format!("cannot write draft: {err:#}")),
504 }
505 }
506
507 pub(crate) fn answer_reply_to(&mut self, use_reply_to: bool) -> Option<Ask> {
509 self.ask_to(use_reply_to)
510 }
511
512 pub(crate) fn answer_to(&mut self, input: &str) -> Option<Ask> {
513 self.setup_to_submitted(input)
514 }
515
516 pub(crate) fn answer_subject(&mut self, input: &str) -> Option<Ask> {
517 self.subject_submitted(input)
518 }
519
520 pub(crate) fn answer_subject_kept(&mut self) -> Option<Ask> {
522 self.subject_ready(String::new())
523 }
524
525 pub(crate) fn answer_include(&mut self, include: bool) -> Option<Ask> {
526 let subject = self.parked_subject();
527 self.finish_compose_setup(&subject, include);
528 None
529 }
530
531 pub(crate) fn answer_forward_attach(&mut self, attach: bool) -> Option<Ask> {
532 let subject = self.parked_subject();
533 if let Some(setup) = &mut self.setup {
534 setup.fwd_attach = Some(attach);
535 }
536 self.finish_compose_setup(&subject, true);
537 None
538 }
539
540 fn parked_subject(&mut self) -> String {
542 self.setup
543 .as_mut()
544 .and_then(|s| s.subject.take())
545 .unwrap_or_default()
546 }
547
548 fn quoted_of<'a>(&self, base: &'a ComposeBase) -> compose::Quoted<'a> {
551 compose::Quoted {
552 from: &base.from_hdr,
553 subject: &base.subject,
554 message_id: base.msg_id.as_deref(),
555 date: base.date,
556 }
557 }
558
559 fn forward_subject(&self, base: &ComposeBase) -> String {
561 compose::forward_subject(
562 self.config
563 .mail
564 .forward_format
565 .as_deref()
566 .unwrap_or(compose::DEFAULT_FORWARD_FORMAT),
567 &self.quoted_of(base),
568 )
569 }
570
571 pub fn cancel_setup(&mut self) {
573 self.setup = None;
574 }
575}