From 3d409ceaceb209b05d1f9bcde935bbec6260d94b Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Thu, 28 Aug 2025 10:46:38 -0400 Subject: [PATCH] feat(messages,drafts): support isPlaintext for messages.send and drafts.create --- CHANGELOG.md | 8 ++- examples/messages/cli-interface.ts | 32 +++++++---- .../messages/examples/buffer-attachments.ts | 9 ++- .../examples/file-path-attachments.ts | 9 ++- .../messages/examples/flexible-attachments.ts | 9 ++- examples/messages/examples/index.ts | 10 ++-- .../messages/examples/stream-attachments.ts | 9 ++- .../messages/examples/string-attachments.ts | 9 ++- examples/messages/messages.ts | 42 ++++++++++++++ src/models/drafts.ts | 10 +++- tests/resources/drafts.spec.ts | 55 +++++++++++++++++++ tests/resources/messages.spec.ts | 55 +++++++++++++++++++ 12 files changed, 223 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f2183d..39fb661c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Support `isPlaintext` boolean for messages send and drafts create requests +- Expose raw response headers on all responses via non-enumerable `rawHeaders` while keeping existing `headers` camelCased + +## [7.12.0] - 2025-08-01 + ### Changed - Upgraded node-fetch from v2 to v3 for better ESM support and compatibility with edge environments @@ -15,8 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated Jest configuration to properly handle ESM modules from node-fetch v3 - Removed incompatible AbortSignal import from node-fetch externals (now uses native Node.js AbortSignal) -### Added -- Expose raw response headers on all responses via non-enumerable `rawHeaders` while keeping existing `headers` camelCased ## [7.11.0] - 2025-06-23 diff --git a/examples/messages/cli-interface.ts b/examples/messages/cli-interface.ts index 16ef2773..3235e7df 100644 --- a/examples/messages/cli-interface.ts +++ b/examples/messages/cli-interface.ts @@ -9,6 +9,7 @@ interface CliOptions { attachmentSize: 'small' | 'large'; format: FileFormat; testEmail?: string; + isPlaintext: boolean; } async function getCliOptions(fileManager: TestFileManager): Promise { @@ -51,6 +52,12 @@ async function getCliOptions(fileManager: TestFileManager): Promise message: 'Recipient email address:', default: process.env.TEST_EMAIL || '', validate: (input: string) => input.includes('@') || 'Please enter a valid email address' + }, + { + type: 'confirm', + name: 'isPlaintext', + message: 'Send as plaintext (no HTML rendering)?', + default: false } ]); @@ -58,7 +65,7 @@ async function getCliOptions(fileManager: TestFileManager): Promise } async function runExample(examples: SendAttachmentsExamples, fileManager: TestFileManager, options: CliOptions): Promise { - const { format, testEmail, attachmentSize } = options; + const { format, testEmail, attachmentSize, isPlaintext } = options; if (!testEmail) { console.log(chalk.yellow('āš ļø No email provided. Skipping send.')); @@ -66,7 +73,7 @@ async function runExample(examples: SendAttachmentsExamples, fileManager: TestFi } try { - console.log(chalk.blue(`\nšŸ“¤ Running ${format} attachment example (${attachmentSize} files)...\n`)); + console.log(chalk.blue(`\nšŸ“¤ Running ${format} attachment example (${attachmentSize} files)${isPlaintext ? ' in plaintext mode' : ''}...\n`)); let result: NylasResponse; const isLarge = attachmentSize === 'large'; @@ -74,19 +81,19 @@ async function runExample(examples: SendAttachmentsExamples, fileManager: TestFi // Route to the appropriate example based on format switch (format) { case 'file': - result = await examples.sendFilePathAttachments(fileManager, testEmail, isLarge); + result = await examples.sendFilePathAttachments(fileManager, testEmail, isLarge, isPlaintext); break; case 'stream': - result = await examples.sendStreamAttachments(fileManager, testEmail, isLarge); + result = await examples.sendStreamAttachments(fileManager, testEmail, isLarge, isPlaintext); break; case 'buffer': - result = await examples.sendBufferAttachments(fileManager, testEmail, isLarge); + result = await examples.sendBufferAttachments(fileManager, testEmail, isLarge, isPlaintext); break; case 'string': - result = await examples.sendStringAttachments(fileManager, testEmail, isLarge); + result = await examples.sendStringAttachments(fileManager, testEmail, isLarge, isPlaintext); break; default: - result = await examples.sendAttachmentsByFormat(fileManager, format, testEmail, attachmentSize); + result = await examples.sendAttachmentsByFormat(fileManager, format, testEmail, attachmentSize, isPlaintext); } console.log(chalk.green.bold('\nāœ… Message sent successfully!')); @@ -103,11 +110,12 @@ async function runExample(examples: SendAttachmentsExamples, fileManager: TestFi } } -async function runBatchMode(examples: SendAttachmentsExamples, fileManager: TestFileManager, size: 'small' | 'large', format: FileFormat, email?: string): Promise { +async function runBatchMode(examples: SendAttachmentsExamples, fileManager: TestFileManager, size: 'small' | 'large', format: FileFormat, email?: string, isPlaintext: boolean = false): Promise { const options: CliOptions = { attachmentSize: size, format, - testEmail: email + testEmail: email, + isPlaintext }; console.log(chalk.blue.bold('\nšŸš€ Nylas Send Attachments (Batch Mode)\n')); @@ -137,8 +145,9 @@ export async function startCli(examples: SendAttachmentsExamples, fileManager: T .description('Send small attachments') .option('-f, --format ', 'format (file|stream|buffer|string)', 'file') .option('-e, --email ', 'recipient email') + .option('--plaintext', 'send as plaintext', false) .action(async (options) => { - await runBatchMode(examples, fileManager, 'small', options.format as FileFormat, options.email || testEmail); + await runBatchMode(examples, fileManager, 'small', options.format as FileFormat, options.email || testEmail, Boolean(options.plaintext)); }); program @@ -146,8 +155,9 @@ export async function startCli(examples: SendAttachmentsExamples, fileManager: T .description('Send large attachment') .option('-f, --format ', 'format (file|stream|buffer|string)', 'file') .option('-e, --email ', 'recipient email') + .option('--plaintext', 'send as plaintext', false) .action(async (options) => { - await runBatchMode(examples, fileManager, 'large', options.format as FileFormat, options.email || testEmail); + await runBatchMode(examples, fileManager, 'large', options.format as FileFormat, options.email || testEmail, Boolean(options.plaintext)); }); program diff --git a/examples/messages/examples/buffer-attachments.ts b/examples/messages/examples/buffer-attachments.ts index 2060b084..57bff8a3 100644 --- a/examples/messages/examples/buffer-attachments.ts +++ b/examples/messages/examples/buffer-attachments.ts @@ -21,7 +21,7 @@ const grantId: string = process.env.NYLAS_GRANT_ID || ''; * Loads the entire file into memory as a Buffer. * Good for small files or when you need to process content. */ -export async function sendBufferAttachments(fileManager: TestFileManager, recipientEmail: string, large: boolean = false): Promise> { +export async function sendBufferAttachments(fileManager: TestFileManager, recipientEmail: string, large: boolean = false, isPlaintext: boolean = false): Promise> { console.log('šŸ’¾ Sending attachments using buffers...'); let sizeDescription; @@ -48,12 +48,15 @@ export async function sendBufferAttachments(fileManager: TestFileManager, recipi const requestBody: SendMessageRequest = { to: [{ name: 'Test Recipient', email: recipientEmail }], subject: 'Nylas SDK - Buffer Attachments', - body: ` + body: isPlaintext + ? 'Buffer Attachments Example\nThis demonstrates sending attachments using Node.js Buffer objects.' + : `

Buffer Attachments Example

This demonstrates sending attachments using Node.js Buffer objects.

Good for small files when you need the content in memory.

`, - attachments: bufferAttachments + attachments: bufferAttachments, + isPlaintext }; // For large files, use a longer timeout (5 minutes) diff --git a/examples/messages/examples/file-path-attachments.ts b/examples/messages/examples/file-path-attachments.ts index dfa3b073..780632f6 100644 --- a/examples/messages/examples/file-path-attachments.ts +++ b/examples/messages/examples/file-path-attachments.ts @@ -21,7 +21,7 @@ const grantId: string = process.env.NYLAS_GRANT_ID || ''; * This is the recommended approach for most use cases. * Uses streams internally for memory efficiency. */ -export async function sendFilePathAttachments(fileManager: TestFileManager, recipientEmail: string, large: boolean = false): Promise> { +export async function sendFilePathAttachments(fileManager: TestFileManager, recipientEmail: string, large: boolean = false, isPlaintext: boolean = false): Promise> { console.log('šŸ“ Sending attachments using file paths...'); let attachments; @@ -42,13 +42,16 @@ export async function sendFilePathAttachments(fileManager: TestFileManager, reci const requestBody: SendMessageRequest = { to: [{ name: 'Test Recipient', email: recipientEmail }], subject: `Nylas SDK - File Path Attachments (${sizeDescription})`, - body: ` + body: isPlaintext + ? `File Path Attachments Example\nThis demonstrates sending attachments using file paths.\nAttachment size: ${sizeDescription} (${attachments.length} file${attachments.length > 1 ? 's' : ''})` + : `

File Path Attachments Example

This demonstrates the most common way to send attachments using file paths.

The SDK uses streams internally for memory efficiency.

Attachment size: ${sizeDescription} (${attachments.length} file${attachments.length > 1 ? 's' : ''})

`, - attachments + attachments, + isPlaintext }; // For large files, use a longer timeout (5 minutes) diff --git a/examples/messages/examples/flexible-attachments.ts b/examples/messages/examples/flexible-attachments.ts index 72795267..86f7d77c 100644 --- a/examples/messages/examples/flexible-attachments.ts +++ b/examples/messages/examples/flexible-attachments.ts @@ -18,7 +18,7 @@ const grantId: string = process.env.NYLAS_GRANT_ID || ''; /** * Flexible attachment sending based on format choice */ -export async function sendAttachmentsByFormat(fileManager: TestFileManager, format: FileFormat, recipientEmail: string, attachmentSize: 'small' | 'large' = 'small'): Promise> { +export async function sendAttachmentsByFormat(fileManager: TestFileManager, format: FileFormat, recipientEmail: string, attachmentSize: 'small' | 'large' = 'small', isPlaintext: boolean = false): Promise> { let attachments: CreateAttachmentRequest[] = []; let subject: string; @@ -40,7 +40,9 @@ export async function sendAttachmentsByFormat(fileManager: TestFileManager, form const requestBody: SendMessageRequest = { to: [{ name: 'Test Recipient', email: recipientEmail }], subject, - body: ` + body: isPlaintext + ? `Attachment Format Test: ${format}\nThis message demonstrates sending attachments using the ${format} format.\nFiles attached: ${attachments.length}` + : `

Attachment Format Test: ${format}

This message demonstrates sending attachments using the ${format} format.

Files attached: ${attachments.length}

@@ -48,7 +50,8 @@ export async function sendAttachmentsByFormat(fileManager: TestFileManager, form ${attachments.map(att => `
  • ${att.filename} (${att.size} bytes)
  • `).join('')} `, - attachments + attachments, + isPlaintext }; // For large files, use a longer timeout (5 minutes) diff --git a/examples/messages/examples/index.ts b/examples/messages/examples/index.ts index 7264115f..7e18192d 100644 --- a/examples/messages/examples/index.ts +++ b/examples/messages/examples/index.ts @@ -5,9 +5,9 @@ import { sendStringAttachments } from './string-attachments'; import { sendAttachmentsByFormat } from './flexible-attachments'; export type SendAttachmentsExamples = { - sendFilePathAttachments: typeof sendFilePathAttachments, - sendStreamAttachments: typeof sendStreamAttachments, - sendBufferAttachments: typeof sendBufferAttachments, - sendStringAttachments: typeof sendStringAttachments, - sendAttachmentsByFormat: typeof sendAttachmentsByFormat + sendFilePathAttachments: (fileManager: Parameters[0], recipientEmail: string, large?: boolean, isPlaintext?: boolean) => ReturnType, + sendStreamAttachments: (fileManager: Parameters[0], recipientEmail: string, large?: boolean, isPlaintext?: boolean) => ReturnType, + sendBufferAttachments: (fileManager: Parameters[0], recipientEmail: string, large?: boolean, isPlaintext?: boolean) => ReturnType, + sendStringAttachments: (fileManager: Parameters[0], recipientEmail: string, large?: boolean, isPlaintext?: boolean) => ReturnType, + sendAttachmentsByFormat: (fileManager: Parameters[0], format: Parameters[1], recipientEmail: string, attachmentSize?: Parameters[3], isPlaintext?: boolean) => ReturnType }; \ No newline at end of file diff --git a/examples/messages/examples/stream-attachments.ts b/examples/messages/examples/stream-attachments.ts index ac40c185..ff9b3d72 100644 --- a/examples/messages/examples/stream-attachments.ts +++ b/examples/messages/examples/stream-attachments.ts @@ -21,7 +21,7 @@ const grantId: string = process.env.NYLAS_GRANT_ID || ''; * Useful when you're working with streams from other sources * or need more control over the stream processing. */ -export async function sendStreamAttachments(fileManager: TestFileManager, recipientEmail: string, large: boolean = false): Promise> { +export async function sendStreamAttachments(fileManager: TestFileManager, recipientEmail: string, large: boolean = false, isPlaintext: boolean = false): Promise> { console.log('🌊 Sending attachments using streams...'); let attachments: CreateAttachmentRequest[] = []; @@ -52,13 +52,16 @@ export async function sendStreamAttachments(fileManager: TestFileManager, recipi const requestBody: SendMessageRequest = { to: [{ name: 'Test Recipient', email: recipientEmail }], subject: `Nylas SDK - Stream Attachments (${sizeDescription})`, - body: ` + body: isPlaintext + ? `Stream Attachments Example\nThis demonstrates sending attachments using readable streams.\nAttachment size: ${sizeDescription} (${attachments.length} file${attachments.length > 1 ? 's' : ''})` + : `

    Stream Attachments Example

    This demonstrates sending attachments using readable streams.

    Useful when you have streams from other sources.

    Attachment size: ${sizeDescription} (${attachments.length} file${attachments.length > 1 ? 's' : ''})

    `, - attachments + attachments, + isPlaintext }; // For large files, use a longer timeout (5 minutes) diff --git a/examples/messages/examples/string-attachments.ts b/examples/messages/examples/string-attachments.ts index 74f47f35..4e92afd6 100644 --- a/examples/messages/examples/string-attachments.ts +++ b/examples/messages/examples/string-attachments.ts @@ -21,7 +21,7 @@ const grantId: string = process.env.NYLAS_GRANT_ID || ''; * Perfect for sending existing files as base64 encoded strings. * This example pulls the same files used by other examples but encodes them as base64 strings. */ -export async function sendStringAttachments(fileManager: TestFileManager, recipientEmail: string, large: boolean = false): Promise> { +export async function sendStringAttachments(fileManager: TestFileManager, recipientEmail: string, large: boolean = false, isPlaintext: boolean = false): Promise> { console.log('šŸ“ Sending base64 encoded file attachments as strings...'); let stringAttachments: CreateAttachmentRequest[] = []; @@ -75,7 +75,9 @@ export async function sendStringAttachments(fileManager: TestFileManager, recipi const requestBody: SendMessageRequest = { to: [{ name: 'Test Recipient', email: recipientEmail }], subject: `Nylas SDK - Base64 String Attachments (${sizeDescription})`, - body: ` + body: isPlaintext + ? `Base64 String Attachments Example\nThis demonstrates sending existing files as base64 encoded strings.\nAttachment size: ${sizeDescription} (${stringAttachments.length} file${stringAttachments.length > 1 ? 's' : ''})` + : `

    Base64 String Attachments Example

    This demonstrates sending existing files as base64 encoded strings.

    Files are converted from the same test files used in other examples.

    @@ -84,7 +86,8 @@ export async function sendStringAttachments(fileManager: TestFileManager, recipi ${stringAttachments.map(att => `
  • ${att.filename} (${att.size} bytes base64 encoded)
  • `).join('')} `, - attachments: stringAttachments + attachments: stringAttachments, + isPlaintext }; // For large files, use a longer timeout (5 minutes) diff --git a/examples/messages/messages.ts b/examples/messages/messages.ts index 04397911..cfb1f484 100644 --- a/examples/messages/messages.ts +++ b/examples/messages/messages.ts @@ -243,6 +243,47 @@ async function demonstrateMessageSending(): Promise | nul } } +/** + * Demonstrates sending a plaintext-only message (no attachments) + */ +async function demonstratePlaintextMessageSending(): Promise | null> { + console.log('\n=== Demonstrating Plaintext Message Sending ==='); + try { + const testEmail = process.env.TEST_EMAIL; + if (!testEmail) { + console.log('TEST_EMAIL environment variable not set. Skipping plaintext message sending demo.'); + return null; + } + + const requestBody: SendMessageRequest = { + to: [{ name: 'Plaintext Recipient', email: testEmail }], + subject: 'Nylas SDK Messages Example - Plaintext', + body: 'This message is sent as plain text only.', + isPlaintext: true, + }; + + const sentMessage = await nylas.messages.send({ + identifier: grantId, + requestBody, + }); + + console.log('Plaintext message sent successfully!'); + console.log(`- Message ID: ${sentMessage.data.id}`); + console.log(`- Subject: ${sentMessage.data.subject}`); + console.log(`- To: ${sentMessage.data.to?.map(t => `${t.name} <${t.email}>`).join(', ')}`); + + return sentMessage; + } catch (error) { + if (error instanceof NylasApiError) { + console.error(`Error sending plaintext message: ${error.message}`); + console.error(`Error details: ${JSON.stringify(error, null, 2)}`); + } else if (error instanceof Error) { + console.error(`Unexpected error in demonstratePlaintextMessageSending: ${error.message}`); + } + return null; + } +} + /** * Demonstrates updating a message */ @@ -464,6 +505,7 @@ async function main(): Promise { // Run all demonstrations await demonstrateMessageFields(); + await demonstratePlaintextMessageSending(); await demonstrateRawMime(); await demonstrateMessageQuerying(); diff --git a/src/models/drafts.ts b/src/models/drafts.ts index b2f38f12..02060557 100644 --- a/src/models/drafts.ts +++ b/src/models/drafts.ts @@ -71,6 +71,11 @@ export interface CreateDraftRequest { * An array of custom headers to add to the message. */ customHeaders?: CustomHeader[]; + /** + * When true, the message body is sent as plain text and the MIME data doesn't include the HTML version of the message. + * When false, the message body is sent as HTML. Defaults to false. + */ + isPlaintext?: boolean; } /** @@ -103,7 +108,10 @@ export interface Draft /** * Interface representing a request to update a draft. */ -export type UpdateDraftRequest = Partial & { +export type UpdateDraftRequest = Omit< + Partial, + 'isPlaintext' +> & { /** * Return drafts that are unread. */ diff --git a/tests/resources/drafts.spec.ts b/tests/resources/drafts.spec.ts index 1c522c2d..a3f52a5b 100644 --- a/tests/resources/drafts.spec.ts +++ b/tests/resources/drafts.spec.ts @@ -377,6 +377,61 @@ describe('Drafts', () => { expect(capturedRequest.method).toEqual('POST'); expect(capturedRequest.path).toEqual('/v3/grants/id123/drafts'); }); + + it('should include isPlaintext in JSON body when provided for create', async () => { + const jsonBody = { + to: [{ name: 'Test', email: 'test@example.com' }], + subject: 'Plain text draft', + body: 'Hello world', + isPlaintext: true, + }; + + await drafts.create({ + identifier: 'id123', + requestBody: jsonBody, + }); + + const capturedRequest = apiClient.request.mock.calls[0][0]; + expect(capturedRequest.method).toEqual('POST'); + expect(capturedRequest.path).toEqual('/v3/grants/id123/drafts'); + expect(capturedRequest.body).toEqual(jsonBody); + }); + + it('should include isPlaintext in multipart form message when provided for create', async () => { + const messageJson = { + to: [{ name: 'Test', email: 'test@example.com' }], + subject: 'Plain text draft', + body: 'Hello world', + isPlaintext: true, + }; + const fileStream = createReadableStream('This is the text from file 1'); + const file1: CreateAttachmentRequest = { + filename: 'file1.txt', + contentType: 'text/plain', + content: fileStream, + size: 3 * 1024 * 1024, + }; + + await drafts.create({ + identifier: 'id123', + requestBody: { + ...messageJson, + attachments: [file1], + }, + }); + + const capturedRequest = apiClient.request.mock.calls[0][0]; + const formData = ( + capturedRequest.form as any as MockedFormData + )._getAppendedData(); + const parsed = JSON.parse(formData.message); + expect(parsed.to).toEqual(messageJson.to); + expect(parsed.subject).toEqual(messageJson.subject); + expect(parsed.body).toEqual(messageJson.body); + expect(parsed.is_plaintext).toBe(true); + expect(capturedRequest.method).toEqual('POST'); + expect(capturedRequest.path).toEqual('/v3/grants/id123/drafts'); + }); }); describe('update', () => { diff --git a/tests/resources/messages.spec.ts b/tests/resources/messages.spec.ts index 4ae7315f..2f370417 100644 --- a/tests/resources/messages.spec.ts +++ b/tests/resources/messages.spec.ts @@ -509,6 +509,61 @@ describe('Messages', () => { expect(capturedRequest.method).toEqual('POST'); expect(capturedRequest.path).toEqual('/v3/grants/id123/messages/send'); }); + + it('should include isPlaintext in JSON body when provided', async () => { + const jsonBody = { + to: [{ name: 'Test', email: 'test@example.com' }], + subject: 'Plain text email', + body: 'Hello world', + isPlaintext: true, + }; + + await messages.send({ + identifier: 'id123', + requestBody: jsonBody, + }); + + const capturedRequest = apiClient.request.mock.calls[0][0]; + expect(capturedRequest.method).toEqual('POST'); + expect(capturedRequest.path).toEqual('/v3/grants/id123/messages/send'); + expect(capturedRequest.body).toEqual(jsonBody); + }); + + it('should include isPlaintext in multipart form message when provided', async () => { + const messageJson = { + to: [{ name: 'Test', email: 'test@example.com' }], + subject: 'Plain text email', + body: 'Hello world', + isPlaintext: true, + }; + const fileStream = createReadableStream('This is the text from file 1'); + const file1: CreateAttachmentRequest = { + filename: 'file1.txt', + contentType: 'text/plain', + content: fileStream, + size: 3 * 1024 * 1024, + }; + + await messages.send({ + identifier: 'id123', + requestBody: { + ...messageJson, + attachments: [file1], + }, + }); + + const capturedRequest = apiClient.request.mock.calls[0][0]; + const formData = ( + capturedRequest.form as any as MockedFormData + )._getAppendedData(); + const parsed = JSON.parse(formData.message); + expect(parsed.to).toEqual(messageJson.to); + expect(parsed.subject).toEqual(messageJson.subject); + expect(parsed.body).toEqual(messageJson.body); + expect(parsed.is_plaintext).toBe(true); + expect(capturedRequest.method).toEqual('POST'); + expect(capturedRequest.path).toEqual('/v3/grants/id123/messages/send'); + }); }); describe('scheduledMessages', () => {