You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

912 lines
32 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. /*
  2. * meli - ui crate
  3. *
  4. * Copyright 2017-2018 Manos Pitsidianakis
  5. *
  6. * This file is part of meli.
  7. *
  8. * meli is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation, either version 3 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * meli is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with meli. If not, see <http://www.gnu.org/licenses/>.
  20. */
  21. use super::*;
  22. use melib::Draft;
  23. use mime_apps::query_mime_info;
  24. use std::str::FromStr;
  25. #[derive(Debug, PartialEq)]
  26. enum Cursor {
  27. Headers,
  28. Body,
  29. //Attachments,
  30. }
  31. #[derive(Debug)]
  32. pub struct Composer {
  33. reply_context: Option<((usize, usize), Box<ThreadView>)>, // (folder_index, thread_node_index)
  34. account_cursor: usize,
  35. cursor: Cursor,
  36. pager: Pager,
  37. draft: Draft,
  38. form: FormWidget,
  39. mode: ViewMode,
  40. dirty: bool,
  41. initialized: bool,
  42. id: ComponentId,
  43. }
  44. impl Default for Composer {
  45. fn default() -> Self {
  46. Composer {
  47. reply_context: None,
  48. account_cursor: 0,
  49. cursor: Cursor::Headers,
  50. pager: Pager::default(),
  51. draft: Draft::default(),
  52. form: FormWidget::default(),
  53. mode: ViewMode::Edit,
  54. dirty: true,
  55. initialized: false,
  56. id: ComponentId::new_v4(),
  57. }
  58. }
  59. }
  60. #[derive(Debug)]
  61. enum ViewMode {
  62. Discard(Uuid),
  63. Edit,
  64. //Selector(Selector),
  65. Overview,
  66. }
  67. impl ViewMode {
  68. fn is_discard(&self) -> bool {
  69. if let ViewMode::Discard(_) = self {
  70. true
  71. } else {
  72. false
  73. }
  74. }
  75. fn is_edit(&self) -> bool {
  76. if let ViewMode::Edit = self {
  77. true
  78. } else {
  79. false
  80. }
  81. }
  82. fn is_overview(&self) -> bool {
  83. if let ViewMode::Overview = self {
  84. true
  85. } else {
  86. false
  87. }
  88. }
  89. }
  90. impl fmt::Display for Composer {
  91. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  92. // TODO display subject/info
  93. if self.reply_context.is_some() {
  94. write!(f, "reply: {:8}", self.draft.headers()["Subject"])
  95. } else {
  96. write!(f, "compose")
  97. }
  98. }
  99. }
  100. impl Composer {
  101. const DESCRIPTION: &'static str = "compose";
  102. pub fn new(account_cursor: usize) -> Self {
  103. Composer {
  104. account_cursor,
  105. id: ComponentId::new_v4(),
  106. ..Default::default()
  107. }
  108. }
  109. /*
  110. * coordinates: (account index, mailbox index, root set thread_node index)
  111. * msg: index of message we reply to in thread_nodes
  112. * context: current context
  113. */
  114. pub fn edit(account_pos: usize, h: EnvelopeHash, context: &Context) -> Result<Self> {
  115. let mut ret = Composer::default();
  116. let op = context.accounts[account_pos].operation(h);
  117. let envelope: &Envelope = context.accounts[account_pos].get_env(&h);
  118. ret.draft = Draft::edit(envelope, op)?;
  119. ret.account_cursor = account_pos;
  120. Ok(ret)
  121. }
  122. pub fn with_context(
  123. coordinates: (usize, usize, usize),
  124. msg: ThreadHash,
  125. context: &Context,
  126. ) -> Self {
  127. let account = &context.accounts[coordinates.0];
  128. let mailbox = &account[coordinates.1].unwrap();
  129. let threads = &account.collection.threads[&mailbox.folder.hash()];
  130. let thread_nodes = &threads.thread_nodes();
  131. let mut ret = Composer::default();
  132. let p = &thread_nodes[&msg];
  133. let parent_message = &account.collection[&p.message().unwrap()];
  134. let mut op = account.operation(parent_message.hash());
  135. let parent_bytes = op.as_bytes();
  136. ret.draft = Draft::new_reply(parent_message, parent_bytes.unwrap());
  137. ret.draft.headers_mut().insert(
  138. "Subject".into(),
  139. if p.show_subject() {
  140. format!(
  141. "Re: {}",
  142. account.get_env(&p.message().unwrap()).subject().clone()
  143. )
  144. } else {
  145. account.get_env(&p.message().unwrap()).subject().into()
  146. },
  147. );
  148. ret.account_cursor = coordinates.0;
  149. ret.reply_context = Some((
  150. (coordinates.1, coordinates.2),
  151. Box::new(ThreadView::new(coordinates, Some(msg), context)),
  152. ));
  153. ret
  154. }
  155. pub fn set_draft(&mut self, draft: Draft) {
  156. self.draft = draft;
  157. self.update_form();
  158. }
  159. fn update_draft(&mut self) {
  160. let header_values = self.form.values_mut();
  161. let draft_header_map = self.draft.headers_mut();
  162. /* avoid extra allocations by updating values instead of inserting */
  163. for (k, v) in draft_header_map.iter_mut() {
  164. if let Some(vn) = header_values.remove(k) {
  165. std::mem::swap(v, &mut vn.into_string());
  166. }
  167. }
  168. }
  169. fn update_form(&mut self) {
  170. let old_cursor = self.form.cursor();
  171. self.form = FormWidget::new("Save".into());
  172. self.form.hide_buttons();
  173. self.form.set_cursor(old_cursor);
  174. let headers = self.draft.headers();
  175. let account_cursor = self.account_cursor;
  176. for &k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] {
  177. if k == "To" || k == "Cc" || k == "Bcc" {
  178. self.form.push_cl((
  179. k.into(),
  180. headers[k].to_string(),
  181. Box::new(move |c, term| {
  182. let book: &AddressBook = &c.accounts[account_cursor].address_book;
  183. let results: Vec<String> = book.search(term);
  184. results
  185. .into_iter()
  186. .map(|r| AutoCompleteEntry::from(r))
  187. .collect::<Vec<AutoCompleteEntry>>()
  188. }),
  189. ));
  190. } else {
  191. self.form.push((k.into(), headers[k].to_string()));
  192. }
  193. }
  194. }
  195. fn draw_attachments(&self, grid: &mut CellBuffer, area: Area, _context: &mut Context) {
  196. let attachments_no = self.draft.attachments().len();
  197. if attachments_no == 0 {
  198. write_string_to_grid(
  199. "no attachments",
  200. grid,
  201. Color::Default,
  202. Color::Default,
  203. Attr::Default,
  204. (pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
  205. false,
  206. );
  207. } else {
  208. write_string_to_grid(
  209. &format!("{} attachments ", attachments_no),
  210. grid,
  211. Color::Default,
  212. Color::Default,
  213. Attr::Default,
  214. (pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
  215. false,
  216. );
  217. for (i, a) in self.draft.attachments().iter().enumerate() {
  218. if let Some(name) = a.content_type().name() {
  219. write_string_to_grid(
  220. &format!(
  221. "[{}] \"{}\", {} {} bytes",
  222. i,
  223. name,
  224. a.content_type(),
  225. a.raw.len()
  226. ),
  227. grid,
  228. Color::Default,
  229. Color::Default,
  230. Attr::Default,
  231. (pos_inc(upper_left!(area), (0, 2 + i)), bottom_right!(area)),
  232. false,
  233. );
  234. } else {
  235. write_string_to_grid(
  236. &format!("[{}] {} {} bytes", i, a.content_type(), a.raw.len()),
  237. grid,
  238. Color::Default,
  239. Color::Default,
  240. Attr::Default,
  241. (pos_inc(upper_left!(area), (0, 2 + i)), bottom_right!(area)),
  242. false,
  243. );
  244. }
  245. }
  246. }
  247. }
  248. }
  249. impl Component for Composer {
  250. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  251. clear_area(grid, area);
  252. let upper_left = upper_left!(area);
  253. let bottom_right = bottom_right!(area);
  254. let upper_left = set_y(upper_left, get_y(upper_left) + 1);
  255. if height!(area) < 4 {
  256. return;
  257. }
  258. let width = if width!(area) > 80 && self.reply_context.is_some() {
  259. width!(area) / 2
  260. } else {
  261. width!(area)
  262. };
  263. if !self.initialized {
  264. if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty()
  265. {
  266. self.draft.headers_mut().insert(
  267. "From".into(),
  268. crate::components::mail::get_display_name(context, self.account_cursor),
  269. );
  270. }
  271. self.pager.update_from_str(self.draft.body(), Some(77));
  272. self.update_form();
  273. self.initialized = true;
  274. }
  275. let header_height = self.form.len();
  276. let mid = if width > 80 {
  277. let width = width - 80;
  278. let mid = if self.reply_context.is_some() {
  279. get_x(upper_left) + width!(area) / 2
  280. } else {
  281. width / 2
  282. };
  283. if self.reply_context.is_some() {
  284. for i in get_y(upper_left) - 1..=get_y(bottom_right) {
  285. set_and_join_box(grid, (mid, i), VERT_BOUNDARY);
  286. grid[(mid, i)].set_fg(Color::Default);
  287. grid[(mid, i)].set_bg(Color::Default);
  288. }
  289. grid[set_x(bottom_right, mid)].set_ch(VERT_BOUNDARY); // Enforce full vert bar at the bottom
  290. grid[set_x(bottom_right, mid)].set_fg(Color::Byte(240));
  291. }
  292. if self.dirty {
  293. for i in get_y(upper_left)..=get_y(bottom_right) {
  294. //set_and_join_box(grid, (mid, i), VERT_BOUNDARY);
  295. grid[(mid, i)].set_fg(Color::Default);
  296. grid[(mid, i)].set_bg(Color::Default);
  297. //set_and_join_box(grid, (mid + 80, i), VERT_BOUNDARY);
  298. grid[(mid + 80, i)].set_fg(Color::Default);
  299. grid[(mid + 80, i)].set_bg(Color::Default);
  300. }
  301. }
  302. mid
  303. } else {
  304. 0
  305. };
  306. if width > 80 && self.reply_context.is_some() {
  307. let area = (pos_dec(upper_left, (0, 1)), set_x(bottom_right, mid - 1));
  308. let view = &mut self.reply_context.as_mut().unwrap().1;
  309. view.set_dirty();
  310. view.draw(grid, area, context);
  311. }
  312. let header_area = if self.reply_context.is_some() {
  313. (
  314. set_x(upper_left, mid + 1),
  315. set_y(bottom_right, get_y(upper_left) + header_height),
  316. )
  317. } else {
  318. (
  319. set_x(upper_left, mid + 1),
  320. (
  321. get_x(bottom_right).saturating_sub(mid),
  322. get_y(upper_left) + header_height,
  323. ),
  324. )
  325. };
  326. let attachments_no = self.draft.attachments().len();
  327. let attachment_area = if self.reply_context.is_some() {
  328. (
  329. (mid + 1, get_y(bottom_right) - 2 - attachments_no),
  330. bottom_right,
  331. )
  332. } else {
  333. (
  334. (mid + 1, get_y(bottom_right) - 2 - attachments_no),
  335. pos_dec(bottom_right, (mid, 0)),
  336. )
  337. };
  338. let body_area = if self.reply_context.is_some() {
  339. (
  340. (mid + 1, get_y(upper_left) + header_height + 1),
  341. set_y(bottom_right, get_y(bottom_right) - 3 - attachments_no),
  342. )
  343. } else {
  344. (
  345. pos_inc(upper_left, (mid + 1, header_height + 1)),
  346. pos_dec(bottom_right, (mid, 3 + attachments_no)),
  347. )
  348. };
  349. let (x, y) = write_string_to_grid(
  350. if self.reply_context.is_some() {
  351. "COMPOSING REPLY"
  352. } else {
  353. "COMPOSING MESSAGE"
  354. },
  355. grid,
  356. Color::Byte(189),
  357. Color::Byte(167),
  358. Attr::Default,
  359. (
  360. pos_dec(upper_left!(header_area), (0, 1)),
  361. bottom_right!(header_area),
  362. ),
  363. false,
  364. );
  365. change_colors(
  366. grid,
  367. (
  368. set_x(pos_dec(upper_left!(header_area), (0, 1)), x),
  369. set_y(bottom_right!(header_area), y),
  370. ),
  371. Color::Byte(189),
  372. Color::Byte(167),
  373. );
  374. /* Regardless of view mode, do the following */
  375. self.form.draw(grid, header_area, context);
  376. match self.mode {
  377. ViewMode::Overview | ViewMode::Edit => {
  378. self.pager.set_dirty();
  379. self.pager.draw(grid, body_area, context);
  380. }
  381. ViewMode::Discard(_) => {
  382. /* Let user choose whether to quit with/without saving or cancel */
  383. let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
  384. let mid_y = { std::cmp::max(height!(area) / 2, 11) - 11 };
  385. let upper_left = upper_left!(body_area);
  386. let bottom_right = bottom_right!(body_area);
  387. let area = (
  388. pos_inc(upper_left, (mid_x, mid_y)),
  389. pos_dec(bottom_right, (mid_x, mid_y)),
  390. );
  391. create_box(grid, area);
  392. let area = (
  393. pos_inc(upper_left, (mid_x + 2, mid_y + 2)),
  394. pos_dec(
  395. bottom_right,
  396. (mid_x.saturating_sub(2), mid_y.saturating_sub(2)),
  397. ),
  398. );
  399. let (_, y) = write_string_to_grid(
  400. &format!("Draft \"{:10}\"", self.draft.headers()["Subject"]),
  401. grid,
  402. Color::Default,
  403. Color::Default,
  404. Attr::Default,
  405. area,
  406. true,
  407. );
  408. let (_, y) = write_string_to_grid(
  409. "[x] quit without saving",
  410. grid,
  411. Color::Byte(124),
  412. Color::Default,
  413. Attr::Default,
  414. (set_y(upper_left!(area), y + 2), bottom_right!(area)),
  415. true,
  416. );
  417. let (_, y) = write_string_to_grid(
  418. "[y] save draft and quit",
  419. grid,
  420. Color::Byte(124),
  421. Color::Default,
  422. Attr::Default,
  423. (set_y(upper_left!(area), y + 1), bottom_right!(area)),
  424. true,
  425. );
  426. write_string_to_grid(
  427. "[n] cancel",
  428. grid,
  429. Color::Byte(124),
  430. Color::Default,
  431. Attr::Default,
  432. (set_y(upper_left!(area), y + 1), bottom_right!(area)),
  433. true,
  434. );
  435. }
  436. }
  437. self.draw_attachments(grid, attachment_area, context);
  438. context.dirty_areas.push_back(area);
  439. }
  440. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  441. match (&mut self.mode, &mut self.reply_context, &event) {
  442. // don't pass Reply command to thread view in reply_context
  443. (_, _, UIEvent::Input(Key::Char('R'))) => {}
  444. (ViewMode::Overview, Some((_, ref mut view)), _) => {
  445. if view.process_event(event, context) {
  446. self.dirty = true;
  447. return true;
  448. }
  449. /* Cannot mutably borrow in pattern guard, pah! */
  450. if self.pager.process_event(event, context) {
  451. return true;
  452. }
  453. }
  454. (ViewMode::Overview, _, _) => {
  455. /* Cannot mutably borrow in pattern guard, pah! */
  456. if self.pager.process_event(event, context) {
  457. return true;
  458. }
  459. }
  460. _ => {}
  461. }
  462. if self.form.process_event(event, context) {
  463. return true;
  464. }
  465. match *event {
  466. UIEvent::Resize => {
  467. self.set_dirty();
  468. }
  469. /*
  470. /* Switch e-mail From: field to the `left` configured account. */
  471. UIEvent::Input(Key::Left) if self.cursor == Cursor::From => {
  472. self.account_cursor = self.account_cursor.saturating_sub(1);
  473. self.draft.headers_mut().insert(
  474. "From".into(),
  475. get_display_name(context, self.account_cursor),
  476. );
  477. self.dirty = true;
  478. return true;
  479. }
  480. /* Switch e-mail From: field to the `right` configured account. */
  481. UIEvent::Input(Key::Right) if self.cursor == Cursor::From => {
  482. if self.account_cursor + 1 < context.accounts.len() {
  483. self.account_cursor += 1;
  484. self.draft.headers_mut().insert(
  485. "From".into(),
  486. get_display_name(context, self.account_cursor),
  487. );
  488. self.dirty = true;
  489. }
  490. return true;
  491. }*/
  492. UIEvent::Input(Key::Up) => {
  493. self.cursor = Cursor::Headers;
  494. }
  495. UIEvent::Input(Key::Down) => {
  496. self.cursor = Cursor::Body;
  497. }
  498. UIEvent::Input(Key::Char(key)) if self.mode.is_discard() => {
  499. match (key, &self.mode) {
  500. ('x', ViewMode::Discard(u)) => {
  501. context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
  502. return true;
  503. }
  504. ('n', _) => {}
  505. ('y', ViewMode::Discard(u)) => {
  506. let mut failure = true;
  507. let draft = std::mem::replace(&mut self.draft, Draft::default());
  508. let draft = draft.finalise().unwrap();
  509. for folder in &[
  510. &context.accounts[self.account_cursor]
  511. .special_use_folder(SpecialUseMailbox::Drafts),
  512. &context.accounts[self.account_cursor]
  513. .special_use_folder(SpecialUseMailbox::Inbox),
  514. &context.accounts[self.account_cursor]
  515. .special_use_folder(SpecialUseMailbox::Normal),
  516. ] {
  517. if folder.is_none() {
  518. continue;
  519. }
  520. let folder = folder.unwrap();
  521. if let Err(e) = context.accounts[self.account_cursor].save(
  522. draft.as_bytes(),
  523. folder,
  524. Some(Flag::SEEN | Flag::DRAFT),
  525. ) {
  526. debug!("{:?} could not save draft msg", e);
  527. log(
  528. format!(
  529. "Could not save draft in '{}' folder: {}.",
  530. folder,
  531. e.to_string()
  532. ),
  533. ERROR,
  534. );
  535. context.replies.push_back(UIEvent::Notification(
  536. Some(format!("Could not save draft in '{}' folder.", folder)),
  537. e.into(),
  538. Some(NotificationType::ERROR),
  539. ));
  540. } else {
  541. failure = false;
  542. break;
  543. }
  544. }
  545. if failure {
  546. let file = create_temp_file(draft.as_bytes(), None, None, false);
  547. debug!("message saved in {}", file.path.display());
  548. log(
  549. format!(
  550. "Message was stored in {} so that you can restore it manually.",
  551. file.path.display()
  552. ),
  553. INFO,
  554. );
  555. context.replies.push_back(UIEvent::Notification(
  556. Some("Could not save in any folder".into()),
  557. format!(
  558. "Message was stored in {} so that you can restore it manually.",
  559. file.path.display()
  560. ),
  561. Some(NotificationType::INFO),
  562. ));
  563. }
  564. context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
  565. return true;
  566. }
  567. _ => {
  568. return false;
  569. }
  570. }
  571. self.mode = ViewMode::Overview;
  572. self.set_dirty();
  573. return true;
  574. }
  575. /* Switch to Overview mode if we're on Edit mode */
  576. UIEvent::Input(Key::Char('v')) if self.mode.is_edit() => {
  577. self.mode = ViewMode::Overview;
  578. self.set_dirty();
  579. return true;
  580. }
  581. /* Switch to Edit mode if we're on Overview mode */
  582. UIEvent::Input(Key::Char('o')) if self.mode.is_overview() => {
  583. self.mode = ViewMode::Edit;
  584. self.set_dirty();
  585. return true;
  586. }
  587. UIEvent::Input(Key::Char('s')) if self.mode.is_overview() => {
  588. self.update_draft();
  589. if send_draft(context, self.account_cursor, self.draft.clone()) {
  590. context
  591. .replies
  592. .push_back(UIEvent::Action(Tab(Kill(self.id))));
  593. }
  594. return true;
  595. }
  596. UIEvent::Input(Key::Char('e')) => {
  597. /* Edit draft in $EDITOR */
  598. use std::process::{Command, Stdio};
  599. let settings = &context.settings;
  600. let editor = if let Some(editor_cmd) = settings.composing.editor_cmd.as_ref() {
  601. editor_cmd.to_string()
  602. } else {
  603. match std::env::var("EDITOR") {
  604. Err(e) => {
  605. context.replies.push_back(UIEvent::Notification(
  606. Some(e.to_string()),
  607. "$EDITOR is not set. You can change an envvar's value with setenv or set composing.editor_cmd setting in your configuration.".to_string(),
  608. Some(NotificationType::ERROR),
  609. ));
  610. return true;
  611. }
  612. Ok(v) => v,
  613. }
  614. };
  615. /* Kill input thread so that spawned command can be sole receiver of stdin */
  616. {
  617. context.input_kill();
  618. }
  619. /* update Draft's headers based on form values */
  620. self.update_draft();
  621. let f = create_temp_file(
  622. self.draft.to_string().unwrap().as_str().as_bytes(),
  623. None,
  624. None,
  625. true,
  626. );
  627. let parts = split_command!(editor);
  628. let (cmd, args) = (parts[0], &parts[1..]);
  629. if let Err(e) = Command::new(cmd)
  630. .args(args)
  631. .arg(&f.path())
  632. .stdin(Stdio::inherit())
  633. .stdout(Stdio::inherit())
  634. .output()
  635. {
  636. context.replies.push_back(UIEvent::Notification(
  637. Some(format!("Failed to execute {}", editor)),
  638. e.to_string(),
  639. Some(NotificationType::ERROR),
  640. ));
  641. context.replies.push_back(UIEvent::Fork(ForkType::Finished));
  642. context.restore_input();
  643. return true;
  644. }
  645. let result = f.read_to_string();
  646. let mut new_draft = Draft::from_str(result.as_str()).unwrap();
  647. std::mem::swap(self.draft.attachments_mut(), new_draft.attachments_mut());
  648. self.draft = new_draft;
  649. self.initialized = false;
  650. context.replies.push_back(UIEvent::Fork(ForkType::Finished));
  651. context.restore_input();
  652. self.dirty = true;
  653. return true;
  654. }
  655. UIEvent::Action(ref a) => {
  656. match a {
  657. Action::Compose(ComposeAction::AddAttachment(ref path)) => {
  658. let mut attachment = match melib::email::attachment_from_file(path) {
  659. Ok(a) => a,
  660. Err(e) => {
  661. context.replies.push_back(UIEvent::Notification(
  662. Some("could not add attachment".to_string()),
  663. e.to_string(),
  664. Some(NotificationType::ERROR),
  665. ));
  666. self.dirty = true;
  667. return true;
  668. }
  669. };
  670. if let Ok(mime_type) = query_mime_info(path) {
  671. match attachment.content_type {
  672. ContentType::Other { ref mut tag, .. } => {
  673. *tag = mime_type;
  674. }
  675. _ => {}
  676. }
  677. }
  678. self.draft.attachments_mut().push(attachment);
  679. self.dirty = true;
  680. return true;
  681. }
  682. Action::Compose(ComposeAction::RemoveAttachment(idx)) => {
  683. if *idx + 1 > self.draft.attachments().len() {
  684. context.replies.push_back(UIEvent::StatusEvent(
  685. StatusEvent::DisplayMessage(
  686. "attachment with given index does not exist".to_string(),
  687. ),
  688. ));
  689. self.dirty = true;
  690. return true;
  691. }
  692. self.draft.attachments_mut().remove(*idx);
  693. context.replies.push_back(UIEvent::StatusEvent(
  694. StatusEvent::DisplayMessage("attachment removed".to_string()),
  695. ));
  696. self.dirty = true;
  697. return true;
  698. }
  699. _ => {}
  700. }
  701. }
  702. _ => {}
  703. }
  704. false
  705. }
  706. fn is_dirty(&self) -> bool {
  707. self.dirty
  708. || self.pager.is_dirty()
  709. || self
  710. .reply_context
  711. .as_ref()
  712. .map(|(_, p)| p.is_dirty())
  713. .unwrap_or(false)
  714. || self.form.is_dirty()
  715. }
  716. fn set_dirty(&mut self) {
  717. self.dirty = true;
  718. self.pager.set_dirty();
  719. self.form.set_dirty();
  720. if let Some((_, ref mut view)) = self.reply_context {
  721. view.set_dirty();
  722. }
  723. }
  724. fn kill(&mut self, uuid: Uuid, _context: &mut Context) {
  725. self.mode = ViewMode::Discard(uuid);
  726. }
  727. fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
  728. let mut map = if self.mode.is_overview() {
  729. self.pager.get_shortcuts(context)
  730. } else {
  731. Default::default()
  732. };
  733. if let Some((_, ref view)) = self.reply_context {
  734. map.extend(view.get_shortcuts(context));
  735. }
  736. let mut our_map: ShortcutMap = Default::default();
  737. if self.mode.is_overview() {
  738. our_map.insert("Switch to edit mode.", Key::Char('o'));
  739. our_map.insert("Deliver draft to mailer.", Key::Char('s'));
  740. }
  741. if self.mode.is_edit() {
  742. our_map.insert("Switch to overview", Key::Char('v'));
  743. }
  744. our_map.insert("Edit in $EDITOR", Key::Char('e'));
  745. map.insert(Composer::DESCRIPTION.to_string(), our_map);
  746. map
  747. }
  748. fn id(&self) -> ComponentId {
  749. self.id
  750. }
  751. fn set_id(&mut self, id: ComponentId) {
  752. self.id = id;
  753. }
  754. fn can_quit_cleanly(&mut self) -> bool {
  755. /* Play it safe and ask user for confirmation */
  756. self.mode = ViewMode::Discard(self.id);
  757. self.set_dirty();
  758. false
  759. }
  760. }
  761. pub fn send_draft(context: &mut Context, account_cursor: usize, draft: Draft) -> bool {
  762. use std::io::Write;
  763. use std::process::{Command, Stdio};
  764. let mut failure = true;
  765. let settings = &context.settings;
  766. let parts = split_command!(settings.composing.mailer_cmd);
  767. let (cmd, args) = (parts[0], &parts[1..]);
  768. let mut msmtp = Command::new(cmd)
  769. .args(args)
  770. .stdin(Stdio::piped())
  771. .stdout(Stdio::piped())
  772. .spawn()
  773. .expect("Failed to start mailer command");
  774. {
  775. let stdin = msmtp.stdin.as_mut().expect("failed to open stdin");
  776. let draft = draft.finalise().unwrap();
  777. stdin
  778. .write_all(draft.as_bytes())
  779. .expect("Failed to write to stdin");
  780. for folder in &[
  781. &context.accounts[account_cursor].special_use_folder(SpecialUseMailbox::Sent),
  782. &context.accounts[account_cursor].special_use_folder(SpecialUseMailbox::Inbox),
  783. &context.accounts[account_cursor].special_use_folder(SpecialUseMailbox::Normal),
  784. ] {
  785. if folder.is_none() {
  786. continue;
  787. }
  788. let folder = folder.unwrap();
  789. if let Err(e) =
  790. context.accounts[account_cursor].save(draft.as_bytes(), folder, Some(Flag::SEEN))
  791. {
  792. debug!("{:?} could not save sent msg", e);
  793. log(
  794. format!("Could not save in '{}' folder: {}.", folder, e.to_string()),
  795. ERROR,
  796. );
  797. context.replies.push_back(UIEvent::Notification(
  798. Some(format!("Could not save in '{}' folder.", folder)),
  799. e.into(),
  800. Some(NotificationType::ERROR),
  801. ));
  802. } else {
  803. failure = false;
  804. break;
  805. }
  806. }
  807. if failure {
  808. let file = create_temp_file(draft.as_bytes(), None, None, false);
  809. debug!("message saved in {}", file.path.display());
  810. log(
  811. format!(
  812. "Message was stored in {} so that you can restore it manually.",
  813. file.path.display()
  814. ),
  815. INFO,
  816. );
  817. context.replies.push_back(UIEvent::Notification(
  818. Some("Could not save in any folder".into()),
  819. format!(
  820. "Message was stored in {} so that you can restore it manually.",
  821. file.path.display()
  822. ),
  823. Some(NotificationType::INFO),
  824. ));
  825. }
  826. }
  827. let output = msmtp.wait().expect("Failed to wait on mailer");
  828. if output.success() {
  829. context.replies.push_back(UIEvent::Notification(
  830. Some("Sent.".into()),
  831. String::new(),
  832. None,
  833. ));
  834. } else {
  835. if let Some(exit_code) = output.code() {
  836. log(
  837. format!(
  838. "Could not send e-mail using `{}`: Process exited with {}",
  839. cmd, exit_code
  840. ),
  841. ERROR,
  842. );
  843. } else {
  844. log(
  845. format!(
  846. "Could not send e-mail using `{}`: Process was killed by signal",
  847. cmd
  848. ),
  849. ERROR,
  850. );
  851. }
  852. }
  853. !failure
  854. }