diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f1e2c..08f132a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Unreleased ---------- * Added Yahoo, Zoom, EWS as providers to models/auth.py * Fixed grants.update() not using the correct "PATCH" method +* Added support for `is_plaintext` property in messages send and drafts create endpoints v6.11.1 ---------- diff --git a/examples/is_plaintext_demo/README.md b/examples/is_plaintext_demo/README.md new file mode 100644 index 0000000..da0d31b --- /dev/null +++ b/examples/is_plaintext_demo/README.md @@ -0,0 +1,156 @@ +# is_plaintext Demo + +This example demonstrates the usage of the new `is_plaintext` property for messages and drafts in the Nylas API. This property controls whether message content is sent as plain text or HTML in the MIME data. + +## Features Demonstrated + +1. **Plain Text Messages**: Shows how to send messages with `is_plaintext=True` to send content as plain text without HTML in MIME data. +2. **HTML Messages**: Demonstrates sending messages with `is_plaintext=False` to include HTML formatting in MIME data. +3. **Backwards Compatibility**: Shows that existing code continues to work without specifying the `is_plaintext` property. +4. **Draft Operations**: Demonstrates using `is_plaintext` with draft creation and updates. + +## API Property Overview + +### is_plaintext + +- **Type**: `boolean` +- **Default**: `false` +- **Available in**: + - `messages.send()` - Send message endpoint + - `drafts.create()` - Create draft endpoint + - `drafts.update()` - Update draft endpoint + +When `is_plaintext` is: +- `true`: The message body is sent as plain text and the MIME data doesn't include the HTML version of the message +- `false`: The message body is sent as HTML and MIME data includes HTML formatting +- Not specified: Uses API default behavior (same as `false`) + +## Setup + +1. Install the SDK in development mode from the repository root: +```bash +cd /path/to/nylas-python +pip install -e . +``` + +2. Set your environment variables: +```bash +export NYLAS_API_KEY="your_api_key" +export NYLAS_GRANT_ID="your_grant_id" +``` + +3. Run the example from the repository root: +```bash +python examples/is_plaintext_demo/is_plaintext_example.py +``` + +## Example Output + +``` +Demonstrating is_plaintext Property Usage +======================================= + +=== Sending Plain Text Message === +Sending message with is_plaintext=True... +āœ“ Message request prepared with is_plaintext=True +šŸ“§ Message configured to be sent as plain text (MIME without HTML version) + +=== Sending HTML Message === +Sending message with is_plaintext=False (HTML)... +āœ“ Message request prepared with is_plaintext=False +🌐 Message configured to be sent as HTML (MIME includes HTML version) + +=== Backwards Compatibility (No is_plaintext specified) === +Sending message without is_plaintext property... +āœ“ Existing code continues to work without modification + +=== Creating Plain Text Draft === +Creating draft with is_plaintext=True... +āœ“ Draft request prepared with is_plaintext=True +šŸ“ Draft configured to be sent as plain text when sent + +Example completed successfully! +``` + +## Use Cases + +### Plain Text (is_plaintext=true) +- **Simple Notifications**: System alerts, password resets, account confirmations +- **Text-Only Emails**: Newsletters or announcements that don't need formatting +- **Lightweight Messaging**: Reduce message size and improve compatibility +- **Accessibility**: Better support for screen readers and text-only email clients + +### HTML (is_plaintext=false) +- **Marketing Emails**: Rich formatting, images, and branded content +- **Newsletters**: Complex layouts with multiple sections and styling +- **Transactional Emails**: Formatted receipts, invoices, and reports +- **Interactive Content**: Buttons, links, and styled call-to-action elements + +## Code Examples + +### Send Plain Text Message +```python +message_request = { + "to": [{"email": "user@example.com", "name": "User"}], + "subject": "Plain Text Notification", + "body": "This is a plain text message.", + "is_plaintext": True +} + +response = client.messages.send( + identifier=grant_id, + request_body=message_request +) +``` + +### Send HTML Message +```python +message_request = { + "to": [{"email": "user@example.com", "name": "User"}], + "subject": "HTML Newsletter", + "body": "

Welcome!

This is HTML content.

", + "is_plaintext": False +} + +response = client.messages.send( + identifier=grant_id, + request_body=message_request +) +``` + +### Create Plain Text Draft +```python +draft_request = { + "to": [{"email": "user@example.com", "name": "User"}], + "subject": "Draft Message", + "body": "This draft will be sent as plain text.", + "is_plaintext": True +} + +response = client.drafts.create( + identifier=grant_id, + request_body=draft_request +) +``` + +## Important Notes + +- **Backwards Compatibility**: Existing code without `is_plaintext` continues to work unchanged +- **Default Behavior**: When `is_plaintext` is not specified, it defaults to `false` (HTML) +- **Content Type**: The property affects MIME structure, not just content rendering +- **Safety**: The example includes commented API calls to prevent unintended message sends + +## Error Handling + +The example includes proper error handling for: +- Missing environment variables +- API authentication errors +- Invalid request parameters +- Network connectivity issues + +## Documentation + +For more information about the Nylas Python SDK and message properties, visit: +- [Nylas Python SDK Documentation](https://developer.nylas.com/docs/sdks/python/) +- [Nylas API Messages Reference](https://developer.nylas.com/docs/api/v3/ecc/#tag--Messages) +- [Nylas API Drafts Reference](https://developer.nylas.com/docs/api/v3/ecc/#tag--Drafts) diff --git a/examples/is_plaintext_demo/is_plaintext_example.py b/examples/is_plaintext_demo/is_plaintext_example.py new file mode 100644 index 0000000..3b3a1c2 --- /dev/null +++ b/examples/is_plaintext_demo/is_plaintext_example.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +""" +Nylas SDK Example: Using is_plaintext for Messages and Drafts + +This example demonstrates how to use the new 'is_plaintext' property when sending +messages and creating drafts to control whether content is sent as plain text or HTML. + +Required Environment Variables: + NYLAS_API_KEY: Your Nylas API key + NYLAS_GRANT_ID: Your Nylas grant ID + +Usage: + First, install the SDK in development mode: + cd /path/to/nylas-python + pip install -e . + + Then set environment variables and run: + export NYLAS_API_KEY="your_api_key" + export NYLAS_GRANT_ID="your_grant_id" + python examples/is_plaintext_demo/is_plaintext_example.py +""" + +import os +import sys +from nylas import Client + + +def get_env_or_exit(var_name: str) -> str: + """Get an environment variable or exit if not found.""" + value = os.getenv(var_name) + if not value: + print(f"Error: {var_name} environment variable is required") + sys.exit(1) + return value + + +def print_separator(title: str) -> None: + """Print a formatted section separator.""" + print(f"\n=== {title} ===") + + +def demonstrate_plaintext_message(client: Client, grant_id: str) -> None: + """Demonstrate sending a message with is_plaintext=True.""" + print_separator("Sending Plain Text Message") + + try: + print("Sending message with is_plaintext=True...") + + # Example message content with HTML tags that will be sent as plain text + body_content = """Hello World! + +This is a test message sent as plain text. +Even if this contained HTML tags, they would be sent as plain text. + +Best regards, +The Nylas SDK Team""" + + # Send message with is_plaintext=True + message_request = { + "to": [{"email": "test@example.com", "name": "Test Recipient"}], + "subject": "Plain Text Message Example", + "body": body_content, + "is_plaintext": True + } + + print("āœ“ Message request prepared with is_plaintext=True") + print(f" Subject: {message_request['subject']}") + print(f" Body preview: {body_content[:100]}...") + print(f" is_plaintext: {message_request['is_plaintext']}") + + # Note: Uncomment the following line to actually send the message + # response = client.messages.send(identifier=grant_id, request_body=message_request) + # print(f"āœ“ Message sent! ID: {response.data.id}") + + print("šŸ“§ Message configured to be sent as plain text (MIME without HTML version)") + + except Exception as e: + print(f"āŒ Error sending plain text message: {e}") + + +def demonstrate_html_message(client: Client, grant_id: str) -> None: + """Demonstrate sending a message with is_plaintext=False (HTML).""" + print_separator("Sending HTML Message") + + try: + print("Sending message with is_plaintext=False (HTML)...") + + # Example message content with HTML formatting + body_content = """ + +

Hello World!

+ +

This is a test message sent as HTML.

+ +

The HTML tags will be properly rendered:

+ + +

Best regards,
+ The Nylas SDK Team

+ +""" + + # Send message with is_plaintext=False (default behavior) + message_request = { + "to": [{"email": "test@example.com", "name": "Test Recipient"}], + "subject": "HTML Message Example", + "body": body_content, + "is_plaintext": False + } + + print("āœ“ Message request prepared with is_plaintext=False") + print(f" Subject: {message_request['subject']}") + print(f" HTML body includes formatting tags") + print(f" is_plaintext: {message_request['is_plaintext']}") + + # Note: Uncomment the following line to actually send the message + # response = client.messages.send(identifier=grant_id, request_body=message_request) + # print(f"āœ“ Message sent! ID: {response.data.id}") + + print("🌐 Message configured to be sent as HTML (MIME includes HTML version)") + + except Exception as e: + print(f"āŒ Error sending HTML message: {e}") + + +def demonstrate_backwards_compatibility(client: Client, grant_id: str) -> None: + """Demonstrate that existing code without is_plaintext still works.""" + print_separator("Backwards Compatibility (No is_plaintext specified)") + + try: + print("Sending message without is_plaintext property...") + + # Traditional message request without is_plaintext + message_request = { + "to": [{"email": "test@example.com", "name": "Test Recipient"}], + "subject": "Traditional Message Example", + "body": "This message doesn't specify is_plaintext, so it uses the default behavior." + } + + print("āœ“ Message request prepared without is_plaintext property") + print(f" Subject: {message_request['subject']}") + print(f" Body: {message_request['body']}") + print(f" is_plaintext: Not specified (uses API default)") + + # Note: Uncomment the following line to actually send the message + # response = client.messages.send(identifier=grant_id, request_body=message_request) + # print(f"āœ“ Message sent! ID: {response.data.id}") + + print("āœ“ Existing code continues to work without modification") + + except Exception as e: + print(f"āŒ Error sending traditional message: {e}") + + +def demonstrate_plaintext_draft(client: Client, grant_id: str) -> None: + """Demonstrate creating a draft with is_plaintext=True.""" + print_separator("Creating Plain Text Draft") + + try: + print("Creating draft with is_plaintext=True...") + + draft_request = { + "to": [{"email": "test@example.com", "name": "Test Recipient"}], + "subject": "Plain Text Draft Example", + "body": "This is a draft that will be sent as plain text when sent.", + "is_plaintext": True + } + + print("āœ“ Draft request prepared with is_plaintext=True") + print(f" Subject: {draft_request['subject']}") + print(f" Body: {draft_request['body']}") + print(f" is_plaintext: {draft_request['is_plaintext']}") + + # Note: Uncomment the following lines to actually create the draft + # response = client.drafts.create(identifier=grant_id, request_body=draft_request) + # print(f"āœ“ Draft created! ID: {response.data.id}") + + print("šŸ“ Draft configured to be sent as plain text when sent") + + except Exception as e: + print(f"āŒ Error creating plain text draft: {e}") + + +def demonstrate_html_draft(client: Client, grant_id: str) -> None: + """Demonstrate creating a draft with is_plaintext=False.""" + print_separator("Creating HTML Draft") + + try: + print("Creating draft with is_plaintext=False...") + + html_body = """ + +

Draft Message

+

This is a draft with HTML formatting.

+

It will include HTML in the MIME data when sent.

+ +""" + + draft_request = { + "to": [{"email": "test@example.com", "name": "Test Recipient"}], + "subject": "HTML Draft Example", + "body": html_body, + "is_plaintext": False + } + + print("āœ“ Draft request prepared with is_plaintext=False") + print(f" Subject: {draft_request['subject']}") + print(f" HTML body includes formatting") + print(f" is_plaintext: {draft_request['is_plaintext']}") + + # Note: Uncomment the following lines to actually create the draft + # response = client.drafts.create(identifier=grant_id, request_body=draft_request) + # print(f"āœ“ Draft created! ID: {response.data.id}") + + print("šŸ“ Draft configured to be sent as HTML when sent") + + except Exception as e: + print(f"āŒ Error creating HTML draft: {e}") + + +def demonstrate_draft_update(client: Client, grant_id: str) -> None: + """Demonstrate updating a draft with is_plaintext property.""" + print_separator("Updating Draft with is_plaintext") + + try: + print("Demonstrating draft update with is_plaintext...") + + # Example update request + update_request = { + "subject": "Updated Draft with Plain Text", + "body": "This draft has been updated to use plain text format.", + "is_plaintext": True + } + + print("āœ“ Draft update request prepared with is_plaintext=True") + print(f" Updated subject: {update_request['subject']}") + print(f" Updated body: {update_request['body']}") + print(f" is_plaintext: {update_request['is_plaintext']}") + + # Note: Uncomment the following lines to actually update a draft + # draft_id = "your_draft_id_here" + # response = client.drafts.update( + # identifier=grant_id, + # draft_id=draft_id, + # request_body=update_request + # ) + # print(f"āœ“ Draft updated! ID: {response.data.id}") + + print("šŸ“ Draft update includes is_plaintext configuration") + + except Exception as e: + print(f"āŒ Error updating draft: {e}") + + +def main(): + """Main function demonstrating is_plaintext usage.""" + # Get required environment variables + api_key = get_env_or_exit("NYLAS_API_KEY") + grant_id = get_env_or_exit("NYLAS_GRANT_ID") + + # Initialize Nylas client + client = Client(api_key=api_key) + + print("Demonstrating is_plaintext Property Usage") + print("=======================================") + print("This shows the new 'is_plaintext' property for messages and drafts") + print("Note: Actual API calls are commented out to prevent unintended sends") + + # Demonstrate message sending with different is_plaintext values + demonstrate_plaintext_message(client, grant_id) + demonstrate_html_message(client, grant_id) + demonstrate_backwards_compatibility(client, grant_id) + + # Demonstrate draft creation and updating with is_plaintext + demonstrate_plaintext_draft(client, grant_id) + demonstrate_html_draft(client, grant_id) + demonstrate_draft_update(client, grant_id) + + print("\n" + "="*60) + print("Example completed successfully!") + print("="*60) + print("\nšŸ’” Key Takeaways:") + print("• is_plaintext=True: Sends content as plain text (no HTML in MIME)") + print("• is_plaintext=False: Sends content as HTML (includes HTML in MIME)") + print("• Not specified: Uses API default behavior (backwards compatible)") + print("• Available in: messages.send(), drafts.create(), drafts.update()") + + +if __name__ == "__main__": + main() diff --git a/nylas/models/drafts.py b/nylas/models/drafts.py index 34361bd..3c8a8c2 100644 --- a/nylas/models/drafts.py +++ b/nylas/models/drafts.py @@ -88,6 +88,8 @@ class CreateDraftRequest(TypedDict): tracking_options: Options for tracking opens, links, and thread replies. custom_headers: Custom headers to add to the message. metadata: A dictionary of key-value pairs storing additional data. + is_plaintext: 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. """ body: NotRequired[str] @@ -103,6 +105,7 @@ class CreateDraftRequest(TypedDict): tracking_options: NotRequired[TrackingOptions] custom_headers: NotRequired[List[CustomHeader]] metadata: NotRequired[Dict[str, Any]] + is_plaintext: NotRequired[bool] UpdateDraftRequest = CreateDraftRequest @@ -178,6 +181,8 @@ class SendMessageRequest(CreateDraftRequest): reply_to_message_id (NotRequired[str]): The ID of the message that you are replying to. tracking_options (NotRequired[TrackingOptions]): Options for tracking opens, links, and thread replies. custom_headers(NotRequired[List[CustomHeader]]): Custom headers to add to the message. + is_plaintext (NotRequired[bool]): 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. from_: The sender of the message. use_draft: Whether or not to use draft support. This is primarily used when dealing with large attachments. """ diff --git a/tests/resources/test_drafts.py b/tests/resources/test_drafts.py index 8613a2f..b0e9fe5 100644 --- a/tests/resources/test_drafts.py +++ b/tests/resources/test_drafts.py @@ -403,3 +403,66 @@ def test_send_draft_encoded_id(self, http_client_response): path="/v3/grants/abc-123/drafts/%3C%21%26%21AAAAAAAAAAAuAAAAAAAAABQ%2FwHZyqaNCptfKg5rnNAoBAMO2jhD3dRHOtM0AqgC7tuYAAAAAAA4AABAAAACTn3BxdTQ%2FT4N%2F0BgqPmf%2BAQAAAAA%3D%40example.com%3E", overrides=None, ) + + def test_create_draft_with_is_plaintext_true(self, http_client_response): + """Test creating a draft with is_plaintext=True.""" + drafts = Drafts(http_client_response) + request_body = { + "subject": "Hello from Nylas!", + "to": [{"name": "Jon Snow", "email": "jsnow@gmail.com"}], + "body": "This is the body of my draft message.", + "is_plaintext": True, + } + + drafts.create(identifier="abc-123", request_body=request_body) + + http_client_response._execute.assert_called_once_with( + "POST", + "/v3/grants/abc-123/drafts", + None, + None, + request_body, + overrides=None, + ) + + def test_create_draft_with_is_plaintext_false(self, http_client_response): + """Test creating a draft with is_plaintext=False.""" + drafts = Drafts(http_client_response) + request_body = { + "subject": "Hello from Nylas!", + "to": [{"name": "Jon Snow", "email": "jsnow@gmail.com"}], + "body": "This is the body of my draft message.", + "is_plaintext": False, + } + + drafts.create(identifier="abc-123", request_body=request_body) + + http_client_response._execute.assert_called_once_with( + "POST", + "/v3/grants/abc-123/drafts", + None, + None, + request_body, + overrides=None, + ) + + def test_create_draft_without_is_plaintext_backwards_compatibility(self, http_client_response): + """Test that existing code without is_plaintext still works (backwards compatibility).""" + drafts = Drafts(http_client_response) + request_body = { + "subject": "Hello from Nylas!", + "to": [{"name": "Jon Snow", "email": "jsnow@gmail.com"}], + "body": "This is the body of my draft message.", + } + + # Should work without any issues + drafts.create(identifier="abc-123", request_body=request_body) + + http_client_response._execute.assert_called_once_with( + "POST", + "/v3/grants/abc-123/drafts", + None, + None, + request_body, + overrides=None, + ) diff --git a/tests/resources/test_messages.py b/tests/resources/test_messages.py index 50f071c..bdf6760 100644 --- a/tests/resources/test_messages.py +++ b/tests/resources/test_messages.py @@ -889,4 +889,64 @@ def test_tracking_options_serialization(self): assert tracking_options_from_dict.opens is True assert tracking_options_from_dict.thread_replies is False assert tracking_options_from_dict.links is True - assert tracking_options_from_dict.label == "Test Campaign" \ No newline at end of file + assert tracking_options_from_dict.label == "Test Campaign" + + def test_send_message_with_is_plaintext_true(self, http_client_response): + """Test sending a message with is_plaintext=True.""" + messages = Messages(http_client_response) + request_body = { + "subject": "Hello from Nylas!", + "to": [{"name": "Jon Snow", "email": "jsnow@gmail.com"}], + "body": "This is the body of my message.", + "is_plaintext": True, + } + + messages.send(identifier="abc-123", request_body=request_body) + + http_client_response._execute.assert_called_once_with( + method="POST", + path="/v3/grants/abc-123/messages/send", + request_body=request_body, + data=None, + overrides=None, + ) + + def test_send_message_with_is_plaintext_false(self, http_client_response): + """Test sending a message with is_plaintext=False.""" + messages = Messages(http_client_response) + request_body = { + "subject": "Hello from Nylas!", + "to": [{"name": "Jon Snow", "email": "jsnow@gmail.com"}], + "body": "This is the body of my message.", + "is_plaintext": False, + } + + messages.send(identifier="abc-123", request_body=request_body) + + http_client_response._execute.assert_called_once_with( + method="POST", + path="/v3/grants/abc-123/messages/send", + request_body=request_body, + data=None, + overrides=None, + ) + + def test_send_message_without_is_plaintext_backwards_compatibility(self, http_client_response): + """Test that existing code without is_plaintext still works (backwards compatibility).""" + messages = Messages(http_client_response) + request_body = { + "subject": "Hello from Nylas!", + "to": [{"name": "Jon Snow", "email": "jsnow@gmail.com"}], + "body": "This is the body of my message.", + } + + # Should work without any issues + messages.send(identifier="abc-123", request_body=request_body) + + http_client_response._execute.assert_called_once_with( + method="POST", + path="/v3/grants/abc-123/messages/send", + request_body=request_body, + data=None, + overrides=None, + ) \ No newline at end of file