feat: Suppression-Check (normalisiert, parametrisiert) und SMTP-Client mit RFC-8058-Header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 10:51:26 +00:00
parent 5b24c9f129
commit a60b08876c
4 changed files with 168 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
const mockClient = vi.hoisted(() => ({
execute: vi.fn(),
query: vi.fn(),
}))
vi.mock('../db/tenant', () => ({
withTenant: vi.fn((_, fn) => fn(mockClient)),
}))
import { checkSuppression } from './check'
describe('checkSuppression', () => {
beforeEach(() => vi.clearAllMocks())
it('gibt true zurück wenn E-Mail in Suppression-Liste', async () => {
mockClient.query.mockResolvedValueOnce([{ exists: true }])
const result = await checkSuppression('tenant1', 'suppressed@example.com')
expect(result).toBe(true)
})
it('gibt false zurück wenn E-Mail nicht in Liste', async () => {
mockClient.query.mockResolvedValueOnce([])
const result = await checkSuppression('tenant1', 'clean@example.com')
expect(result).toBe(false)
})
it('normalisiert E-Mail (lowercase + trim) vor dem Check', async () => {
mockClient.query.mockResolvedValueOnce([])
await checkSuppression('tenant1', ' TEST@EXAMPLE.COM ')
expect(mockClient.query).toHaveBeenCalledWith(
expect.any(String),
['test@example.com']
)
})
it('Query ist parametrisiert (kein String-Interpolation)', async () => {
mockClient.query.mockResolvedValueOnce([])
await checkSuppression('tenant1', 'x@x.com')
const call = mockClient.query.mock.calls[0]
expect(call[0]).toContain('$1')
expect(call[1]).toEqual(['x@x.com'])
})
})

View File

@@ -0,0 +1,9 @@
import { withTenant } from '../db/tenant'
export async function checkSuppression(tenantId: string, email: string): Promise<boolean> {
const normalized = email.toLowerCase().trim()
const rows = await withTenant(tenantId, (client) =>
client.query('SELECT 1 FROM suppression_list WHERE email = $1 LIMIT 1', [normalized])
)
return rows.length > 0
}