fix(attachments): route all viewer blob fetches for cross-account messages
Extend the cross-account blob routing beyond download/preview to every blob fetch in the message viewer, so a message opened from a different account in the unified / All-Mail view renders and exports correctly instead of 404ing against the active account: - inline cid: images, drag-to-desktop, attachment thumbnails, the "download all" zip bundle, and the S/MIME / TNEF / embedded-rfc822 blob reads now use a resolved blobClient (getClientForAccount(sourceClientAccountId)) and the owner blobAccountId (sourceAccountId), computed once from the open message's source; - fetchBlobAsObjectUrl / fetchBlobArrayBuffer / fetchBlob calls pass the accountId (the client methods gained the param in the previous commit); - non-cross-account behaviour is unchanged (blobClient === active client). Extends 10-attachments with an inline-image case (verified to fall back to the placeholder without the routing). The SMTP helper can now send multipart/related inline images.
This commit is contained in:
@@ -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.
|
"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
|
Shared ⇆ shared (same owner) moves work. Pinned with `test.fail` in
|
||||||
`08-shared-moves`.
|
`08-shared-moves`.
|
||||||
- **Cross-account attachments (fixed).** Blobs are account-scoped, so viewing/
|
- **Cross-account attachments & inline images (fixed).** Blobs are account-
|
||||||
downloading an attachment on an All-Mail message from another account 404'd
|
scoped, so viewing/downloading/previewing an attachment, rendering an inline
|
||||||
against the active account. The download/preview path now routes to the
|
`cid:` image, dragging out, and the bundle/S-MIME/TNEF/embedded-message
|
||||||
message's owning client + accountId (`10-attachments`).
|
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
|
## How the tests work
|
||||||
|
|
||||||
|
|||||||
@@ -74,4 +74,38 @@ test.describe('Cross-account attachments', () => {
|
|||||||
for await (const c of stream) chunks.push(c as Buffer);
|
for await (const c of stream) chunks.push(c as Buffer);
|
||||||
expect(Buffer.concat(chunks).toString()).toContain(ATT.content);
|
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: '<p>see below</p><img src="cid:inlinepic" alt="pic" width="1" height="1" />' },
|
||||||
|
});
|
||||||
|
|
||||||
|
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:/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ interface SendOptions {
|
|||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
/** Optional single attachment (sent as multipart/mixed, base64). */
|
/** Optional single attachment (sent as multipart/mixed, base64). */
|
||||||
attachment?: { filename: string; contentType: string; content: string };
|
attachment?: { filename: string; contentType: string; content: string };
|
||||||
|
/**
|
||||||
|
* Optional inline image referenced by the HTML body via `cid:<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 {}
|
class SmtpError extends Error {}
|
||||||
@@ -116,7 +121,26 @@ export async function sendMail(opts: SendOptions): Promise<void> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mime: string;
|
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';
|
const boundary = 'itmixed_boundary_0001';
|
||||||
headers['MIME-Version'] = '1.0';
|
headers['MIME-Version'] = '1.0';
|
||||||
headers['Content-Type'] = `multipart/mixed; boundary="${boundary}"`;
|
headers['Content-Type'] = `multipart/mixed; boundary="${boundary}"`;
|
||||||
|
|||||||
Reference in New Issue
Block a user