Compare commits

...
Author SHA1 Message Date
Floris Bruynooghe 9009cfcb64 Create configuration items as structs
This allows to have strongly typed config items.
2019-12-08 17:04:51 +01:00
holger krekel 3f2a371599 run lint without installing the package 2019-12-07 22:56:54 +01:00
holger krekel a3ca3d9179 allow to use a different buildhost :) 2019-12-07 22:56:54 +01:00
holger krekel 86ace1a4af - test and fix receiving text/html attachment in multipart/mixed situations
They are now preserved as attachment, instead of diving into parsing-html
  and simplifying.

- adapt mime-debugging
2019-12-07 22:56:54 +01:00
holger krekel 1abdf62045 passes test but needs cleanup 2019-12-07 22:56:54 +01:00
holger krekel 9e2a96675d try fix incoming text/html in multipart/mixed 2019-12-07 22:56:54 +01:00
13 changed files with 492 additions and 117 deletions
Generated
+12
View File
@@ -620,6 +620,7 @@ dependencies = [
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"debug_stub_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"deltachat_derive 0.1.0",
"derive_deref 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"encoded-words 0.1.0 (git+https://github.com/async-email/encoded-words)",
"escaper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -716,6 +717,16 @@ dependencies = [
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "derive_deref"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "derive_more"
version = "0.14.1"
@@ -3452,6 +3463,7 @@ dependencies = [
"checksum deltachat-provider-database 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "814dba060d9fdc7a989fccdc4810ada9d1c7a1f09131c78e42412bc6c634b93b"
"checksum derive_builder 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ac53fa6a3cda160df823a9346442525dcaf1e171999a1cf23e67067e4fd64d4"
"checksum derive_builder_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0288a23da9333c246bb18c143426074a6ae96747995c5819d2947b64cd942b37"
"checksum derive_deref 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "11554fdb0aa42363a442e0c4278f51c9621e20c1ce3bac51d79e60646f3b8b8f"
"checksum derive_more 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6d944ac6003ed268757ef1ee686753b57efc5fcf0ebe7b64c9fc81e7e32ff839"
"checksum des 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "74ba5f1b5aee9772379c2670ba81306e65a93c0ee3caade7a1d22b188d88a3af"
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+1
View File
@@ -55,6 +55,7 @@ webpki-roots = "0.18.0"
webpki = "0.21.0"
mailparse = "0.10.1"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
derive_deref = "1.1.0"
[dev-dependencies]
tempfile = "3.0"
+1 -1
View File
@@ -2,7 +2,7 @@
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=ci@b1.delta.chat
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
+1 -1
View File
@@ -2,7 +2,7 @@
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=ci@b1.delta.chat
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
+24
View File
@@ -477,6 +477,30 @@ class TestOnlineAccount:
assert msg2.filename.endswith("html.zip")
assert msg.filename != msg2.filename
def test_send_file_html_attachment(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2)
basename = "test.html"
content = "<html><body>text</body>data"
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
# write wrong html to see if core tries to parse it
# (it shouldn't as it's a file attachment)
f.write(content)
lp.sec("ac1: prepare and send attachment + text to ac2")
chat.send_file(p, mime_type="text/html")
lp.sec("ac2: receive message")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev[2] > const.DC_CHAT_ID_LAST_SPECIAL
msg = ac2.get_message_by_id(ev[2])
assert open(msg.filename).read() == content
assert msg.filename.endswith(basename)
def test_mvbox_sentbox_threads(self, acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.get_online_configuring_account(mvbox=True, sentbox=True)
+1 -1
View File
@@ -32,7 +32,7 @@ commands =
[testenv:lint]
skipsdist = True
usedevelop = True
skip_install = True
deps =
flake8
# pygments required by rst-lint
+242 -1
View File
@@ -1,5 +1,6 @@
//! # Key-value configuration management
use derive_deref::{Deref, DerefMut};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -8,6 +9,7 @@ use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::job::*;
use crate::sql;
use crate::stock::StockMessage;
/// The available configuration keys.
@@ -88,6 +90,164 @@ pub enum Config {
SysConfigKeys,
}
/// A trait defining a [Context]-wide configuration item.
///
/// Configuration items are stored in database of a [Context]. Most
/// configuration items are newtypes which implement [std::ops::Deref]
/// and [std::ops::DerefMut] though this is not required. However
/// what **is required** for the struct to implement
/// [rusqlite::ToSql] and [rusqlite::types::FromSql].
pub trait ConfigItem {
/// Returns the name of the key used in the SQLite database.
fn keyname() -> &'static str;
/// Loads the configuration item from the [Context]'s database.
///
/// If the configuration item is not available in the database,
/// `None` will be returned.
fn load(context: &Context) -> Result<Option<Self>, sql::Error>
where
Self: std::marker::Sized + rusqlite::types::FromSql,
{
context
.sql
.query_row(
"SELECT value FROM config WHERE keyname=?;",
params!(Self::keyname()),
|row| row.get(0),
)
.or_else(|err| match err {
sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
e => Err(e),
})
}
/// Stores the configuration item in the [Context]'s database.
fn store(&self, context: &Context) -> Result<(), sql::Error>
where
Self: rusqlite::ToSql,
{
if context.sql.exists(
"select value FROM config WHERE keyname=?;",
params!(Self::keyname()),
)? {
context.sql.execute(
"UPDATE config SET value=? WHERE keyname=?",
params![&self, Self::keyname()],
)?;
} else {
context.sql.execute(
"INSERT INTO config (keyname, value) VALUES (?, ?)",
params![Self::keyname(), &self],
)?;
}
Ok(())
}
/// Removes the configuration item from the [Context]'s database.
fn delete(context: &Context) -> Result<(), sql::Error> {
context
.sql
.execute(
"DELETE FROM config WHERE keyname=?",
params![Self::keyname()],
)
.and(Ok(()))
}
}
/// Configuration item: display address for this account.
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct Addr(pub String);
impl rusqlite::ToSql for Addr {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
self.0.to_sql()
}
}
impl rusqlite::types::FromSql for Addr {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
value.as_str().map(|v| Addr(v.to_string()))
}
}
impl ConfigItem for Addr {
fn keyname() -> &'static str {
"addr"
}
}
/// Configuration item:
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct MailServer(pub String);
impl rusqlite::ToSql for MailServer {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
self.0.to_sql()
}
}
impl rusqlite::types::FromSql for MailServer {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
value.as_str().map(|v| MailServer(v.to_string()))
}
}
impl ConfigItem for MailServer {
fn keyname() -> &'static str {
"mail_server"
}
}
/// Configuration item:
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct MailUser(pub String);
// XXX TODO
/// Configuration item: whether to watch the INBOX folder for changes.
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct InboxWatch(pub bool);
impl Default for InboxWatch {
fn default() -> Self {
InboxWatch(true)
}
}
impl rusqlite::ToSql for InboxWatch {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
// Column affinity is "text" so gets stored as string by SQLite.
let obj = rusqlite::types::Value::Integer(self.0 as i64);
Ok(rusqlite::types::ToSqlOutput::Owned(obj))
}
}
impl rusqlite::types::FromSql for InboxWatch {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let str_to_int = |s: &str| {
s.parse::<i64>()
.map_err(|e| rusqlite::types::FromSqlError::Other(Box::new(e)))
};
let int_to_bool = |i| match i {
0 => Ok(false),
1 => Ok(true),
v => Err(rusqlite::types::FromSqlError::OutOfRange(v)),
};
value
.as_str()
.and_then(str_to_int)
.and_then(int_to_bool)
.map(InboxWatch)
}
}
impl ConfigItem for InboxWatch {
fn keyname() -> &'static str {
"inbox_watch"
}
}
impl Context {
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub fn get_config(&self, key: Config) -> Option<String> {
@@ -175,11 +335,16 @@ fn get_config_keys_string() -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use std::str::FromStr;
use std::string::ToString;
use crate::test_utils::*;
use lazy_static::lazy_static;
lazy_static! {
static ref TC: TestContext = dummy_context();
}
#[test]
fn test_to_string() {
@@ -225,4 +390,80 @@ mod tests {
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
Ok(())
}
#[test]
fn test_inbox_watch() {
// Loading from context when it is not in the DB.
let val = InboxWatch::load(&TC.ctx).unwrap();
assert_eq!(val, None);
// Create in-memory from default.
let mut val = InboxWatch::default();
assert_eq!(*val, true);
// Assign using deref.
*val = false;
assert_eq!(*val, false);
// Construct newtype directly.
let val = InboxWatch(false);
assert_eq!(*val, false);
// Helper to query raw DB value.
let query_db_raw = || {
TC.ctx
.sql
.query_row(
"SELECT value FROM config WHERE KEYNAME=?",
params![InboxWatch::keyname()],
|row| row.get::<_, String>(0),
)
.unwrap()
};
// Save (non-default) value to the DB.
InboxWatch(false).store(&TC.ctx).unwrap();
assert_eq!(query_db_raw(), "0");
let val = InboxWatch::load(&TC.ctx).unwrap().unwrap();
assert_eq!(val, InboxWatch(false));
// Save true (aka default) value to the DB.
InboxWatch(true).store(&TC.ctx).unwrap();
assert_eq!(query_db_raw(), "1");
let val = InboxWatch::load(&TC.ctx).unwrap().unwrap();
assert_eq!(val, InboxWatch(true));
// Delete the value from the DB.
InboxWatch::delete(&TC.ctx).unwrap();
assert!(!TC
.ctx
.sql
.exists(
"SELECT value FROM config WHERE KEYNAME=?",
params![InboxWatch::keyname()],
)
.unwrap());
let val = InboxWatch::load(&TC.ctx).unwrap();
assert_eq!(val, None);
}
#[test]
fn test_addr() {
// In-memory creation
let val = Addr("me@example.com".into());
assert_eq!(*val, "me@example.com");
// Load when DB is empty.
let val = Addr::load(&TC.ctx).unwrap();
assert_eq!(val, None);
// Store and load.
Addr("me@example.com".into()).store(&TC.ctx).unwrap();
let val = Addr::load(&TC.ctx).unwrap();
assert_eq!(val, Some(Addr("me@example.com".into())));
// Delete
Addr::delete(&TC.ctx).unwrap();
assert_eq!(Addr::load(&TC.ctx).unwrap(), None);
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ pub fn dc_receive_imf(
server_uid,
);
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
info!(context, "dc_receive_imf: incoming message mime-body:");
println!("{}", String::from_utf8_lossy(imf_raw));
}
+3 -3
View File
@@ -247,13 +247,13 @@ fn decrypt_if_autocrypt_message<'a>(
// Errors are returned for failures related to decryption of AC-messages.
let encrypted_data_part = match wrapmime::get_autocrypt_mime(mail) {
Err(err) => {
// not a proper autocrypt message, abort and ignore
info!(context, "Not an autocrypt message: {:?}", err);
Err(_) => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
Ok(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
decrypt_part(
context,
+6
View File
@@ -525,6 +525,12 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
outer_message = outer_message.header(header);
}
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(self.context, "mimefactory: outgoing message mime:");
let raw_message = message.clone().build().as_string();
println!("{}", raw_message);
}
let encrypted =
encrypt_helper.encrypt(self.context, min_verified, message, &peerstates)?;
+135 -109
View File
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
use deltachat_derive::{FromSql, ToSql};
use lettre_email::mime::{self, Mime};
use mailparse::MailHeaderMap;
use mailparse::{DispositionType, MailHeaderMap};
use crate::aheader::Aheader;
use crate::blob::BlobObject;
@@ -22,6 +22,7 @@ use crate::param::*;
use crate::peerstate::Peerstate;
use crate::securejoin::handle_degrade_event;
use crate::stock::StockMessage;
use crate::{bail, ensure};
#[derive(Debug)]
pub struct MimeParser<'a> {
@@ -109,6 +110,10 @@ impl<'a> MimeParser<'a> {
if let Some(raw) = raw {
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "decrypted message mime-body:");
println!("{}", String::from_utf8_lossy(&mail_raw));
}
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
@@ -487,41 +492,10 @@ impl<'a> MimeParser<'a> {
}
}
_ => {
// Add all parts (in fact,
// AddSinglePartIfKnown() later check if the parts are really supported)
// HACK: the following lines are a hack for clients who use
// multipart/mixed instead of multipart/alternative for
// combined text/html messages (eg. Stock Android "Mail" does so).
// So, if we detect such a message below, we skip the Html
// part. However, not sure, if there are useful situations to use
// plain+html in multipart/mixed - if so, we should disable the hack.
let mut skip_part = -1;
let mut html_part = -1;
let mut plain_cnt = 0;
let mut html_cnt = 0;
for (i, cur_data) in mail.subparts.iter().enumerate() {
match get_mime_type(cur_data)?.0.type_() {
mime::TEXT => {
plain_cnt += 1;
}
mime::HTML => {
html_part = i as isize;
html_cnt += 1;
}
_ => {}
}
}
if plain_cnt == 1 && html_cnt == 1 {
warn!(
self.context,
"HACK: multipart/mixed message found with Plain and HTML, we\'ll skip the HTML part as this seems to be unwanted."
);
skip_part = html_part;
}
for (i, cur_data) in mail.subparts.iter().enumerate() {
if i as isize != skip_part && self.parse_mime_recursive(cur_data)? {
// Add all parts (in fact, AddSinglePartIfKnown() later check if
// the parts are really supported)
for cur_data in mail.subparts.iter() {
if self.parse_mime_recursive(cur_data)? {
any_part_added = true;
}
}
@@ -536,84 +510,63 @@ impl<'a> MimeParser<'a> {
let (mime_type, msg_type) = get_mime_type(mail)?;
let raw_mime = mail.ctype.mimetype.to_lowercase();
let filename = get_attachment_filename(mail);
info!(
self.context,
"add_single_part_if_known {:?} {:?} {:?}", mime_type, msg_type, filename
);
let old_part_count = self.parts.len();
// regard `Content-Transfer-Encoding:`
match mime_type.type_() {
mime::TEXT | mime::HTML => {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(self.context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
};
// check header directly as is_send_by_messenger is not yet set up
let is_msgrmsg = self.lookup_field("Chat-Version").is_some();
let mut simplifier = Simplify::new();
let simplified_txt = if decoded_data.is_empty() {
"".into()
} else {
let is_html = mime_type == mime::TEXT_HTML;
simplifier.simplify(&decoded_data, is_html, is_msgrmsg)
};
if !simplified_txt.is_empty() {
let mut part = Part::default();
part.typ = Viewtype::Text;
part.mimetype = Some(mime_type);
part.msg = simplified_txt;
part.msg_raw = Some(decoded_data);
self.do_add_single_part(part);
if let Ok(filename) = filename {
self.do_add_single_file_part(
msg_type,
mime_type,
&raw_mime,
&mail.get_body_raw()?,
&filename,
);
} else {
match mime_type.type_() {
mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
bail!("missing attachment");
}
mime::TEXT | mime::HTML => {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(self.context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
};
if simplifier.is_forwarded {
self.is_forwarded = true;
}
}
mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
// try to get file name from
// `Content-Disposition: ... filename*=...`
// or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
// or `Content-Disposition: ... filename=...`
// check header directly as is_send_by_messenger is not yet set up
let is_msgrmsg = self.lookup_field("Chat-Version").is_some();
let ct = mail.get_content_disposition()?;
let mut desired_filename = ct
.params
.iter()
.filter(|(key, _value)| key.starts_with("filename"))
.fold(String::new(), |mut acc, (_key, value)| {
acc += value;
acc
});
if desired_filename.is_empty() {
if let Some(param) = ct.params.get("name") {
// might be a wrongly encoded filename
desired_filename = param.to_string();
}
}
// if there is still no filename, guess one
if desired_filename.is_empty() {
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
desired_filename = format!("file.{}", subtype,);
let mut simplifier = Simplify::new();
let simplified_txt = if decoded_data.is_empty() {
"".into()
} else {
return Ok(false);
let is_html = mime_type == mime::TEXT_HTML;
simplifier.simplify(&decoded_data, is_html, is_msgrmsg)
};
if !simplified_txt.is_empty() {
let mut part = Part::default();
part.typ = Viewtype::Text;
part.mimetype = Some(mime_type);
part.msg = simplified_txt;
part.msg_raw = Some(decoded_data);
self.do_add_single_part(part);
}
if simplifier.is_forwarded {
self.is_forwarded = true;
}
}
self.do_add_single_file_part(
msg_type,
mime_type,
&raw_mime,
&mail.get_body_raw()?,
&desired_filename,
);
_ => {}
}
_ => {}
}
// add object? (we do not add all objects, eg. signatures etc. are ignored)
@@ -662,6 +615,7 @@ impl<'a> MimeParser<'a> {
return;
}
};
info!(self.context, "added blobfile: {:?}", blob.as_name());
/* create and register Mime part referencing the new Blob object */
let mut part = Part::default();
@@ -913,12 +867,13 @@ pub struct Part {
pub param: Params,
}
/// return mimetype and viewtype for a parsed mail
fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
let viewtype = match mimetype.type_() {
mime::TEXT => {
if !mailmime_is_attachment_disposition(mail) {
if !is_attachment_disposition(mail) {
match mimetype.subtype() {
mime::PLAIN | mime::HTML => Viewtype::Text,
_ => Viewtype::File,
@@ -952,14 +907,62 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
Ok((mimetype, viewtype))
}
fn mailmime_is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
if let Some(ct) = mail.ctype.params.get("Content-Disposition") {
return ct.to_lowercase().starts_with("attachment");
fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
if let Ok(ct) = mail.get_content_disposition() {
return ct.disposition == DispositionType::Attachment
&& ct
.params
.iter()
.any(|(key, _value)| key.starts_with("filename"));
}
false
}
fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<String> {
// try to get file name from
// `Content-Disposition: ... filename*=...`
// or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
// or `Content-Disposition: ... filename=...`
let ct = mail.get_content_disposition()?;
ensure!(
ct.disposition == DispositionType::Attachment,
"disposition not an attachment: {:?}",
ct.disposition
);
let mut desired_filename = ct
.params
.iter()
.filter(|(key, _value)| key.starts_with("filename"))
.fold(String::new(), |mut acc, (_key, value)| {
acc += value;
acc
});
println!("get_attachment_filename1: {:?}", desired_filename);
if desired_filename.is_empty() {
if let Some(param) = ct.params.get("name") {
// might be a wrongly encoded filename
desired_filename = param.to_string();
}
}
println!("get_attachment_filename2: {:?}", desired_filename);
// if there is still no filename, guess one
if desired_filename.is_empty() {
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
desired_filename = format!("file.{}", subtype,);
} else {
bail!("could not determine filename: {:?}", ct.disposition);
}
}
println!("get_attachment_filename3: {:?}", desired_filename);
Ok(desired_filename)
}
// returned addresses are normalized.
fn get_recipients<S: AsRef<str>, T: Iterator<Item = (S, S)>>(headers: T) -> HashSet<String> {
let mut recipients: HashSet<String> = Default::default();
@@ -1064,6 +1067,29 @@ mod tests {
assert_eq!(recipients.len(), 2);
}
#[test]
fn test_is_attachment() {
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
let mail = mailparse::parse_mail(raw).unwrap();
assert!(!is_attachment_disposition(&mail));
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
let mail = mailparse::parse_mail(raw).unwrap();
assert!(!is_attachment_disposition(&mail));
assert!(!is_attachment_disposition(&mail.subparts[0]));
assert!(is_attachment_disposition(&mail.subparts[1]));
}
#[test]
fn test_get_attachment_filename() {
let raw = include_bytes!("../test-data/message/html_attach.eml");
let mail = mailparse::parse_mail(raw).unwrap();
assert!(get_attachment_filename(&mail).is_err());
assert!(get_attachment_filename(&mail.subparts[0]).is_err());
let filename = get_attachment_filename(&mail.subparts[1]).unwrap();
assert_eq!(filename, "test.html")
}
#[test]
fn test_mailparse_content_type() {
let ctype =
+25
View File
@@ -0,0 +1,25 @@
Chat-Disposition-Notification-To: tmp_6272287793210918@testrun.org
Subject: =?utf-8?q?Chat=3A_File_=E2=80=93_test=2Ehtml?=
Message-ID: Mr.XA6y3og8-az.WGbH9_dNcQx@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
X-Mailer: Delta Chat Core 1.0.0-beta.12/DcFFI
Chat-Version: 1.0
To: <tmp_5890965001269692@testrun.org>
From: "=?utf-8?q??=" <tmp_6272287793210918@testrun.org>
Content-Type: multipart/mixed; boundary="mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z"
--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z
Content-Type: text/plain; charset=utf-8
--
Sent with my Delta Chat Messenger: https://delta.chat
--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z
Content-Type: text/html
Content-Disposition: attachment; filename="test.html"
Content-Transfer-Encoding: base64
PGh0bWw+PGJvZHk+dGV4dDwvYm9keT5kYXRh
--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z--
+40
View File
@@ -0,0 +1,40 @@
From holger@merlinux.eu Sat Dec 7 11:53:58 2019
Return-Path: <holger@merlinux.eu>
X-Original-To: holger+test@merlinux.eu
Delivered-To: holger+test@merlinux.eu
Received: from [127.0.0.1] (localhost [127.0.0.1])
(using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits))
(No client certificate requested)
by mail.merlinux.eu (Postfix) with ESMTPSA id 61825100531;
Sat, 7 Dec 2019 10:53:58 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=merlinux.eu;
s=default; t=1575716038;
bh=xhaPssQzVOHRafcciTQqDnZ1Zi4GMwsmg9pHLH3i8P8=;
h=Date:From:To:Subject;
b=U4HxGDZ8RwLwRPFtIvRsb+x5BiyICnbbY2ZOGlZdLt12MuDTfiYi/phHiQUC402EY
GXb8dYgYr5+0PDiPBa7dyt2VQLC/h9QRfOA82tb1vpJYC+KksSAH0nYQqJvs7XrqCN
i95/jwZnsWrV7w72+xsrO5qPujIE68TmM5I9Cyec=
Received: by beto.merlinux.eu (Postfix, from userid 1000)
id 229D3820070; Sat, 7 Dec 2019 11:53:58 +0100 (CET)
Date: Sat, 7 Dec 2019 11:53:57 +0100
From: holger krekel <holger@merlinux.eu>
To: holger+test@merlinux.eu
Subject: hello
Message-ID: <20191207105357.GA6266@beto>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="YiEDa0DAkWCtVeE4"
Content-Disposition: inline
--YiEDa0DAkWCtVeE4
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
siehe anhang
--YiEDa0DAkWCtVeE4
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="x.txt"
hello
--YiEDa0DAkWCtVeE4--