Skip to content

Commit e89a924

Browse files
committed
fix: send Buffer for Outlook sendMail base64 payload to avoid JSON quoting
String payloads passed to OutlookOauth.request() go through JSON.stringify(), wrapping the base64 body in quotes. Microsoft Graph expects raw base64 with Content-Type: text/plain. Wrapping the base64 string in Buffer.from() routes it through the Buffer code path which sends the body as-is.
1 parent 3a73704 commit e89a924

File tree

2 files changed

+71
-1
lines changed

2 files changed

+71
-1
lines changed

lib/email-client/outlook-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2287,7 +2287,7 @@ class OutlookClient extends BaseClient {
22872287
// Use raw MIME format (default)
22882288
// Preserves calendar invites and special MIME types, but ignores from field
22892289
try {
2290-
await this.request(`/${this.oauth2UserPath}/sendMail`, 'post', raw.toString('base64'), {
2290+
await this.request(`/${this.oauth2UserPath}/sendMail`, 'post', Buffer.from(raw.toString('base64')), {
22912291
contentType: 'text/plain',
22922292
returnText: true
22932293
});

test/oauth-request-dispatch-test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,34 @@ async function stopServer(server) {
5252
await new Promise(resolve => server.close(resolve));
5353
}
5454

55+
/**
56+
* Creates and starts a test HTTP server that captures the raw request body.
57+
*
58+
* @returns {Promise<{server: http.Server, baseUrl: string, getBody: () => Buffer}>}
59+
*/
60+
async function startBodyCapturingServer() {
61+
let capturedBody = null;
62+
63+
const server = http.createServer((req, res) => {
64+
const chunks = [];
65+
req.on('data', chunk => chunks.push(chunk));
66+
req.on('end', () => {
67+
capturedBody = Buffer.concat(chunks);
68+
res.writeHead(200, { 'Content-Type': 'application/json' });
69+
res.end(JSON.stringify({ ok: true }));
70+
});
71+
});
72+
73+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
74+
const { port } = server.address();
75+
76+
return {
77+
server,
78+
baseUrl: `http://127.0.0.1:${port}`,
79+
getBody: () => capturedBody
80+
};
81+
}
82+
5583
test('Buffer payload dispatcher and Gmail endpoint selection', async t => {
5684
t.after(() => {
5785
setTimeout(() => process.exit(), 1000).unref();
@@ -375,4 +403,46 @@ test('Buffer payload dispatcher and Gmail endpoint selection', async t => {
375403

376404
assert.ok(totalJsonBody < GMAIL_JSON_LIMIT, `Max JSON body (${totalJsonBody} bytes) should be under Gmail 5MB limit (${GMAIL_JSON_LIMIT} bytes)`);
377405
});
406+
407+
// ---------------------------------------------------------------
408+
// Fix 3: Outlook sendMail base64 body must not be JSON-quoted.
409+
//
410+
// When outlook-client.js sends a base64-encoded MIME message via
411+
// sendMail, the payload must arrive as a raw base64 string with
412+
// Content-Type: text/plain. If the payload is passed as a JS string
413+
// instead of a Buffer, the non-Buffer branch in outlook.js wraps it
414+
// with JSON.stringify(), adding quotes around the base64 content.
415+
// The fix wraps the base64 string in a Buffer so it takes the
416+
// Buffer code path, which sends the body as-is.
417+
// ---------------------------------------------------------------
418+
419+
await t.test('Outlook: Buffer payload with text/plain contentType is sent without JSON quoting', async () => {
420+
const { server, baseUrl, getBody } = await startBodyCapturingServer();
421+
try {
422+
const outlook = new OutlookOauth({
423+
clientId: 'test-id',
424+
clientSecret: 'test-secret',
425+
authority: 'common',
426+
redirectUrl: 'http://localhost/callback',
427+
setFlag: async () => {}
428+
});
429+
430+
const base64Content = Buffer.from('From: a@b.com\r\nTo: c@d.com\r\nSubject: Test\r\n\r\nHello').toString('base64');
431+
const payload = Buffer.from(base64Content);
432+
433+
await outlook.request('fake-token', `${baseUrl}/me/sendMail`, 'post', payload, {
434+
contentType: 'text/plain',
435+
returnText: true
436+
});
437+
438+
const receivedBody = getBody().toString();
439+
440+
// The body must be the raw base64 string, not wrapped in JSON quotes
441+
assert.strictEqual(receivedBody, base64Content, 'Body should be raw base64, not JSON-stringified');
442+
assert.ok(!receivedBody.startsWith('"'), 'Body must not start with a JSON quote');
443+
assert.ok(!receivedBody.endsWith('"'), 'Body must not end with a JSON quote');
444+
} finally {
445+
await stopServer(server);
446+
}
447+
});
378448
});

0 commit comments

Comments
 (0)