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.

1331 lines
56KB

  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 linkify::{Link, LinkFinder};
  23. use std::convert::TryFrom;
  24. use std::process::{Command, Stdio};
  25. pub mod list_management;
  26. mod html;
  27. pub use self::html::*;
  28. mod thread;
  29. pub use self::thread::*;
  30. mod envelope;
  31. pub use self::envelope::*;
  32. use mime_apps::query_default_app;
  33. #[derive(PartialEq, Debug, Clone)]
  34. enum ViewMode {
  35. Normal,
  36. Url,
  37. Attachment(usize),
  38. Raw,
  39. Subview,
  40. ContactSelector(Selector<Card>),
  41. }
  42. impl Default for ViewMode {
  43. fn default() -> Self {
  44. ViewMode::Normal
  45. }
  46. }
  47. impl ViewMode {
  48. fn is_attachment(&self) -> bool {
  49. match self {
  50. ViewMode::Attachment(_) => true,
  51. _ => false,
  52. }
  53. }
  54. fn is_contact_selector(&self) -> bool {
  55. match self {
  56. ViewMode::ContactSelector(_) => true,
  57. _ => false,
  58. }
  59. }
  60. }
  61. /// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
  62. /// menus
  63. #[derive(Debug, Default)]
  64. pub struct MailView {
  65. coordinates: (usize, usize, EnvelopeHash),
  66. pager: Option<Pager>,
  67. subview: Option<Box<dyn Component>>,
  68. dirty: bool,
  69. mode: ViewMode,
  70. expand_headers: bool,
  71. cmd_buf: String,
  72. id: ComponentId,
  73. }
  74. impl Clone for MailView {
  75. fn clone(&self) -> Self {
  76. MailView {
  77. subview: None,
  78. cmd_buf: String::with_capacity(4),
  79. pager: self.pager.clone(),
  80. mode: self.mode.clone(),
  81. ..*self
  82. }
  83. }
  84. }
  85. impl fmt::Display for MailView {
  86. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  87. // TODO display subject/info
  88. write!(f, "{}", MailView::DESCRIPTION)
  89. }
  90. }
  91. impl MailView {
  92. const DESCRIPTION: &'static str = "mail";
  93. pub fn new(
  94. coordinates: (usize, usize, EnvelopeHash),
  95. pager: Option<Pager>,
  96. subview: Option<Box<dyn Component>>,
  97. ) -> Self {
  98. MailView {
  99. coordinates,
  100. pager,
  101. subview,
  102. dirty: true,
  103. mode: ViewMode::Normal,
  104. expand_headers: false,
  105. cmd_buf: String::with_capacity(4),
  106. id: ComponentId::new_v4(),
  107. }
  108. }
  109. /// Returns the string to be displayed in the Viewer
  110. fn attachment_to_text<'closure, 's: 'closure, 'context: 's>(
  111. &'s self,
  112. body: &'context Attachment,
  113. context: &'context mut Context,
  114. ) -> String {
  115. let finder = LinkFinder::new();
  116. let body_text = String::from_utf8_lossy(&decode_rec(
  117. body,
  118. Some(Box::new(move |a: &'closure Attachment, v: &mut Vec<u8>| {
  119. if a.content_type().is_text_html() {
  120. use std::io::Write;
  121. let settings = &context.settings;
  122. /* FIXME: duplication with view/html.rs */
  123. if let Some(filter_invocation) = settings.pager.html_filter.as_ref() {
  124. let parts = split_command!(filter_invocation);
  125. let (cmd, args) = (parts[0], &parts[1..]);
  126. let command_obj = Command::new(cmd)
  127. .args(args)
  128. .stdin(Stdio::piped())
  129. .stdout(Stdio::piped())
  130. .spawn();
  131. if command_obj.is_err() {
  132. context.replies.push_back(UIEvent::Notification(
  133. Some(format!(
  134. "Failed to start html filter process: {}",
  135. filter_invocation,
  136. )),
  137. String::new(),
  138. Some(NotificationType::ERROR),
  139. ));
  140. return;
  141. }
  142. let mut html_filter = command_obj.unwrap();
  143. html_filter
  144. .stdin
  145. .as_mut()
  146. .unwrap()
  147. .write_all(&v)
  148. .expect("Failed to write to stdin");
  149. *v = format!(
  150. "Text piped through `{}`. Press `v` to open in web browser. \n\n",
  151. filter_invocation
  152. )
  153. .into_bytes();
  154. v.extend(html_filter.wait_with_output().unwrap().stdout);
  155. } else {
  156. if let Ok(mut html_filter) = Command::new("w3m")
  157. .args(&["-I", "utf-8", "-T", "text/html"])
  158. .stdin(Stdio::piped())
  159. .stdout(Stdio::piped())
  160. .spawn()
  161. {
  162. html_filter
  163. .stdin
  164. .as_mut()
  165. .unwrap()
  166. .write_all(&v)
  167. .expect("Failed to write to html filter stdin");
  168. *v = String::from(
  169. "Text piped through `w3m`. Press `v` to open in web browser. \n\n",
  170. )
  171. .into_bytes();
  172. v.extend(html_filter.wait_with_output().unwrap().stdout);
  173. } else {
  174. context.replies.push_back(UIEvent::Notification(
  175. Some(
  176. "Failed to find any application to use as html filter"
  177. .to_string(),
  178. ),
  179. String::new(),
  180. Some(NotificationType::ERROR),
  181. ));
  182. return;
  183. }
  184. }
  185. } else if a.is_signed() {
  186. v.clear();
  187. if context.settings.pgp.auto_verify_signatures {
  188. v.extend(crate::mail::pgp::verify_signature(a, context).into_iter());
  189. }
  190. }
  191. })),
  192. ))
  193. .into_owned();
  194. match self.mode {
  195. ViewMode::Normal | ViewMode::Subview | ViewMode::ContactSelector(_) => {
  196. let mut t = body_text.to_string();
  197. t.push('\n');
  198. if body.count_attachments() > 1 {
  199. t = body
  200. .attachments()
  201. .iter()
  202. .enumerate()
  203. .fold(t, |mut s, (idx, a)| {
  204. s.push_str(&format!("\n[{}] {}\n", idx, a));
  205. s
  206. });
  207. }
  208. t
  209. }
  210. ViewMode::Raw => String::from_utf8_lossy(body.body()).into_owned(),
  211. ViewMode::Url => {
  212. let mut t = body_text.to_string();
  213. for (lidx, l) in finder.links(&body.text()).enumerate() {
  214. let offset = if lidx < 10 {
  215. lidx * 3
  216. } else if lidx < 100 {
  217. 26 + (lidx - 9) * 4
  218. } else if lidx < 1000 {
  219. 385 + (lidx - 99) * 5
  220. } else {
  221. panic!("FIXME: Message body with more than 100 urls, fix this");
  222. };
  223. t.insert_str(l.start() + offset, &format!("[{}]", lidx));
  224. }
  225. if body.count_attachments() > 1 {
  226. t = body
  227. .attachments()
  228. .iter()
  229. .enumerate()
  230. .fold(t, |mut s, (idx, a)| {
  231. s.push_str(&format!("[{}] {}\n\n", idx, a));
  232. s
  233. });
  234. }
  235. t
  236. }
  237. ViewMode::Attachment(aidx) => {
  238. let attachments = body.attachments();
  239. let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
  240. ret.push_str(&attachments[aidx].text());
  241. ret
  242. }
  243. }
  244. }
  245. pub fn plain_text_to_buf(s: &str, highlight_urls: bool) -> CellBuffer {
  246. let mut buf = CellBuffer::from(s);
  247. if highlight_urls {
  248. let lines: Vec<&str> = s.split('\n').map(|l| l.trim_end()).collect();
  249. let mut shift = 0;
  250. let mut lidx_total = 0;
  251. let finder = LinkFinder::new();
  252. for r in &lines {
  253. for l in finder.links(&r) {
  254. let offset = if lidx_total < 10 {
  255. 3
  256. } else if lidx_total < 100 {
  257. 4
  258. } else if lidx_total < 1000 {
  259. 5
  260. } else {
  261. panic!("BUG: Message body with more than 100 urls");
  262. };
  263. for i in 1..=offset {
  264. buf[(l.start() + shift - i, 0)].set_fg(Color::Byte(226));
  265. //buf[(l.start() + shift - 2, 0)].set_fg(Color::Byte(226));
  266. //buf[(l.start() + shift - 3, 0)].set_fg(Color::Byte(226));
  267. }
  268. lidx_total += 1;
  269. }
  270. // Each Cell represents one char so next line will be:
  271. shift += r.chars().count() + 1;
  272. }
  273. }
  274. buf
  275. }
  276. pub fn update(&mut self, new_coordinates: (usize, usize, EnvelopeHash)) {
  277. self.coordinates = new_coordinates;
  278. self.mode = ViewMode::Normal;
  279. self.set_dirty();
  280. }
  281. }
  282. impl Component for MailView {
  283. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  284. if !self.is_dirty() {
  285. return;
  286. }
  287. let upper_left = upper_left!(area);
  288. let bottom_right = bottom_right!(area);
  289. let y: usize = {
  290. let account = &mut context.accounts[self.coordinates.0];
  291. if !account.contains_key(self.coordinates.2) {
  292. /* The envelope has been renamed or removed, so wait for the appropriate event to
  293. * arrive */
  294. self.dirty = false;
  295. return;
  296. }
  297. let (hash, is_seen) = {
  298. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  299. (envelope.hash(), envelope.is_seen())
  300. };
  301. if !is_seen {
  302. let op = account.operation(hash);
  303. let mut envelope: EnvelopeRefMut =
  304. account.collection.get_env_mut(self.coordinates.2);
  305. if let Err(e) = envelope.set_seen(op) {
  306. context
  307. .replies
  308. .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!(
  309. "Could not set message as seen: {}",
  310. e
  311. ))));
  312. }
  313. }
  314. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  315. let header_fg = if context.settings.terminal.theme == "light" {
  316. Color::Black
  317. } else {
  318. Color::Byte(33)
  319. };
  320. if self.mode == ViewMode::Raw {
  321. clear_area(grid, area);
  322. context.dirty_areas.push_back(area);
  323. get_y(upper_left) - 1
  324. } else {
  325. let (x, y) = write_string_to_grid(
  326. &format!("Date: {}", envelope.date_as_str()),
  327. grid,
  328. header_fg,
  329. Color::Default,
  330. Attr::Default,
  331. area,
  332. true,
  333. );
  334. for x in x..=get_x(bottom_right) {
  335. grid[(x, y)].set_ch(' ');
  336. grid[(x, y)].set_bg(Color::Default);
  337. grid[(x, y)].set_fg(Color::Default);
  338. }
  339. let (x, y) = write_string_to_grid(
  340. &format!("From: {}", envelope.field_from_to_string()),
  341. grid,
  342. header_fg,
  343. Color::Default,
  344. Attr::Default,
  345. (set_y(upper_left, y + 1), bottom_right),
  346. true,
  347. );
  348. for x in x..=get_x(bottom_right) {
  349. grid[(x, y)].set_ch(' ');
  350. grid[(x, y)].set_bg(Color::Default);
  351. grid[(x, y)].set_fg(Color::Default);
  352. }
  353. let (x, y) = write_string_to_grid(
  354. &format!("To: {}", envelope.field_to_to_string()),
  355. grid,
  356. header_fg,
  357. Color::Default,
  358. Attr::Default,
  359. (set_y(upper_left, y + 1), bottom_right),
  360. true,
  361. );
  362. for x in x..=get_x(bottom_right) {
  363. grid[(x, y)].set_ch(' ');
  364. grid[(x, y)].set_bg(Color::Default);
  365. grid[(x, y)].set_fg(Color::Default);
  366. }
  367. let (x, y) = write_string_to_grid(
  368. &format!("Subject: {}", envelope.subject()),
  369. grid,
  370. header_fg,
  371. Color::Default,
  372. Attr::Default,
  373. (set_y(upper_left, y + 1), bottom_right),
  374. true,
  375. );
  376. for x in x..=get_x(bottom_right) {
  377. grid[(x, y)].set_ch(' ');
  378. grid[(x, y)].set_bg(Color::Default);
  379. grid[(x, y)].set_fg(Color::Default);
  380. }
  381. let (x, mut y) = write_string_to_grid(
  382. &format!("Message-ID: <{}>", envelope.message_id_raw()),
  383. grid,
  384. header_fg,
  385. Color::Default,
  386. Attr::Default,
  387. (set_y(upper_left, y + 1), bottom_right),
  388. true,
  389. );
  390. for x in x..=get_x(bottom_right) {
  391. grid[(x, y)].set_ch(' ');
  392. grid[(x, y)].set_bg(Color::Default);
  393. grid[(x, y)].set_fg(Color::Default);
  394. }
  395. if self.expand_headers && envelope.in_reply_to().is_some() {
  396. let (x, _y) = write_string_to_grid(
  397. &format!("In-Reply-To: {}", envelope.in_reply_to_display().unwrap()),
  398. grid,
  399. header_fg,
  400. Color::Default,
  401. Attr::Default,
  402. (set_y(upper_left, y + 1), bottom_right),
  403. true,
  404. );
  405. for x in x..=get_x(bottom_right) {
  406. grid[(x, _y)].set_ch(' ');
  407. grid[(x, _y)].set_bg(Color::Default);
  408. grid[(x, _y)].set_fg(Color::Default);
  409. }
  410. let (x, _y) = write_string_to_grid(
  411. &format!(
  412. "References: {}",
  413. envelope
  414. .references()
  415. .iter()
  416. .map(std::string::ToString::to_string)
  417. .collect::<Vec<String>>()
  418. .join(", ")
  419. ),
  420. grid,
  421. header_fg,
  422. Color::Default,
  423. Attr::Default,
  424. (set_y(upper_left, _y + 1), bottom_right),
  425. true,
  426. );
  427. for x in x..=get_x(bottom_right) {
  428. grid[(x, _y)].set_ch(' ');
  429. grid[(x, _y)].set_bg(Color::Default);
  430. grid[(x, _y)].set_fg(Color::Default);
  431. }
  432. y = _y;
  433. }
  434. if let Some(list_management::ListActions {
  435. ref id,
  436. ref archive,
  437. ref post,
  438. ref unsubscribe,
  439. }) = list_management::detect(&envelope)
  440. {
  441. let mut x = get_x(upper_left);
  442. y += 1;
  443. if let Some(id) = id {
  444. let (_x, _) = write_string_to_grid(
  445. "List-ID: ",
  446. grid,
  447. header_fg,
  448. Color::Default,
  449. Attr::Default,
  450. (set_y(upper_left, y), bottom_right),
  451. false,
  452. );
  453. let (_x, _y) = write_string_to_grid(
  454. id,
  455. grid,
  456. Color::Default,
  457. Color::Default,
  458. Attr::Default,
  459. ((_x, y), bottom_right),
  460. false,
  461. );
  462. x = _x;
  463. if _y != y {
  464. x = get_x(upper_left);
  465. }
  466. y = _y;
  467. }
  468. if archive.is_some() || post.is_some() || unsubscribe.is_some() {
  469. let (_x, _y) = write_string_to_grid(
  470. " Available actions: [ ",
  471. grid,
  472. header_fg,
  473. Color::Default,
  474. Attr::Default,
  475. ((x, y), bottom_right),
  476. true,
  477. );
  478. x = _x;
  479. if _y != y {
  480. x = get_x(upper_left);
  481. }
  482. y = _y;
  483. }
  484. if archive.is_some() {
  485. let (_x, _y) = write_string_to_grid(
  486. "list-archive, ",
  487. grid,
  488. Color::Default,
  489. Color::Default,
  490. Attr::Default,
  491. ((x, y), bottom_right),
  492. true,
  493. );
  494. x = _x;
  495. if _y != y {
  496. x = get_x(upper_left);
  497. }
  498. y = _y;
  499. }
  500. if post.is_some() {
  501. let (_x, _y) = write_string_to_grid(
  502. "list-post, ",
  503. grid,
  504. Color::Default,
  505. Color::Default,
  506. Attr::Default,
  507. ((x, y), bottom_right),
  508. true,
  509. );
  510. x = _x;
  511. if _y != y {
  512. x = get_x(upper_left);
  513. }
  514. y = _y;
  515. }
  516. if unsubscribe.is_some() {
  517. let (_x, _y) = write_string_to_grid(
  518. "list-unsubscribe, ",
  519. grid,
  520. Color::Default,
  521. Color::Default,
  522. Attr::Default,
  523. ((x, y), bottom_right),
  524. true,
  525. );
  526. x = _x;
  527. if _y != y {
  528. x = get_x(upper_left);
  529. }
  530. y = _y;
  531. }
  532. if archive.is_some() || post.is_some() || unsubscribe.is_some() {
  533. grid[(x - 2, y)].set_ch(' ');
  534. grid[(x - 1, y)].set_fg(header_fg);
  535. grid[(x - 1, y)].set_bg(Color::Default);
  536. grid[(x - 1, y)].set_ch(']');
  537. }
  538. for x in x..=get_x(bottom_right) {
  539. grid[(x, y)].set_ch(' ');
  540. grid[(x, y)].set_bg(Color::Default);
  541. grid[(x, y)].set_fg(Color::Default);
  542. }
  543. }
  544. clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 1)));
  545. context
  546. .dirty_areas
  547. .push_back((upper_left, set_y(bottom_right, y + 1)));
  548. y + 1
  549. }
  550. };
  551. if self.dirty {
  552. let body = {
  553. let account = &mut context.accounts[self.coordinates.0];
  554. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  555. let op = account.operation(envelope.hash());
  556. match envelope.body(op) {
  557. Ok(body) => body,
  558. Err(e) => {
  559. context.replies.push_back(UIEvent::Notification(
  560. Some("Failed to open e-mail".to_string()),
  561. e.to_string(),
  562. Some(NotificationType::ERROR),
  563. ));
  564. log(
  565. format!(
  566. "Failed to open envelope {}: {}",
  567. envelope.message_id_display(),
  568. e.to_string()
  569. ),
  570. ERROR,
  571. );
  572. return;
  573. }
  574. }
  575. };
  576. match self.mode {
  577. ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
  578. self.pager = None;
  579. let attachment = &body.attachments()[aidx];
  580. self.subview = Some(Box::new(HtmlView::new(&attachment, context)));
  581. self.mode = ViewMode::Subview;
  582. }
  583. ViewMode::Normal if body.is_html() => {
  584. self.subview = Some(Box::new(HtmlView::new(&body, context)));
  585. self.pager = None;
  586. self.mode = ViewMode::Subview;
  587. }
  588. ViewMode::Subview | ViewMode::ContactSelector(_) => {}
  589. ViewMode::Raw => {
  590. let text = {
  591. let account = &mut context.accounts[self.coordinates.0];
  592. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  593. let mut op = account.operation(envelope.hash());
  594. op.as_bytes()
  595. .map(|v| String::from_utf8_lossy(v).into_owned())
  596. .unwrap_or_else(|e| e.to_string())
  597. };
  598. self.pager = Some(Pager::from_string(text, Some(context), None, None));
  599. self.subview = None;
  600. }
  601. _ => {
  602. let text = {
  603. self.attachment_to_text(&body, context)
  604. /*
  605. // URL indexes must be colored (ugh..)
  606. MailView::plain_text_to_buf(&text, self.mode == ViewMode::Url)
  607. */
  608. };
  609. let cursor_pos = if self.mode.is_attachment() {
  610. Some(0)
  611. } else {
  612. self.pager.as_mut().map(|p| p.cursor_pos())
  613. };
  614. self.pager = Some(Pager::from_string(text, Some(context), cursor_pos, None));
  615. self.subview = None;
  616. }
  617. };
  618. }
  619. match self.mode {
  620. ViewMode::Subview if self.subview.is_some() => {
  621. if let Some(s) = self.subview.as_mut() {
  622. s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  623. }
  624. }
  625. _ => {
  626. if let Some(p) = self.pager.as_mut() {
  627. p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  628. }
  629. }
  630. }
  631. if let ViewMode::ContactSelector(ref mut s) = self.mode {
  632. s.draw(grid, center_area(area, s.content.size()), context);
  633. }
  634. self.dirty = false;
  635. }
  636. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  637. match self.mode {
  638. ViewMode::Subview => {
  639. if let Some(s) = self.subview.as_mut() {
  640. if s.process_event(event, context) {
  641. return true;
  642. }
  643. }
  644. }
  645. ViewMode::ContactSelector(ref mut s) => {
  646. if s.process_event(event, context) {
  647. if s.is_done() {
  648. if let ViewMode::ContactSelector(s) =
  649. std::mem::replace(&mut self.mode, ViewMode::Normal)
  650. {
  651. let account = &mut context.accounts[self.coordinates.0];
  652. {
  653. for card in s.collect() {
  654. account.address_book.add_card(card);
  655. }
  656. }
  657. }
  658. self.set_dirty();
  659. }
  660. return true;
  661. }
  662. if let Some(p) = self.pager.as_mut() {
  663. if p.process_event(event, context) {
  664. return true;
  665. }
  666. }
  667. }
  668. _ => {
  669. if let Some(p) = self.pager.as_mut() {
  670. if p.process_event(event, context) {
  671. return true;
  672. }
  673. }
  674. }
  675. }
  676. let shortcuts = &self.get_shortcuts(context)[MailView::DESCRIPTION];
  677. match *event {
  678. UIEvent::Input(ref key)
  679. if !self.mode.is_contact_selector()
  680. && *key == shortcuts["add_addresses_to_contacts"] =>
  681. {
  682. let account = &mut context.accounts[self.coordinates.0];
  683. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  684. let mut entries = Vec::new();
  685. for addr in envelope.from().iter().chain(envelope.to().iter()) {
  686. let mut new_card: Card = Card::new();
  687. new_card.set_email(addr.get_email());
  688. new_card.set_name(addr.get_display_name());
  689. entries.push((new_card, format!("{}", addr)));
  690. }
  691. drop(envelope);
  692. self.mode = ViewMode::ContactSelector(Selector::new(
  693. "select contacts to add",
  694. entries,
  695. false,
  696. context,
  697. ));
  698. self.dirty = true;
  699. return true;
  700. }
  701. UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
  702. if self.mode.is_contact_selector() =>
  703. {
  704. self.mode = ViewMode::Normal;
  705. self.set_dirty();
  706. return true;
  707. }
  708. UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) => {
  709. self.cmd_buf.clear();
  710. context
  711. .replies
  712. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  713. return true;
  714. }
  715. UIEvent::Input(Key::Char(c)) if c >= '0' && c <= '9' => {
  716. self.cmd_buf.push(c);
  717. context
  718. .replies
  719. .push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
  720. self.cmd_buf.clone(),
  721. )));
  722. return true;
  723. }
  724. UIEvent::Input(ref key)
  725. if (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview)
  726. && *key == shortcuts["view_raw_source"] =>
  727. {
  728. self.mode = ViewMode::Raw;
  729. self.set_dirty();
  730. return true;
  731. }
  732. UIEvent::Input(ref key)
  733. if (self.mode.is_attachment()
  734. || self.mode == ViewMode::Subview
  735. || self.mode == ViewMode::Url
  736. || self.mode == ViewMode::Raw)
  737. && *key == shortcuts["return_to_normal_view"] =>
  738. {
  739. self.mode = ViewMode::Normal;
  740. self.set_dirty();
  741. return true;
  742. }
  743. UIEvent::Input(ref key)
  744. if (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview)
  745. && !self.cmd_buf.is_empty()
  746. && *key == shortcuts["open_mailcap"] =>
  747. {
  748. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  749. self.cmd_buf.clear();
  750. context
  751. .replies
  752. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  753. {
  754. let account = &mut context.accounts[self.coordinates.0];
  755. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  756. let op = account.operation(envelope.hash());
  757. let attachments = match envelope.body(op) {
  758. Ok(body) => body.attachments(),
  759. Err(e) => {
  760. context.replies.push_back(UIEvent::Notification(
  761. Some("Failed to open e-mail".to_string()),
  762. e.to_string(),
  763. Some(NotificationType::ERROR),
  764. ));
  765. log(
  766. format!(
  767. "Failed to open envelope {}: {}",
  768. envelope.message_id_display(),
  769. e.to_string()
  770. ),
  771. ERROR,
  772. );
  773. return true;
  774. }
  775. };
  776. drop(envelope);
  777. drop(account);
  778. if let Some(u) = attachments.get(lidx) {
  779. if let Ok(()) = crate::mailcap::MailcapEntry::execute(u, context) {
  780. self.set_dirty();
  781. } else {
  782. context.replies.push_back(UIEvent::StatusEvent(
  783. StatusEvent::DisplayMessage(format!(
  784. "no mailcap entry found for {}",
  785. u.content_type()
  786. )),
  787. ));
  788. }
  789. } else {
  790. context.replies.push_back(UIEvent::StatusEvent(
  791. StatusEvent::DisplayMessage(format!(
  792. "Attachment `{}` not found.",
  793. lidx
  794. )),
  795. ));
  796. }
  797. return true;
  798. }
  799. }
  800. UIEvent::Input(ref key)
  801. if *key == shortcuts["open_attachment"]
  802. && !self.cmd_buf.is_empty()
  803. && (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) =>
  804. {
  805. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  806. self.cmd_buf.clear();
  807. context
  808. .replies
  809. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  810. {
  811. let account = &mut context.accounts[self.coordinates.0];
  812. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  813. let op = account.operation(envelope.hash());
  814. let attachments = match envelope.body(op) {
  815. Ok(body) => body.attachments(),
  816. Err(e) => {
  817. context.replies.push_back(UIEvent::Notification(
  818. Some("Failed to open e-mail".to_string()),
  819. e.to_string(),
  820. Some(NotificationType::ERROR),
  821. ));
  822. log(
  823. format!(
  824. "Failed to open envelope {}: {}",
  825. envelope.message_id_display(),
  826. e.to_string()
  827. ),
  828. ERROR,
  829. );
  830. return true;
  831. }
  832. };
  833. if let Some(u) = attachments.get(lidx) {
  834. match u.content_type() {
  835. ContentType::MessageRfc822 => {
  836. match EnvelopeWrapper::new(u.body().to_vec()) {
  837. Ok(wrapper) => {
  838. context.replies.push_back(UIEvent::Action(Tab(New(Some(
  839. Box::new(EnvelopeView::new(
  840. wrapper,
  841. None,
  842. None,
  843. self.coordinates.0,
  844. )),
  845. )))));
  846. }
  847. Err(e) => {
  848. context.replies.push_back(UIEvent::StatusEvent(
  849. StatusEvent::DisplayMessage(format!("{}", e)),
  850. ));
  851. }
  852. }
  853. return true;
  854. }
  855. ContentType::Text { .. } | ContentType::PGPSignature => {
  856. self.mode = ViewMode::Attachment(lidx);
  857. self.dirty = true;
  858. }
  859. ContentType::Multipart { .. } => {
  860. context.replies.push_back(UIEvent::StatusEvent(
  861. StatusEvent::DisplayMessage(
  862. "Multipart attachments are not supported yet.".to_string(),
  863. ),
  864. ));
  865. return true;
  866. }
  867. ContentType::Other { ref name, .. } => {
  868. let attachment_type = u.mime_type();
  869. let binary = query_default_app(&attachment_type);
  870. let mut name_opt = name.as_ref().and_then(|n| {
  871. melib::email::parser::phrase(n.as_bytes())
  872. .to_full_result()
  873. .ok()
  874. .and_then(|n| String::from_utf8(n).ok())
  875. });
  876. if name_opt.is_none() {
  877. name_opt = name.as_ref().map(|n| n.clone());
  878. }
  879. if let Ok(binary) = binary {
  880. let p =
  881. create_temp_file(&decode(u, None), name_opt, None, true);
  882. Command::new(&binary)
  883. .arg(p.path())
  884. .stdin(Stdio::piped())
  885. .stdout(Stdio::piped())
  886. .spawn()
  887. .unwrap_or_else(|_| {
  888. panic!("Failed to start {}", binary.display())
  889. });
  890. context.temp_files.push(p);
  891. } else {
  892. context.replies.push_back(UIEvent::StatusEvent(
  893. StatusEvent::DisplayMessage(if name.is_some() {
  894. format!(
  895. "Couldn't find a default application for file {} (type {})",
  896. name.as_ref().unwrap(), attachment_type
  897. )
  898. } else {
  899. format!( "Couldn't find a default application for type {}", attachment_type)
  900. }
  901. ,
  902. )));
  903. return true;
  904. }
  905. }
  906. ContentType::OctetStream { ref name } => {
  907. context.replies.push_back(UIEvent::StatusEvent(
  908. StatusEvent::DisplayMessage(
  909. format!(
  910. "Failed to open {}. application/octet-stream isn't supported yet",
  911. name.as_ref().map(|n| n.as_str()).unwrap_or("file")
  912. )
  913. ),
  914. ));
  915. return true;
  916. }
  917. }
  918. } else {
  919. context.replies.push_back(UIEvent::StatusEvent(
  920. StatusEvent::DisplayMessage(format!(
  921. "Attachment `{}` not found.",
  922. lidx
  923. )),
  924. ));
  925. return true;
  926. }
  927. };
  928. }
  929. UIEvent::Input(ref key) if *key == shortcuts["toggle_expand_headers"] => {
  930. self.expand_headers = !self.expand_headers;
  931. self.dirty = true;
  932. return true;
  933. }
  934. UIEvent::Input(ref key)
  935. if !self.cmd_buf.is_empty()
  936. && self.mode == ViewMode::Url
  937. && *key == shortcuts["go_to_url"] =>
  938. {
  939. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  940. self.cmd_buf.clear();
  941. context
  942. .replies
  943. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  944. let url = {
  945. let account = &mut context.accounts[self.coordinates.0];
  946. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  947. let finder = LinkFinder::new();
  948. let op = account.operation(envelope.hash());
  949. let t = match envelope.body(op) {
  950. Ok(body) => body.text().to_string(),
  951. Err(e) => {
  952. context.replies.push_back(UIEvent::Notification(
  953. Some("Failed to open e-mail".to_string()),
  954. e.to_string(),
  955. Some(NotificationType::ERROR),
  956. ));
  957. log(
  958. format!(
  959. "Failed to open envelope {}: {}",
  960. envelope.message_id_display(),
  961. e.to_string()
  962. ),
  963. ERROR,
  964. );
  965. return true;
  966. }
  967. };
  968. let links: Vec<Link> = finder.links(&t).collect();
  969. if let Some(u) = links.get(lidx) {
  970. u.as_str().to_string()
  971. } else {
  972. context.replies.push_back(UIEvent::StatusEvent(
  973. StatusEvent::DisplayMessage(format!("Link `{}` not found.", lidx)),
  974. ));
  975. return true;
  976. }
  977. };
  978. if let Err(e) = Command::new("xdg-open")
  979. .arg(url)
  980. .stdin(Stdio::piped())
  981. .stdout(Stdio::piped())
  982. .spawn()
  983. {
  984. context.replies.push_back(UIEvent::Notification(
  985. Some("Failed to launch xdg-open".to_string()),
  986. e.to_string(),
  987. Some(NotificationType::ERROR),
  988. ));
  989. }
  990. return true;
  991. }
  992. UIEvent::Input(ref key)
  993. if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url)
  994. && *key == shortcuts["toggle_url_mode"] =>
  995. {
  996. match self.mode {
  997. ViewMode::Normal => self.mode = ViewMode::Url,
  998. ViewMode::Url => self.mode = ViewMode::Normal,
  999. _ => {}
  1000. }
  1001. self.dirty = true;
  1002. return true;
  1003. }
  1004. UIEvent::EnvelopeRename(old_hash, new_hash) if self.coordinates.2 == old_hash => {
  1005. self.coordinates.2 = new_hash;
  1006. }
  1007. UIEvent::Action(View(ViewAction::SaveAttachment(a_i, ref path))) => {
  1008. use std::io::Write;
  1009. let account = &mut context.accounts[self.coordinates.0];
  1010. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  1011. let op = account.operation(envelope.hash());
  1012. let attachments = match envelope.body(op) {
  1013. Ok(body) => body.attachments(),
  1014. Err(e) => {
  1015. context.replies.push_back(UIEvent::Notification(
  1016. Some("Failed to open e-mail".to_string()),
  1017. e.to_string(),
  1018. Some(NotificationType::ERROR),
  1019. ));
  1020. log(
  1021. format!(
  1022. "Failed to open envelope {}: {}",
  1023. envelope.message_id_display(),
  1024. e.to_string()
  1025. ),
  1026. ERROR,
  1027. );
  1028. return true;
  1029. }
  1030. };
  1031. if let Some(u) = attachments.get(a_i) {
  1032. match u.content_type() {
  1033. ContentType::MessageRfc822
  1034. | ContentType::Text { .. }
  1035. | ContentType::PGPSignature => {
  1036. debug!(path);
  1037. let mut f = match std::fs::File::create(path) {
  1038. Err(e) => {
  1039. context.replies.push_back(UIEvent::Notification(
  1040. Some(format!("Failed to create file at {}", path)),
  1041. e.to_string(),
  1042. Some(NotificationType::ERROR),
  1043. ));
  1044. log(
  1045. format!(
  1046. "Failed to create file at {}: {}",
  1047. path,
  1048. e.to_string()
  1049. ),
  1050. ERROR,
  1051. );
  1052. return true;
  1053. }
  1054. Ok(f) => f,
  1055. };
  1056. f.write_all(&decode(u, None)).unwrap();
  1057. f.flush().unwrap();
  1058. }
  1059. ContentType::Multipart { .. } => {
  1060. context.replies.push_back(UIEvent::StatusEvent(
  1061. StatusEvent::DisplayMessage(
  1062. "Multipart attachments are not supported yet.".to_string(),
  1063. ),
  1064. ));
  1065. return true;
  1066. }
  1067. ContentType::OctetStream { name: ref _name }
  1068. | ContentType::Other {
  1069. name: ref _name, ..
  1070. } => {
  1071. let mut f = match std::fs::File::create(path.trim()) {
  1072. Err(e) => {
  1073. context.replies.push_back(UIEvent::Notification(
  1074. Some(format!("Failed to create file at {}", path)),
  1075. e.to_string(),
  1076. Some(NotificationType::ERROR),
  1077. ));
  1078. log(
  1079. format!(
  1080. "Failed to create file at {}: {}",
  1081. path,
  1082. e.to_string()
  1083. ),
  1084. ERROR,
  1085. );
  1086. return true;
  1087. }
  1088. Ok(f) => f,
  1089. };
  1090. f.write_all(&decode(u, None)).unwrap();
  1091. f.flush().unwrap();
  1092. }
  1093. }
  1094. context.replies.push_back(UIEvent::Notification(
  1095. None,
  1096. format!("Saved at {}", &path),
  1097. Some(NotificationType::INFO),
  1098. ));
  1099. } else {
  1100. context
  1101. .replies
  1102. .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!(
  1103. "Attachment `{}` not found.",
  1104. a_i
  1105. ))));
  1106. return true;
  1107. }
  1108. }
  1109. UIEvent::Action(MailingListAction(ref e)) => {
  1110. let unsafe_context = context as *mut Context;
  1111. let account = &context.accounts[self.coordinates.0];
  1112. if !account.contains_key(self.coordinates.2) {
  1113. /* The envelope has been renamed or removed, so wait for the appropriate event to
  1114. * arrive */
  1115. return true;
  1116. }
  1117. let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
  1118. if let Some(actions) = list_management::detect(&envelope) {
  1119. match e {
  1120. MailingListAction::ListPost if actions.post.is_some() => {
  1121. /* open composer */
  1122. let mut draft = Draft::default();
  1123. draft.set_header("To", actions.post.unwrap().to_string());
  1124. context.replies.push_back(UIEvent::Action(Tab(NewDraft(
  1125. self.coordinates.0,
  1126. Some(draft),
  1127. ))));
  1128. return true;
  1129. }
  1130. MailingListAction::ListUnsubscribe if actions.unsubscribe.is_some() => {
  1131. /* autosend or open unsubscribe option*/
  1132. let unsubscribe = actions.unsubscribe.unwrap();
  1133. for option in unsubscribe {
  1134. /* TODO: Ask for confirmation before proceding with an action */
  1135. match option {
  1136. list_management::UnsubscribeOption::Email(email) => {
  1137. if let Ok(mailto) = Mailto::try_from(email) {
  1138. let mut draft: Draft = mailto.into();
  1139. draft.headers_mut().insert(
  1140. "From".into(),
  1141. crate::components::mail::get_display_name(
  1142. context,
  1143. self.coordinates.0,
  1144. ),
  1145. );
  1146. if super::compose::send_draft(
  1147. ToggleFlag::False,
  1148. /* FIXME: refactor to avoid unsafe.
  1149. *
  1150. * actions contains byte slices from the envelope's
  1151. * headers send_draft only needs a mut ref for
  1152. * context to push back replies and save the sent
  1153. * message */
  1154. unsafe { &mut *(unsafe_context) },
  1155. self.coordinates.0,
  1156. draft,
  1157. ) {
  1158. context.replies.push_back(UIEvent::Notification(
  1159. Some("Sent unsubscribe email.".into()),
  1160. "Sent unsubscribe email".to_string(),
  1161. None,
  1162. ));
  1163. return true;
  1164. }
  1165. }
  1166. }
  1167. list_management::UnsubscribeOption::Url(url) => {
  1168. if let Err(e) = Command::new("xdg-open")
  1169. .arg(String::from_utf8_lossy(url).into_owned())
  1170. .stdin(Stdio::piped())
  1171. .stdout(Stdio::piped())
  1172. .spawn()
  1173. {
  1174. context.replies.push_back(UIEvent::StatusEvent(
  1175. StatusEvent::DisplayMessage(format!(
  1176. "Couldn't launch xdg-open: {}",
  1177. e
  1178. )),
  1179. ));
  1180. }
  1181. return true;
  1182. }
  1183. }
  1184. }
  1185. }
  1186. MailingListAction::ListArchive if actions.archive.is_some() => {
  1187. /* open archive url with xdg-open */
  1188. if let Err(e) = Command::new("xdg-open")
  1189. .arg(actions.archive.unwrap())
  1190. .stdin(Stdio::piped())
  1191. .stdout(Stdio::piped())
  1192. .spawn()
  1193. {
  1194. context.replies.push_back(UIEvent::StatusEvent(
  1195. StatusEvent::DisplayMessage(format!(
  1196. "Couldn't launch xdg-open: {}",
  1197. e
  1198. )),
  1199. ));
  1200. }
  1201. return true;
  1202. }
  1203. _ => { /* error print message to user */ }
  1204. }
  1205. }
  1206. }
  1207. UIEvent::Action(Listing(OpenInNewTab)) => {
  1208. context
  1209. .replies
  1210. .push_back(UIEvent::Action(Tab(New(Some(Box::new(self.clone()))))));
  1211. return true;
  1212. }
  1213. _ => {}
  1214. }
  1215. false
  1216. }
  1217. fn is_dirty(&self) -> bool {
  1218. self.dirty
  1219. || self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  1220. || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  1221. || if let ViewMode::ContactSelector(ref s) = self.mode {
  1222. s.is_dirty()
  1223. } else {
  1224. false
  1225. }
  1226. }
  1227. fn set_dirty(&mut self) {
  1228. self.dirty = true;
  1229. match self.mode {
  1230. ViewMode::Normal => {
  1231. if let Some(p) = self.pager.as_mut() {
  1232. p.set_dirty();
  1233. }
  1234. }
  1235. ViewMode::Subview => {
  1236. if let Some(s) = self.subview.as_mut() {
  1237. s.set_dirty();
  1238. }
  1239. }
  1240. _ => {}
  1241. }
  1242. }
  1243. fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
  1244. let mut map = if let Some(ref sbv) = self.subview {
  1245. sbv.get_shortcuts(context)
  1246. } else if let Some(ref pgr) = self.pager {
  1247. pgr.get_shortcuts(context)
  1248. } else {
  1249. Default::default()
  1250. };
  1251. let mut our_map = FnvHashMap::with_capacity_and_hasher(4, Default::default());
  1252. our_map.insert("add_addresses_to_contacts", Key::Char('c'));
  1253. our_map.insert("view_raw_source", Key::Alt('r'));
  1254. if self.mode.is_attachment()
  1255. || self.mode == ViewMode::Subview
  1256. || self.mode == ViewMode::Raw
  1257. || self.mode == ViewMode::Url
  1258. {
  1259. our_map.insert("return_to_normal_view", Key::Char('r'));
  1260. }
  1261. our_map.insert("open_attachment", Key::Char('a'));
  1262. our_map.insert("open_mailcap", Key::Char('m'));
  1263. if self.mode == ViewMode::Url {
  1264. our_map.insert("go_to_url", Key::Char('g'));
  1265. }
  1266. if self.mode == ViewMode::Normal || self.mode == ViewMode::Url {
  1267. our_map.insert("toggle_url_mode", Key::Char('u'));
  1268. }
  1269. our_map.insert("toggle_expand_headers", Key::Char('h'));
  1270. map.insert(MailView::DESCRIPTION.to_string(), our_map);
  1271. map
  1272. }
  1273. fn id(&self) -> ComponentId {
  1274. self.id
  1275. }
  1276. fn set_id(&mut self, id: ComponentId) {
  1277. self.id = id;
  1278. }
  1279. fn kill(&mut self, id: ComponentId, context: &mut Context) {
  1280. debug_assert!(self.id == id);
  1281. context
  1282. .replies
  1283. .push_back(UIEvent::Action(Tab(Kill(self.id))));
  1284. }
  1285. }