nylas 7.10.0 → 7.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -156,10 +156,6 @@ class APIClient {
156
156
  }
157
157
  if (optionParams.form) {
158
158
  requestOptions.body = optionParams.form;
159
- requestOptions.headers = {
160
- ...requestOptions.headers,
161
- ...optionParams.form.getHeaders(),
162
- };
163
159
  }
164
160
  return requestOptions;
165
161
  }
@@ -198,6 +194,10 @@ class APIClient {
198
194
  }
199
195
  async requestStream(options) {
200
196
  const response = await this.sendRequest(options);
197
+ // TODO: See if we can fix this in a backwards compatible way
198
+ if (!response.body) {
199
+ throw new Error('No response body');
200
+ }
201
201
  return response.body;
202
202
  }
203
203
  }
package/lib/cjs/config.js CHANGED
@@ -8,7 +8,7 @@ var Region;
8
8
  (function (Region) {
9
9
  Region["Us"] = "us";
10
10
  Region["Eu"] = "eu";
11
- })(Region = exports.Region || (exports.Region = {}));
11
+ })(Region || (exports.Region = Region = {}));
12
12
  /**
13
13
  * The default Nylas API region.
14
14
  * @default Region.Us
@@ -9,4 +9,4 @@ var AvailabilityMethod;
9
9
  AvailabilityMethod["MaxFairness"] = "max-fairness";
10
10
  AvailabilityMethod["MaxAvailability"] = "max-availability";
11
11
  AvailabilityMethod["Collective"] = "collective";
12
- })(AvailabilityMethod = exports.AvailabilityMethod || (exports.AvailabilityMethod = {}));
12
+ })(AvailabilityMethod || (exports.AvailabilityMethod = AvailabilityMethod = {}));
@@ -9,4 +9,4 @@ var CredentialType;
9
9
  CredentialType["ADMINCONSENT"] = "adminconsent";
10
10
  CredentialType["SERVICEACCOUNT"] = "serviceaccount";
11
11
  CredentialType["CONNECTOR"] = "connector";
12
- })(CredentialType = exports.CredentialType || (exports.CredentialType = {}));
12
+ })(CredentialType || (exports.CredentialType = CredentialType = {}));
@@ -10,4 +10,4 @@ var WhenType;
10
10
  WhenType["Timespan"] = "timespan";
11
11
  WhenType["Date"] = "date";
12
12
  WhenType["Datespan"] = "datespan";
13
- })(WhenType = exports.WhenType || (exports.WhenType = {}));
13
+ })(WhenType || (exports.WhenType = WhenType = {}));
@@ -8,4 +8,4 @@ var FreeBusyType;
8
8
  (function (FreeBusyType) {
9
9
  FreeBusyType["FREE_BUSY"] = "free_busy";
10
10
  FreeBusyType["ERROR"] = "error";
11
- })(FreeBusyType = exports.FreeBusyType || (exports.FreeBusyType = {}));
11
+ })(FreeBusyType || (exports.FreeBusyType = FreeBusyType = {}));
@@ -8,4 +8,6 @@ var MessageFields;
8
8
  (function (MessageFields) {
9
9
  MessageFields["STANDARD"] = "standard";
10
10
  MessageFields["INCLUDE_HEADERS"] = "include_headers";
11
- })(MessageFields = exports.MessageFields || (exports.MessageFields = {}));
11
+ MessageFields["INCLUDE_TRACKING_OPTIONS"] = "include_tracking_options";
12
+ MessageFields["RAW_MIME"] = "raw_mime";
13
+ })(MessageFields || (exports.MessageFields = MessageFields = {}));
@@ -45,4 +45,4 @@ var WebhookTriggers;
45
45
  WebhookTriggers["BookingRescheduled"] = "booking.rescheduled";
46
46
  WebhookTriggers["BookingCancelled"] = "booking.cancelled";
47
47
  WebhookTriggers["BookingReminder"] = "booking.reminder";
48
- })(WebhookTriggers = exports.WebhookTriggers || (exports.WebhookTriggers = {}));
48
+ })(WebhookTriggers || (exports.WebhookTriggers = WebhookTriggers = {}));
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Auth = void 0;
4
4
  const uuid_1 = require("uuid");
5
- const crypto_1 = require("crypto");
5
+ const node_crypto_1 = require("node:crypto");
6
6
  const resource_js_1 = require("./resource.js");
7
7
  const utils_js_1 = require("../utils.js");
8
8
  /**
@@ -166,7 +166,7 @@ class Auth extends resource_js_1.Resource {
166
166
  return url;
167
167
  }
168
168
  hashPKCESecret(secret) {
169
- const hash = (0, crypto_1.createHash)('sha256').update(secret).digest('hex');
169
+ const hash = (0, node_crypto_1.createHash)('sha256').update(secret).digest('hex');
170
170
  return Buffer.from(hash).toString('base64').replace(/=+$/, '');
171
171
  }
172
172
  getTokenInfo(params) {
@@ -43,11 +43,9 @@ class Drafts extends resource_js_1.Resource {
43
43
  const path = (0, utils_js_2.makePathParams)('/v3/grants/{identifier}/drafts', {
44
44
  identifier,
45
45
  });
46
- // Use form data only if the attachment size is greater than 3mb
47
- const attachmentSize = requestBody.attachments?.reduce((total, attachment) => {
48
- return total + (attachment.size || 0);
49
- }, 0) || 0;
50
- if (attachmentSize >= messages_js_1.Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
46
+ // Use form data if the total payload size (body + attachments) is greater than 3mb
47
+ const totalPayloadSize = (0, utils_js_1.calculateTotalPayloadSize)(requestBody);
48
+ if (totalPayloadSize >= messages_js_1.Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
51
49
  const form = messages_js_1.Messages._buildFormRequest(requestBody);
52
50
  return this.apiClient.request({
53
51
  method: 'POST',
@@ -57,7 +55,7 @@ class Drafts extends resource_js_1.Resource {
57
55
  });
58
56
  }
59
57
  else if (requestBody.attachments) {
60
- const processedAttachments = await (0, utils_js_1.encodeAttachmentStreams)(requestBody.attachments);
58
+ const processedAttachments = await (0, utils_js_1.encodeAttachmentContent)(requestBody.attachments);
61
59
  requestBody = {
62
60
  ...requestBody,
63
61
  attachments: processedAttachments,
@@ -78,11 +76,9 @@ class Drafts extends resource_js_1.Resource {
78
76
  identifier,
79
77
  draftId,
80
78
  });
81
- // Use form data only if the attachment size is greater than 3mb
82
- const attachmentSize = requestBody.attachments?.reduce((total, attachment) => {
83
- return total + (attachment.size || 0);
84
- }, 0) || 0;
85
- if (attachmentSize >= messages_js_1.Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
79
+ // Use form data if the total payload size (body + attachments) is greater than 3mb
80
+ const totalPayloadSize = (0, utils_js_1.calculateTotalPayloadSize)(requestBody);
81
+ if (totalPayloadSize >= messages_js_1.Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
86
82
  const form = messages_js_1.Messages._buildFormRequest(requestBody);
87
83
  return this.apiClient.request({
88
84
  method: 'PUT',
@@ -92,7 +88,7 @@ class Drafts extends resource_js_1.Resource {
92
88
  });
93
89
  }
94
90
  else if (requestBody.attachments) {
95
- const processedAttachments = await (0, utils_js_1.encodeAttachmentStreams)(requestBody.attachments);
91
+ const processedAttachments = await (0, utils_js_1.encodeAttachmentContent)(requestBody.attachments);
96
92
  requestBody = {
97
93
  ...requestBody,
98
94
  attachments: processedAttachments,
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Messages = void 0;
4
+ const formdata_node_1 = require("formdata-node");
4
5
  const utils_js_1 = require("../utils.js");
5
6
  const resource_js_1 = require("./resource.js");
6
7
  const smartCompose_js_1 = require("./smartCompose.js");
@@ -89,16 +90,14 @@ class Messages extends resource_js_1.Resource {
89
90
  path,
90
91
  overrides,
91
92
  };
92
- // Use form data only if the attachment size is greater than 3mb
93
- const attachmentSize = requestBody.attachments?.reduce((total, attachment) => {
94
- return total + (attachment.size || 0);
95
- }, 0) || 0;
96
- if (attachmentSize >= Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
93
+ // Use form data if the total payload size (body + attachments) is greater than 3mb
94
+ const totalPayloadSize = (0, utils_js_1.calculateTotalPayloadSize)(requestBody);
95
+ if (totalPayloadSize >= Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
97
96
  requestOptions.form = Messages._buildFormRequest(requestBody);
98
97
  }
99
98
  else {
100
99
  if (requestBody.attachments) {
101
- const processedAttachments = await (0, utils_js_1.encodeAttachmentStreams)(requestBody.attachments);
100
+ const processedAttachments = await (0, utils_js_1.encodeAttachmentContent)(requestBody.attachments);
102
101
  requestOptions.body = {
103
102
  ...requestBody,
104
103
  attachments: processedAttachments,
@@ -157,10 +156,7 @@ class Messages extends resource_js_1.Resource {
157
156
  });
158
157
  }
159
158
  static _buildFormRequest(requestBody) {
160
- // FormData imports are funky, cjs needs to use .default, es6 doesn't
161
- const FD = require('form-data');
162
- const FormDataConstructor = FD.default || FD;
163
- const form = new FormDataConstructor();
159
+ const form = new formdata_node_1.FormData();
164
160
  // Split out the message payload from the attachments
165
161
  const messagePayload = {
166
162
  ...requestBody,
@@ -168,13 +164,30 @@ class Messages extends resource_js_1.Resource {
168
164
  };
169
165
  form.append('message', JSON.stringify((0, utils_js_1.objKeysToSnakeCase)(messagePayload)));
170
166
  // Add a separate form field for each attachment
171
- requestBody.attachments?.forEach((attachment, index) => {
172
- const contentId = attachment.contentId || `file${index}`;
173
- form.append(contentId, attachment.content, {
174
- filename: attachment.filename,
175
- contentType: attachment.contentType,
167
+ if (requestBody.attachments && requestBody.attachments.length > 0) {
168
+ requestBody.attachments.map((attachment, index) => {
169
+ const contentId = attachment.contentId || `file${index}`;
170
+ // Handle different content types for formdata-node
171
+ if (typeof attachment.content === 'string') {
172
+ // Base64 string - create a Blob
173
+ const buffer = Buffer.from(attachment.content, 'base64');
174
+ const blob = new formdata_node_1.Blob([buffer], { type: attachment.contentType });
175
+ form.append(contentId, blob, attachment.filename);
176
+ }
177
+ else if (Buffer.isBuffer(attachment.content)) {
178
+ // Buffer - create a Blob
179
+ const blob = new formdata_node_1.Blob([attachment.content], {
180
+ type: attachment.contentType,
181
+ });
182
+ form.append(contentId, blob, attachment.filename);
183
+ }
184
+ else {
185
+ // ReadableStream - create a proper file-like object according to formdata-node docs
186
+ const file = (0, utils_js_1.attachmentStreamToFile)(attachment);
187
+ form.append(contentId, file, attachment.filename);
188
+ }
176
189
  });
177
- });
190
+ }
178
191
  return form;
179
192
  }
180
193
  }
package/lib/cjs/utils.js CHANGED
@@ -1,11 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.makePathParams = exports.safePath = exports.objKeysToSnakeCase = exports.objKeysToCamelCase = exports.encodeAttachmentStreams = exports.createFileRequestBuilder = void 0;
3
+ exports.createFileRequestBuilder = createFileRequestBuilder;
4
+ exports.attachmentStreamToFile = attachmentStreamToFile;
5
+ exports.encodeAttachmentContent = encodeAttachmentContent;
6
+ exports.encodeAttachmentStreams = encodeAttachmentStreams;
7
+ exports.objKeysToCamelCase = objKeysToCamelCase;
8
+ exports.objKeysToSnakeCase = objKeysToSnakeCase;
9
+ exports.safePath = safePath;
10
+ exports.makePathParams = makePathParams;
11
+ exports.calculateTotalPayloadSize = calculateTotalPayloadSize;
4
12
  const change_case_1 = require("change-case");
5
- const fs = require("fs");
6
- const path = require("path");
13
+ const fs = require("node:fs");
14
+ const path = require("node:path");
7
15
  const mime = require("mime-types");
8
- const stream_1 = require("stream");
16
+ const node_stream_1 = require("node:stream");
9
17
  function createFileRequestBuilder(filePath) {
10
18
  const stats = fs.statSync(filePath);
11
19
  const filename = path.basename(filePath);
@@ -18,7 +26,6 @@ function createFileRequestBuilder(filePath) {
18
26
  size: stats.size,
19
27
  };
20
28
  }
21
- exports.createFileRequestBuilder = createFileRequestBuilder;
22
29
  /**
23
30
  * Converts a ReadableStream to a base64 encoded string.
24
31
  * @param stream The ReadableStream containing the binary data.
@@ -40,19 +47,67 @@ function streamToBase64(stream) {
40
47
  });
41
48
  }
42
49
  /**
43
- * Encodes the content of each attachment stream to base64.
50
+ * Converts a ReadableStream to a File-like object that can be used with FormData.
51
+ * @param attachment The attachment containing the stream and metadata.
52
+ * @param mimeType The MIME type for the file (optional).
53
+ * @returns A File-like object that properly handles the stream.
54
+ */
55
+ function attachmentStreamToFile(attachment, mimeType) {
56
+ if (mimeType != null && typeof mimeType !== 'string') {
57
+ throw new Error('Invalid mimetype, expected string.');
58
+ }
59
+ const content = attachment.content;
60
+ if (typeof content === 'string' || Buffer.isBuffer(content)) {
61
+ throw new Error('Invalid attachment content, expected ReadableStream.');
62
+ }
63
+ // Create a file-shaped object that FormData can handle properly
64
+ const fileObject = {
65
+ type: mimeType || attachment.contentType,
66
+ name: attachment.filename,
67
+ [Symbol.toStringTag]: 'File',
68
+ stream() {
69
+ return content;
70
+ },
71
+ };
72
+ // Add size if available
73
+ if (attachment.size !== undefined) {
74
+ fileObject.size = attachment.size;
75
+ }
76
+ return fileObject;
77
+ }
78
+ /**
79
+ * Encodes the content of each attachment to base64.
80
+ * Handles ReadableStream, Buffer, and string content types.
44
81
  * @param attachments The attachments to encode.
45
82
  * @returns The attachments with their content encoded to base64.
46
83
  */
47
- async function encodeAttachmentStreams(attachments) {
84
+ async function encodeAttachmentContent(attachments) {
48
85
  return await Promise.all(attachments.map(async (attachment) => {
49
- const base64EncodedContent = attachment.content instanceof stream_1.Readable
50
- ? await streamToBase64(attachment.content)
51
- : attachment.content;
52
- return { ...attachment, content: base64EncodedContent }; // Replace the stream with its base64 string
86
+ let base64EncodedContent;
87
+ if (attachment.content instanceof node_stream_1.Readable) {
88
+ // ReadableStream -> base64
89
+ base64EncodedContent = await streamToBase64(attachment.content);
90
+ }
91
+ else if (Buffer.isBuffer(attachment.content)) {
92
+ // Buffer -> base64
93
+ base64EncodedContent = attachment.content.toString('base64');
94
+ }
95
+ else {
96
+ // string (assumed to already be base64)
97
+ base64EncodedContent = attachment.content;
98
+ }
99
+ return { ...attachment, content: base64EncodedContent };
53
100
  }));
54
101
  }
55
- exports.encodeAttachmentStreams = encodeAttachmentStreams;
102
+ /**
103
+ * @deprecated Use encodeAttachmentContent instead. This alias is provided for backwards compatibility.
104
+ * Encodes the content of each attachment stream to base64.
105
+ * @param attachments The attachments to encode.
106
+ * @returns The attachments with their content encoded to base64.
107
+ */
108
+ async function encodeAttachmentStreams(attachments) {
109
+ return encodeAttachmentContent(attachments);
110
+ }
56
111
  /**
57
112
  * Applies the casing function and ensures numeric parts are preceded by underscores in snake_case.
58
113
  * @param casingFunction The original casing function.
@@ -111,7 +166,6 @@ function convertCase(obj, casingFunction, excludeKeys) {
111
166
  function objKeysToCamelCase(obj, exclude) {
112
167
  return convertCase(obj, change_case_1.camelCase, exclude);
113
168
  }
114
- exports.objKeysToCamelCase = objKeysToCamelCase;
115
169
  /**
116
170
  * A utility function that recursively converts all keys in an object to snake_case.
117
171
  * @param obj The object to convert
@@ -121,7 +175,6 @@ exports.objKeysToCamelCase = objKeysToCamelCase;
121
175
  function objKeysToSnakeCase(obj, exclude) {
122
176
  return convertCase(obj, change_case_1.snakeCase, exclude);
123
177
  }
124
- exports.objKeysToSnakeCase = objKeysToSnakeCase;
125
178
  /**
126
179
  * Safely encodes a path template with replacements.
127
180
  * @param pathTemplate The path template to encode.
@@ -145,9 +198,29 @@ function safePath(pathTemplate, replacements) {
145
198
  }
146
199
  });
147
200
  }
148
- exports.safePath = safePath;
149
201
  // Helper to create PathParams with type safety and runtime interpolation
150
202
  function makePathParams(path, params) {
151
203
  return safePath(path, params);
152
204
  }
153
- exports.makePathParams = makePathParams;
205
+ /**
206
+ * Calculates the total payload size for a message request, including body and attachments.
207
+ * This is used to determine if multipart/form-data should be used instead of JSON.
208
+ * @param requestBody The message request body
209
+ * @returns The total estimated payload size in bytes
210
+ */
211
+ function calculateTotalPayloadSize(requestBody) {
212
+ let totalSize = 0;
213
+ // Calculate size of the message body (JSON payload without attachments)
214
+ const messagePayloadWithoutAttachments = {
215
+ ...requestBody,
216
+ attachments: undefined,
217
+ };
218
+ const messagePayloadString = JSON.stringify(objKeysToSnakeCase(messagePayloadWithoutAttachments));
219
+ totalSize += Buffer.byteLength(messagePayloadString, 'utf8');
220
+ // Add attachment sizes
221
+ const attachmentSize = requestBody.attachments?.reduce((total, attachment) => {
222
+ return total + (attachment.size || 0);
223
+ }, 0) || 0;
224
+ totalSize += attachmentSize;
225
+ return totalSize;
226
+ }
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_VERSION = void 0;
4
4
  // This file is generated by scripts/exportVersion.js
5
- exports.SDK_VERSION = '7.10.0';
5
+ exports.SDK_VERSION = '7.12.0';
@@ -153,10 +153,6 @@ export default class APIClient {
153
153
  }
154
154
  if (optionParams.form) {
155
155
  requestOptions.body = optionParams.form;
156
- requestOptions.headers = {
157
- ...requestOptions.headers,
158
- ...optionParams.form.getHeaders(),
159
- };
160
156
  }
161
157
  return requestOptions;
162
158
  }
@@ -195,6 +191,10 @@ export default class APIClient {
195
191
  }
196
192
  async requestStream(options) {
197
193
  const response = await this.sendRequest(options);
194
+ // TODO: See if we can fix this in a backwards compatible way
195
+ if (!response.body) {
196
+ throw new Error('No response body');
197
+ }
198
198
  return response.body;
199
199
  }
200
200
  }
@@ -5,4 +5,6 @@ export var MessageFields;
5
5
  (function (MessageFields) {
6
6
  MessageFields["STANDARD"] = "standard";
7
7
  MessageFields["INCLUDE_HEADERS"] = "include_headers";
8
+ MessageFields["INCLUDE_TRACKING_OPTIONS"] = "include_tracking_options";
9
+ MessageFields["RAW_MIME"] = "raw_mime";
8
10
  })(MessageFields || (MessageFields = {}));
@@ -1,5 +1,5 @@
1
1
  import { v4 as uuid } from 'uuid';
2
- import { createHash } from 'crypto';
2
+ import { createHash } from 'node:crypto';
3
3
  import { Resource } from './resource.js';
4
4
  import { makePathParams } from '../utils.js';
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  import { Messages } from './messages.js';
2
2
  import { Resource } from './resource.js';
3
- import { encodeAttachmentStreams } from '../utils.js';
3
+ import { encodeAttachmentContent, calculateTotalPayloadSize, } from '../utils.js';
4
4
  import { makePathParams } from '../utils.js';
5
5
  /**
6
6
  * Nylas Drafts API
@@ -40,11 +40,9 @@ export class Drafts extends Resource {
40
40
  const path = makePathParams('/v3/grants/{identifier}/drafts', {
41
41
  identifier,
42
42
  });
43
- // Use form data only if the attachment size is greater than 3mb
44
- const attachmentSize = requestBody.attachments?.reduce((total, attachment) => {
45
- return total + (attachment.size || 0);
46
- }, 0) || 0;
47
- if (attachmentSize >= Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
43
+ // Use form data if the total payload size (body + attachments) is greater than 3mb
44
+ const totalPayloadSize = calculateTotalPayloadSize(requestBody);
45
+ if (totalPayloadSize >= Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
48
46
  const form = Messages._buildFormRequest(requestBody);
49
47
  return this.apiClient.request({
50
48
  method: 'POST',
@@ -54,7 +52,7 @@ export class Drafts extends Resource {
54
52
  });
55
53
  }
56
54
  else if (requestBody.attachments) {
57
- const processedAttachments = await encodeAttachmentStreams(requestBody.attachments);
55
+ const processedAttachments = await encodeAttachmentContent(requestBody.attachments);
58
56
  requestBody = {
59
57
  ...requestBody,
60
58
  attachments: processedAttachments,
@@ -75,11 +73,9 @@ export class Drafts extends Resource {
75
73
  identifier,
76
74
  draftId,
77
75
  });
78
- // Use form data only if the attachment size is greater than 3mb
79
- const attachmentSize = requestBody.attachments?.reduce((total, attachment) => {
80
- return total + (attachment.size || 0);
81
- }, 0) || 0;
82
- if (attachmentSize >= Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
76
+ // Use form data if the total payload size (body + attachments) is greater than 3mb
77
+ const totalPayloadSize = calculateTotalPayloadSize(requestBody);
78
+ if (totalPayloadSize >= Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
83
79
  const form = Messages._buildFormRequest(requestBody);
84
80
  return this.apiClient.request({
85
81
  method: 'PUT',
@@ -89,7 +85,7 @@ export class Drafts extends Resource {
89
85
  });
90
86
  }
91
87
  else if (requestBody.attachments) {
92
- const processedAttachments = await encodeAttachmentStreams(requestBody.attachments);
88
+ const processedAttachments = await encodeAttachmentContent(requestBody.attachments);
93
89
  requestBody = {
94
90
  ...requestBody,
95
91
  attachments: processedAttachments,
@@ -1,4 +1,5 @@
1
- import { encodeAttachmentStreams, objKeysToSnakeCase, makePathParams, } from '../utils.js';
1
+ import { Blob, FormData } from 'formdata-node';
2
+ import { attachmentStreamToFile, calculateTotalPayloadSize, encodeAttachmentContent, makePathParams, objKeysToSnakeCase, } from '../utils.js';
2
3
  import { Resource } from './resource.js';
3
4
  import { SmartCompose } from './smartCompose.js';
4
5
  /**
@@ -86,16 +87,14 @@ export class Messages extends Resource {
86
87
  path,
87
88
  overrides,
88
89
  };
89
- // Use form data only if the attachment size is greater than 3mb
90
- const attachmentSize = requestBody.attachments?.reduce((total, attachment) => {
91
- return total + (attachment.size || 0);
92
- }, 0) || 0;
93
- if (attachmentSize >= Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
90
+ // Use form data if the total payload size (body + attachments) is greater than 3mb
91
+ const totalPayloadSize = calculateTotalPayloadSize(requestBody);
92
+ if (totalPayloadSize >= Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) {
94
93
  requestOptions.form = Messages._buildFormRequest(requestBody);
95
94
  }
96
95
  else {
97
96
  if (requestBody.attachments) {
98
- const processedAttachments = await encodeAttachmentStreams(requestBody.attachments);
97
+ const processedAttachments = await encodeAttachmentContent(requestBody.attachments);
99
98
  requestOptions.body = {
100
99
  ...requestBody,
101
100
  attachments: processedAttachments,
@@ -154,10 +153,7 @@ export class Messages extends Resource {
154
153
  });
155
154
  }
156
155
  static _buildFormRequest(requestBody) {
157
- // FormData imports are funky, cjs needs to use .default, es6 doesn't
158
- const FD = require('form-data');
159
- const FormDataConstructor = FD.default || FD;
160
- const form = new FormDataConstructor();
156
+ const form = new FormData();
161
157
  // Split out the message payload from the attachments
162
158
  const messagePayload = {
163
159
  ...requestBody,
@@ -165,13 +161,30 @@ export class Messages extends Resource {
165
161
  };
166
162
  form.append('message', JSON.stringify(objKeysToSnakeCase(messagePayload)));
167
163
  // Add a separate form field for each attachment
168
- requestBody.attachments?.forEach((attachment, index) => {
169
- const contentId = attachment.contentId || `file${index}`;
170
- form.append(contentId, attachment.content, {
171
- filename: attachment.filename,
172
- contentType: attachment.contentType,
164
+ if (requestBody.attachments && requestBody.attachments.length > 0) {
165
+ requestBody.attachments.map((attachment, index) => {
166
+ const contentId = attachment.contentId || `file${index}`;
167
+ // Handle different content types for formdata-node
168
+ if (typeof attachment.content === 'string') {
169
+ // Base64 string - create a Blob
170
+ const buffer = Buffer.from(attachment.content, 'base64');
171
+ const blob = new Blob([buffer], { type: attachment.contentType });
172
+ form.append(contentId, blob, attachment.filename);
173
+ }
174
+ else if (Buffer.isBuffer(attachment.content)) {
175
+ // Buffer - create a Blob
176
+ const blob = new Blob([attachment.content], {
177
+ type: attachment.contentType,
178
+ });
179
+ form.append(contentId, blob, attachment.filename);
180
+ }
181
+ else {
182
+ // ReadableStream - create a proper file-like object according to formdata-node docs
183
+ const file = attachmentStreamToFile(attachment);
184
+ form.append(contentId, file, attachment.filename);
185
+ }
173
186
  });
174
- });
187
+ }
175
188
  return form;
176
189
  }
177
190
  }
package/lib/esm/utils.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { camelCase, snakeCase } from 'change-case';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
4
  import * as mime from 'mime-types';
5
- import { Readable } from 'stream';
5
+ import { Readable } from 'node:stream';
6
6
  export function createFileRequestBuilder(filePath) {
7
7
  const stats = fs.statSync(filePath);
8
8
  const filename = path.basename(filePath);
@@ -36,18 +36,67 @@ function streamToBase64(stream) {
36
36
  });
37
37
  }
38
38
  /**
39
- * Encodes the content of each attachment stream to base64.
39
+ * Converts a ReadableStream to a File-like object that can be used with FormData.
40
+ * @param attachment The attachment containing the stream and metadata.
41
+ * @param mimeType The MIME type for the file (optional).
42
+ * @returns A File-like object that properly handles the stream.
43
+ */
44
+ export function attachmentStreamToFile(attachment, mimeType) {
45
+ if (mimeType != null && typeof mimeType !== 'string') {
46
+ throw new Error('Invalid mimetype, expected string.');
47
+ }
48
+ const content = attachment.content;
49
+ if (typeof content === 'string' || Buffer.isBuffer(content)) {
50
+ throw new Error('Invalid attachment content, expected ReadableStream.');
51
+ }
52
+ // Create a file-shaped object that FormData can handle properly
53
+ const fileObject = {
54
+ type: mimeType || attachment.contentType,
55
+ name: attachment.filename,
56
+ [Symbol.toStringTag]: 'File',
57
+ stream() {
58
+ return content;
59
+ },
60
+ };
61
+ // Add size if available
62
+ if (attachment.size !== undefined) {
63
+ fileObject.size = attachment.size;
64
+ }
65
+ return fileObject;
66
+ }
67
+ /**
68
+ * Encodes the content of each attachment to base64.
69
+ * Handles ReadableStream, Buffer, and string content types.
40
70
  * @param attachments The attachments to encode.
41
71
  * @returns The attachments with their content encoded to base64.
42
72
  */
43
- export async function encodeAttachmentStreams(attachments) {
73
+ export async function encodeAttachmentContent(attachments) {
44
74
  return await Promise.all(attachments.map(async (attachment) => {
45
- const base64EncodedContent = attachment.content instanceof Readable
46
- ? await streamToBase64(attachment.content)
47
- : attachment.content;
48
- return { ...attachment, content: base64EncodedContent }; // Replace the stream with its base64 string
75
+ let base64EncodedContent;
76
+ if (attachment.content instanceof Readable) {
77
+ // ReadableStream -> base64
78
+ base64EncodedContent = await streamToBase64(attachment.content);
79
+ }
80
+ else if (Buffer.isBuffer(attachment.content)) {
81
+ // Buffer -> base64
82
+ base64EncodedContent = attachment.content.toString('base64');
83
+ }
84
+ else {
85
+ // string (assumed to already be base64)
86
+ base64EncodedContent = attachment.content;
87
+ }
88
+ return { ...attachment, content: base64EncodedContent };
49
89
  }));
50
90
  }
91
+ /**
92
+ * @deprecated Use encodeAttachmentContent instead. This alias is provided for backwards compatibility.
93
+ * Encodes the content of each attachment stream to base64.
94
+ * @param attachments The attachments to encode.
95
+ * @returns The attachments with their content encoded to base64.
96
+ */
97
+ export async function encodeAttachmentStreams(attachments) {
98
+ return encodeAttachmentContent(attachments);
99
+ }
51
100
  /**
52
101
  * Applies the casing function and ensures numeric parts are preceded by underscores in snake_case.
53
102
  * @param casingFunction The original casing function.
@@ -142,3 +191,25 @@ export function safePath(pathTemplate, replacements) {
142
191
  export function makePathParams(path, params) {
143
192
  return safePath(path, params);
144
193
  }
194
+ /**
195
+ * Calculates the total payload size for a message request, including body and attachments.
196
+ * This is used to determine if multipart/form-data should be used instead of JSON.
197
+ * @param requestBody The message request body
198
+ * @returns The total estimated payload size in bytes
199
+ */
200
+ export function calculateTotalPayloadSize(requestBody) {
201
+ let totalSize = 0;
202
+ // Calculate size of the message body (JSON payload without attachments)
203
+ const messagePayloadWithoutAttachments = {
204
+ ...requestBody,
205
+ attachments: undefined,
206
+ };
207
+ const messagePayloadString = JSON.stringify(objKeysToSnakeCase(messagePayloadWithoutAttachments));
208
+ totalSize += Buffer.byteLength(messagePayloadString, 'utf8');
209
+ // Add attachment sizes
210
+ const attachmentSize = requestBody.attachments?.reduce((total, attachment) => {
211
+ return total + (attachment.size || 0);
212
+ }, 0) || 0;
213
+ totalSize += attachmentSize;
214
+ return totalSize;
215
+ }
@@ -1,2 +1,2 @@
1
1
  // This file is generated by scripts/exportVersion.js
2
- export const SDK_VERSION = '7.10.0';
2
+ export const SDK_VERSION = '7.12.0';
@@ -1,8 +1,6 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  import { Request, Response } from 'node-fetch';
4
2
  import { NylasConfig, OverridableNylasConfig } from './config.js';
5
- import FormData from 'form-data';
3
+ import { FormData } from 'formdata-node';
6
4
  /**
7
5
  * The header key for the debugging flow ID
8
6
  */
@@ -1,5 +1,3 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  /**
4
2
  * Interface of an attachment object from Nylas.
5
3
  */
@@ -84,5 +84,15 @@ export interface ListFolderQueryParams extends ListQueryParams {
84
84
  * (Microsoft and EWS only.) Use the ID of a folder to find all child folders it contains.
85
85
  */
86
86
  parentId?: string;
87
+ /**
88
+ * (Microsoft only) When true, Nylas includes hidden folders in its response.
89
+ */
90
+ includeHiddenFolders?: boolean;
91
+ /**
92
+ * (Microsoft only) If true, retrieves folders from a single-level hierarchy only.
93
+ * If false, retrieves folders across a multi-level hierarchy.
94
+ * @default false
95
+ */
96
+ singleLevel?: boolean;
87
97
  }
88
98
  export type UpdateFolderRequest = Partial<CreateFolderRequest>;
@@ -74,6 +74,28 @@ export interface BaseMessage {
74
74
  */
75
75
  unread?: boolean;
76
76
  }
77
+ /**
78
+ * Interface representing message tracking options.
79
+ */
80
+ export interface MessageTrackingOptions {
81
+ /**
82
+ * When true, shows that message open tracking is enabled.
83
+ */
84
+ opens: boolean;
85
+ /**
86
+ * When true, shows that thread replied tracking is enabled.
87
+ */
88
+ threadReplies: boolean;
89
+ /**
90
+ * When true, shows that link clicked tracking is enabled.
91
+ */
92
+ links: boolean;
93
+ /**
94
+ * A label describing the message tracking purpose.
95
+ * Maximum length: 2048 characters.
96
+ */
97
+ label: string;
98
+ }
77
99
  /**
78
100
  * Interface representing a Nylas Message object.
79
101
  */
@@ -87,6 +109,17 @@ export interface Message extends BaseMessage {
87
109
  * Only present if the 'fields' query parameter is set to includeHeaders.
88
110
  */
89
111
  headers?: MessageHeaders[];
112
+ /**
113
+ * The message tracking options.
114
+ * Only present if the 'fields' query parameter is set to include_tracking_options.
115
+ */
116
+ trackingOptions?: MessageTrackingOptions;
117
+ /**
118
+ * A Base64url-encoded string containing the message data (including the body content).
119
+ * Only present if the 'fields' query parameter is set to raw_mime.
120
+ * When this field is requested, only grant_id, object, id, and raw_mime fields are returned.
121
+ */
122
+ rawMime?: string;
90
123
  /**
91
124
  * A list of key-value pairs storing additional data.
92
125
  */
@@ -144,7 +177,9 @@ export interface MessageHeaders {
144
177
  */
145
178
  export declare enum MessageFields {
146
179
  STANDARD = "standard",
147
- INCLUDE_HEADERS = "include_headers"
180
+ INCLUDE_HEADERS = "include_headers",
181
+ INCLUDE_TRACKING_OPTIONS = "include_tracking_options",
182
+ RAW_MIME = "raw_mime"
148
183
  }
149
184
  /**
150
185
  * Interface representing information about a scheduled message.
@@ -1,5 +1,3 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  import { Overrides } from '../config.js';
4
2
  import { Attachment, FindAttachmentQueryParams, DownloadAttachmentQueryParams } from '../models/attachments.js';
5
3
  import { NylasResponse } from '../models/response.js';
@@ -1,4 +1,4 @@
1
- import FormData from 'form-data';
1
+ import { FormData } from 'formdata-node';
2
2
  import APIClient from '../apiClient.js';
3
3
  import { Overrides } from '../config.js';
4
4
  import { CreateDraftRequest, SendMessageRequest, UpdateDraftRequest } from '../models/drafts.js';
@@ -1,5 +1,3 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  import APIClient from '../apiClient.js';
4
2
  import { OverridableNylasConfig } from '../config.js';
5
3
  import { ListQueryParams } from '../models/listQueryParams.js';
@@ -1,6 +1,21 @@
1
1
  import { CreateAttachmentRequest } from './models/attachments.js';
2
2
  export declare function createFileRequestBuilder(filePath: string): CreateAttachmentRequest;
3
3
  /**
4
+ * Converts a ReadableStream to a File-like object that can be used with FormData.
5
+ * @param attachment The attachment containing the stream and metadata.
6
+ * @param mimeType The MIME type for the file (optional).
7
+ * @returns A File-like object that properly handles the stream.
8
+ */
9
+ export declare function attachmentStreamToFile(attachment: CreateAttachmentRequest, mimeType?: string): any;
10
+ /**
11
+ * Encodes the content of each attachment to base64.
12
+ * Handles ReadableStream, Buffer, and string content types.
13
+ * @param attachments The attachments to encode.
14
+ * @returns The attachments with their content encoded to base64.
15
+ */
16
+ export declare function encodeAttachmentContent(attachments: CreateAttachmentRequest[]): Promise<CreateAttachmentRequest[]>;
17
+ /**
18
+ * @deprecated Use encodeAttachmentContent instead. This alias is provided for backwards compatibility.
4
19
  * Encodes the content of each attachment stream to base64.
5
20
  * @param attachments The attachments to encode.
6
21
  * @returns The attachments with their content encoded to base64.
@@ -43,3 +58,10 @@ export interface PathParams<Path extends string> {
43
58
  toPath(): string;
44
59
  }
45
60
  export declare function makePathParams<Path extends string>(path: Path, params: Record<ExtractPathParams<Path>, string>): string;
61
+ /**
62
+ * Calculates the total payload size for a message request, including body and attachments.
63
+ * This is used to determine if multipart/form-data should be used instead of JSON.
64
+ * @param requestBody The message request body
65
+ * @returns The total estimated payload size in bytes
66
+ */
67
+ export declare function calculateTotalPayloadSize(requestBody: any): number;
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "7.10.0";
1
+ export declare const SDK_VERSION = "7.12.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nylas",
3
- "version": "7.10.0",
3
+ "version": "7.12.0",
4
4
  "description": "A NodeJS wrapper for the Nylas REST API for email, contacts, and calendar.",
5
5
  "main": "lib/cjs/nylas.js",
6
6
  "types": "lib/types/nylas.d.ts",
@@ -40,9 +40,10 @@
40
40
  "license": "MIT",
41
41
  "dependencies": {
42
42
  "change-case": "^4.1.2",
43
- "form-data": "^4.0.0",
43
+ "form-data-encoder": "^4.1.0",
44
+ "formdata-node": "^6.0.3",
44
45
  "mime-types": "^2.1.35",
45
- "node-fetch": "^2.6.12",
46
+ "node-fetch": "^3.3.2",
46
47
  "uuid": "^8.3.2"
47
48
  },
48
49
  "devDependencies": {
@@ -50,7 +51,6 @@
50
51
  "@types/jest": "^29.5.2",
51
52
  "@types/mime-types": "^2.1.2",
52
53
  "@types/node": "^22.15.21",
53
- "@types/node-fetch": "^2.6.4",
54
54
  "@types/uuid": "^8.3.4",
55
55
  "@typescript-eslint/eslint-plugin": "^2.25.0",
56
56
  "@typescript-eslint/parser": "^2.25.0",
@@ -60,6 +60,7 @@
60
60
  "eslint-plugin-import": "^2.28.1",
61
61
  "eslint-plugin-prettier": "^3.0.1",
62
62
  "jest": "^29.6.1",
63
+ "jest-fetch-mock": "^3.0.3",
63
64
  "prettier": "^3.5.3",
64
65
  "ts-jest": "^29.1.1",
65
66
  "typedoc": "^0.28.4",