diff --git a/integration/README.md b/integration/README.md
index d6e6c50f..303b9df1 100644
--- a/integration/README.md
+++ b/integration/README.md
@@ -112,10 +112,12 @@ because the UI behaviour is currently incomplete. Worth a look:
"Move to" submenu offers the shared folder, but clicking it is a no-op.
Shared ⇆ shared (same owner) moves work. Pinned with `test.fail` in
`08-shared-moves`.
-- **Cross-account attachments (fixed).** Blobs are account-scoped, so viewing/
- downloading an attachment on an All-Mail message from another account 404'd
- against the active account. The download/preview path now routes to the
- message's owning client + accountId (`10-attachments`).
+- **Cross-account attachments & inline images (fixed).** Blobs are account-
+ scoped, so viewing/downloading/previewing an attachment, rendering an inline
+ `cid:` image, dragging out, and the bundle/S-MIME/TNEF/embedded-message
+ fetches on an All-Mail message from another account 404'd against the active
+ account. Every viewer blob fetch now routes to the message's owning client +
+ accountId (`10-attachments` covers download + inline image).
## How the tests work
diff --git a/integration/tests/10-attachments.spec.ts b/integration/tests/10-attachments.spec.ts
index 159e5fa8..42315cd2 100644
--- a/integration/tests/10-attachments.spec.ts
+++ b/integration/tests/10-attachments.spec.ts
@@ -74,4 +74,38 @@ test.describe('Cross-account attachments', () => {
for await (const c of stream) chunks.push(c as Buffer);
expect(Buffer.concat(chunks).toString()).toContain(ATT.content);
});
+
+ test('an inline image on another account\'s All-Mail message renders', async ({ page }) => {
+ const subject = `IT inline ${Date.now()}`;
+ // 1x1 PNG referenced from the HTML body via cid.
+ const png = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
+ await sendMail({
+ from: bob.email, authPass: bob.password, to: bob.email, subject, body: '',
+ inlineImage: { cid: 'inlinepic', contentType: 'image/png', base64: png, html: '
see below
' },
+ });
+
+ await seedSettings(page, {
+ enableUnifiedMailbox: true,
+ enableCrossAllView: true,
+ unifiedCrossAccount: true,
+ includeGroupInUnified: true,
+ });
+
+ await login(page, alice);
+ await addAccount(page, bob);
+ await switchAccount(page, alice.email);
+ await forceSync(page);
+
+ await openFolder(page, { name: '__cross_all__' });
+ await forceSync(page);
+ await expectEmailVisible(page, subject);
+ await emailItem(page, subject).first().click();
+
+ // The inline cid: image resolves to a blob URL fetched from bob's account.
+ // Pre-fix the fetch 404s and it falls back to the data:image/gif placeholder.
+ const img = page.frameLocator('iframe[title="Email content"]').locator('img').first();
+ await expect
+ .poll(async () => (await img.getAttribute('src').catch(() => '')) ?? '', { timeout: 15000 })
+ .toMatch(/^blob:/);
+ });
});
diff --git a/integration/tests/helpers/smtp.ts b/integration/tests/helpers/smtp.ts
index cd994206..734c2e00 100644
--- a/integration/tests/helpers/smtp.ts
+++ b/integration/tests/helpers/smtp.ts
@@ -26,6 +26,11 @@ interface SendOptions {
headers?: Record;
/** Optional single attachment (sent as multipart/mixed, base64). */
attachment?: { filename: string; contentType: string; content: string };
+ /**
+ * Optional inline image referenced by the HTML body via `cid:`. Sent as
+ * multipart/related; `base64` is the pre-encoded image payload.
+ */
+ inlineImage?: { cid: string; contentType: string; base64: string; html: string };
}
class SmtpError extends Error {}
@@ -116,7 +121,26 @@ export async function sendMail(opts: SendOptions): Promise {
};
let mime: string;
- if (opts.attachment) {
+ if (opts.inlineImage) {
+ const boundary = 'itrelated_boundary_0001';
+ headers['MIME-Version'] = '1.0';
+ headers['Content-Type'] = `multipart/related; boundary="${boundary}"`;
+ const b64 = opts.inlineImage.base64.replace(/(.{76})/g, '$1\r\n');
+ mime = [
+ `--${boundary}`,
+ 'Content-Type: text/html; charset=utf-8',
+ '',
+ crlf(opts.inlineImage.html),
+ `--${boundary}`,
+ `Content-Type: ${opts.inlineImage.contentType}`,
+ `Content-ID: <${opts.inlineImage.cid}>`,
+ 'Content-Disposition: inline',
+ 'Content-Transfer-Encoding: base64',
+ '',
+ b64,
+ `--${boundary}--`,
+ ].join('\r\n');
+ } else if (opts.attachment) {
const boundary = 'itmixed_boundary_0001';
headers['MIME-Version'] = '1.0';
headers['Content-Type'] = `multipart/mixed; boundary="${boundary}"`;