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.

551 lines
22 KiB

4 years ago
  1. /*
  2. * meli
  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::process::{Command, Stdio};
  24. use xdg_utils::query_default_app;
  25. #[derive(PartialEq, Eq, Debug)]
  26. enum ViewMode {
  27. Normal,
  28. Url,
  29. Attachment(usize),
  30. Raw,
  31. Subview,
  32. }
  33. impl ViewMode {
  34. fn is_attachment(&self) -> bool {
  35. matches!(self, ViewMode::Attachment(_))
  36. }
  37. }
  38. /// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
  39. /// menus
  40. #[derive(Debug)]
  41. pub struct EnvelopeView {
  42. pager: Option<Pager>,
  43. subview: Option<Box<dyn Component>>,
  44. dirty: bool,
  45. mode: ViewMode,
  46. mail: Mail,
  47. _account_hash: AccountHash,
  48. cmd_buf: String,
  49. id: ComponentId,
  50. }
  51. impl fmt::Display for EnvelopeView {
  52. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  53. write!(f, "view mail")
  54. }
  55. }
  56. impl EnvelopeView {
  57. pub fn new(
  58. mail: Mail,
  59. pager: Option<Pager>,
  60. subview: Option<Box<dyn Component>>,
  61. _account_hash: AccountHash,
  62. ) -> Self {
  63. EnvelopeView {
  64. pager,
  65. subview,
  66. dirty: true,
  67. mode: ViewMode::Normal,
  68. mail,
  69. _account_hash,
  70. cmd_buf: String::with_capacity(4),
  71. id: ComponentId::new_v4(),
  72. }
  73. }
  74. /// Returns the string to be displayed in the Viewer
  75. fn attachment_to_text(&self, body: &Attachment, context: &mut Context) -> String {
  76. let finder = LinkFinder::new();
  77. let body_text = String::from_utf8_lossy(&body.decode_rec(DecodeOptions {
  78. filter: Some(Box::new(|a: &Attachment, v: &mut Vec<u8>| {
  79. if a.content_type().is_text_html() {
  80. let settings = &context.settings;
  81. if let Some(filter_invocation) = settings.pager.html_filter.as_ref() {
  82. let command_obj = Command::new("sh")
  83. .args(&["-c", filter_invocation])
  84. .stdin(Stdio::piped())
  85. .stdout(Stdio::piped())
  86. .spawn();
  87. match command_obj {
  88. Err(err) => {
  89. context.replies.push_back(UIEvent::Notification(
  90. Some(format!(
  91. "Failed to start html filter process: {}",
  92. filter_invocation,
  93. )),
  94. err.to_string(),
  95. Some(NotificationType::Error(melib::ErrorKind::External)),
  96. ));
  97. }
  98. Ok(mut html_filter) => {
  99. html_filter
  100. .stdin
  101. .as_mut()
  102. .unwrap()
  103. .write_all(v)
  104. .expect("Failed to write to stdin");
  105. *v = format!(
  106. "Text piped through `{}`. Press `v` to open in web browser. \n\n",
  107. filter_invocation
  108. )
  109. .into_bytes();
  110. v.extend(html_filter.wait_with_output().unwrap().stdout);
  111. }
  112. }
  113. }
  114. }
  115. })),
  116. ..Default::default()
  117. }))
  118. .into_owned();
  119. match self.mode {
  120. ViewMode::Normal | ViewMode::Subview => {
  121. let mut t = body_text;
  122. if body.count_attachments() > 1 {
  123. t = body
  124. .attachments()
  125. .iter()
  126. .enumerate()
  127. .fold(t, |mut s, (idx, a)| {
  128. let _ = writeln!(s, "[{}] {}\n", idx, a);
  129. s
  130. });
  131. }
  132. t
  133. }
  134. ViewMode::Raw => String::from_utf8_lossy(body.body()).into_owned(),
  135. ViewMode::Url => {
  136. let mut t = body_text;
  137. for (lidx, l) in finder.links(&body.text()).enumerate() {
  138. let offset = if lidx < 10 {
  139. lidx * 3
  140. } else if lidx < 100 {
  141. 26 + (lidx - 9) * 4
  142. } else if lidx < 1000 {
  143. 385 + (lidx - 99) * 5
  144. } else {
  145. panic!("BUG: Message body with more than 100 urls, fix this");
  146. };
  147. t.insert_str(l.start() + offset, &format!("[{}]", lidx));
  148. }
  149. if body.count_attachments() > 1 {
  150. t = body
  151. .attachments()
  152. .iter()
  153. .enumerate()
  154. .fold(t, |mut s, (idx, a)| {
  155. let _ = writeln!(s, "[{}] {}\n", idx, a);
  156. s
  157. });
  158. }
  159. t
  160. }
  161. ViewMode::Attachment(aidx) => {
  162. let attachments = body.attachments();
  163. let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
  164. ret.push_str(&attachments[aidx].text());
  165. ret
  166. }
  167. }
  168. }
  169. }
  170. impl Component for EnvelopeView {
  171. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  172. let upper_left = upper_left!(area);
  173. let bottom_right = bottom_right!(area);
  174. let theme_default = crate::conf::value(context, "theme_default");
  175. let email_header_theme = crate::conf::value(context, "email_header");
  176. let y: usize = {
  177. if self.mode == ViewMode::Raw {
  178. clear_area(grid, area, crate::conf::value(context, "theme_default"));
  179. context.dirty_areas.push_back(area);
  180. get_y(upper_left).saturating_sub(1)
  181. } else {
  182. let (x, y) = write_string_to_grid(
  183. &format!("Date: {}", self.mail.date_as_str()),
  184. grid,
  185. email_header_theme.fg,
  186. email_header_theme.bg,
  187. email_header_theme.attrs,
  188. area,
  189. Some(get_x(upper_left)),
  190. );
  191. for x in x..=get_x(bottom_right) {
  192. grid[(x, y)]
  193. .set_ch(' ')
  194. .set_fg(theme_default.fg)
  195. .set_bg(theme_default.bg);
  196. }
  197. let (x, y) = write_string_to_grid(
  198. &format!("From: {}", self.mail.field_from_to_string()),
  199. grid,
  200. email_header_theme.fg,
  201. email_header_theme.bg,
  202. email_header_theme.attrs,
  203. (set_y(upper_left, y + 1), bottom_right),
  204. Some(get_x(upper_left)),
  205. );
  206. for x in x..=get_x(bottom_right) {
  207. grid[(x, y)]
  208. .set_ch(' ')
  209. .set_fg(theme_default.fg)
  210. .set_bg(theme_default.bg);
  211. }
  212. let (x, y) = write_string_to_grid(
  213. &format!("To: {}", self.mail.field_to_to_string()),
  214. grid,
  215. email_header_theme.fg,
  216. email_header_theme.bg,
  217. email_header_theme.attrs,
  218. (set_y(upper_left, y + 1), bottom_right),
  219. Some(get_x(upper_left)),
  220. );
  221. for x in x..=get_x(bottom_right) {
  222. grid[(x, y)]
  223. .set_ch(' ')
  224. .set_fg(theme_default.fg)
  225. .set_bg(theme_default.bg);
  226. }
  227. let (x, y) = write_string_to_grid(
  228. &format!("Subject: {}", self.mail.subject()),
  229. grid,
  230. email_header_theme.fg,
  231. email_header_theme.bg,
  232. email_header_theme.attrs,
  233. (set_y(upper_left, y + 1), bottom_right),
  234. Some(get_x(upper_left)),
  235. );
  236. for x in x..=get_x(bottom_right) {
  237. grid[(x, y)]
  238. .set_ch(' ')
  239. .set_fg(theme_default.fg)
  240. .set_bg(theme_default.bg);
  241. }
  242. let (x, y) = write_string_to_grid(
  243. &format!("Message-ID: <{}>", self.mail.message_id_raw()),
  244. grid,
  245. email_header_theme.fg,
  246. email_header_theme.bg,
  247. email_header_theme.attrs,
  248. (set_y(upper_left, y + 1), bottom_right),
  249. Some(get_x(upper_left)),
  250. );
  251. for x in x..=get_x(bottom_right) {
  252. grid[(x, y)]
  253. .set_ch(' ')
  254. .set_fg(theme_default.fg)
  255. .set_bg(theme_default.bg);
  256. }
  257. clear_area(
  258. grid,
  259. (set_y(upper_left, y + 1), set_y(bottom_right, y + 2)),
  260. crate::conf::value(context, "theme_default"),
  261. );
  262. context
  263. .dirty_areas
  264. .push_back((upper_left, set_y(bottom_right, y + 1)));
  265. y + 1
  266. }
  267. };
  268. if self.dirty {
  269. let body = self.mail.body();
  270. match self.mode {
  271. ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
  272. let attachment = &body.attachments()[aidx];
  273. self.subview = Some(Box::new(HtmlView::new(attachment, context)));
  274. }
  275. ViewMode::Normal if body.is_html() => {
  276. self.subview = Some(Box::new(HtmlView::new(&body, context)));
  277. self.mode = ViewMode::Subview;
  278. }
  279. _ => {
  280. let text = { self.attachment_to_text(&body, context) };
  281. let cursor_pos = if self.mode.is_attachment() {
  282. Some(0)
  283. } else {
  284. self.pager.as_ref().map(Pager::cursor_pos)
  285. };
  286. let colors = crate::conf::value(context, "mail.view.body");
  287. self.pager = Some(Pager::from_string(
  288. text,
  289. Some(context),
  290. cursor_pos,
  291. None,
  292. colors,
  293. ));
  294. }
  295. };
  296. self.dirty = false;
  297. }
  298. if let Some(s) = self.subview.as_mut() {
  299. s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  300. } else if let Some(p) = self.pager.as_mut() {
  301. p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
  302. }
  303. }
  304. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  305. if let Some(ref mut sub) = self.subview {
  306. if sub.process_event(event, context) {
  307. return true;
  308. }
  309. } else if let Some(ref mut p) = self.pager {
  310. if p.process_event(event, context) {
  311. return true;
  312. }
  313. }
  314. match *event {
  315. UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => {
  316. self.cmd_buf.clear();
  317. context
  318. .replies
  319. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  320. return true;
  321. }
  322. UIEvent::Input(Key::Char(c)) if ('0'..='9').contains(&c) => {
  323. self.cmd_buf.push(c);
  324. return true;
  325. }
  326. UIEvent::Input(Key::Char('r'))
  327. if self.mode == ViewMode::Normal || self.mode == ViewMode::Raw =>
  328. {
  329. self.mode = if self.mode == ViewMode::Raw {
  330. ViewMode::Normal
  331. } else {
  332. ViewMode::Raw
  333. };
  334. self.dirty = true;
  335. return true;
  336. }
  337. UIEvent::Input(Key::Char('r'))
  338. if self.mode.is_attachment() || self.mode == ViewMode::Subview =>
  339. {
  340. self.mode = ViewMode::Normal;
  341. self.subview.take();
  342. self.dirty = true;
  343. return true;
  344. }
  345. UIEvent::Input(Key::Char('a'))
  346. if !self.cmd_buf.is_empty() && self.mode == ViewMode::Normal =>
  347. {
  348. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  349. self.cmd_buf.clear();
  350. context
  351. .replies
  352. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  353. if let Some(u) = self.mail.body().attachments().get(lidx) {
  354. match u.content_type() {
  355. ContentType::MessageRfc822 => {
  356. self.mode = ViewMode::Subview;
  357. let colors = crate::conf::value(context, "mail.view.body");
  358. self.subview = Some(Box::new(Pager::from_string(
  359. String::from_utf8_lossy(&u.decode_rec(Default::default()))
  360. .to_string(),
  361. Some(context),
  362. None,
  363. None,
  364. colors,
  365. )));
  366. }
  367. ContentType::Text { .. }
  368. | ContentType::PGPSignature
  369. | ContentType::CMSSignature => {
  370. self.mode = ViewMode::Attachment(lidx);
  371. self.dirty = true;
  372. }
  373. ContentType::Multipart { .. } => {
  374. context.replies.push_back(UIEvent::StatusEvent(
  375. StatusEvent::DisplayMessage(
  376. "Multipart attachments are not supported yet.".to_string(),
  377. ),
  378. ));
  379. return true;
  380. }
  381. ContentType::Other { .. } => {
  382. let attachment_type = u.mime_type();
  383. let filename = u.filename();
  384. if let Ok(command) = query_default_app(&attachment_type) {
  385. let p = create_temp_file(
  386. &u.decode(Default::default()),
  387. filename.as_deref(),
  388. None,
  389. true,
  390. );
  391. let (exec_cmd, argument) = super::desktop_exec_to_command(
  392. &command,
  393. p.path.display().to_string(),
  394. false,
  395. );
  396. match Command::new(&exec_cmd)
  397. .arg(&argument)
  398. .stdin(Stdio::piped())
  399. .stdout(Stdio::piped())
  400. .spawn()
  401. {
  402. Ok(child) => {
  403. context.temp_files.push(p);
  404. context.children.push(child);
  405. }
  406. Err(err) => {
  407. context.replies.push_back(UIEvent::StatusEvent(
  408. StatusEvent::DisplayMessage(format!(
  409. "Failed to start `{} {}`: {}",
  410. &exec_cmd, &argument, err
  411. )),
  412. ));
  413. }
  414. }
  415. } else {
  416. context.replies.push_back(UIEvent::StatusEvent(
  417. StatusEvent::DisplayMessage(if let Some(filename) = filename.as_ref() {
  418. format!(
  419. "Couldn't find a default application for file {} (type {})",
  420. filename,
  421. attachment_type
  422. )
  423. } else {
  424. format!(
  425. "Couldn't find a default application for type {}",
  426. attachment_type
  427. )
  428. }),
  429. ));
  430. return true;
  431. }
  432. }
  433. ContentType::OctetStream { .. } => {
  434. context.replies.push_back(UIEvent::StatusEvent(
  435. StatusEvent::DisplayMessage(
  436. "application/octet-stream isn't supported yet".to_string(),
  437. ),
  438. ));
  439. return true;
  440. }
  441. }
  442. } else {
  443. context
  444. .replies
  445. .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!(
  446. "Attachment `{}` not found.",
  447. lidx
  448. ))));
  449. return true;
  450. }
  451. return true;
  452. }
  453. UIEvent::Input(Key::Char('g'))
  454. if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url =>
  455. {
  456. let lidx = self.cmd_buf.parse::<usize>().unwrap();
  457. self.cmd_buf.clear();
  458. context
  459. .replies
  460. .push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
  461. let url = {
  462. let finder = LinkFinder::new();
  463. let t = self.mail.body().text();
  464. let links: Vec<Link> = finder.links(&t).collect();
  465. if let Some(u) = links.get(lidx) {
  466. u.as_str().to_string()
  467. } else {
  468. context.replies.push_back(UIEvent::StatusEvent(
  469. StatusEvent::DisplayMessage(format!("Link `{}` not found.", lidx)),
  470. ));
  471. return true;
  472. }
  473. };
  474. let url_launcher = context.settings.pager.url_launcher.as_deref().unwrap_or(
  475. #[cfg(target_os = "macos")]
  476. {
  477. "open"
  478. },
  479. #[cfg(not(target_os = "macos"))]
  480. {
  481. "xdg-open"
  482. },
  483. );
  484. match Command::new(url_launcher)
  485. .arg(url)
  486. .stdin(Stdio::piped())
  487. .stdout(Stdio::piped())
  488. .spawn()
  489. {
  490. Ok(child) => context.children.push(child),
  491. Err(err) => context.replies.push_back(UIEvent::Notification(
  492. Some(format!("Failed to launch {:?}", url_launcher)),
  493. err.to_string(),
  494. Some(NotificationType::Error(melib::ErrorKind::External)),
  495. )),
  496. }
  497. return true;
  498. }
  499. UIEvent::Input(Key::Char('u')) => {
  500. match self.mode {
  501. ViewMode::Normal => self.mode = ViewMode::Url,
  502. ViewMode::Url => self.mode = ViewMode::Normal,
  503. _ => {}
  504. }
  505. self.dirty = true;
  506. return true;
  507. }
  508. _ => {}
  509. }
  510. false
  511. }
  512. fn is_dirty(&self) -> bool {
  513. self.dirty
  514. || self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  515. || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  516. }
  517. fn set_dirty(&mut self, value: bool) {
  518. self.dirty = value;
  519. }
  520. fn id(&self) -> ComponentId {
  521. self.id
  522. }
  523. fn kill(&mut self, id: ComponentId, context: &mut Context) {
  524. debug_assert!(self.id == id);
  525. context
  526. .replies
  527. .push_back(UIEvent::Action(Tab(Kill(self.id))));
  528. }
  529. fn set_id(&mut self, id: ComponentId) {
  530. self.id = id;
  531. }
  532. }