diff --git a/CHANGELOG.md b/CHANGELOG.md index c25166fb..ce1f5d88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Added +* `is_plaintext` support in `SendMessageRequest` and `CreateDraftRequest` to control plain text vs HTML message formatting + ### Changed * `SendMessageRequest.sendAt` field changed from `Int?` to `Long?` to support Unix timestamps beyond 2038. Maintains backward compatibility through method overloading - existing `Int` parameters are automatically converted to `Long`. **Note:** Kotlin users passing integer literals may need to specify type explicitly (e.g., `1620000000L` or `1620000000 as Int`). diff --git a/examples/src/main/java/com/nylas/examples/MessagesExample.java b/examples/src/main/java/com/nylas/examples/MessagesExample.java index 9a350c68..1f14e588 100644 --- a/examples/src/main/java/com/nylas/examples/MessagesExample.java +++ b/examples/src/main/java/com/nylas/examples/MessagesExample.java @@ -1,6 +1,7 @@ package com.nylas.examples; import com.nylas.NylasClient; +import com.nylas.models.EmailName; import com.nylas.models.FindMessageQueryParams; import com.nylas.models.ListMessagesQueryParams; import com.nylas.models.ListResponse; @@ -9,6 +10,7 @@ import com.nylas.models.NylasApiError; import com.nylas.models.NylasSdkTimeoutError; import com.nylas.models.Response; +import com.nylas.models.SendMessageRequest; import com.nylas.models.TrackingOptions; import okhttp3.OkHttpClient; @@ -134,6 +136,9 @@ private static void runMessagesExample(NylasClient nylas, Map co // 5. Find a specific message with different field options demonstrateMessageFinding(nylas, grantId); + + // 6. Demonstrate is_plaintext feature for sending messages + demonstratePlaintextFeature(nylas, grantId, config); } private static void demonstrateStandardMessageListing(NylasClient nylas, String grantId) throws NylasApiError, NylasSdkTimeoutError { @@ -301,4 +306,60 @@ private static void printMessageDetails(Message message, String requestType) { System.out.println(" Raw MIME length: " + message.getRawMime().length() + " characters"); } } + + private static void demonstratePlaintextFeature(NylasClient nylas, String grantId, Map config) throws NylasApiError, NylasSdkTimeoutError { + System.out.println("📝 6. Demonstrating is_plaintext feature for sending messages (NEW FEATURE):"); + + String recipientEmail = config.get("NYLAS_RECIPIENT_EMAIL"); + if (recipientEmail == null) { + System.out.println(" ⚠️ Skipping send examples - NYLAS_RECIPIENT_EMAIL not configured"); + System.out.println(" To enable this demo, set NYLAS_RECIPIENT_EMAIL in your .env file"); + return; + } + + System.out.println(" Sending test messages to: " + recipientEmail); + + // 1. Send HTML message (default behavior) + System.out.println("\n 📧 Sending HTML message (is_plaintext = false or not specified):"); + + SendMessageRequest htmlRequest = new SendMessageRequest.Builder( + Arrays.asList(new EmailName(recipientEmail, "Test Recipient")) + ) + .subject("HTML Message Test - Nylas SDK") + .body("

Hello from Nylas!

This is an HTML message with formatting.

") + .isPlaintext(false) // Explicitly set to false (this is also the default) + .build(); + + try { + Response htmlResponse = nylas.messages().send(grantId, htmlRequest); + System.out.println(" ✅ HTML message sent successfully"); + System.out.println(" Message ID: " + htmlResponse.getData().getId()); + } catch (Exception e) { + System.out.println(" ❌ Failed to send HTML message: " + e.getMessage()); + } + + // 2. Send plain text message using is_plaintext feature + System.out.println("\n 📄 Sending plain text message (is_plaintext = true):"); + + SendMessageRequest plaintextRequest = new SendMessageRequest.Builder( + Arrays.asList(new EmailName(recipientEmail, "Test Recipient")) + ) + .subject("Plain Text Message Test - Nylas SDK") + .body("Hello from Nylas!\n\nThis is a PLAIN TEXT message.\nNo HTML formatting will be applied.\n\nBest regards,\nNylas SDK") + .isPlaintext(true) // NEW FEATURE: Force plain text mode + .build(); + + try { + Response plaintextResponse = nylas.messages().send(grantId, plaintextRequest); + System.out.println(" ✅ Plain text message sent successfully"); + System.out.println(" Message ID: " + plaintextResponse.getData().getId()); + } catch (Exception e) { + System.out.println(" ❌ Failed to send plain text message: " + e.getMessage()); + } + + System.out.println("\n 💡 Key differences:"); + System.out.println(" - HTML message (is_plaintext=false): Message body is sent as HTML with MIME formatting"); + System.out.println(" - Plain text message (is_plaintext=true): Message body is sent as plain text, no HTML in MIME data"); + System.out.println(" - Default behavior: is_plaintext=false (HTML formatting)"); + } } \ No newline at end of file diff --git a/examples/src/main/kotlin/com/nylas/examples/KotlinMessagesExample.kt b/examples/src/main/kotlin/com/nylas/examples/KotlinMessagesExample.kt index e3a82c5e..c3c55ef1 100644 --- a/examples/src/main/kotlin/com/nylas/examples/KotlinMessagesExample.kt +++ b/examples/src/main/kotlin/com/nylas/examples/KotlinMessagesExample.kt @@ -117,6 +117,9 @@ private fun runMessagesExample(nylas: NylasClient, config: Map) // 5. Find a specific message with different field options demonstrateMessageFinding(nylas, grantId) + + // 6. Demonstrate is_plaintext feature for sending messages + demonstratePlaintextFeature(nylas, grantId, config) } private fun demonstrateStandardMessageListing(nylas: NylasClient, grantId: String) { @@ -277,4 +280,60 @@ private fun printMessageDetails(message: Message, requestType: String) { message.rawMime?.let { rawMime -> println(" Raw MIME length: ${rawMime.length} characters") } +} + +private fun demonstratePlaintextFeature(nylas: NylasClient, grantId: String, config: Map) { + println("📝 6. Demonstrating is_plaintext feature for sending messages (NEW FEATURE):") + + val recipientEmail = config["NYLAS_RECIPIENT_EMAIL"] + if (recipientEmail == null) { + println(" ⚠️ Skipping send examples - NYLAS_RECIPIENT_EMAIL not configured") + println(" To enable this demo, set NYLAS_RECIPIENT_EMAIL in your .env file") + return + } + + println(" Sending test messages to: $recipientEmail") + + // 1. Send HTML message (default behavior) + println("\n 📧 Sending HTML message (is_plaintext = false or not specified):") + + val htmlRequest = SendMessageRequest.Builder( + listOf(EmailName(recipientEmail, "Test Recipient")) + ) + .subject("HTML Message Test - Nylas SDK") + .body("

Hello from Nylas!

This is an HTML message with formatting.

") + .isPlaintext(false) // Explicitly set to false (this is also the default) + .build() + + try { + val htmlResponse = nylas.messages().send(grantId, htmlRequest) + println(" ✅ HTML message sent successfully") + println(" Message ID: ${htmlResponse.data.id}") + } catch (e: Exception) { + println(" ❌ Failed to send HTML message: ${e.message}") + } + + // 2. Send plain text message using is_plaintext feature + println("\n 📄 Sending plain text message (is_plaintext = true):") + + val plaintextRequest = SendMessageRequest.Builder( + listOf(EmailName(recipientEmail, "Test Recipient")) + ) + .subject("Plain Text Message Test - Nylas SDK") + .body("Hello from Nylas!\n\nThis is a PLAIN TEXT message.\nNo HTML formatting will be applied.\n\nBest regards,\nNylas SDK") + .isPlaintext(true) // NEW FEATURE: Force plain text mode + .build() + + try { + val plaintextResponse = nylas.messages().send(grantId, plaintextRequest) + println(" ✅ Plain text message sent successfully") + println(" Message ID: ${plaintextResponse.data.id}") + } catch (e: Exception) { + println(" ❌ Failed to send plain text message: ${e.message}") + } + + println("\n 💡 Key differences:") + println(" - HTML message (is_plaintext=false): Message body is sent as HTML with MIME formatting") + println(" - Plain text message (is_plaintext=true): Message body is sent as plain text, no HTML in MIME data") + println(" - Default behavior: is_plaintext=false (HTML formatting)") } \ No newline at end of file diff --git a/src/main/kotlin/com/nylas/models/CreateDraftRequest.kt b/src/main/kotlin/com/nylas/models/CreateDraftRequest.kt index f45cf326..256a74ef 100644 --- a/src/main/kotlin/com/nylas/models/CreateDraftRequest.kt +++ b/src/main/kotlin/com/nylas/models/CreateDraftRequest.kt @@ -72,6 +72,12 @@ data class CreateDraftRequest( */ @Json(name = "custom_headers") val customHeaders: List? = null, + /** + * 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. + */ + @Json(name = "is_plaintext") + val isPlaintext: Boolean? = null, ) : IMessageAttachmentRequest { /** * Builder for [CreateDraftRequest]. @@ -90,6 +96,7 @@ data class CreateDraftRequest( private var replyToMessageId: String? = null private var trackingOptions: TrackingOptions? = null private var customHeaders: List? = null + private var isPlaintext: Boolean? = null /** * Sets the from address. @@ -184,8 +191,17 @@ data class CreateDraftRequest( fun customHeaders(customHeaders: List?) = apply { this.customHeaders = customHeaders } /** - * Builds a [SendMessageRequest] instance. - * @return The [SendMessageRequest] instance. + * Sets whether the message body is sent as plain text. + * 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. + * @param isPlaintext Whether the message body is sent as plain text. + * @return The builder. + */ + fun isPlaintext(isPlaintext: Boolean?) = apply { this.isPlaintext = isPlaintext } + + /** + * Builds a [CreateDraftRequest] instance. + * @return The [CreateDraftRequest] instance. */ fun build() = CreateDraftRequest( @@ -202,6 +218,7 @@ data class CreateDraftRequest( replyToMessageId, trackingOptions, customHeaders, + isPlaintext, ) } } diff --git a/src/main/kotlin/com/nylas/models/SendMessageRequest.kt b/src/main/kotlin/com/nylas/models/SendMessageRequest.kt index 7366b1ab..b64dd9d4 100644 --- a/src/main/kotlin/com/nylas/models/SendMessageRequest.kt +++ b/src/main/kotlin/com/nylas/models/SendMessageRequest.kt @@ -78,6 +78,12 @@ data class SendMessageRequest( */ @Json(name = "custom_headers") val customHeaders: List? = null, + /** + * 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. + */ + @Json(name = "is_plaintext") + val isPlaintext: Boolean? = null, ) : IMessageAttachmentRequest { /** * Builder for [SendMessageRequest]. @@ -99,6 +105,7 @@ data class SendMessageRequest( private var trackingOptions: TrackingOptions? = null private var useDraft: Boolean? = null private var customHeaders: List? = null + private var isPlaintext: Boolean? = null /** * Sets the bcc recipients. @@ -200,6 +207,15 @@ data class SendMessageRequest( */ fun customHeaders(customHeaders: List?) = apply { this.customHeaders = customHeaders } + /** + * Sets whether the message body is sent as plain text. + * 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. + * @param isPlaintext Whether the message body is sent as plain text. + * @return The builder. + */ + fun isPlaintext(isPlaintext: Boolean?) = apply { this.isPlaintext = isPlaintext } + /** * Builds a [SendMessageRequest] instance. * @return The [SendMessageRequest] instance. @@ -220,6 +236,7 @@ data class SendMessageRequest( trackingOptions, useDraft, customHeaders, + isPlaintext, ) } } diff --git a/src/test/kotlin/com/nylas/resources/DraftsTests.kt b/src/test/kotlin/com/nylas/resources/DraftsTests.kt index 3c613bd5..23878253 100644 --- a/src/test/kotlin/com/nylas/resources/DraftsTests.kt +++ b/src/test/kotlin/com/nylas/resources/DraftsTests.kt @@ -19,8 +19,10 @@ import java.io.ByteArrayInputStream import java.lang.reflect.Type import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNull +import kotlin.test.assertTrue class DraftsTests { private val mockHttpClient: OkHttpClient = Mockito.mock(OkHttpClient::class.java) @@ -254,6 +256,67 @@ class DraftsTests { assertNull(queryParamCaptor.firstValue) } + @Test + fun `creating a draft with is_plaintext true serializes correctly`() { + val adapter = JsonHelper.moshi().adapter(CreateDraftRequest::class.java) + val createDraftRequest = + CreateDraftRequest( + body = "Hello, I just created a draft using Nylas!", + subject = "Hello from Nylas!", + isPlaintext = true, + ) + + val serializedRequest = adapter.toJson(createDraftRequest) + assertTrue(serializedRequest.contains("\"is_plaintext\":true")) + + drafts.create(grantId, createDraftRequest) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + val overrideParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture(), + overrideParamCaptor.capture(), + ) + + assertEquals("v3/grants/$grantId/drafts", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Draft::class.java), typeCaptor.firstValue) + assertEquals(adapter.toJson(createDraftRequest), requestBodyCaptor.firstValue) + assertNull(queryParamCaptor.firstValue) + } + + @Test + fun `creating a draft with is_plaintext false or not specified defaults correctly`() { + val adapter = JsonHelper.moshi().adapter(CreateDraftRequest::class.java) + + // Test with explicit false + val createDraftRequestFalse = + CreateDraftRequest( + body = "Hello, I just created a draft using Nylas!", + subject = "Hello from Nylas!", + isPlaintext = false, + ) + + val serializedRequestFalse = adapter.toJson(createDraftRequestFalse) + assertTrue(serializedRequestFalse.contains("\"is_plaintext\":false")) + + // Test with not specified (should default to false) + val createDraftRequestDefault = + CreateDraftRequest( + body = "Hello, I just created a draft using Nylas!", + subject = "Hello from Nylas!", + ) + + val serializedRequestDefault = adapter.toJson(createDraftRequestDefault) + // When null/not specified, the field should not be included in JSON or be false + assertFalse(serializedRequestDefault.contains("\"is_plaintext\":true")) + } + @Test fun `creating a draft with small attachment calls requests with the correct params`() { val adapter = JsonHelper.moshi().adapter(CreateDraftRequest::class.java) diff --git a/src/test/kotlin/com/nylas/resources/MessagesTests.kt b/src/test/kotlin/com/nylas/resources/MessagesTests.kt index 0fd52dad..8c2808b5 100644 --- a/src/test/kotlin/com/nylas/resources/MessagesTests.kt +++ b/src/test/kotlin/com/nylas/resources/MessagesTests.kt @@ -16,8 +16,10 @@ import java.io.ByteArrayInputStream import java.lang.reflect.Type import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNull +import kotlin.test.assertTrue class MessagesTests { private val mockHttpClient: OkHttpClient = Mockito.mock(OkHttpClient::class.java) @@ -558,6 +560,70 @@ class MessagesTests { assertNull(queryParamCaptor.firstValue) } + @Test + fun `sending a message with is_plaintext true serializes correctly`() { + val adapter = JsonHelper.moshi().adapter(SendMessageRequest::class.java) + val sendMessageRequest = + SendMessageRequest( + to = listOf(EmailName(email = "test@gmail.com", name = "Test")), + body = "Hello, I just sent a message using Nylas!", + subject = "Hello from Nylas!", + isPlaintext = true, + ) + + val serializedRequest = adapter.toJson(sendMessageRequest) + assertTrue(serializedRequest.contains("\"is_plaintext\":true")) + + messages.send(grantId, sendMessageRequest) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + val overrideParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture(), + overrideParamCaptor.capture(), + ) + + assertEquals("v3/grants/$grantId/messages/send", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Message::class.java), typeCaptor.firstValue) + assertEquals(adapter.toJson(sendMessageRequest), requestBodyCaptor.firstValue) + assertNull(queryParamCaptor.firstValue) + } + + @Test + fun `sending a message with is_plaintext false or not specified defaults correctly`() { + val adapter = JsonHelper.moshi().adapter(SendMessageRequest::class.java) + + // Test with explicit false + val sendMessageRequestFalse = + SendMessageRequest( + to = listOf(EmailName(email = "test@gmail.com", name = "Test")), + body = "Hello, I just sent a message using Nylas!", + subject = "Hello from Nylas!", + isPlaintext = false, + ) + + val serializedRequestFalse = adapter.toJson(sendMessageRequestFalse) + assertTrue(serializedRequestFalse.contains("\"is_plaintext\":false")) + + // Test with not specified (should default to false) + val sendMessageRequestDefault = + SendMessageRequest( + to = listOf(EmailName(email = "test@gmail.com", name = "Test")), + body = "Hello, I just sent a message using Nylas!", + subject = "Hello from Nylas!", + ) + + val serializedRequestDefault = adapter.toJson(sendMessageRequestDefault) + // When null/not specified, the field should not be included in JSON or be false + assertFalse(serializedRequestDefault.contains("\"is_plaintext\":true")) + } + @Test fun `sending a message with a small attachment calls requests with the correct params`() { val adapter = JsonHelper.moshi().adapter(SendMessageRequest::class.java)