/** 发信:先落 sending 再发;厂商失败落 failed 不抛;幂等键不发两封、失败可重发;附件取自 upload;from 对账号 */ import { describe, expect, it } from 'vitest'; import { MailAccountNotFoundError, MailNoDefaultAccountError, MailSecretMissingError, MailSendInvalidError } from '../contract.ts'; import { createMailService } from '../internal/usecase/mail-service.ts'; import { createPostalMimeParser } from '../internal/adapter/postal-mime-parser.ts'; import { account, fakeProvider, fixedClock, memoryAccounts, memoryAttachments, memoryInbound, memoryOutbound, plainSecrets, seqIds } from './fakes.ts'; function harness() { const clock = fixedClock(); const accounts = memoryAccounts(clock); const outbound = memoryOutbound(); const attachments = memoryAttachments(); const imap = fakeProvider('imap-smtp'); const wecom = fakeProvider('wecom'); const service = createMailService({ accounts, inbound: memoryInbound(), outbound, attachments, providers: { 'imap-smtp': imap, wecom }, mime: createPostalMimeParser(), secrets: plainSecrets, clock, ids: seqIds('out'), notify: { changed: () => undefined }, }); return { service, accounts, outbound, attachments, imap, clock }; } const base = { to: ['b@acme.com'], cc: [], bcc: [], subject: '报价', text: '见附件', references: [], attachments: [], requestedBy: { kind: 'ops' as const } }; describe('send', () => { it('默认账号发出:留痕 sent、Message-ID 是 、发件人带显示名、返回 providerRef', async () => { const h = harness(); await h.accounts.save(account()); const result = await h.service.send(base); expect(result).toEqual({ outboundId: 'out-1', accountId: 'acc-1', messageId: '', state: 'sent', providerRef: 'fake:1' }); expect(h.imap.sent[0]?.from).toEqual({ address: 'sales@we.com', name: '销售部' }); expect(h.imap.sent[0]?.messageId).toBe(''); const row = h.outbound.rows.get('out-1'); expect(row?.state).toBe('sent'); expect(row?.sentAt).toEqual(h.clock.now()); expect((await h.service.listOutbound({}))[0]).toMatchObject({ id: 'out-1', state: 'sent', to: ['b@acme.com'] }); }); it('厂商失败:落 failed(原话脱敏)、不抛、返回 state=failed', async () => { const h = harness(); await h.accounts.save(account()); h.imap.failSendWith(Object.assign(new Error('Invalid login'), { response: '535 5.7.8 pw-1234 rejected' })); const result = await h.service.send(base); expect(result.state).toBe('failed'); expect(result.error).toBe('Invalid login —— 535 5.7.8 *** rejected'); expect(h.outbound.rows.get('out-1')?.state).toBe('failed'); }); it('幂等键:已 sent 的直接回原结果不再发;失败的那一行复用重发', async () => { const h = harness(); await h.accounts.save(account()); h.imap.failSendWith(new Error('暂时不行')); const failed = await h.service.send({ ...base, idempotencyKey: 'k-1' }); expect(failed.state).toBe('failed'); h.imap.failSendWith(undefined); const retried = await h.service.send({ ...base, idempotencyKey: 'k-1' }); expect(retried).toMatchObject({ outboundId: failed.outboundId, state: 'sent' }); const again = await h.service.send({ ...base, idempotencyKey: 'k-1' }); expect(again).toMatchObject({ outboundId: failed.outboundId, state: 'sent' }); expect(h.imap.sent).toHaveLength(1); expect(h.outbound.rows.size).toBe(1); }); it('入参问题抛而不落行:无正文 400、没默认账号 412、from 对不上 404、凭证空 412', async () => { const h = harness(); await expect(h.service.send(base)).rejects.toBeInstanceOf(MailNoDefaultAccountError); await h.accounts.save(account()); await expect(h.service.send({ ...base, text: undefined })).rejects.toBeInstanceOf(MailSendInvalidError); await expect(h.service.send({ ...base, from: 'nobody@we.com' })).rejects.toBeInstanceOf(MailAccountNotFoundError); await h.accounts.save(account({ secretCipher: null })); await expect(h.service.send(base)).rejects.toBeInstanceOf(MailSecretMissingError); expect(h.outbound.rows.size).toBe(0); }); it('附件:uploadRef 取 upload 的字节与类型;dataBase64 直接用;两者都没有 400', async () => { const h = harness(); await h.accounts.save(account()); const { ref } = await h.attachments.put({ name: 'q.pdf', contentType: 'application/pdf', bytes: new Uint8Array([1, 2]), note: '' }); await h.service.send({ ...base, attachments: [ { name: 'q.pdf', contentType: 'application/octet-stream', uploadRef: ref }, { name: 'n.txt', contentType: 'text/plain', dataBase64: Buffer.from('hi').toString('base64') }, ], }); expect(h.imap.sent[0]?.attachments.map((a) => [a.name, a.contentType, a.bytes.byteLength])).toEqual([ ['q.pdf', 'application/pdf', 2], ['n.txt', 'text/plain', 2], ]); await expect(h.service.send({ ...base, attachments: [{ name: 'x', contentType: 'text/plain' }] })).rejects.toBeInstanceOf(MailSendInvalidError); await expect(h.service.send({ ...base, attachments: [{ name: 'x', contentType: 'text/plain', uploadRef: 'up-404' }] })).rejects.toBeInstanceOf(MailSendInvalidError); }); it('from 指定发件地址(channel 用):按地址找账号,inReplyTo / references 原样交给 provider', async () => { const h = harness(); await h.accounts.save(account({ isDefault: false })); const result = await h.service.send({ ...base, from: 'SALES@we.com', inReplyTo: '', references: [''], requestedBy: { kind: 'channel', taskId: 't-1' } }); expect(result.accountId).toBe('acc-1'); expect(h.imap.sent[0]?.inReplyTo).toBe(''); expect(h.outbound.rows.get('out-1')?.requestedBy).toEqual({ kind: 'channel', taskId: 't-1' }); }); });