diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fe7ba88..863cbca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Nylas Java SDK Changelog +## [Unreleased] + +### Added +* Support for `include_tracking_options` and `raw_mime` values in `MessageFields` enum +* Support for `tracking_options` field in `Message` model to access message tracking settings +* Support for `raw_mime` field in `Message` model to access Base64url-encoded message data +* Support for query parameters in `Messages.find()` method to specify fields like `include_tracking_options` and `raw_mime` +* Added `Builder` pattern to `FindMessageQueryParams` for consistency with other query parameter classes + ## [2.9.0] - Release 2025-05-27 ### Added diff --git a/examples/.env.example b/examples/.env.example index 2fb9dc1a..9b015c6e 100644 --- a/examples/.env.example +++ b/examples/.env.example @@ -8,4 +8,8 @@ MEETING_LINK=your_meeting_link_here # Nylas API URI - Optional (defaults to https://api.us.nylas.com) # Only change this if instructed by Nylas support -NYLAS_API_URI=https://api.us.nylas.com \ No newline at end of file +NYLAS_API_URI=https://api.us.nylas.com + +# Grant ID - Required for message and event examples +# You can get this from your Nylas Dashboard after connecting an account +NYLAS_GRANT_ID=your_grant_id_here \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 98320822..84567b6b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,6 +4,24 @@ Simple examples demonstrating how to use the Nylas Java/Kotlin SDK. ## Available Examples +### Messages Example + +The `MessagesExample` and `KotlinMessagesExample` demonstrate how to use the new message features in the Nylas Java/Kotlin SDK: + +- Use the new `MessageFields.INCLUDE_TRACKING_OPTIONS` and `MessageFields.RAW_MIME` enum values +- Access tracking options (opens, thread_replies, links, label) from messages +- Retrieve raw MIME content for messages +- Use the new `FindMessageQueryParams` to specify fields when finding specific messages +- Compare different field options and their effects on returned data + +### Events Example + +The `EventsExample` demonstrates how to use the Nylas Java/Kotlin SDK to interact with the Events API: + +- List events from a calendar +- Filter events by date range +- Show event details + ### Notetaker Example The `NotetakerExample` demonstrates how to use the Nylas Java/Kotlin SDK to interact with the Notetakers API: @@ -28,7 +46,10 @@ Edit the `.env` file with your details: # Get your API key from the Nylas Dashboard NYLAS_API_KEY=your_api_key_here -# Add your meeting link (Zoom, Google Meet, or Microsoft Teams) +# Your grant ID (required for message examples) +NYLAS_GRANT_ID=your_grant_id_here + +# Add your meeting link (Zoom, Google Meet, or Microsoft Teams) - for Notetaker example MEETING_LINK=your_meeting_link_here ``` @@ -36,12 +57,27 @@ MEETING_LINK=your_meeting_link_here #### Option 1: Using Gradle -Run Java example: +Run Java Messages example: +```bash +./gradlew :examples:run -PmainClass=com.nylas.examples.MessagesExample +``` + +Run Kotlin Messages example: +```bash +./gradlew :examples:run -PmainClass=com.nylas.examples.KotlinMessagesExampleKt +``` + +Run Java Events example: +```bash +./gradlew :examples:run -PmainClass=com.nylas.examples.EventsExample +``` + +Run Java Notetaker example: ```bash ./gradlew :examples:run -PmainClass=com.nylas.examples.NotetakerExample ``` -Run Kotlin example: +Run Kotlin Notetaker example: ```bash ./gradlew :examples:run -PmainClass=com.nylas.examples.KotlinNotetakerExampleKt ``` @@ -53,12 +89,12 @@ List available examples: make list ``` -Run the Java example: +Run the Java Notetaker example: ```bash make java ``` -Run the Kotlin example: +Run the Kotlin Notetaker example: ```bash make kotlin-way ``` @@ -67,9 +103,12 @@ make kotlin-way 1. Open the project in your IDE (IntelliJ IDEA, Eclipse, etc.) 2. Set the required environment variables in your run configuration -3. Run the main method in either: - - `NotetakerExample.java` (Java) - - `KotlinNotetakerExample.kt` (Kotlin) +3. Run the main method in any of the example files: + - `MessagesExample.java` (Java - demonstrates new message features) + - `KotlinMessagesExample.kt` (Kotlin - demonstrates new message features) + - `EventsExample.java` (Java - demonstrates events) + - `NotetakerExample.java` (Java - demonstrates notetakers) + - `KotlinNotetakerExample.kt` (Kotlin - demonstrates notetakers) ## Project Structure @@ -83,12 +122,31 @@ examples/ └── main/ ├── java/ # Java examples │ └── com/nylas/examples/ - │ └── NotetakerExample.java + │ ├── MessagesExample.java # NEW: Message features demo + │ ├── EventsExample.java # Events API demo + │ └── NotetakerExample.java # Notetaker API demo └── kotlin/ # Kotlin examples └── com/nylas/examples/ - └── KotlinNotetakerExample.kt + ├── KotlinMessagesExample.kt # NEW: Message features demo + └── KotlinNotetakerExample.kt # Notetaker API demo ``` +## Message Features Demonstrated + +The Messages examples showcase the following new features added to the Nylas SDK: + +1. **New MessageFields enum values:** + - `MessageFields.INCLUDE_TRACKING_OPTIONS` - Returns tracking options data + - `MessageFields.RAW_MIME` - Returns raw MIME message content + +2. **New Message model properties:** + - `trackingOptions` - Contains opens, thread_replies, links, and label tracking data + - `rawMime` - Contains Base64url-encoded raw message data + +3. **Enhanced Messages API methods:** + - `Messages.find()` now accepts `FindMessageQueryParams` to specify which fields to include + - Both list and find operations support the new field options + ## Additional Information For more information about the Nylas API, refer to the [Nylas API documentation](https://developer.nylas.com/). \ No newline at end of file diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 963cf2e4..efd81f99 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -53,7 +53,9 @@ tasks.register("listExamples") { println("Available examples:") println("- Java-Notetaker: com.nylas.examples.NotetakerExample") println("- Java-Events: com.nylas.examples.EventsExample") + println("- Java-Messages: com.nylas.examples.MessagesExample") println("- Kotlin-Notetaker: com.nylas.examples.KotlinNotetakerExampleKt") + println("- Kotlin-Messages: com.nylas.examples.KotlinMessagesExampleKt") println("\nRun an example with: ./gradlew :examples:run -PmainClass=") } } diff --git a/examples/src/main/java/com/nylas/examples/MessagesExample.java b/examples/src/main/java/com/nylas/examples/MessagesExample.java new file mode 100644 index 00000000..9a350c68 --- /dev/null +++ b/examples/src/main/java/com/nylas/examples/MessagesExample.java @@ -0,0 +1,304 @@ +package com.nylas.examples; + +import com.nylas.NylasClient; +import com.nylas.models.FindMessageQueryParams; +import com.nylas.models.ListMessagesQueryParams; +import com.nylas.models.ListResponse; +import com.nylas.models.Message; +import com.nylas.models.MessageFields; +import com.nylas.models.NylasApiError; +import com.nylas.models.NylasSdkTimeoutError; +import com.nylas.models.Response; +import com.nylas.models.TrackingOptions; +import okhttp3.OkHttpClient; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Example demonstrating the new message features in Nylas Java SDK: + * - MessageFields.INCLUDE_TRACKING_OPTIONS and MessageFields.RAW_MIME + * - tracking_options property in Message model + * - raw_mime property in Message model + * - FindMessageQueryParams with fields parameter + */ +public class MessagesExample { + public static void main(String[] args) { + try { + // Load configuration from environment variables or .env file + Map config = loadConfig(); + + // Initialize the Nylas client with your API key + NylasClient nylas = new NylasClient( + config.get("NYLAS_API_KEY"), + new OkHttpClient.Builder(), + config.getOrDefault("NYLAS_API_URI", "https://api.us.nylas.com") + ); + + // Run the messages example workflow + runMessagesExample(nylas, config); + + // Exit successfully + System.exit(0); + + } catch (NylasApiError e) { + System.out.println("\n❌ Nylas API Error: " + e.getMessage()); + System.out.println(" Status code: " + e.getStatusCode()); + System.out.println(" Request ID: " + e.getRequestId()); + System.exit(1); + } catch (IllegalArgumentException e) { + System.out.println("\n❌ Configuration Error: " + e.getMessage()); + System.exit(1); + } catch (Exception e) { + System.out.println("\n❌ Unexpected Error: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + /** + * Loads configuration from environment variables and .env file + * @throws IllegalArgumentException if required configuration is missing + */ + private static Map loadConfig() { + Map config = new HashMap<>(); + + // Try loading from environment variables first + System.getenv().entrySet().stream() + .filter(entry -> entry.getKey().startsWith("NYLAS_")) + .forEach(entry -> config.put(entry.getKey(), entry.getValue())); + + // Then try loading from .env file if needed + List envPaths = Arrays.asList("examples/.env", ".env"); + for (String path : envPaths) { + File envFile = new File(path); + if (envFile.exists()) { + System.out.println("📝 Loading configuration from " + envFile.getAbsolutePath()); + try { + Files.lines(envFile.toPath()) + .filter(line -> !line.trim().isEmpty() && !line.startsWith("#")) + .forEach(line -> { + String[] parts = line.split("=", 2); + if (parts.length == 2) { + String key = parts[0].trim(); + String value = parts[1].trim(); + if (!config.containsKey(key)) { + config.put(key, value); + } + } + }); + } catch (IOException e) { + System.out.println("Warning: Failed to load .env file: " + e.getMessage()); + } + } + } + + // Validate required configuration + List requiredKeys = Arrays.asList("NYLAS_API_KEY", "NYLAS_GRANT_ID"); + List missingKeys = requiredKeys.stream() + .filter(key -> !config.containsKey(key)) + .collect(Collectors.toList()); + + if (!missingKeys.isEmpty()) { + throw new IllegalArgumentException( + "Missing required configuration: " + String.join(", ", missingKeys) + "\n" + + "Please set these in examples/.env or as environment variables." + ); + } + + return config; + } + + private static void runMessagesExample(NylasClient nylas, Map config) throws NylasApiError, NylasSdkTimeoutError { + String grantId = config.get("NYLAS_GRANT_ID"); + + System.out.println("🔍 Demonstrating Nylas Messages API with new features...\n"); + + // 1. List messages with standard fields (default behavior) + demonstrateStandardMessageListing(nylas, grantId); + + // 2. List messages with include_headers field + demonstrateIncludeHeadersListing(nylas, grantId); + + // 3. List messages with include_tracking_options field (new feature) + demonstrateTrackingOptionsListing(nylas, grantId); + + // 4. List messages with raw_mime field (new feature) + demonstrateRawMimeListing(nylas, grantId); + + // 5. Find a specific message with different field options + demonstrateMessageFinding(nylas, grantId); + } + + private static void demonstrateStandardMessageListing(NylasClient nylas, String grantId) throws NylasApiError, NylasSdkTimeoutError { + System.out.println("📧 1. Listing messages with standard fields:"); + + ListMessagesQueryParams queryParams = new ListMessagesQueryParams.Builder() + .limit(5) + .fields(MessageFields.STANDARD) + .build(); + + ListResponse messages = nylas.messages().list(grantId, queryParams); + + System.out.println(" Found " + messages.getData().size() + " messages"); + for (Message message : messages.getData()) { + System.out.println(" - ID: " + message.getId()); + System.out.println(" Subject: " + (message.getSubject() != null ? message.getSubject() : "No subject")); + System.out.println(" Headers: " + (message.getHeaders() != null ? "Present" : "Not included")); + System.out.println(" Tracking Options: " + (message.getTrackingOptions() != null ? "Present" : "Not included")); + System.out.println(" Raw MIME: " + (message.getRawMime() != null ? "Present" : "Not included")); + System.out.println(); + } + } + + private static void demonstrateIncludeHeadersListing(NylasClient nylas, String grantId) throws NylasApiError, NylasSdkTimeoutError { + System.out.println("📧 2. Listing messages with headers included:"); + + ListMessagesQueryParams queryParams = new ListMessagesQueryParams.Builder() + .limit(3) + .fields(MessageFields.INCLUDE_HEADERS) + .build(); + + ListResponse messages = nylas.messages().list(grantId, queryParams); + + System.out.println(" Found " + messages.getData().size() + " messages"); + for (Message message : messages.getData()) { + System.out.println(" - ID: " + message.getId()); + System.out.println(" Subject: " + (message.getSubject() != null ? message.getSubject() : "No subject")); + System.out.println(" Headers: " + (message.getHeaders() != null ? message.getHeaders().size() + " headers" : "Not included")); + System.out.println(" Tracking Options: " + (message.getTrackingOptions() != null ? "Present" : "Not included")); + System.out.println(" Raw MIME: " + (message.getRawMime() != null ? "Present" : "Not included")); + System.out.println(); + } + } + + private static void demonstrateTrackingOptionsListing(NylasClient nylas, String grantId) throws NylasApiError, NylasSdkTimeoutError { + System.out.println("📊 3. Listing messages with tracking options included (NEW FEATURE):"); + + ListMessagesQueryParams queryParams = new ListMessagesQueryParams.Builder() + .limit(3) + .fields(MessageFields.INCLUDE_TRACKING_OPTIONS) + .build(); + + ListResponse messages = nylas.messages().list(grantId, queryParams); + + System.out.println(" Found " + messages.getData().size() + " messages"); + for (Message message : messages.getData()) { + System.out.println(" - ID: " + message.getId()); + System.out.println(" Subject: " + (message.getSubject() != null ? message.getSubject() : "No subject")); + System.out.println(" Headers: " + (message.getHeaders() != null ? "Present" : "Not included")); + System.out.println(" Raw MIME: " + (message.getRawMime() != null ? "Present" : "Not included")); + + if (message.getTrackingOptions() != null) { + TrackingOptions tracking = message.getTrackingOptions(); + System.out.println(" ✅ Tracking Options:"); + System.out.println(" - Opens: " + tracking.getOpens()); + System.out.println(" - Thread Replies: " + tracking.getThreadReplies()); + System.out.println(" - Links: " + tracking.getLinks()); + System.out.println(" - Label: " + tracking.getLabel()); + } else { + System.out.println(" Tracking Options: Not available"); + } + System.out.println(); + } + } + + private static void demonstrateRawMimeListing(NylasClient nylas, String grantId) throws NylasApiError, NylasSdkTimeoutError { + System.out.println("📄 4. Listing messages with raw MIME included (NEW FEATURE):"); + + ListMessagesQueryParams queryParams = new ListMessagesQueryParams.Builder() + .limit(2) + .fields(MessageFields.RAW_MIME) + .build(); + + ListResponse messages = nylas.messages().list(grantId, queryParams); + + System.out.println(" Found " + messages.getData().size() + " messages"); + for (Message message : messages.getData()) { + System.out.println(" - ID: " + message.getId()); + System.out.println(" Subject: " + (message.getSubject() != null ? message.getSubject() : "No subject")); + System.out.println(" Headers: " + (message.getHeaders() != null ? "Present" : "Not included")); + System.out.println(" Tracking Options: " + (message.getTrackingOptions() != null ? "Present" : "Not included")); + + if (message.getRawMime() != null) { + String rawMime = message.getRawMime(); + System.out.println(" ✅ Raw MIME: Present (" + rawMime.length() + " characters)"); + // Show first 100 characters as preview + if (rawMime.length() > 100) { + System.out.println(" Preview: " + rawMime.substring(0, 100) + "..."); + } else { + System.out.println(" Content: " + rawMime); + } + } else { + System.out.println(" Raw MIME: Not available"); + } + System.out.println(); + } + } + + private static void demonstrateMessageFinding(NylasClient nylas, String grantId) throws NylasApiError, NylasSdkTimeoutError { + System.out.println("🔍 5. Finding specific messages with query parameters (NEW FEATURE):"); + + // First get a message ID to work with + ListMessagesQueryParams listParams = new ListMessagesQueryParams.Builder() + .limit(1) + .build(); + ListResponse messages = nylas.messages().list(grantId, listParams); + + if (messages.getData().isEmpty()) { + System.out.println(" No messages found to demonstrate with."); + return; + } + + String messageId = messages.getData().get(0).getId(); + System.out.println(" Using message ID: " + messageId); + + // Find message with standard fields + System.out.println("\n 📧 Finding with standard fields:"); + Response standardMessage = nylas.messages().find(grantId, messageId); + printMessageDetails(standardMessage.getData(), "standard"); + + // Find message with tracking options using new FindMessageQueryParams + System.out.println("\n 📊 Finding with tracking options (using new FindMessageQueryParams):"); + FindMessageQueryParams trackingParams = new FindMessageQueryParams.Builder() + .fields(MessageFields.INCLUDE_TRACKING_OPTIONS) + .build(); + Response trackingMessage = nylas.messages().find(grantId, messageId, trackingParams); + printMessageDetails(trackingMessage.getData(), "tracking options"); + + // Find message with raw MIME using new FindMessageQueryParams + System.out.println("\n 📄 Finding with raw MIME (using new FindMessageQueryParams):"); + FindMessageQueryParams rawMimeParams = new FindMessageQueryParams.Builder() + .fields(MessageFields.RAW_MIME) + .build(); + Response rawMimeMessage = nylas.messages().find(grantId, messageId, rawMimeParams); + printMessageDetails(rawMimeMessage.getData(), "raw MIME"); + } + + private static void printMessageDetails(Message message, String requestType) { + System.out.println(" Request type: " + requestType); + System.out.println(" ID: " + message.getId()); + System.out.println(" Subject: " + (message.getSubject() != null ? message.getSubject() : "No subject")); + System.out.println(" Headers included: " + (message.getHeaders() != null)); + System.out.println(" Tracking options included: " + (message.getTrackingOptions() != null)); + System.out.println(" Raw MIME included: " + (message.getRawMime() != null)); + + if (message.getTrackingOptions() != null) { + TrackingOptions tracking = message.getTrackingOptions(); + System.out.println(" Tracking details: opens=" + tracking.getOpens() + + ", replies=" + tracking.getThreadReplies() + + ", links=" + tracking.getLinks() + + ", label=" + tracking.getLabel()); + } + + if (message.getRawMime() != null) { + System.out.println(" Raw MIME length: " + message.getRawMime().length() + " characters"); + } + } +} \ 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 new file mode 100644 index 00000000..e3a82c5e --- /dev/null +++ b/examples/src/main/kotlin/com/nylas/examples/KotlinMessagesExample.kt @@ -0,0 +1,280 @@ +package com.nylas.examples + +import com.nylas.NylasClient +import com.nylas.models.* +import okhttp3.OkHttpClient +import java.io.File +import java.io.IOException +import java.nio.file.Files +import kotlin.system.exitProcess + +/** + * Example demonstrating the new message features in Nylas Java/Kotlin SDK: + * - MessageFields.INCLUDE_TRACKING_OPTIONS and MessageFields.RAW_MIME + * - tracking_options property in Message model + * - raw_mime property in Message model + * - FindMessageQueryParams with fields parameter + */ +fun main() { + try { + // Load configuration from environment variables or .env file + val config = loadConfig() + + // Initialize the Nylas client with your API key + val nylas = NylasClient( + config["NYLAS_API_KEY"]!!, + OkHttpClient.Builder(), + config.getOrDefault("NYLAS_API_URI", "https://api.us.nylas.com") + ) + + // Run the messages example workflow + runMessagesExample(nylas, config) + + // Exit successfully + exitProcess(0) + + } catch (e: NylasApiError) { + println("\n❌ Nylas API Error: ${e.message}") + println(" Status code: ${e.statusCode}") + println(" Request ID: ${e.requestId}") + exitProcess(1) + } catch (e: IllegalArgumentException) { + println("\n❌ Configuration Error: ${e.message}") + exitProcess(1) + } catch (e: Exception) { + println("\n❌ Unexpected Error: ${e.message}") + e.printStackTrace() + exitProcess(1) + } +} + +/** + * Loads configuration from environment variables and .env file + * @throws IllegalArgumentException if required configuration is missing + */ +private fun loadConfig(): Map { + val config = mutableMapOf() + + // Try loading from environment variables first + System.getenv().filter { it.key.startsWith("NYLAS_") } + .forEach { config[it.key] = it.value } + + // Then try loading from .env file if needed + val envPaths = listOf("examples/.env", ".env") + for (path in envPaths) { + val envFile = File(path) + if (envFile.exists()) { + println("📝 Loading configuration from ${envFile.absolutePath}") + try { + Files.lines(envFile.toPath()) + .filter { it.trim().isNotEmpty() && !it.startsWith("#") } + .forEach { line -> + val parts = line.split("=", limit = 2) + if (parts.size == 2) { + val key = parts[0].trim() + val value = parts[1].trim() + if (key !in config) { + config[key] = value + } + } + } + } catch (e: IOException) { + println("Warning: Failed to load .env file: ${e.message}") + } + } + } + + // Validate required configuration + val requiredKeys = listOf("NYLAS_API_KEY", "NYLAS_GRANT_ID") + val missingKeys = requiredKeys.filter { it !in config } + + if (missingKeys.isNotEmpty()) { + throw IllegalArgumentException( + "Missing required configuration: ${missingKeys.joinToString(", ")}\n" + + "Please set these in examples/.env or as environment variables." + ) + } + + return config +} + +private fun runMessagesExample(nylas: NylasClient, config: Map) { + val grantId = config["NYLAS_GRANT_ID"]!! + + println("🔍 Demonstrating Nylas Messages API with new features...\n") + + // 1. List messages with standard fields (default behavior) + demonstrateStandardMessageListing(nylas, grantId) + + // 2. List messages with include_headers field + demonstrateIncludeHeadersListing(nylas, grantId) + + // 3. List messages with include_tracking_options field (new feature) + demonstrateTrackingOptionsListing(nylas, grantId) + + // 4. List messages with raw_mime field (new feature) + demonstrateRawMimeListing(nylas, grantId) + + // 5. Find a specific message with different field options + demonstrateMessageFinding(nylas, grantId) +} + +private fun demonstrateStandardMessageListing(nylas: NylasClient, grantId: String) { + println("📧 1. Listing messages with standard fields:") + + val queryParams = ListMessagesQueryParams.Builder() + .limit(5) + .fields(MessageFields.STANDARD) + .build() + + val messages = nylas.messages().list(grantId, queryParams) + + println(" Found ${messages.data.size} messages") + messages.data.forEach { message -> + println(" - ID: ${message.id}") + println(" Subject: ${message.subject ?: "No subject"}") + println(" Headers: ${if (message.headers != null) "Present" else "Not included"}") + println(" Tracking Options: ${if (message.trackingOptions != null) "Present" else "Not included"}") + println(" Raw MIME: ${if (message.rawMime != null) "Present" else "Not included"}") + println() + } +} + +private fun demonstrateIncludeHeadersListing(nylas: NylasClient, grantId: String) { + println("📧 2. Listing messages with headers included:") + + val queryParams = ListMessagesQueryParams.Builder() + .limit(3) + .fields(MessageFields.INCLUDE_HEADERS) + .build() + + val messages = nylas.messages().list(grantId, queryParams) + + println(" Found ${messages.data.size} messages") + messages.data.forEach { message -> + println(" - ID: ${message.id}") + println(" Subject: ${message.subject ?: "No subject"}") + val headers = message.headers + println(" Headers: ${if (headers != null) "${headers.size} headers" else "Not included"}") + println(" Tracking Options: ${if (message.trackingOptions != null) "Present" else "Not included"}") + println(" Raw MIME: ${if (message.rawMime != null) "Present" else "Not included"}") + println() + } +} + +private fun demonstrateTrackingOptionsListing(nylas: NylasClient, grantId: String) { + println("📊 3. Listing messages with tracking options included (NEW FEATURE):") + + val queryParams = ListMessagesQueryParams.Builder() + .limit(3) + .fields(MessageFields.INCLUDE_TRACKING_OPTIONS) + .build() + + val messages = nylas.messages().list(grantId, queryParams) + + println(" Found ${messages.data.size} messages") + messages.data.forEach { message -> + println(" - ID: ${message.id}") + println(" Subject: ${message.subject ?: "No subject"}") + println(" Headers: ${if (message.headers != null) "Present" else "Not included"}") + println(" Raw MIME: ${if (message.rawMime != null) "Present" else "Not included"}") + + message.trackingOptions?.let { tracking -> + println(" ✅ Tracking Options:") + println(" - Opens: ${tracking.opens}") + println(" - Thread Replies: ${tracking.threadReplies}") + println(" - Links: ${tracking.links}") + println(" - Label: ${tracking.label}") + } ?: println(" Tracking Options: Not available") + println() + } +} + +private fun demonstrateRawMimeListing(nylas: NylasClient, grantId: String) { + println("📄 4. Listing messages with raw MIME included (NEW FEATURE):") + + val queryParams = ListMessagesQueryParams.Builder() + .limit(2) + .fields(MessageFields.RAW_MIME) + .build() + + val messages = nylas.messages().list(grantId, queryParams) + + println(" Found ${messages.data.size} messages") + messages.data.forEach { message -> + println(" - ID: ${message.id}") + println(" Subject: ${message.subject ?: "No subject"}") + println(" Headers: ${if (message.headers != null) "Present" else "Not included"}") + println(" Tracking Options: ${if (message.trackingOptions != null) "Present" else "Not included"}") + + message.rawMime?.let { rawMime -> + println(" ✅ Raw MIME: Present (${rawMime.length} characters)") + // Show first 100 characters as preview + if (rawMime.length > 100) { + println(" Preview: ${rawMime.substring(0, 100)}...") + } else { + println(" Content: $rawMime") + } + } ?: println(" Raw MIME: Not available") + println() + } +} + +private fun demonstrateMessageFinding(nylas: NylasClient, grantId: String) { + println("🔍 5. Finding specific messages with query parameters (NEW FEATURE):") + + // First get a message ID to work with + val listParams = ListMessagesQueryParams.Builder() + .limit(1) + .build() + val messages = nylas.messages().list(grantId, listParams) + + if (messages.data.isEmpty()) { + println(" No messages found to demonstrate with.") + return + } + + val messageId = messages.data[0].id!! + println(" Using message ID: $messageId") + + // Find message with standard fields + println("\n 📧 Finding with standard fields:") + val standardMessage = nylas.messages().find(grantId, messageId) + printMessageDetails(standardMessage.data, "standard") + + // Find message with tracking options using new FindMessageQueryParams + println("\n 📊 Finding with tracking options (using new FindMessageQueryParams):") + val trackingParams = FindMessageQueryParams.Builder() + .fields(MessageFields.INCLUDE_TRACKING_OPTIONS) + .build() + val trackingMessage = nylas.messages().find(grantId, messageId, trackingParams) + printMessageDetails(trackingMessage.data, "tracking options") + + // Find message with raw MIME using new FindMessageQueryParams + println("\n 📄 Finding with raw MIME (using new FindMessageQueryParams):") + val rawMimeParams = FindMessageQueryParams.Builder() + .fields(MessageFields.RAW_MIME) + .build() + val rawMimeMessage = nylas.messages().find(grantId, messageId, rawMimeParams) + printMessageDetails(rawMimeMessage.data, "raw MIME") +} + +private fun printMessageDetails(message: Message, requestType: String) { + println(" Request type: $requestType") + println(" ID: ${message.id}") + println(" Subject: ${message.subject ?: "No subject"}") + println(" Headers included: ${message.headers != null}") + println(" Tracking options included: ${message.trackingOptions != null}") + println(" Raw MIME included: ${message.rawMime != null}") + + message.trackingOptions?.let { tracking -> + println(" Tracking details: opens=${tracking.opens}, " + + "replies=${tracking.threadReplies}, " + + "links=${tracking.links}, " + + "label=${tracking.label}") + } + + message.rawMime?.let { rawMime -> + println(" Raw MIME length: ${rawMime.length} characters") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/nylas/models/FindMessageQueryParams.kt b/src/main/kotlin/com/nylas/models/FindMessageQueryParams.kt index 5b1e0e0d..a2e5936e 100644 --- a/src/main/kotlin/com/nylas/models/FindMessageQueryParams.kt +++ b/src/main/kotlin/com/nylas/models/FindMessageQueryParams.kt @@ -11,4 +11,26 @@ data class FindMessageQueryParams( */ @Json(name = "fields") val fields: MessageFields? = null, -) +) : IQueryParams { + /** + * Builder for [FindMessageQueryParams]. + */ + class Builder { + private var fields: MessageFields? = null + + /** + * Set the fields to include in the response. + * @param fields The fields to include in the response. + * @return The builder. + */ + fun fields(fields: MessageFields?) = apply { this.fields = fields } + + /** + * Builds the [FindMessageQueryParams] object. + * @return The [FindMessageQueryParams] object. + */ + fun build() = FindMessageQueryParams( + fields = fields, + ) + } +} diff --git a/src/main/kotlin/com/nylas/models/Message.kt b/src/main/kotlin/com/nylas/models/Message.kt index d7fb3faf..cf794a48 100644 --- a/src/main/kotlin/com/nylas/models/Message.kt +++ b/src/main/kotlin/com/nylas/models/Message.kt @@ -124,6 +124,18 @@ data class Message( */ @Json(name = "send_at") val sendAt: Long? = null, + /** + * Options for tracking opens, links, and thread replies. + * Only present if the 'fields' query parameter is set to include_tracking_options. + */ + @Json(name = "tracking_options") + val trackingOptions: TrackingOptions? = null, + /** + * A Base64url-encoded string containing the message data (including the body content). + * Only present if the 'fields' query parameter is set to raw_mime. + */ + @Json(name = "raw_mime") + val rawMime: String? = null, ) : IMessage { /** * Get the type of object. diff --git a/src/main/kotlin/com/nylas/models/MessageFields.kt b/src/main/kotlin/com/nylas/models/MessageFields.kt index 741ca17d..eefeb076 100644 --- a/src/main/kotlin/com/nylas/models/MessageFields.kt +++ b/src/main/kotlin/com/nylas/models/MessageFields.kt @@ -8,4 +8,10 @@ enum class MessageFields { @Json(name = "include_headers") INCLUDE_HEADERS, + + @Json(name = "include_tracking_options") + INCLUDE_TRACKING_OPTIONS, + + @Json(name = "raw_mime") + RAW_MIME, } diff --git a/src/main/kotlin/com/nylas/resources/Messages.kt b/src/main/kotlin/com/nylas/resources/Messages.kt index cfe9a742..0632490b 100644 --- a/src/main/kotlin/com/nylas/resources/Messages.kt +++ b/src/main/kotlin/com/nylas/resources/Messages.kt @@ -33,14 +33,15 @@ class Messages(client: NylasClient) : Resource(client, Message::class.j * Return a Message * @param identifier The identifier of the grant to act upon * @param messageId The id of the Message to retrieve. + * @param queryParams The query parameters to include in the request * @param overrides Optional request overrides to apply * @return The Message */ @Throws(NylasApiError::class, NylasSdkTimeoutError::class) @JvmOverloads - fun find(identifier: String, messageId: String, overrides: RequestOverrides? = null): Response { + fun find(identifier: String, messageId: String, queryParams: FindMessageQueryParams? = null, overrides: RequestOverrides? = null): Response { val path = String.format("v3/grants/%s/messages/%s", identifier, messageId) - return findResource(path, overrides = overrides) + return findResource(path, queryParams, overrides = overrides) } /** diff --git a/src/test/kotlin/com/nylas/models/FindMessageQueryParamsTests.kt b/src/test/kotlin/com/nylas/models/FindMessageQueryParamsTests.kt new file mode 100644 index 00000000..a9b66b70 --- /dev/null +++ b/src/test/kotlin/com/nylas/models/FindMessageQueryParamsTests.kt @@ -0,0 +1,31 @@ +package com.nylas.models + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class FindMessageQueryParamsTests { + @Test + fun `FindMessageQueryParams with fields builds correctly`() { + val queryParams = FindMessageQueryParams(fields = MessageFields.INCLUDE_TRACKING_OPTIONS) + + assertEquals(MessageFields.INCLUDE_TRACKING_OPTIONS, queryParams.fields) + } + + @Test + fun `FindMessageQueryParams builder pattern works correctly`() { + val queryParams = FindMessageQueryParams.Builder() + .fields(MessageFields.RAW_MIME) + .build() + + assertEquals(MessageFields.RAW_MIME, queryParams.fields) + } + + @Test + fun `FindMessageQueryParams builder with null fields builds correctly`() { + val queryParams = FindMessageQueryParams.Builder() + .fields(null) + .build() + + assertEquals(null, queryParams.fields) + } +} diff --git a/src/test/kotlin/com/nylas/models/MessageFieldsTests.kt b/src/test/kotlin/com/nylas/models/MessageFieldsTests.kt new file mode 100644 index 00000000..b1b6ff2c --- /dev/null +++ b/src/test/kotlin/com/nylas/models/MessageFieldsTests.kt @@ -0,0 +1,27 @@ +package com.nylas.models + +import com.nylas.util.JsonHelper +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class MessageFieldsTests { + @Test + fun `MessageFields enum values serialize correctly`() { + val adapter = JsonHelper.moshi().adapter(MessageFields::class.java) + + assertEquals("\"standard\"", adapter.toJson(MessageFields.STANDARD)) + assertEquals("\"include_headers\"", adapter.toJson(MessageFields.INCLUDE_HEADERS)) + assertEquals("\"include_tracking_options\"", adapter.toJson(MessageFields.INCLUDE_TRACKING_OPTIONS)) + assertEquals("\"raw_mime\"", adapter.toJson(MessageFields.RAW_MIME)) + } + + @Test + fun `MessageFields enum values deserialize correctly`() { + val adapter = JsonHelper.moshi().adapter(MessageFields::class.java) + + assertEquals(MessageFields.STANDARD, adapter.fromJson("\"standard\"")) + assertEquals(MessageFields.INCLUDE_HEADERS, adapter.fromJson("\"include_headers\"")) + assertEquals(MessageFields.INCLUDE_TRACKING_OPTIONS, adapter.fromJson("\"include_tracking_options\"")) + assertEquals(MessageFields.RAW_MIME, adapter.fromJson("\"raw_mime\"")) + } +} diff --git a/src/test/kotlin/com/nylas/resources/MessagesTests.kt b/src/test/kotlin/com/nylas/resources/MessagesTests.kt index 0d888b81..0bad6ae6 100644 --- a/src/test/kotlin/com/nylas/resources/MessagesTests.kt +++ b/src/test/kotlin/com/nylas/resources/MessagesTests.kt @@ -127,6 +127,60 @@ class MessagesTests { assertEquals("j.snow@example.com", message.to?.get(0)?.email) assertEquals(true, message.unread) } + + @Test + fun `Message with tracking_options serializes properly`() { + val adapter = JsonHelper.moshi().adapter(Message::class.java) + val jsonBuffer = + Buffer().writeUtf8( + """ + { + "grant_id": "41009df5-bf11-4c97-aa18-b285b5f2e386", + "id": "5d3qmne77v32r8l4phyuksl2x", + "object": "message", + "subject": "Hello from Nylas!", + "tracking_options": { + "opens": true, + "thread_replies": false, + "links": true, + "label": "test-campaign" + } + } + """.trimIndent(), + ) + + val message = adapter.fromJson(jsonBuffer)!! + assertIs(message) + assertEquals("41009df5-bf11-4c97-aa18-b285b5f2e386", message.grantId) + assertEquals("5d3qmne77v32r8l4phyuksl2x", message.id) + assertEquals("Hello from Nylas!", message.subject) + assertEquals(true, message.trackingOptions?.opens) + assertEquals(false, message.trackingOptions?.threadReplies) + assertEquals(true, message.trackingOptions?.links) + assertEquals("test-campaign", message.trackingOptions?.label) + } + + @Test + fun `Message with raw_mime serializes properly`() { + val adapter = JsonHelper.moshi().adapter(Message::class.java) + val jsonBuffer = + Buffer().writeUtf8( + """ + { + "grant_id": "41009df5-bf11-4c97-aa18-b285b5f2e386", + "id": "5d3qmne77v32r8l4phyuksl2x", + "object": "message", + "raw_mime": "UmVjZWl2ZWQ6IGJ5IDEwLjEwLjEwLjEwOyBXZWQsIDEwIEp1bCAyMDI0IDE1OjM2OjEwICswMDAwDQpGcm9tOiB0ZXN0QG55bGFzLmNvbQ0KVG86IHJlY2VpdmVyQGV4YW1wbGUuY29tDQpTdWJqZWN0OiBUZXN0IEVtYWlsDQpEYXRlOiBXZWQsIDEwIEp1bCAyMDI0IDE1OjM2OjEwICswMDAwDQoNCkhlbGxvLCB0aGlzIGlzIGEgdGVzdCBlbWFpbC4=" + } + """.trimIndent(), + ) + + val message = adapter.fromJson(jsonBuffer)!! + assertIs(message) + assertEquals("41009df5-bf11-4c97-aa18-b285b5f2e386", message.grantId) + assertEquals("5d3qmne77v32r8l4phyuksl2x", message.id) + assertEquals("UmVjZWl2ZWQ6IGJ5IDEwLjEwLjEwLjEwOyBXZWQsIDEwIEp1bCAyMDI0IDE1OjM2OjEwICswMDAwDQpGcm9tOiB0ZXN0QG55bGFzLmNvbQ0KVG86IHJlY2VpdmVyQGV4YW1wbGUuY29tDQpTdWJqZWN0OiBUZXN0IEVtYWlsDQpEYXRlOiBXZWQsIDEwIEp1bCAyMDI0IDE1OjM2OjEwICswMDAwDQoNCkhlbGxvLCB0aGlzIGlzIGEgdGVzdCBlbWFpbC4=", message.rawMime) + } } @Nested @@ -182,6 +236,56 @@ class MessagesTests { assertEquals(queryParams, queryParamCaptor.firstValue) } + @Test + fun `listing messages with include_tracking_options field calls requests with the correct params`() { + val queryParams = + ListMessagesQueryParams( + fields = MessageFields.INCLUDE_TRACKING_OPTIONS, + ) + + messages.list(grantId, queryParams) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + val overrideParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGet>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture(), + overrideParamCaptor.capture(), + ) + + assertEquals("v3/grants/$grantId/messages", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(ListResponse::class.java, Message::class.java), typeCaptor.firstValue) + assertEquals(queryParams, queryParamCaptor.firstValue) + } + + @Test + fun `listing messages with raw_mime field calls requests with the correct params`() { + val queryParams = + ListMessagesQueryParams( + fields = MessageFields.RAW_MIME, + ) + + messages.list(grantId, queryParams) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + val overrideParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGet>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture(), + overrideParamCaptor.capture(), + ) + + assertEquals("v3/grants/$grantId/messages", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(ListResponse::class.java, Message::class.java), typeCaptor.firstValue) + assertEquals(queryParams, queryParamCaptor.firstValue) + } + @Test fun `listing messages without query params calls requests with the correct params`() { messages.list(grantId) @@ -224,6 +328,52 @@ class MessagesTests { assertNull(queryParamCaptor.firstValue) } + @Test + fun `finding a message with query params calls requests with the correct params`() { + val messageId = "message-123" + val queryParams = FindMessageQueryParams(fields = MessageFields.INCLUDE_TRACKING_OPTIONS) + + messages.find(grantId, messageId, queryParams) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + val overrideParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGet>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture(), + overrideParamCaptor.capture(), + ) + + assertEquals("v3/grants/$grantId/messages/$messageId", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Message::class.java), typeCaptor.firstValue) + assertEquals(queryParams, queryParamCaptor.firstValue) + } + + @Test + fun `finding a message with raw_mime field calls requests with the correct params`() { + val messageId = "message-123" + val queryParams = FindMessageQueryParams(fields = MessageFields.RAW_MIME) + + messages.find(grantId, messageId, queryParams) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + val overrideParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGet>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture(), + overrideParamCaptor.capture(), + ) + + assertEquals("v3/grants/$grantId/messages/$messageId", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Message::class.java), typeCaptor.firstValue) + assertEquals(queryParams, queryParamCaptor.firstValue) + } + @Test fun `updating a message calls requests with the correct params`() { val messageId = "message-123"