Nylas is not maintaining my inline image reference in multipart form data with other attachments > 3mb

I was previously using the nylas v3 api to send inline images + attachments of size less than 3mb.

However, now the usecase has changed, and we need to include attachments of file size >3mb and <=25mb.

My inline images are sent in base64, whereas attachments are in stream content.

I went through the Nylas code, and it auto switches to multipart in case of large files.

However, after sending nylas doesn’t maintain my inline images

async sendEmailMessage(params: {

    grantId: string;

body: string;

subject: string;

to: EmailName[];

attachments?: EmailAttachmentDto[];

trackingOptions?: TrackingOptions;

  }) {

const { grantId, body, subject, to, attachments, trackingOptions } = params;




try {

return this.nylasClient.messages.send({

identifier: grantId,

requestBody: {

body,

subject,

to,

attachments: attachments?.map((attachment) => {

// Inline image (base64 content)

if (attachment.contentBase64) {

return {

filename: attachment.filename,

contentType: attachment.contentType,

content: attachment.contentBase64,    // base64 string required

contentId: attachment.contentId,      // required for cid:<id>

size: attachment?.size

              };

            }

if (attachment.contentStream) {

return {

filename: attachment.filename,

contentType: attachment.contentType,

content: attachment.contentStream,

contentId: attachment.contentId,

size: attachment?.size

              };

            }





trackingOptions,

        },

      });
static _buildFormRequest(

    requestBody: CreateDraftRequest | UpdateDraftRequest | SendMessageRequest

  ): FormData {

const form = new FormData();




// Split out the message payload from the attachments

const messagePayload = {

...requestBody,

attachments: undefined,

    };

form.append('message', JSON.stringify(objKeysToSnakeCase(messagePayload)));




// Add a separate form field for each attachment

if (requestBody.attachments && requestBody.attachments.length > 0) {

requestBody.attachments.map((attachment, index) => {

const contentId = attachment.contentId || `file${index}`;

// Handle different content types for formdata-node

if (typeof attachment.content === 'string') {

// Base64 string - create a Blob

const buffer = Buffer.from(attachment.content, 'base64');

const blob = new Blob([buffer], { type: attachment.contentType });

form.append(contentId, blob, attachment.filename);

        } else if (Buffer.isBuffer(attachment.content)) {

// Buffer - create a Blob

const blob = new Blob([attachment.content], {

type: attachment.contentType,

          });

form.append(contentId, blob, attachment.filename);

        } else {

// ReadableStream - create a proper file-like object according to formdata-node docs

const file = attachmentStreamToFile(attachment);

form.append(contentId, file, attachment.filename);

        }

      });

    }


Looking over this code in Nylas github,
it has not kept the attachments in the message payload, which is okay, but neither does it maintain the inline attribute.
Also, I am using nylas 7.4.0

Hello,

We’d be happy to help.

For inline images to work correctly with large attachments (>3MB), you must ensure:

  1. Set explicit contentId on inline images:

const inlineImage = {
filename: ‘logo.png’,
contentType: ‘image/png’,
content: imageStream, // or base64 string
contentId: ‘logo123’, // :warning: CRITICAL: Must be set!
size: imageSize
};

2. Reference the contentId in your HTML body using cid: scheme:

const body = `

Hello,

Company Logo

Best regards

`;

Here is an article that talks about attachments as well, see the last example: https://support.nylas.com/hc/en-us/articles/19492515408669-Sending-attachments-in-API-V3-larger-than-3MB-via-multipart

Many thanks,
Samuel R.
Support Engineer, Nylas

body: // please check the cid in the tag

<html><p><br><br></p><p></p><table style="color: rgb(136, 136, 136); font-family: Arial, Helvetica, sans-serif; font-size: small; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: in…solid rgb(217, 217, 217); vertical-align: middle; padding: 2pt; overflow: hidden;"><p style="line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;"><img height="23" width="82" src="cid:image-0" style="margin-left: 0px; margin-top: 0px;"></p></td><td rowspan="1" colspan="1" style="margin: 0px; border-left: 0.6pt solid rgb(217, 217, 217); vertical-align: top; padding: 5pt; overflow: hidden;"><p style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;"></p></td></tr></tbody></table></html>'

Attachments

{
    filename: "image-1.png",
    contentType: "image/png",
    contentBase64: "iVBORw0KGgoAAAANSUhEUgAAAsQAAADICAYAAADvCPfRAAAlCElEQVR4Xu2djXXkOJI1Vt6FLJ8So5A30bwjYvJYMxUW/XbnAe/djbeFQMdgpHtE/M9gbNs3gnxzib77jN77FxPCs5DlW/+uShcG3L/vPbOu41+FoMP9NX3p4Ry6NVXiWu5HiVnIGznrJ8xRWNhZZb6H2qx7/NSckz9xEHOR4Vg53C0fwXmihfcO71HU1GavPgWCo5BlX7Bfs/+R6Fx90U9fNo4XZb7uBY0bunsIcn0qBjuFSjbmZJunko35KOQ8layYzsJ1UOorNNs2Gt1Aa9XJZPpns3C9WWlcJx79c+vn48q5k5A9G+OwcO3bgU479s9VfRL8K/oWtL6topHP1W2uj/QeglyfivHD/3cyqFqYk0XYwnx8DHSqepKUKVwv9u+MOUHug/4FrwQbs8fHZO5jkvzsPr7yZ39ccJ80xhgzKD+v3vXVkGcPoN+xT34/Vpsqc95UGnMEPy9eVK6rn93ety6/sf9M/9l+jahc6ygYY4wxL0BfaVr48zF3HxiDxz86XLnW2SfjfeDtx+srXAVjjDHGGGMGoPzh8gX7JLv8oR+tGmPMC/B/AaG6uzb0NqgzAAAAAElFTkSuQmCC",
    contentId: "image-1",
    contentDisposition: "inline",
    isInline: true,
  },
  {
    contentType: "application/pdf",
    filename: "20mb.pdf",
    contentStream: {
      _events: {
        close: undefined,
        error: function() {},
        data: function () { [native code] },
        end: undefined,
        readable: undefined,
      },
      _readableState: {
        highWaterMark: 16,
        buffer: [
        ],
        bufferIndex: 0,
        length: 0,
        pipes: [
        ],
        awaitDrainWriters: null,
      },
      _read: function() {
        this.push(iterable);
        this.push(null);
      },
      _maxListeners: undefined,
      emit: function() {
        delayedStream._handleEmit(arguments);
        return realEmit.apply(source, arguments);
      },
      _eventsCount: 2,
    },
    contentId: "743906d4-8ecd-4f32-98e2-2fb3d7b37100",
    isInline: false,
    contentDisposition: "attachment",
    size: 21005621,
  },

Message API

try {
      return this.nylasClient.messages.send({
        identifier: grantId,
        requestBody: {
          body, // This body has a cid attached, example given above
          subject,
          to,
          attachments: attachments?.map((attachment) => ({
            content: (attachment.contentStream || attachment.contentBase64)!,
            contentType: attachment.contentType,
            filename: attachment.filename,
            contentId: attachment.contentId,
            isInline: attachment.isInline,
            contentDisposition: attachment.contentDisposition,
            size: attachment.size,
          })),
          trackingOptions,
        },
      });
    } 

Hi Samuel, the above is the payload of attachments, and body that I am sending over to Nylas API.

Nylas auto-switches to multipart/form-data, but it is losing the inline state.
Can you spot an error in my implementation above @Samuel.R ?

Hello ahsannaqvii,

Thank you for providing those details. I’ve identified the issue: there is a mismatch between your HTML cid: reference and your attachment’s contentId.

The Problem:

In your HTML body, you’re referencing:

<img src="cid:image-0" ...>

But in your attachments array, you have:

{
  filename: "image-1.png",
  contentId: "image-1",  // This doesn't match
  contentBase64: "iVBORw0KGgo...",
  isInline: true,
}

Why This Matters:

When Nylas switches to multipart/form-data for large files, the backend determines if an attachment is inline by:

  • Using the contentId as the multipart form field name

  • Searching your HTML body for <img.src=“cid:CONTENT_ID”.>

If it finds a match, it marks the attachment as isInline: true.
Since your HTML references cid:image-0 but your attachment has contentId: “image-1”, the backend can’t match them. As a result, it treats the image as a regular attachment instead of inline.

The Solution:

Change your attachment’s contentId to match what’s in your HTML:

{
  filename: "image-1.png",
  contentId: "image-0",  // Now matches cid:image-0 in HTML
  contentBase64: "iVBORw0KGgo...",
  contentType: "image/png",
  isInline: true,
  size: attachment.size,
}

Or alternatively, update your HTML to reference image-1:

<img src="cid:image-1" ...>

The key is ensuring the contentId value exactly matches the cid reference in your HTML (without the “cid:” prefix).

Why Your PDF Works Correctly:

Your PDF attachment has a UUID contentId: “743906d4-8ecd-4f32-98e2-2fb3d7b37100”, which is not referenced anywhere in your HTML body, so the backend correctly treats it as a regular attachment with isInline: false.

Once you fix the contentId mismatch, your inline images should display correctly even with files larger than 3MB when multipart/form-data is automatically used.

Feel free to reach out if you have any questions.

Many thanks,
Samuel R.
Support Engineer, Nylas

@Samuel.R I was debugging this issue for a couple of days, and I pasted the wrong data to you,

below is the payload that I pass to the send fn of Nylas. Now I am assuming it will auto-switch to multipart behind the scenes.

{
  grantId: "xxx",
  body: "<html><p><br><br></p><p></p><table style=\"color: rgb(136, 136, 136); font-family: Arial, Helvetica, sans-serif; font-size: small; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; border: none; border-collapse: collapse; min-width: 50px\"><colgroup><col style=\"min-width: 25px\"><col style=\"min-width: 25px\"></colgroup><tbody><tr style=\"height: 55.6017pt;\"><td rowspan=\"1\" colspan=\"1\" style=\"margin: 0px; border-right: 0.6pt solid rgb(217, 217, 217); vertical-align: middle; padding: 2pt; overflow: hidden;\"><p style=\"line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;\"><img height=\"23\" width=\"82\" src=\"cid:image-0\" style=\"margin-left: 0px; margin-top: 0px;\"></p></td><td rowspan=\"1\" colspan=\"1\" style=\"margin: 0px; border-left: 0.6pt solid rgb(217, 217, 217); vertical-align: top; padding: 5pt; overflow: hidden;\"><p style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\"></p></td></tr></tbody></table></html>",
  subject: "Testing nylas",
  to: [
    {
      email: "ahsanakhtar@gmail.com",
      
    },
    
  ],
  attachments: [
    {
      filename: "image-0.png",
      contentType: "image/png",
      contentBase64: "iVBORw0KGgoAAAANSUhEUgAAAsQAAADICAYAAADvCPfRAAAlCEEYFnLd9jF+vES3pT/B2f7sKN5DkOtTMdgpVLIxJ9s8lWzMRyHnqWRFG3/7hL4rc0+GPxh57+WjbTyOkR/hq1r5NUH+nRG8hyDXp2KwU6hkY062eSrZmI9CzlPJyhg0cuz2fPvNSuE16DdnK7kNXtHGY2nkY9jbrPyeRv69EbyHINenYrBTqGRjTrZ5zh8gX7JLv8oR+tGmPMC/B/AaG6uzb0NqgzAAAAAElFTkSuQmCC", //dummy
      contentId: "image-0",
    },
    {
      contentType: "application/pdf",
      filename: "Free_Test_Data_6MB_PDF.pdf",
      contentStream: {
        _events: {
          close: undefined,
          error: undefined,
          data: undefined,
          end: undefined,
          readable: undefined,
          
        },
        _readableState: {
          highWaterMark: 16,
          buffer: [
            
          ],
          bufferIndex: 0,
          length: 0,
          pipes: [
            
          ],
          awaitDrainWriters: null,
          
        },
        _read: function(){
          this.push(iterable);this.push(null);
        },
        _maxListeners: undefined,
        
      },
      contentId: "54e63e27-a400-4061-a6c6-da596c9fdc8e",
      isInline: false,
      contentDisposition: "attachment",
      size: 6235548,
      
    },
    
  ],
  trackingOptions: {
    threadReplies: true,
    
  },
  
}

the response I get back from Nylas

{
  body: "<html><p><br><br></p><p></p><table style=\"color: rgb(136, 136, 136); font-family: Arial, Helvetica, sans-serif; font-size: small; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; border: none; border-collapse: collapse; min-width: 50px\"><colgroup><col style=\"min-width: 25px\"><col style=\"min-width: 25px\"></colgroup><tbody><tr style=\"height: 55.6017pt;\"><td rowspan=\"1\" colspan=\"1\" style=\"margin: 0px; border-right: 0.6pt solid rgb(217, 217, 217); vertical-align: middle; padding: 2pt; overflow: hidden;\"><p style=\"line-height: 1.38; margin-top: 0pt; margin-bottom: 0pt;\"><img height=\"23\" width=\"82\" src=\"cid:image-0\" style=\"margin-left: 0px; margin-top: 0px;\"></p></td><td rowspan=\"1\" colspan=\"1\" style=\"margin: 0px; border-left: 0.6pt solid rgb(217, 217, 217); vertical-align: top; padding: 5pt; overflow: hidden;\"><p style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\"></p></td></tr></tbody></table></html>",
  attachments: [
    {
      id: "v0:RnJlZV9UZXN0X0RhdGFfNk1CX1BERi5wZGY=:YXBwbGljYXRpb24vcGRm:6235548",
      filename: "Free_Test_Data_6MB_PDF.pdf",
      size: 6235548,
      contentType: "application/pdf",
      isInline: false,
      contentDisposition: "attachment; filename=\"Free_Test_Data_6MB_PDF.pdf\"",
    },
    {
      id: "v0:aW1hZ2UtMC5wbmc=:aW1hZ2UvcG5n:12716",
      filename: "image-0.png",
      size: 12716,
      contentType: "image/png",
      isInline: false,
      contentDisposition: "attachment; filename=\"image-0.png\"",
    },
  ],
  from: [
    {
      name: "Tech Eng",
      email: "abbc@abc.so",
    },
  ],
  object: "message",
  subject: "Testing nylas", // dummy
  to: [
    {
      email: "hallucinogenizer@gmail.com",// dummy
    },
  ],
  folders: [
    "SENT",
  ],
  trackingOptions: {
    label: "",
    links: null,
    opens: null,
    threadReplies: true,
  },
  threadId: "19a9cd50b2ac0f55",
  date: 1763567790,
  requestId: "724019751-7b84dd0f-6270-4f77-a26e-29d9fe37dbdb",
  grantId: "xx",
  id: "19a9cd50b2ac0f55",
}

Now if you could spot the error please, it would just be very helpful, I am stuck in this debugging for the past few days.

Hi @Samuel.R , it would be pleasure if you could pick this up in priority and review please?
I am quite blocked
Also, I am using nylas 7.4.0 version

Hello ahsannaqvii,

I believe the issue is related to version 7.4.0. I was able to reproduce the problem, and after updating to the latest version (7.13.3), it worked correctly for me.

I used the following code if you would like to test it yourself:

import Nylas from 'nylas';
import fs from 'fs';

const nylas = new Nylas({
  apiKey: ‘api’_key,
  apiUri: 'https://api.us.nylas.com'
});

const grantId = ‘grant_id’;

// Send message with inline and regular attachments
const sendMessageWithAttachments = async () => {
  // Get file sizes for the SDK to detect large files
  const inlineImagePath = ‘file_path/images.jpeg';
  const largeImagePath = '/file_path/Sample-jpg-image-15mb.jpeg';
  
  const inlineImageStats = fs.statSync(inlineImagePath);
  const largeImageStats = fs.statSync(largeImagePath);
  
  const message = await nylas.messages.send({
    identifier: grantId,
    requestBody: {
      to: [{ 
        email: ‘test1@hotmail.com',
        name: 'Test Name' 
      }],
      subject: 'Testing out inline attachments',
      body: 'Before image <br> <img src="cid:xyzpdqnyla"/> <br> After image',
      attachments: [
        {
          // Inline attachment - referenced in the HTML body
          filename: 'images.jpeg',
          contentType: 'image/jpeg',
          contentId: 'xyzpdqnyla',  // This matches the cid: in the HTML
          isInline: true,
          content: fs.createReadStream(inlineImagePath),
          size: inlineImageStats.size
        },
        {
          // Regular attachment - appears as downloadable file
          filename: 'Sample-jpg-image-15mb.jpeg',
          contentType: 'image/jpeg',
          contentId: 'xyzpdqlogo',
          content: fs.createReadStream(largeImagePath),
          size: largeImageStats.size
        }
      ]
    }
  });

  console.log('Message sent!', message.data);
};

sendMessageWithAttachments();


Please let me know how it goes.

Many thanks,
Samuel R.
Support Engineer, Nylas

Thanks alot @Samuel.R ,

I had understood the issue yesterday before this message came up – the issue was related to the package. I had upgraded my version to 7.13.

Thanks alot for promptly replying, and hats off to your dedication.