diff --git a/lib/mail-import/__tests__/engine.test.ts b/lib/mail-import/__tests__/engine.test.ts index 160105ca2..50104c371 100644 --- a/lib/mail-import/__tests__/engine.test.ts +++ b/lib/mail-import/__tests__/engine.test.ts @@ -39,7 +39,9 @@ function fakeClient(opts: { const created: Array<{ name: string; parentId?: string; accountId?: string }> = []; const mailboxes: Mailbox[] = [mailbox('inbox', 'Inbox', { role: 'inbox' }), mailbox('target', 'Target')]; const importCalls: number[] = []; + const uploadAccounts: (string | undefined)[] = []; const client = { + getAccountId: () => 'acc-own', getCapabilities: () => ({ 'urn:ietf:params:jmap:core': { maxConcurrentUpload: opts.uploadConcurrency ?? 4 } }), getMaxConcurrentRequests: () => 4, getMaxSizeUpload: () => 1000, @@ -51,6 +53,7 @@ function fakeClient(opts: { }), uploadBlob: vi.fn(async (f: File, o: { accountId?: string; signal?: AbortSignal }) => { const n = uploads++; + uploadAccounts.push(o.accountId); inFlight++; maxInFlight = Math.max(maxInFlight, inFlight); try { @@ -82,7 +85,7 @@ function fakeClient(opts: { }); }), } as unknown as IJMAPClient; - return { client, imported, created, importCalls, get maxInFlight() { return maxInFlight; }, get uploads() { return uploads; } }; + return { client, imported, created, importCalls, uploadAccounts, get maxInFlight() { return maxInFlight; }, get uploads() { return uploads; } }; } function options(client: IJMAPClient, files: File[], extra: Partial = {}): ImportOptions { @@ -113,6 +116,8 @@ describe('runImport', () => { expect(result.imported).toBe(3); expect(result.skipped).toEqual({ deleted: 1 }); expect(fake.imported.map((e) => e.accountId)).toEqual(['acc-shared', 'acc-shared', 'acc-shared']); + // Uploads go to the user's own account: one in the shared account can't be imported there. + expect(new Set(fake.uploadAccounts)).toEqual(new Set(['acc-own'])); expect(fake.imported.every((e) => e.mailboxIds.target)).toBe(true); expect(fake.imported.map((e) => e.keywords)).toEqual([{ $seen: true }, { $flagged: true }, { $seen: true }]); expect(fake.imported[0].receivedAt).toBe('2020-05-05T10:00:00Z'); diff --git a/lib/mail-import/engine.ts b/lib/mail-import/engine.ts index 375ec932a..87a8ee104 100644 --- a/lib/mail-import/engine.ts +++ b/lib/mail-import/engine.ts @@ -299,6 +299,7 @@ export async function runImport(options: ImportOptions): Promise const folders = new FolderResolver(client, accountId, options.targetMailboxId); const maxSize = client.getMaxSizeUpload() || Number.POSITIVE_INFINITY; const lanes = maxConcurrentUploads(client); + const uploadAccountId = client.getAccountId?.() || accountId; let lastReport = 0; const report = (force = false) => { @@ -448,7 +449,10 @@ export async function runImport(options: ImportOptions): Promise for (let attempt = 0; ; attempt++) { await waitWhilePaused(); try { - const { blobId } = await client.uploadBlob(file, { accountId, signal }); + // Into the user's own account: Stalwart (0.16 and 1.0) keeps a blob uploaded to a + // shared account's upload URL where Email/import there can't find it, while one in + // the user's own account imports into any account the user may add mail to. + const { blobId } = await client.uploadBlob(file, { accountId: uploadAccountId, signal }); networkErrors = 0; enqueue({ index: item.index,