Bjarn
(Bjarn)
March 13, 2026, 3:10pm
1
Hi all,
We noticed that any `text/*` attachment submitted via the HTTP injection endpoint is build as:
Content-Type: text/plain;
charset="utf-8";
name="test.csv"
Content-Transfer-Encoding: quoted-printable
This, sadly, breaks in a lot of clients I tested this with. If I create an MIME with that same file, but as base64:
Content-Type: text/plain; name=test.csv
Content-Transfer-Encoding: base64
Content-Disposition: attachment; name=test.csv; filename=test.csv
It keeps the content disposition + uses base64 encoding. This was generated via Symfony.
With the first one, it ends up as text, appended to the body in e.g. Apple Mail. In the Migadu Webmail, it doesn’t show up at all.
The base64 one shows up as an attachment in both.
Looking at Attachment interface in the docs, it does show a base64 option, but only to decode it as far as I understand in `inject_v1.rs`.
Could this be caused by the fix non-conformance as the docblock mentions?
Some parts where I noticed this:
// Remove any rfc2047 headers that might reflect how the content
// is encoded. Note that we preserve Content-Disposition as that
// isn't related purely to the how the content is encoded
self.headers.remove_all_named("Content-Type");
self.headers.remove_all_named("Content-Transfer-Encoding");
// And add any from the new part
self.headers.append(&mut new_part.headers.headers);
Ok(())
}
pub fn replace_binary_body(&mut self, content_type: &str, content: &[u8]) -> Result<()> {
let mut new_part = Self::new_binary(content_type, content, None)?;
self.bytes = new_part.bytes;
self.body_offset = new_part.body_offset;
self.body_len = new_part.body_len;
// Remove any rfc2047 headers that might reflect how the content
// is encoded. Note that we preserve Content-Disposition as that
// isn't related purely to the how the content is encoded
self.headers.remove_all_named("Content-Type");
self.headers.remove_all_named("Content-Transfer-Encoding");
// And add any from the new part
Ok((DecodedBody::Binary(bytes), self.conformance))
}
}
/// Re-constitute the message.
/// Each element will be parsed out, and the parsed form used
/// to build a new message.
/// This has the side effect of "fixing" non-conforming elements,
/// but may come at the cost of "losing" the non-sensical or otherwise
/// out of spec elements in the rebuilt message
pub fn rebuild(&self, settings: Option<&CheckFixSettings>) -> Result<Self> {
let info = Rfc2045Info::new(&self.headers);
let mut children = vec![];
for part in &self.parts {
children.push(part.rebuild(settings)?);
}
let mut rebuilt = if children.is_empty() {
let (body, _conformance) = self.extract_body(settings)?;
match body {
wez
(Wez Furlong)
March 14, 2026, 6:57am
2
Please share details on what you’re actually injecting, and the version of KumoMTA you’re using.
Thanks!
Bjarn
(Bjarn)
March 14, 2026, 11:32am
3
We run this commit via Docker: sha-813c793
The payload we send to the HTTP injection endpoint looks like this:
{
"envelope_sender": "feedback-3734b1f2-e49a-4769-8a02-fb6ecd852146@xxx.xxx.com",
"recipient": "xxx@xxx.com",
"content": {
"from": {
"email": "info@xxx.com",
"name": "xxx Test" },
"subject": "=?UTF-8?Q?[Test=202\/2]=20CSV=20attachment=20with=20Content-Type:=20text\/csv?=",
"headers": {
"Message-ID": "<3734b1f2-e49a-4769-8a02-fb6ecd852146@xxx.net>",
"Date": "Sat, 14 Mar 2026 11:31:03 +0000",
"To": "xxx@xxx.com" },
"html_body": "<p>CSV - test reproduction.<\/p><p>Please check the attachment headers in the received email.<\/p><img src=\"http:\/\/p10000.localhost\/t\/o\/YTc5OWZiN2YwYTY2MzEzNDY3ZDdlZjAwMzQ0ODIyZjU5YjgzMTQzYmM1M2NhZDI4NDkwYThmMmNhNzMyODVhM3x7Im1lc3NhZ2VfaWQiOiIzNzM0YjFmMi1lNDlhLTQ3NjktOGEwMi1mYjZlY2Q4NTIxNDYiLCJyZWNpcGllbnRfZW1haWwiOiJnMTBzeXAwa3VsQG1haWx0b3dpbi5jb20iLCJleHBpcmVzX2F0IjoxNzc2MDc5ODY2LCJwcm9qZWN0X2lkIjoiOWVlZjc5ZjEtMGEyNy00MmUxLThhMDktY2M0YmZiZWRkM2Y1In0=\" width=\"1\" height=\"1\" style=\"display:none\" alt=\"\">",
"text_body": "CSV - test reproduction.\r\n\r\nPlease check the attachment headers in the received email.",
"attachments": [
{
"data": "SUQ7VmFsdWUNCjE7VGVzdA0KMjtUZXN0Mg0KMztUZXN0Mw==",
"base64": true,
"content_type": "text\/csv",
"file_name": "test.csv"
}
]
}
}
wez
(Wez Furlong)
March 16, 2026, 8:45am
4
It sounds like the message is being rebuilt after generation, which is where the damage is coming from; is that something you’re doing in your policy?
Bjarn
(Bjarn)
March 16, 2026, 9:20am
5
Yep that’s indeed set up. We use the check_fix_conformance. Though I will check if we still need this, just a bit anxious on edge cases still haha
local failed = msg:check_fix_conformance(
-- check for and reject messages with these issues:
"MISSING_COLON_VALUE",
-- fix messages with these issues:
"LINE_TOO_LONG|NAME_ENDS_WITH_SPACE|NEEDS_TRANSFER_ENCODING|NON_CANONICAL_LINE_ENDINGS|MISSING_DATE_HEADER|MISSING_MESSAGE_ID_HEADER|MISSING_MIME_VERSION"
)
wez
(Wez Furlong)
March 16, 2026, 2:34pm
6
I think the rebuild is triggered by long Subject header you provide; you could avoid that by removing LINE_TOO_LONG from the list of fixes.
I do think that the rebuild should try to preserve the attachment disposition. And we probably should also automatically wrap the subject header during the initial build.
Bjarn
(Bjarn)
March 27, 2026, 2:29pm
7
Forgot to reply - but if I were to remove that fix, it will break if the initial build isn’t wrapped either right? As in, delivery might be impacted.
Will do some testing and come back!