From ca28bd01af7a1a3321c6770a070f9ff4b093dd10 Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Thu, 14 May 2026 19:44:09 +0200 Subject: [PATCH] fix(imap): fetch full message for attachment download to fix base64 decoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A partial BODY.PEEK[n] fetch omits the section's MIME headers, so enough_mail's decodeContentBinary() has no Content-Transfer-Encoding and returns the raw base64 string instead of the decoded bytes. Fetching BODY.PEEK[] gives enough_mail the full MIME structure and getPart(fetchPartId) correctly decodes the attachment. Also adds an integration test that creates an email with a binary attachment, syncs it, and asserts the downloaded bytes match the original — this test failed before the fix. Closes #70 Co-Authored-By: Claude Sonnet 4.6 --- .../repositories/email_repository_impl.dart | 6 +- .../email_repository_imap_test.dart | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/data/repositories/email_repository_impl.dart b/lib/data/repositories/email_repository_impl.dart index 09b6ed1..3bb546e 100644 --- a/lib/data/repositories/email_repository_impl.dart +++ b/lib/data/repositories/email_repository_impl.dart @@ -2509,9 +2509,13 @@ class EmailRepositoryImpl implements EmailRepository { ); try { await client.selectMailboxByPath(emailRow.mailboxPath); + // Fetch the full message so enough_mail has MIME headers (including + // Content-Transfer-Encoding) and getPart() can decode the part correctly. + // A partial BODY.PEEK[n] fetch omits those headers, causing + // decodeContentBinary() to return raw base64 instead of decoded bytes. final fetch = await client.uidFetchMessage( emailRow.uid, - 'BODY.PEEK[${attachment.fetchPartId}]', + 'BODY.PEEK[]', ); final msg = fetch.messages.first; final part = msg.getPart(attachment.fetchPartId) ?? msg; diff --git a/test/integration/email_repository_imap_test.dart b/test/integration/email_repository_imap_test.dart index c2b3f96..c83421b 100644 --- a/test/integration/email_repository_imap_test.dart +++ b/test/integration/email_repository_imap_test.dart @@ -8,6 +8,7 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:drift/drift.dart' show Value; import 'package:enough_mail/enough_mail.dart'; @@ -564,4 +565,60 @@ void main() { expect(pending, hasLength(1)); expect(pending.first.changeType, 'delete'); }); + + test('downloadAttachment fetches binary attachment bytes from IMAP', + () async { + final attachmentBytes = Uint8List.fromList( + List.generate(32, (i) => i + 1), + ); + const attachmentName = 'hello.bin'; + const attachmentMime = 'application/octet-stream'; + + // Build a multipart email with a binary attachment and append it. + final client = await _imapConnect( + host: imapHost, + port: imapPort, + user: userEmail, + pass: userPass, + ); + try { + final builder = MessageBuilder() + ..from = [MailAddress('Alice', userEmail)] + ..to = [MailAddress('Alice', userEmail)] + ..subject = 'attach-${DateTime.now().millisecondsSinceEpoch}' + ..text = 'See attachment.'; + builder.addBinary( + attachmentBytes, + MediaType.fromText(attachmentMime), + filename: attachmentName, + ); + await client.appendMessage( + builder.buildMimeMessage(), + targetMailboxPath: 'INBOX', + ); + } finally { + await client.logout(); + } + + final r = makeRepo(); + await r.accounts.addAccount(account, userPass); + await r.emails.syncEmails('test', 'INBOX'); + + final emails = await r.emails.observeEmails('test', 'INBOX').first; + expect(emails, hasLength(1)); + expect(emails.first.hasAttachment, isTrue); + + final body = await r.emails.getEmailBody(emails.first.id); + expect(body.attachments, hasLength(1)); + expect(body.attachments.first.filename, attachmentName); + expect(body.attachments.first.contentType, attachmentMime); + expect(body.attachments.first.fetchPartId, isNotEmpty); + + final path = await r.emails.downloadAttachment( + emails.first.id, + body.attachments.first, + ); + final downloaded = await File(path).readAsBytes(); + expect(downloaded, equals(attachmentBytes)); + }); }