diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 7666cb438..5edab792c 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -1116,7 +1116,7 @@ INSERT INTO msgs txt, subject, txt_raw, param, bytes, mime_headers, mime_in_reply_to, mime_references, mime_modified, error, ephemeral_timer, - ephemeral_timestamp, download_state + ephemeral_timestamp, download_state, hop_info ) VALUES ( ?, ?, ?, ?, @@ -1125,7 +1125,7 @@ INSERT INTO msgs ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, - ? + ?, ? ); "#, )?; @@ -1199,6 +1199,7 @@ INSERT INTO msgs } else { DownloadState::Done }, + mime_parser.hop_info ])?; let row_id = conn.last_insert_rowid(); diff --git a/src/dc_tools.rs b/src/dc_tools.rs index 7f8733c80..f2b9ae586 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -5,6 +5,7 @@ use core::cmp::{max, min}; use std::borrow::Cow; use std::fmt; use std::io::Cursor; +use std::str::from_utf8; use std::str::FromStr; use std::time::{Duration, SystemTime}; @@ -14,6 +15,9 @@ use async_std::{fs, io}; use anyhow::{bail, Error}; use chrono::{Local, TimeZone}; +use mailparse::dateparse; +use mailparse::headers::Headers; +use mailparse::MailHeaderMap; use rand::{thread_rng, Rng}; use crate::chat::{add_device_msg, add_device_msg_with_importance}; @@ -670,13 +674,126 @@ pub fn remove_subject_prefix(last_subject: &str) -> String { .to_string() } +// Types and methods to create hop-info for message-info + +fn extract_address_from_receive_header<'a>(header: &'a str, start: &str) -> Option<&'a str> { + let header_len = header.len(); + header.find(start).and_then(|mut begin| { + begin += start.len(); + let end = header + .get(begin..)? + .find(|c: char| c.is_whitespace()) + .unwrap_or(header_len); + header.get(begin..begin + end) + }) +} + +pub(crate) fn parse_receive_header(header: &str) -> String { + let header = header.replace(&['\r', '\n'][..], ""); + let mut hop_info = String::from("Hop: "); + + if let Some(from) = extract_address_from_receive_header(&header, "from ") { + hop_info += &format!("From: {}; ", from.trim()); + } + + if let Some(by) = extract_address_from_receive_header(&header, "by ") { + hop_info += &format!("By: {}; ", by.trim()); + } + + if let Ok(date) = dateparse(&header) { + // In tests, use the UTC timezone so that the test is reproducible + #[cfg(test)] + let date_obj = chrono::Utc.timestamp(date, 0); + #[cfg(not(test))] + let date_obj = Local.timestamp(date, 0); + + hop_info += &format!("Date: {}", date_obj.to_rfc2822()); + }; + + hop_info +} + +/// parses "receive"-headers +pub(crate) fn parse_receive_headers(headers: &Headers) -> String { + headers + .get_all_headers("Received") + .iter() + .rev() + .filter_map(|header_map_item| from_utf8(header_map_item.get_value_raw()).ok()) + .map(parse_receive_header) + .collect::>() + .join("\n") +} + #[cfg(test)] mod tests { #![allow(clippy::indexing_slicing)] use super::*; - use crate::test_utils::TestContext; + use crate::{ + config::Config, dc_receive_imf::dc_receive_imf, message::get_msg_info, + test_utils::TestContext, + }; + + #[test] + fn test_parse_receive_headers() { + // Test `parse_receive_headers()` with some more-or-less random emails from the test-data + let raw = include_bytes!("../test-data/message/mail_with_cc.txt"); + let expected = + "Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000\n\ + Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000"; + check_parse_receive_headers(raw, expected); + + let raw = include_bytes!("../test-data/message/wrong-html.eml"); + let expected = + "Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 06 Aug 2020 16:40:31 +0000\n\ + Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 06 Aug 2020 16:40:32 +0000"; + check_parse_receive_headers(raw, expected); + + let raw = include_bytes!("../test-data/message/posteo_ndn.eml"); + let expected = + "Hop: By: mout01.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\ + Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\ + Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\ + Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\ + Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\ + Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 09 Jun 2020 18:44:24 +0000"; + check_parse_receive_headers(raw, expected); + } + + fn check_parse_receive_headers(raw: &[u8], expected: &str) { + let mail = mailparse::parse_mail(raw).unwrap(); + let hop_info = parse_receive_headers(&mail.get_headers()); + assert_eq!(hop_info, expected) + } + + #[async_std::test] + async fn test_parse_receive_headers_integration() -> anyhow::Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + let raw = include_bytes!("../test-data/message/mail_with_cc.txt"); + dc_receive_imf(&t, raw, "INBOX", 1, false).await.unwrap(); + let g = t.get_last_msg().await; + + let expected = r"State: Fresh + +hi + +Message-ID: 2dfdbde7@example.org +Last seen as: INBOX/1 + +Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000 +Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000"; + let result = get_msg_info(&t, g.id).await.unwrap(); + // little hack to ignore the first row of a parsed email because it contains a + // send time that depends and the test runtime which makes it impossible to + // compare with a static string + let capped_result = &result[result.find("State").unwrap()..]; + + assert_eq!(expected, capped_result); + Ok(()) + } #[test] fn test_rust_ftoa() { diff --git a/src/message.rs b/src/message.rs index 0d24ac913..fb32421ff 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1169,9 +1169,16 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { } if let Some(ref server_folder) = msg.server_folder { if !server_folder.is_empty() { - ret += &format!("\nLast seen as: {}/{}", server_folder, msg.server_uid); + ret += &format!("\nLast seen as: {}/{}\n", server_folder, msg.server_uid); } } + let hop_info: Option = context + .sql + .query_get_value("SELECT hop_info FROM msgs WHERE id=?;", paramsv![msg_id]) + .await?; + + ret += "\n"; + ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned()); Ok(ret) } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 794be2d6d..712e366c8 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -15,7 +15,7 @@ use crate::blob::BlobObject; use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS}; use crate::contact::addr_normalize; use crate::context::Context; -use crate::dc_tools::{dc_get_filemeta, dc_truncate}; +use crate::dc_tools::{dc_get_filemeta, dc_truncate, parse_receive_headers}; use crate::dehtml::dehtml; use crate::e2ee; use crate::events::EventType; @@ -82,6 +82,8 @@ pub struct MimeMessage { /// This is non-empty only if the message was actually encrypted. It is used /// for e.g. late-parsing HTML. pub decoded_data: Vec, + + pub(crate) hop_info: String, } #[derive(Debug, PartialEq)] @@ -294,6 +296,7 @@ impl MimeMessage { footer: None, is_mime_modified: false, decoded_data: Vec::new(), + hop_info: parse_receive_headers(&mail.get_headers()), }; match partial { diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index b74330577..aced6906a 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -497,7 +497,10 @@ item TEXT DEFAULT '');"#, ) .await?; } - + if dbversion < 81 { + sql.execute_migration("ALTER TABLE msgs ADD COLUMN hop_info TEXT;", 81) + .await?; + } Ok(( recalc_fingerprints, update_icons,