Skip to content

Commit df2f0c9

Browse files
authored
[CUST-4448] Fix: content id not being respected for large inline attachments. (#440)
1 parent 15aae0f commit df2f0c9

File tree

5 files changed

+387
-5
lines changed

5 files changed

+387
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ nylas-python Changelog
44
Unreleased
55
----------
66
* Fixed from field handling in messages.send() to properly map "from_" field to "from field
7+
* Fixed content_id handling for large inline attachments to use content_id as field name instead of generic file{index}
78

89
v6.12.0
910
----------
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Inline Attachment Example
2+
3+
This example demonstrates how to send messages and drafts with inline attachments using the `content_id` field in the Nylas Python SDK.
4+
5+
## What This Example Shows
6+
7+
- How to create inline attachments with `content_id` for HTML emails
8+
- How the SDK properly handles `content_id` for large attachments (>3MB)
9+
- The difference between inline attachments and regular attachments
10+
- How to reference inline attachments in HTML email bodies using `cid:` syntax
11+
12+
## Key Features Demonstrated
13+
14+
### Content ID Usage
15+
When an attachment includes a `content_id` field, the SDK will use this as the field name in multipart form data instead of the generic `file{index}` pattern. This is crucial for inline attachments that need to be referenced in the email body.
16+
17+
### HTML Email with Inline Images
18+
The example shows how to:
19+
1. Set the `content_id` field in the attachment
20+
2. Reference the attachment in HTML using `src="cid:your-content-id"`
21+
3. Set appropriate inline properties (`is_inline: True`, `content_disposition: "inline"`)
22+
23+
### Large Attachment Handling
24+
For attachments larger than 3MB, the SDK automatically switches from JSON to multipart form data. With this fix, the `content_id` is now properly respected in the form field names.
25+
26+
## Running the Example
27+
28+
1. Set your Nylas API key:
29+
```bash
30+
export NYLAS_API_KEY='your-api-key-here'
31+
```
32+
33+
2. Update the grant ID and email addresses in the script
34+
35+
3. Run the example:
36+
```bash
37+
python inline_attachment_example.py
38+
```
39+
40+
## Important Notes
41+
42+
- **Content ID Format**: Use a unique identifier for each inline attachment (e.g., `"[email protected]"`, `"logo"`, `"banner-image"`)
43+
- **HTML Reference**: Reference inline attachments in HTML using `src="cid:your-content-id"`
44+
- **Backward Compatibility**: Attachments without `content_id` still work as before using `file{index}` naming
45+
- **File Size Threshold**: The 3MB threshold determines whether JSON or form data is used for the request
46+
47+
## Expected Behavior
48+
49+
### Before the Fix (Problematic)
50+
```
51+
Form data fields:
52+
- message: (JSON payload)
53+
- file0: (inline image - content_id ignored)
54+
- file1: (regular attachment)
55+
```
56+
57+
### After the Fix (Correct)
58+
```
59+
Form data fields:
60+
- message: (JSON payload)
61+
- my-inline-image: (inline image - uses content_id)
62+
- file1: (regular attachment - fallback to file{index})
63+
```
64+
65+
This ensures that email clients can properly display inline images by matching the `content_id` in the HTML `cid:` reference with the multipart form field name.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env python3
2+
3+
import base64
4+
import io
5+
import os
6+
from nylas import Client
7+
8+
9+
def send_message_with_inline_attachment():
10+
"""
11+
This example demonstrates how to send a message with an inline attachment
12+
that uses a content_id for referencing in HTML email bodies.
13+
14+
This is particularly useful for embedding images directly in HTML emails
15+
where the image is referenced using 'cid:' in the src attribute.
16+
"""
17+
18+
# Initialize the Nylas client
19+
nylas = Client(
20+
api_key=os.environ.get("NYLAS_API_KEY"), # Replace with your API key
21+
)
22+
23+
# Get test email
24+
test_email = os.environ.get("TEST_EMAIL")
25+
26+
# Get grant
27+
grant_id = os.environ.get("NYLAS_GRANT_ID")
28+
29+
# Create a sample image content using base64 decoded data
30+
# This is a small PNG image that can be used for demonstration
31+
base64_image = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEQUlEQVRYhe2WW2wUVRyHv5nZS7vb3XbLdrfXjSZ9AxOqDRANGjDRSKLRKoIYNLENpEgJRQIKKvSi0tI28oImhtISqg9cTEzUhIgmSkAQY4UWQ2yALi3d7S4UaPcyO7MzPkwlJXG3S02sD5yH8zLJ//vO7/zPmSMsqR0MMIvDBHhnU0CcTfh9gf+FgOnfFlCTIKs6mg6iABaTgFn6DwQ0DUYnNBaVW3iyMpvcHImJmMaPvTG+75PxOkSkDPKdkUBC1bFZRb76sIhyn/WubyufyePaqEJdW5DAWBKrWUhb6557QFZ13E6Jb/eUUe6zcuzkOMu3DlG0/DLPbrrKl8dvUewxc6S1lJI5EqqWvp6wpHZQzxiu6HjyJI60lCKKsLkjwI1xjfdr3PiKLYyMKnQcvE7oVpIDjSWEx1QW1w1R5Ey9zowTkBUd7xR4fVsAm1Wgc0cxD5RY6P8zRrHHTPtbhZS6TXzxzU3cLhNVC7NJqKnXmJGArOgUuiQOtxrwjbsDOG0ijW96kRM6T2+4yorGIAuqB1FUnc2vu9lz9BYAC+ZmkVBT155WQFZ0ilwSh1tKEQXY0DqCK0ekYZ2HeEJn6Xo/t6MahU6RuKJztj9Kfq7EwA1j8y1mkXR7nFZASeoU50scai1FEKCuZQS3U2JHrYe4rLGgZpDmmjn4CiRkVcdmFaica2PsdpLyfKN0QtFIdw5SHkNdB6tZ4FBLKQDrd43gdUm8t9ZDTNaorPbzycYCHq+0Y8sSWdMe4sTeMswmgd1dYTZU5QJwpj+OJc1hT5uApsGAX6Zu1wiF+ZPwuMbD1X4+rTfgv5yPUtUU5MTeMhx2kY4DYQaDKquW5REeUzl6OobFlDqDlAKCYKSwuP4aZV4T767xEI1rVFT7+WxTAYsfsXPmfJTnGoL07fPhsIu0d4c5e1Gm54MSANa1BCiwp2+ztDfheFxny/MO6le7icY0Kmr8dG4u4LEKO6fPRXmhKciFTh8up8TurjC9AzKfT8Jf3T7M0PUkWdPchCkFlCQ8Mc9K/Wo3kZix8q4tBTw6387Pv0d4sXmUC50+8hwSrftDnLuUoKfZgK/aPszlUZXsaeCQZgskEa4EVH76NcKitX66txrwU70RXpoCb+kM0XcpwcEmA/7KtmGuZAhPm4AoQHhcY1lTkO+aCln4kI2Tv0VY8dEo/ft95OZI7NoX4o/BBAcm4Su3DeMPqdPGnpEAQHhC44fmQirnGfA32kL0d/pw5hixX7qm0N1owFe8M8TV8PR7nrFARNZpeM1lwHsjPLUzyLGdXpw5Em3dYT7+eoLjrUUAvPz2UEYN908j5d8woeosnZ/N3ActNPTcxOMQybOJlHkkTl1M4MgSUJNGryQ1HbN07/C0An9LKEmwW43img5JjTtPLn1yEmbGBqbpAYtJuOsaFQUQp7z3hDvTzMesv4rvC8y6gAmIzKbAX+u0pDGsEb6KAAAAAElFTkSuQmCC"
32+
image_content = base64.b64decode(base64_image)
33+
34+
# Create the message with inline attachment
35+
message_request = {
36+
"to": [{"email": test_email, "name": "Recipient Name"}],
37+
"from": [{"email": test_email, "name": "Sender Name"}],
38+
"subject": "Message with Inline Image",
39+
"body": """
40+
<html>
41+
<body>
42+
<h1>Hello!</h1>
43+
<p>This email contains an inline image:</p>
44+
<img src="cid:my-inline-image" alt="Inline Image" style="max-width: 200px;">
45+
<p>The image above is embedded directly in the email using content_id.</p>
46+
</body>
47+
</html>
48+
""",
49+
"attachments": [
50+
{
51+
"filename": "inline-image.png",
52+
"content_type": "image/png",
53+
"content": io.BytesIO(image_content),
54+
"size": len(image_content),
55+
"content_id": "my-inline-image", # This is the key for inline attachments
56+
"is_inline": True,
57+
"content_disposition": "inline"
58+
},
59+
{
60+
# Regular attachment without content_id for comparison
61+
"filename": "regular-attachment.txt",
62+
"content_type": "text/plain",
63+
"content": io.BytesIO(b"This is a regular attachment"),
64+
"size": 28,
65+
# No content_id - this will use the default file{index} naming
66+
}
67+
]
68+
}
69+
70+
try:
71+
# Send the message
72+
response = nylas.messages.send(
73+
identifier=grant_id, # Replace with your grant ID
74+
request_body=message_request
75+
)
76+
77+
print("Message sent successfully!")
78+
print(f"Message ID: {response.data.id}")
79+
print(f"Thread ID: {response.data.thread_id}")
80+
81+
# The inline attachment will be referenced by its content_id in the form data
82+
# instead of a generic file{index} name, allowing proper inline display
83+
84+
except Exception as e:
85+
print(f"Error sending message: {e}")
86+
87+
88+
def send_draft_with_inline_attachment():
89+
"""
90+
This example demonstrates how to create and send a draft with an inline attachment.
91+
"""
92+
93+
# Initialize the Nylas client
94+
nylas = Client(
95+
api_key=os.environ.get("NYLAS_API_KEY"), # Replace with your API key
96+
)
97+
98+
# Get test email
99+
test_email = os.environ.get("TEST_EMAIL")
100+
101+
# Get grant
102+
grant_id = os.environ.get("NYLAS_GRANT_ID")
103+
104+
# Create a larger image content to trigger form data usage (>3MB threshold)
105+
# For demo purposes, we'll replicate the same image data multiple times
106+
# In real usage, large images would automatically use the content_id functionality
107+
base64_image = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEQUlEQVRYhe2WW2wUVRyHv5nZS7vb3XbLdrfXjSZ9AxOqDRANGjDRSKLRKoIYNLENpEgJRQIKKvSi0tI28oImhtISqg9cTEzUhIgmSkAQY4UWQ2yALi3d7S4UaPcyO7MzPkwlJXG3S02sD5yH8zLJ//vO7/zPmSMsqR0MMIvDBHhnU0CcTfh9gf+FgOnfFlCTIKs6mg6iABaTgFn6DwQ0DUYnNBaVW3iyMpvcHImJmMaPvTG+75PxOkSkDPKdkUBC1bFZRb76sIhyn/WubyufyePaqEJdW5DAWBKrWUhb6557QFZ13E6Jb/eUUe6zcuzkOMu3DlG0/DLPbrrKl8dvUewxc6S1lJI5EqqWvp6wpHZQzxiu6HjyJI60lCKKsLkjwI1xjfdr3PiKLYyMKnQcvE7oVpIDjSWEx1QW1w1R5Ey9zowTkBUd7xR4fVsAm1Wgc0cxD5RY6P8zRrHHTPtbhZS6TXzxzU3cLhNVC7NJqKnXmJGArOgUuiQOtxrwjbsDOG0ijW96kRM6T2+4yorGIAuqB1FUnc2vu9lz9BYAC+ZmkVBT155WQFZ0ilwSh1tKEQXY0DqCK0ekYZ2HeEJn6Xo/t6MahU6RuKJztj9Kfq7EwA1j8y1mkXR7nFZASeoU50scai1FEKCuZQS3U2JHrYe4rLGgZpDmmjn4CiRkVcdmFaica2PsdpLyfKN0QtFIdw5SHkNdB6tZ4FBLKQDrd43gdUm8t9ZDTNaorPbzycYCHq+0Y8sSWdMe4sTeMswmgd1dYTZU5QJwpj+OJc1hT5uApsGAX6Zu1wiF+ZPwuMbD1X4+rTfgv5yPUtUU5MTeMhx2kY4DYQaDKquW5REeUzl6OobFlDqDlAKCYKSwuP4aZV4T767xEI1rVFT7+WxTAYsfsXPmfJTnGoL07fPhsIu0d4c5e1Gm54MSANa1BCiwp2+ztDfheFxny/MO6le7icY0Kmr8dG4u4LEKO6fPRXmhKciFTh8up8TurjC9AzKfT8Jf3T7M0PUkWdPchCkFlCQ8Mc9K/Wo3kZix8q4tBTw6387Pv0d4sXmUC50+8hwSrftDnLuUoKfZgK/aPszlUZXsaeCQZgskEa4EVH76NcKitX66txrwU70RXpoCb+kM0XcpwcEmA/7KtmGuZAhPm4AoQHhcY1lTkO+aCln4kI2Tv0VY8dEo/ft95OZI7NoX4o/BBAcm4Su3DeMPqdPGnpEAQHhC44fmQirnGfA32kL0d/pw5hixX7qm0N1owFe8M8TV8PR7nrFARNZpeM1lwHsjPLUzyLGdXpw5Em3dYT7+eoLjrUUAvPz2UEYN908j5d8woeosnZ/N3ActNPTcxOMQybOJlHkkTl1M4MgSUJNGryQ1HbN07/C0An9LKEmwW43img5JjTtPLn1yEmbGBqbpAYtJuOsaFQUQp7z3hDvTzMesv4rvC8y6gAmIzKbAX+u0pDGsEb6KAAAAAElFTkSuQmCC"
108+
large_image_content = base64.b64decode(base64_image) * 1000 # Replicated to make it large
109+
110+
# Create the draft with inline attachment
111+
draft_request = {
112+
"to": [{"email": test_email, "name": "Recipient Name"}],
113+
"from": [{"email": test_email, "name": "Sender Name"}],
114+
"subject": "Draft with Inline Image",
115+
"body": """
116+
<html>
117+
<body>
118+
<h1>Draft Email</h1>
119+
<p>This draft contains an inline image:</p>
120+
<img src="cid:logo-image" alt="Company Logo" style="max-width: 300px;">
121+
<p>Best regards,<br>Your Team</p>
122+
</body>
123+
</html>
124+
""",
125+
"attachments": [
126+
{
127+
"filename": "company-logo.png",
128+
"content_type": "image/png",
129+
"content": io.BytesIO(large_image_content),
130+
"size": len(large_image_content),
131+
"content_id": "logo-image", # Content ID for inline reference
132+
"is_inline": True,
133+
"content_disposition": "inline"
134+
}
135+
]
136+
}
137+
138+
try:
139+
# Create the draft
140+
draft_response = nylas.drafts.create(
141+
identifier=grant_id, # Replace with your grant ID
142+
request_body=draft_request
143+
)
144+
145+
print("Draft created successfully!")
146+
print(f"Draft ID: {draft_response.data.id}")
147+
148+
# Send the draft
149+
send_response = nylas.drafts.send(
150+
identifier=grant_id, # Replace with your grant ID
151+
draft_id=draft_response.data.id
152+
)
153+
154+
print("Draft sent successfully!")
155+
print(f"Message ID: {send_response.data.id}")
156+
157+
except Exception as e:
158+
print(f"Error with draft: {e}")
159+
160+
161+
if __name__ == "__main__":
162+
print("Inline Attachment Example")
163+
print("=" * 50)
164+
print()
165+
166+
# Check if API key is set
167+
if not os.environ.get("NYLAS_API_KEY"):
168+
print("Please set the NYLAS_API_KEY environment variable")
169+
print("export NYLAS_API_KEY='your-api-key-here'")
170+
exit(1)
171+
172+
# Check if grant ID is set
173+
if not os.environ.get("NYLAS_GRANT_ID"):
174+
print("Please set the NYLAS_GRANT_ID environment variable")
175+
print("export NYLAS_GRANT_ID='your-grant-id-here'")
176+
exit(1)
177+
178+
# Check if test email is set
179+
if not os.environ.get("TEST_EMAIL"):
180+
print("Please set the TEST_EMAIL environment variable")
181+
print("export TEST_EMAIL='your-test-email-here'")
182+
exit(1)
183+
184+
print("1. Sending message with inline attachment...")
185+
send_message_with_inline_attachment()
186+
187+
print("\n2. Creating and sending draft with inline attachment...")
188+
send_draft_with_inline_attachment()
189+
190+
print("\nNote: The content_id field ensures that large inline attachments")
191+
print("are properly referenced in the multipart form data, allowing")
192+
print("email clients to display them inline correctly.")

nylas/utils/file_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def _build_form_request(request_body: dict) -> MultipartEncoder:
7070
# Create the multipart/form-data encoder
7171
fields = {"message": ("", message_payload, "application/json")}
7272
for index, attachment in enumerate(attachments):
73-
fields[f"file{index}"] = (
73+
# Use content_id as field name if provided, otherwise fallback to file{index}
74+
field_name = attachment.get("content_id", f"file{index}")
75+
fields[field_name] = (
7476
attachment["filename"],
7577
attachment["content"],
7678
attachment["content_type"],

0 commit comments

Comments
 (0)