@@ -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+
5583test ( '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