1use crate::Session;
9use clap::{Subcommand, ValueEnum};
10use sioul_core::compose::{self, Draft, DraftKind};
11use sioul_core::config::Account;
12use sioul_core::maildir;
13use sioul_sync::mailbox::{self, Action};
14use sioul_sync::{SyncError, secret};
15use std::path::PathBuf;
16
17#[derive(Subcommand)]
18pub(crate) enum MailCommand {
19 Folders { account: String },
21 List {
23 account: String,
24 folder: Option<String>,
26 },
27 Act {
29 file: PathBuf,
31 #[arg(value_enum)]
32 action: ActionArg,
33 #[arg(long)]
35 to: Option<String>,
36 },
37 Send {
39 #[arg(long)]
40 account: String,
41 #[arg(long)]
42 to: Vec<String>,
43 #[arg(long)]
44 cc: Vec<String>,
45 #[arg(long)]
46 subject: Option<String>,
47 #[arg(long)]
49 body: String,
50 #[arg(long)]
51 attach: Vec<PathBuf>,
52 #[arg(long)]
54 reply: Option<PathBuf>,
55 #[arg(long)]
57 reply_all: Option<PathBuf>,
58 #[arg(long)]
60 forward: Option<PathBuf>,
61 #[arg(long)]
63 sign: bool,
64 #[arg(long)]
66 encrypt: bool,
67 },
68}
69
70#[derive(Clone, Copy, ValueEnum)]
71pub(crate) enum ActionArg {
72 Read,
73 Unread,
74 Flag,
75 Unflag,
76 Trash,
77 Junk,
78 Archive,
79 NotJunk,
80 NotSpam,
82 Move,
83}
84
85pub(crate) fn run(s: &Session, command: MailCommand) -> Result<(), String> {
86 match command {
87 MailCommand::Folders { account } => {
88 for folder in mailbox::folders(&mail_account(s, &account)?.id) {
89 println!("{} · {} · {}", s.tr.text(folder.role.message_id(), None), crate::one_line(&folder.display), crate::one_line(&folder.name));
91 }
92 Ok(())
93 }
94 MailCommand::List { account, folder } => list(s, &account, folder.as_deref()),
95 MailCommand::Act { file, action, to } => act(s, &file, action, to),
96 MailCommand::Send { account, to, cc, subject, body, attach, reply, reply_all, forward, sign, encrypt } => {
97 let account = mail_account(s, &account)?.clone();
98 let own = own_addresses(s);
99 let answered = [(reply, DraftKind::Reply), (reply_all, DraftKind::ReplyAll), (forward, DraftKind::Forward)]
100 .into_iter()
101 .find_map(|(file, kind)| file.map(|f| (f, kind)));
102 let mut draft = match answered {
103 Some((file, kind)) => compose::answer(&file, kind, &account.id, &own).ok_or_else(|| s.tr.text("mail-message-gone", None))?,
104 None => Draft::new(&account.id),
105 };
106 draft.to.extend(to.iter().flat_map(|t| compose::split_addresses(t)));
107 draft.cc.extend(cc.iter().flat_map(|c| compose::split_addresses(c)));
108 if let Some(subject) = subject {
109 draft.subject = subject;
110 }
111 draft.body = body;
112 draft.add_signature(account.signature.as_deref());
113 draft.attachments = attach;
114 draft.sign = sign;
115 draft.encrypt = encrypt;
116 send_draft(s, &account, &draft)
117 }
118 }
119}
120
121fn mail_account<'a>(s: &'a Session, id: &str) -> Result<&'a Account, String> {
122 s.config.account(id).filter(|a| a.syncs()).ok_or_else(|| s.say("account-unknown", &[("id", id.to_string())]))
123}
124
125pub(crate) fn own_addresses(s: &Session) -> Vec<String> {
127 s.config.accounts.iter().filter_map(|a| a.address.clone()).collect()
128}
129
130fn list(s: &Session, account: &str, folder: Option<&str>) -> Result<(), String> {
131 let account = mail_account(s, account)?;
132 let folders = mailbox::folders(&account.id);
133 let missing = |name: &str| s.say("mail-no-such-folder", &[("folder", name.to_string()), ("account", account.id.clone())]);
134 let chosen = match folder {
135 Some(name) => folders.iter().find(|f| f.name == name || f.display == name).ok_or_else(|| missing(name))?,
136 None => folders.iter().find(|f| f.local.is_empty()).ok_or_else(|| missing("INBOX"))?,
137 };
138 let mut cards = maildir::read_messages(&account.maildir_path().join(&chosen.local));
139 cards.reverse();
140 for card in cards {
141 let path = card.path.clone().unwrap_or_default();
142 let letters = maildir::flags_of(&path);
143 let keywords = maildir::KEYWORDS.iter().filter(|(letter, _)| letters.contains(*letter)).map(|(_, name)| name.to_string());
145 let flags = std::iter::once(letters.chars().filter(char::is_ascii_uppercase).collect::<String>()).chain(keywords).filter(|f| !f.is_empty()).collect::<Vec<_>>().join(" ");
146 println!("{} · {} · {} · [{flags}] · {}", crate::date(s, card.date), crate::one_line(card.sender()), crate::one_line(&card.subject), crate::one_line(&path.display().to_string()));
148 }
149 Ok(())
150}
151
152fn act(s: &Session, file: &std::path::Path, action: ActionArg, to: Option<String>) -> Result<(), String> {
153 let account = s.config.account_of(file).ok_or_else(|| s.say("mail-not-in-accounts", &[("file", file.display().to_string())]))?;
154 let action = match action {
155 ActionArg::Read => Action::Read(true),
156 ActionArg::Unread => Action::Read(false),
157 ActionArg::Flag => Action::Flag(true),
158 ActionArg::Unflag => Action::Flag(false),
159 ActionArg::Trash => Action::Trash,
160 ActionArg::Junk => Action::Junk,
161 ActionArg::Archive => Action::Archive,
162 ActionArg::NotJunk => Action::NotJunk,
163 ActionArg::NotSpam => Action::NotSpam,
164 ActionArg::Move => Action::Move(to.ok_or_else(|| s.tr.text("mail-move-needs-to", None))?),
165 };
166 let password = secret::password(account).map_err(|e| e.sentence(&s.tr, &account.id))?;
167 mailbox::act(account, &password, file, &action).map_err(|e| e.sentence(&s.tr, &account.id))?;
168 println!("{}", s.tr.text("mail-done", None));
169 Ok(())
170}
171
172pub(crate) fn send_draft(s: &Session, account: &Account, draft: &Draft) -> Result<(), String> {
174 let password = secret::password(account).map_err(|e| e.sentence(&s.tr, &account.id))?;
175 match sioul_sync::send::send_draft(&s.config_path, account, &password, draft, &s.tr) {
176 Ok(()) => {
177 draft.discard();
178 println!("{}", s.tr.text("mail-sent", None));
179 Ok(())
180 }
181 Err(e @ SyncError::NotFiled(_)) => {
182 draft.discard();
183 println!("{}", crate::plain_lines(&e.sentence(&s.tr, &account.id)));
184 Ok(())
185 }
186 Err(e) => {
187 let _ = draft.save();
188 Err(e.sentence(&s.tr, &account.id))
189 }
190 }
191}