Compare commits

...

4 Commits

Author SHA1 Message Date
ivn
11b7a5c9d4 fix!: Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx (#6825)
BREAKING CHANGE: messages with invalid images, images of unknown size,
huge images, will have Viewtype::File

After changing the logic of Viewtype selection, I had to fix 3 old tests
that used invalid Base64 image data.

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-07-10 17:54:56 -03:00
link2xt
5ab107866a feat: log emitted logging events with tracing 2025-07-10 00:27:24 +00:00
iequidoo
374a5ef687 feat: Don't apply chat name and avatar changes from non-members
Non-members can't modify the member list (incl. adding themselves), modify an ephemeral timer, so
they shouldn't be able to change the group name or avatar, just for consistency. Even if messages
are reordered and a group name change from a new member arrives before its addition, the new group
name will be applied on a receipt of the next message following the addition message because
Chat-Group-Name-Timestamp increases. While Delta Chat groups aimed for chatting with trusted
contacts, accepting group changes from everyone knowing Chat-Group-Id means that if any of the past
members have the key compromised, the group should be recreated which looks impractical.
2025-07-09 17:39:55 -03:00
iequidoo
1a2e355bb8 feat: migrations: Use tools::Time to measure time for logging
There's a comment in `tools` that tells to use `tools::Time` instead of `Instant` because on Android
the latter doesn't advance in the deep sleep mode. The only place except `migrations` where
`Instant` is used is tests, but we don't run CI on Android. It's unlikely that Delta Chat goes to
the deep sleep while executing migrations, but still possible, so let's use `tools::Time` as
everywhere else.
2025-07-09 17:13:07 -03:00
13 changed files with 162 additions and 50 deletions

1
Cargo.lock generated
View File

@@ -1367,6 +1367,7 @@ dependencies = [
"tokio-tar",
"tokio-util",
"toml",
"tracing",
"url",
"uuid",
"webpki-roots",

View File

@@ -108,6 +108,7 @@ tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.8"
tracing = "0.1.41"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.8"

View File

@@ -53,6 +53,14 @@ impl Accounts {
Accounts::open(dir, writable).await
}
/// Get the ID used to log events.
///
/// Account manager logs events with ID 0
/// which is not used by any accounts.
fn get_id(&self) -> u32 {
0
}
/// Creates a new default structure.
async fn create(dir: &Path) -> Result<()> {
fs::create_dir_all(dir)

View File

@@ -223,6 +223,10 @@ pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outloo
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640;
/// Limit for received images size. Bigger images become `Viewtype::File` to avoid excessive memory
/// usage by UIs.
pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
// Key for the folder configuration version (see below).
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
// this value can be increased if the folder configuration is changed and must be redone on next program start

View File

@@ -14,6 +14,7 @@ macro_rules! info {
file = file!(),
line = line!(),
msg = &formatted);
::tracing::event!(::tracing::Level::INFO, account_id = $ctx.get_id(), "{}", &formatted);
$ctx.emit_event($crate::EventType::Info(full));
}};
}
@@ -33,6 +34,7 @@ mod warn_macro_mod {
file = file!(),
line = line!(),
msg = &formatted);
::tracing::event!(::tracing::Level::WARN, account_id = $ctx.get_id(), "{}", &formatted);
$ctx.emit_event($crate::EventType::Warning(full));
}};
}
@@ -48,6 +50,7 @@ macro_rules! error {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
::tracing::event!(::tracing::Level::ERROR, account_id = $ctx.get_id(), "{}", &formatted);
$ctx.set_last_error(&formatted);
$ctx.emit_event($crate::EventType::Error(formatted));
}};
@@ -113,6 +116,12 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
);
// We can't use the warn!() macro here as the file!() and line!() macros
// don't work with #[track_caller]
tracing::event!(
::tracing::Level::WARN,
account_id = context.get_id(),
"{}",
&full
);
context.emit_event(crate::EventType::Warning(full));
};
self

View File

@@ -1393,6 +1393,20 @@ impl MimeMessage {
} else {
Viewtype::File
}
} else if msg_type == Viewtype::Image
|| msg_type == Viewtype::Gif
|| msg_type == Viewtype::Sticker
{
match get_filemeta(decoded_data) {
// image size is known, not too big, keep msg_type:
Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
part.param.set_i64(Param::Width, width.into());
part.param.set_i64(Param::Height, height.into());
msg_type
}
// image is too big or size is unknown, display as file:
_ => Viewtype::File,
}
} else {
msg_type
};
@@ -1413,13 +1427,6 @@ impl MimeMessage {
};
info!(context, "added blobfile: {:?}", blob.as_name());
if mime_type.type_() == mime::IMAGE {
if let Ok((width, height)) = get_filemeta(decoded_data) {
part.param.set_int(Param::Width, width as i32);
part.param.set_int(Param::Height, height as i32);
}
}
part.typ = msg_type;
part.org_filename = Some(filename.to_string());
part.mimetype = Some(mime_type);

View File

@@ -835,15 +835,7 @@ Content-Transfer-Encoding: base64
Content-Disposition: inline;
filename="JPEG_filename.jpg"
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFuG0UUhr/szuxkxtlduzVx00jGUAhcI56CF0DiHXgFHqDiMfoCvAJC4gIJEBdVaZoGOY5dJ45r7zozmRlvzIVDm3Iu//PpP7/+s/OC/0+UREkEGZGr5N6mAX78MwISQEYJ6j6QYqZf7fzsYyRGJDISuQ9g6uMjW00WRCSRCP4DwNSEg496v828fC++B4yBGro7h+PliiiJHtR9B1vrmbtg359cOk+UqA8cDJm+eHu8yDfii6sAxK0u3hlsnPFn1WvT4XxYUqqtKu8cTMNKT8+nz3/aaWft3svKecBD/O9ETu2O//G7fXt5mHX2xwGP32aIEUNNvX/uh3z2pAkcKD+9XOLVNoMESw7YC+aPP8nqqSluyiuVaZRXCKLEQK3PxsP27UHe1lXV9eczCu2V8ippJA2gz+YTNTK9ttZOoYr+aeXwHp+k245cnFRNt1tmMJg3XV0dXQd3F5LcGn5fDnVQgwINRyFvtddnNmwBSW1qXfzNSDwoM+2A+aTbShfDd89Ka9ybiiLvGa33gK8THrWfVNUSSGLTALMRo/xBXnLbACu3QtRXY+sgkWnawKxCcLjPtr29gVZdPTidBUcSaXJeTfUL+mW4631Fmse+772xIGjSza/KTW/MYE/UCXPbEWs//Xxvqf8qZgYhG0jt8cnTnJpEgfDQ6rvE0n9tq66ICafil5OnOYt0hY94FUPoxI136uPpw4VIYNd+M5utrqvGpg0hc50bRl6BvLanX4pb/3347uWi/WgNHtYyyrdwszsX5fjZtwghy7C5sK2WgUoErcjIoEWr4fAHHhoRP+XZ86L/uMAarLFgsMBaAJhM8Ief9Ey7yHSmyXTp0GRoF7KQEdD/ArmtONKkgCleAAAAAElFTkSuQmCC
----11019878869865180--
@@ -908,15 +900,7 @@ Content-ID: <part1.9DFA679B.52A88D69@example.org>
Content-Disposition: inline;
filename="1.png"
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFuG0UUhr/szuxkxtlduzVx00jGUAhcI56CF0DiHXgFHqDiMfoCvAJC4gIJEBdVaZoGOY5dJ45r7zozmRlvzIVDm3Iu//PpP7/+s/OC/0+UREkEGZGr5N6mAX78MwISQEYJ6j6QYqZf7fzsYyRGJDISuQ9g6uMjW00WRCSRCP4DwNSEg496v828fC++B4yBGro7h+PliiiJHtR9B1vrmbtg359cOk+UqA8cDJm+eHu8yDfii6sAxK0u3hlsnPFn1WvT4XxYUqqtKu8cTMNKT8+nz3/aaWft3svKecBD/O9ETu2O//G7fXt5mHX2xwGP32aIEUNNvX/uh3z2pAkcKD+9XOLVNoMESw7YC+aPP8nqqSluyiuVaZRXCKLEQK3PxsP27UHe1lXV9eczCu2V8ippJA2gz+YTNTK9ttZOoYr+aeXwHp+k245cnFRNt1tmMJg3XV0dXQd3F5LcGn5fDnVQgwINRyFvtddnNmwBSW1qXfzNSDwoM+2A+aTbShfDd89Ka9ybiiLvGa33gK8THrWfVNUSSGLTALMRo/xBXnLbACu3QtRXY+sgkWnawKxCcLjPtr29gVZdPTidBUcSaXJeTfUL+mW4631Fmse+772xIGjSza/KTW/MYE/UCXPbEWs//Xxvqf8qZgYhG0jt8cnTnJpEgfDQ6rvE0n9tq66ICafil5OnOYt0hY94FUPoxI136uPpw4VIYNd+M5utrqvGpg0hc50bRl6BvLanX4pb/3347uWi/WgNHtYyyrdwszsX5fjZtwghy7C5sK2WgUoErcjIoEWr4fAHHhoRP+XZ86L/uMAarLFgsMBaAJhM8Ief9Ey7yHSmyXTp0GRoF7KQEdD/ArmtONKkgCleAAAAAElFTkSuQmCC
--------------10CC6C2609EB38DA782C5CA9--
--------------779C1631600DF3DB8C02E53A--"#;
@@ -979,15 +963,7 @@ Content-Type: image/jpeg;
Content-Transfer-Encoding: base64
Content-ID: <image001.jpg@01D622B3.C9D8D750>
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFuG0UUhr/szuxkxtlduzVx00jGUAhcI56CF0DiHXgFHqDiMfoCvAJC4gIJEBdVaZoGOY5dJ45r7zozmRlvzIVDm3Iu//PpP7/+s/OC/0+UREkEGZGr5N6mAX78MwISQEYJ6j6QYqZf7fzsYyRGJDISuQ9g6uMjW00WRCSRCP4DwNSEg496v828fC++B4yBGro7h+PliiiJHtR9B1vrmbtg359cOk+UqA8cDJm+eHu8yDfii6sAxK0u3hlsnPFn1WvT4XxYUqqtKu8cTMNKT8+nz3/aaWft3svKecBD/O9ETu2O//G7fXt5mHX2xwGP32aIEUNNvX/uh3z2pAkcKD+9XOLVNoMESw7YC+aPP8nqqSluyiuVaZRXCKLEQK3PxsP27UHe1lXV9eczCu2V8ippJA2gz+YTNTK9ttZOoYr+aeXwHp+k245cnFRNt1tmMJg3XV0dXQd3F5LcGn5fDnVQgwINRyFvtddnNmwBSW1qXfzNSDwoM+2A+aTbShfDd89Ka9ybiiLvGa33gK8THrWfVNUSSGLTALMRo/xBXnLbACu3QtRXY+sgkWnawKxCcLjPtr29gVZdPTidBUcSaXJeTfUL+mW4631Fmse+772xIGjSza/KTW/MYE/UCXPbEWs//Xxvqf8qZgYhG0jt8cnTnJpEgfDQ6rvE0n9tq66ICafil5OnOYt0hY94FUPoxI136uPpw4VIYNd+M5utrqvGpg0hc50bRl6BvLanX4pb/3347uWi/WgNHtYyyrdwszsX5fjZtwghy7C5sK2WgUoErcjIoEWr4fAHHhoRP+XZ86L/uMAarLFgsMBaAJhM8Ief9Ey7yHSmyXTp0GRoF7KQEdD/ArmtONKkgCleAAAAAElFTkSuQmCC
------=_NextPart_000_0003_01D622B3.CA753E60--
"#;
@@ -2022,3 +1998,48 @@ async fn test_protected_date() -> Result<()> {
assert_eq!(alice_msg.get_timestamp(), bob_msg.get_timestamp());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_huge_image_becomes_file() -> Result<()> {
let t = TestContext::new_alice().await;
let msg_id = receive_imf(
&t,
include_bytes!("../../test-data/message/image_huge_64M.eml"),
false,
)
.await?
.unwrap()
.msg_ids[0];
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
// Huge image should be treated as file:
assert_eq!(msg.viewtype, Viewtype::File);
assert!(msg.get_file(&t).is_some());
assert_eq!(msg.get_filename().unwrap(), "huge_image.png");
assert_eq!(msg.get_filemime().unwrap(), "image/png");
// File has no width or height
assert!(msg.param.get_int(Param::Width).is_none());
assert!(msg.param.get_int(Param::Height).is_none());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_4k_image_stays_image() -> Result<()> {
let t = TestContext::new_alice().await;
let msg_id = receive_imf(
&t,
include_bytes!("../../test-data/message/image_4k.eml"),
false,
)
.await?
.unwrap()
.msg_ids[0];
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
// 4K image should be treated as image:
assert_eq!(msg.viewtype, Viewtype::Image);
assert!(msg.get_file(&t).is_some());
assert_eq!(msg.get_filename().unwrap(), "4k_image.png");
assert_eq!(msg.get_filemime().unwrap(), "image/png");
assert_eq!(msg.param.get_int(Param::Width).unwrap_or_default(), 3840);
assert_eq!(msg.param.get_int(Param::Height).unwrap_or_default(), 2160);
Ok(())
}

View File

@@ -2899,17 +2899,17 @@ async fn apply_group_changes(
}
}
apply_chat_name_and_avatar_changes(
context,
mime_parser,
from_id,
chat,
&mut send_event_chat_modified,
&mut better_msg,
)
.await?;
if is_from_in_chat {
apply_chat_name_and_avatar_changes(
context,
mime_parser,
from_id,
chat,
&mut send_event_chat_modified,
&mut better_msg,
)
.await?;
if chat.member_list_is_stale(context).await? {
info!(context, "Member list is stale.");
let mut new_members: HashSet<ContactId> =

View File

@@ -4212,14 +4212,18 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
fiona_chat_id.accept(&fiona).await?;
send_text_msg(&fiona, fiona_chat_id, "hi".to_string()).await?;
SystemTime::shift(Duration::from_secs(60));
chat::set_chat_name(&fiona, fiona_chat_id, "Renamed").await?;
bob.recv_msg(&fiona.pop_sent_msg().await).await;
// Bob missed the message adding fiona, but mustn't recreate the member list.
// Bob missed the message adding fiona, but mustn't recreate the member list or apply the group
// name change.
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await;
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert_eq!(chat.get_name(), "Group");
Ok(())
}

View File

@@ -2,7 +2,7 @@
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::time::{Duration, Instant};
use std::time::Duration;
use anyhow::{Context as _, Result, ensure};
use deltachat_contact_tools::EmailAddress;
@@ -1237,13 +1237,13 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
inc_and_check(&mut migration_version, 132)?;
if dbversion < migration_version {
let start = Instant::now();
let start = Time::now();
sql.execute_migration_transaction(|t| migrate_key_contacts(context, t), migration_version)
.await?;
info!(
context,
"key-contacts migration took {:?} in total.",
start.elapsed()
time_elapsed(&start),
);
// Schedule `msgs_to_key_contacts()`.
context

View File

@@ -14,7 +14,8 @@ use std::str::from_utf8;
// `Instant` may use `libc::clock_gettime(CLOCK_MONOTONIC)`, e.g. on Android, and does not advance
// while being in deep sleep mode, we use `SystemTime` instead, but add an alias for it to document
// why `Instant` isn't used in those places. Also this can help to switch to another clock impl if
// we find any.
// we find any. Another reason is that `Instant` may reintroduce panics in the future versions:
// https://doc.rust-lang.org/1.87.0/std/time/struct.Instant.html#method.elapsed.
use std::time::Duration;
pub use std::time::SystemTime as Time;
#[cfg(not(test))]

View File

@@ -0,0 +1,28 @@
Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)
From: Test Sender <sender@testrun.org>
Subject: Large image test
To: alice@example.org
Message-ID: <big-image-test@testrun.org>
Date: Thu, 17 Dec 2020 15:38:45 +0100
User-Agent: Test-Agent/1.0
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="------------BIG_IMAGE_BOUNDARY"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------BIG_IMAGE_BOUNDARY
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
Here is a 4k image
--------------BIG_IMAGE_BOUNDARY
Content-Type: image/png;
name="4k_image.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="4k_image.png"
iVBORw0KGgoAAAANSUhEUgAADwAAAAhwCAIAAAAf3FwlAAAAEElEQVR4nAEFAPr/AP////8AAAAFCeiupAAAAABJRU5ErkJggg==
--------------BIG_IMAGE_BOUNDARY--

View File

@@ -0,0 +1,28 @@
Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)
From: Test Sender <sender@testrun.org>
Subject: Large image test
To: alice@example.org
Message-ID: <huge-image-test@testrun.org>
Date: Thu, 17 Dec 2020 15:38:45 +0100
User-Agent: Test-Agent/1.0
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="------------HUGE_IMAGE_BOUNDARY"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------HUGE_IMAGE_BOUNDARY
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
Here is a huge image
--------------HUGE_IMAGE_BOUNDARY
Content-Type: image/png;
name="huge_image.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="huge_image.png"
iVBORw0KGgoAAAANSUhEUgAAH0AAAB9ACAIAAACJkzqjAAAAEElEQVR4nAEFAPr/AP////8AAAAFCeiupAAAAABJRU5ErkJggg==
--------------HUGE_IMAGE_BOUNDARY--