Skip to content

Commit 193c834

Browse files
committed
fix(email): review round 1 + audit hardening for Gmail provider
- a 2xx from Gmail with an empty/malformed body no longer surfaces as a send failure (the mailer's fallback chain would deliver the same email twice); covered by a regression test - normalize bare-LF line endings in html/text bodies to CRLF (RFC 822) before composing the raw message - add multi-recipient and text-only test cases
1 parent dc38f2e commit 193c834

2 files changed

Lines changed: 51 additions & 3 deletions

File tree

apps/sim/lib/messaging/email/providers/gmail.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,43 @@ describe('Gmail mail provider', () => {
164164
expect(raw).toContain(Buffer.from('report body').toString('base64'))
165165
})
166166

167+
it('joins multiple recipients into one To header', async () => {
168+
mockFetch.mockResolvedValueOnce(
169+
new Response(JSON.stringify({ id: 'msg-3' }), { status: 200 })
170+
)
171+
172+
const provider = createGmailProvider()
173+
await provider!.send({ ...BASE_DATA, to: ['a@example.com', 'b@example.com'] })
174+
175+
const [, init] = mockFetch.mock.calls[0]
176+
expect((init.body as Buffer).toString()).toContain('To: a@example.com, b@example.com')
177+
})
178+
179+
it('sends text-only messages', async () => {
180+
mockFetch.mockResolvedValueOnce(
181+
new Response(JSON.stringify({ id: 'msg-4' }), { status: 200 })
182+
)
183+
184+
const provider = createGmailProvider()
185+
await provider!.send({ ...BASE_DATA, html: undefined, text: 'plain body' })
186+
187+
const [, init] = mockFetch.mock.calls[0]
188+
const raw = (init.body as Buffer).toString()
189+
expect(raw).toContain('Content-Type: text/plain')
190+
expect(raw).toContain('plain body')
191+
expect(raw).not.toContain('text/html')
192+
})
193+
194+
it('treats an accepted send with an empty response body as success (no fallback re-send)', async () => {
195+
mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 }))
196+
197+
const provider = createGmailProvider()
198+
const result = await provider!.send(BASE_DATA)
199+
200+
expect(result.success).toBe(true)
201+
expect(result.data).toEqual({ id: undefined })
202+
})
203+
167204
it('throws when no access token can be obtained', async () => {
168205
mockGetAccessToken.mockResolvedValueOnce({ token: null })
169206

apps/sim/lib/messaging/email/providers/gmail.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,22 @@ function parseServiceAccount(credentialsJson: string): GmailServiceAccount | nul
4141
return credentials as GmailServiceAccount
4242
}
4343

44+
/**
45+
* RFC 822 requires CRLF line endings throughout; MailComposer preserves
46+
* caller-supplied bare-LF endings inside html/text bodies, so normalize them.
47+
*/
48+
function normalizeCrlf(value: string | undefined): string | undefined {
49+
return value?.replace(/\r?\n/g, '\r\n')
50+
}
51+
4452
/** Build the raw RFC 822 message via nodemailer's composer (multipart, attachments, headers). */
4553
function buildRawMessage(data: ProcessedEmailData): Promise<Buffer> {
4654
const composer = new MailComposer({
4755
from: data.senderEmail,
4856
to: data.to,
4957
subject: data.subject,
50-
html: data.html,
51-
text: data.text,
58+
html: normalizeCrlf(data.html),
59+
text: normalizeCrlf(data.text),
5260
replyTo: data.replyTo,
5361
headers: Object.keys(data.headers).length > 0 ? data.headers : undefined,
5462
attachments: data.attachments?.map((att) => ({
@@ -123,7 +131,10 @@ export function createGmailProvider(): MailProvider | null {
123131
)
124132
}
125133

126-
const result = (await response.json()) as { id?: string }
134+
// Gmail accepted the message once the status is 2xx; a missing or
135+
// malformed body must not surface as a send failure, or the mailer's
136+
// fallback chain would deliver the same email again via another provider.
137+
const result = (await response.json().catch(() => ({}))) as { id?: string }
127138
return {
128139
success: true,
129140
message: 'Email sent successfully via Gmail',

0 commit comments

Comments
 (0)