Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Nylas Java SDK Changelog

## Unreleased

### Added
* Optional `TrackingOptions.domainName` support for custom link and open tracking hostnames in regular sends, Transactional Send, drafts, and scheduled sends. The field serializes as `tracking_options.domain_name` and is omitted when unset.

## [v2.18.0] - Release 2026-07-10

### Added
Expand Down
10 changes: 9 additions & 1 deletion examples/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ NYLAS_API_URI=https://api.us.nylas.com
NYLAS_GRANT_ID=your_grant_id_here

# Test email
NYLAS_TEST_EMAIL=your@email.com
NYLAS_TEST_EMAIL=your@email.com

# Custom tracking hostname example
RECIPIENT_EMAIL=recipient@example.com
NYLAS_TRACKING_HOSTNAME=tracking.example.com

# Transactional Send operation only: the verified sender domain is separate from the tracking hostname
NYLAS_TRANSACTIONAL_SENDER_DOMAIN=sender.example.com
SENDER_EMAIL=support@sender.example.com
28 changes: 27 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ The `LargeAttachmentsExample` demonstrates how to send emails with large file at
- Use the `FileUtils.attachFileRequestBuilder()` helper method for easy file attachment
- Automatic cleanup of temporary test files

### Custom Tracking Hostname Example

`CustomTrackingDomainExample` demonstrates the optional `TrackingOptions.domainName` field for:

- regular grant-based sends;
- draft creation;
- scheduled grant-based sends; and
- Transactional Send.

The hostname must be active and owned by the authenticated organization. Enable link tracking, open tracking, or both when setting it. Omit `domainName` to keep using the default Nylas tracking hostname.

For Transactional Send, the values are intentionally distinct: `NYLAS_TRANSACTIONAL_SENDER_DOMAIN` is the verified sender domain used in `/v3/domains/{domain_name}/messages/send`, while `NYLAS_TRACKING_HOSTNAME` becomes `tracking_options.domain_name` in the request body.

## Setup

### 1. Environment Setup
Expand All @@ -72,6 +85,10 @@ NYLAS_GRANT_ID=your_grant_id_here
# Test email address (required for large attachments example)
NYLAS_TEST_EMAIL=test@example.com

# An active custom hostname owned by your organization (custom tracking example)
RECIPIENT_EMAIL=recipient@example.com
NYLAS_TRACKING_HOSTNAME=tracking.example.com

# Add your meeting link (Zoom, Google Meet, or Microsoft Teams) - for Notetaker example
MEETING_LINK=your_meeting_link_here
```
Expand Down Expand Up @@ -120,6 +137,14 @@ Run Java Large Attachments example:
./gradlew :examples:run -PmainClass=com.nylas.examples.LargeAttachmentsExample
```

Run the custom tracking hostname example (defaults to a regular send):
```bash
NYLAS_CUSTOM_TRACKING_OPERATION=regular \
./gradlew :examples:run -PmainClass=com.nylas.examples.CustomTrackingDomainExample
```

Choose `draft`, `scheduled`, or `transactional` for the other operations. Scheduled sends also require `NYLAS_SEND_AT` as a Unix timestamp. Transactional Send requires `NYLAS_TRANSACTIONAL_SENDER_DOMAIN` and `SENDER_EMAIL`; the non-transactional operations require `NYLAS_GRANT_ID`.

#### Option 2: Using the Makefile

List available examples:
Expand Down Expand Up @@ -147,6 +172,7 @@ make kotlin-way
- `EventsExample.java` (Java - demonstrates events)
- `FoldersExample.java` (Java - demonstrates folders and single_level parameter)
- `LargeAttachmentsExample.java` (Java - demonstrates large file attachments)
- `CustomTrackingDomainExample.java` (Java - demonstrates custom link and open tracking hostnames)
- `NotetakerExample.java` (Java - demonstrates notetakers)
- `KotlinNotetakerExample.kt` (Kotlin - demonstrates notetakers)
- `KotlinFoldersExample.kt` (Kotlin - demonstrates folders and single_level parameter)
Expand Down Expand Up @@ -193,4 +219,4 @@ The Messages examples showcase the following new features added to the Nylas SDK

## Additional Information

For more information about the Nylas API, refer to the [Nylas API documentation](https://developer.nylas.com/).
For more information about the Nylas API, refer to the [Nylas API documentation](https://developer.nylas.com/).
3 changes: 2 additions & 1 deletion examples/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ tasks.register("listExamples") {
println("- Java-Messages: com.nylas.examples.MessagesExample")
println("- Java-Folders: com.nylas.examples.FoldersExample")
println("- Java-Large-Attachments: com.nylas.examples.LargeAttachmentsExample")
println("- Java-Custom-Tracking-Domain: com.nylas.examples.CustomTrackingDomainExample")
println("- Kotlin-Notetaker: com.nylas.examples.KotlinNotetakerExampleKt")
println("- Kotlin-Messages: com.nylas.examples.KotlinMessagesExampleKt")
println("- Kotlin-Folders: com.nylas.examples.KotlinFoldersExampleKt")
Expand All @@ -73,4 +74,4 @@ sourceSets {
srcDir("src/main/kotlin")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package com.nylas.examples;

import com.nylas.NylasClient;
import com.nylas.models.CreateDraftRequest;
import com.nylas.models.Draft;
import com.nylas.models.EmailName;
import com.nylas.models.Message;
import com.nylas.models.NylasApiError;
import com.nylas.models.NylasSdkTimeoutError;
import com.nylas.models.Response;
import com.nylas.models.SendMessageRequest;
import com.nylas.models.SendTransactionalEmailRequest;
import com.nylas.models.TrackingOptions;
import okhttp3.OkHttpClient;

import java.util.Collections;

/**
* Demonstrates custom tracking hostnames for regular, draft, scheduled, and Transactional Send requests.
*/
public class CustomTrackingDomainExample {
private static final String LINK_BODY = "<a href=\"https://example.com\">Open example</a>";

public static void main(String[] args) throws NylasApiError, NylasSdkTimeoutError {
String operation = System.getenv().getOrDefault("NYLAS_CUSTOM_TRACKING_OPERATION", "regular");
String apiKey = requireEnvironmentVariable("NYLAS_API_KEY");
String recipientEmail = requireEnvironmentVariable("RECIPIENT_EMAIL");
String trackingHostname = requireEnvironmentVariable("NYLAS_TRACKING_HOSTNAME");
String apiUri = System.getenv().getOrDefault("NYLAS_API_URI", "https://api.us.nylas.com");

NylasClient nylas = new NylasClient(apiKey, new OkHttpClient.Builder(), apiUri);

switch (operation) {
case "regular":
sendRegularMessage(
nylas,
requireEnvironmentVariable("NYLAS_GRANT_ID"),
recipientEmail,
trackingHostname);
break;
case "draft":
createTrackedDraft(
nylas,
requireEnvironmentVariable("NYLAS_GRANT_ID"),
recipientEmail,
trackingHostname);
break;
case "scheduled":
scheduleTrackedMessage(
nylas,
requireEnvironmentVariable("NYLAS_GRANT_ID"),
recipientEmail,
trackingHostname,
requireSendAt());
break;
case "transactional":
sendTransactionalMessage(
nylas,
requireEnvironmentVariable("NYLAS_TRANSACTIONAL_SENDER_DOMAIN"),
requireEnvironmentVariable("SENDER_EMAIL"),
recipientEmail,
trackingHostname);
break;
default:
throw new IllegalArgumentException(
"NYLAS_CUSTOM_TRACKING_OPERATION must be regular, draft, scheduled, or transactional");
}
}

private static TrackingOptions buildTrackingOptions(String trackingHostname) {
return new TrackingOptions.Builder()
.links(true)
.opens(true)
.domainName(trackingHostname)
.build();
}

private static void sendRegularMessage(
NylasClient nylas, String grantId, String recipientEmail, String trackingHostname)
throws NylasApiError, NylasSdkTimeoutError {
SendMessageRequest request = new SendMessageRequest.Builder(
Collections.singletonList(new EmailName(recipientEmail, "Recipient")))
.subject("Tracked update")
.body(LINK_BODY)
.trackingOptions(buildTrackingOptions(trackingHostname))
.build();

Response<Message> response = nylas.messages().send(grantId, request);
System.out.println("Sent message: " + response.getData().getId());
}

private static void createTrackedDraft(
NylasClient nylas, String grantId, String recipientEmail, String trackingHostname)
throws NylasApiError, NylasSdkTimeoutError {
CreateDraftRequest request = new CreateDraftRequest.Builder()
.to(Collections.singletonList(new EmailName(recipientEmail, "Recipient")))
.subject("Tracked draft")
.body(LINK_BODY)
.trackingOptions(buildTrackingOptions(trackingHostname))
.build();

Response<Draft> response = nylas.drafts().create(grantId, request);
System.out.println("Created draft: " + response.getData().getId());
}

private static void scheduleTrackedMessage(
NylasClient nylas,
String grantId,
String recipientEmail,
String trackingHostname,
long sendAt)
throws NylasApiError, NylasSdkTimeoutError {
SendMessageRequest request = new SendMessageRequest.Builder(
Collections.singletonList(new EmailName(recipientEmail, "Recipient")))
.subject("Scheduled tracked update")
.body(LINK_BODY)
.sendAt(sendAt)
.trackingOptions(buildTrackingOptions(trackingHostname))
.build();

Response<Message> response = nylas.messages().send(grantId, request);
System.out.println("Scheduled message: " + response.getData().getScheduleId());
}

private static void sendTransactionalMessage(
NylasClient nylas,
String senderDomain,
String senderEmail,
String recipientEmail,
String trackingHostname)
throws NylasApiError, NylasSdkTimeoutError {
SendTransactionalEmailRequest request = new SendTransactionalEmailRequest.Builder(
Collections.singletonList(new EmailName(recipientEmail, "Recipient")),
new EmailName(senderEmail, "Sender"))
.subject("Transactional tracked update")
.body(LINK_BODY)
.trackingOptions(buildTrackingOptions(trackingHostname))
.build();

// The route value is the verified sender domain. The nested domainName is the tracking hostname.
Response<Message> response = nylas.domains().sendTransactionalEmail(senderDomain, request);
System.out.println("Sent transactional message: " + response.getData().getId());
}

private static String requireEnvironmentVariable(String name) {
String value = System.getenv(name);
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(name + " environment variable is required");
}
return value;
}

private static long requireSendAt() {
String value = requireEnvironmentVariable("NYLAS_SEND_AT");
try {
long sendAt = Long.parseLong(value);
if (sendAt <= 0) {
throw new IllegalArgumentException("NYLAS_SEND_AT must be a positive Unix timestamp");
}
return sendAt;
} catch (NumberFormatException error) {
throw new IllegalArgumentException("NYLAS_SEND_AT must be a Unix timestamp", error);
}
}
}
62 changes: 60 additions & 2 deletions src/main/kotlin/com/nylas/models/TrackingOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.squareup.moshi.Json
/**
* Class representing the different tracking options for when a message is sent.
*/
data class TrackingOptions(
data class TrackingOptions @JvmOverloads constructor(
/**
* The label to apply to tracked messages.
*/
Expand All @@ -26,4 +26,62 @@ data class TrackingOptions(
*/
@Json(name = "thread_replies")
val threadReplies: Boolean? = null,
)
/**
* The custom hostname to use for link and open tracking.
* The hostname must be active and owned by the authenticated organization.
*/
@Json(name = "domain_name")
val domainName: String? = null,
) {
/**
* Builder for [TrackingOptions].
*/
class Builder {
private var label: String? = null
private var links: Boolean? = null
private var opens: Boolean? = null
private var threadReplies: Boolean? = null
private var domainName: String? = null

/**
* Set the label to apply to tracked messages.
* @param label The tracking label.
* @return The builder.
*/
fun label(label: String?) = apply { this.label = label }

/**
* Set whether to track links.
* @param links Whether to track links.
* @return The builder.
*/
fun links(links: Boolean?) = apply { this.links = links }

/**
* Set whether to track opens.
* @param opens Whether to track opens.
* @return The builder.
*/
fun opens(opens: Boolean?) = apply { this.opens = opens }

/**
* Set whether to track thread replies.
* @param threadReplies Whether to track thread replies.
* @return The builder.
*/
fun threadReplies(threadReplies: Boolean?) = apply { this.threadReplies = threadReplies }

/**
* Set the custom hostname used for link and open tracking.
* @param domainName An active custom hostname owned by the authenticated organization.
* @return The builder.
*/
fun domainName(domainName: String?) = apply { this.domainName = domainName }

/**
* Build the [TrackingOptions].
* @return The built [TrackingOptions].
*/
fun build() = TrackingOptions(label, links, opens, threadReplies, domainName)
}
}
56 changes: 56 additions & 0 deletions src/test/kotlin/com/nylas/models/TrackingOptionsTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.nylas.models

import com.nylas.util.JsonHelper
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class TrackingOptionsTests {
private val adapter = JsonHelper.moshi().adapter(TrackingOptions::class.java)

@Test
fun `builder exposes custom tracking hostname`() {
val options =
TrackingOptions.Builder()
.links(true)
.opens(true)
.domainName("tracking.example.com")
.build()

assertEquals("tracking.example.com", options.domainName)
}

@Test
fun `custom tracking hostname serializes as domain_name`() {
val options = TrackingOptions(links = true, opens = true, domainName = "tracking.example.com")

val json = adapter.toJson(options)

assertEquals("""{"links":true,"opens":true,"domain_name":"tracking.example.com"}""", json)
assertFalse(json.contains("domainName"))
}

@Test
fun `omitted custom tracking hostname leaves existing JSON unchanged`() {
val options = TrackingOptions(links = true, opens = false)

val json = adapter.toJson(options)

assertEquals("""{"links":true,"opens":false}""", json)
assertFalse(json.contains("domain_name"))
}

@Test
fun `send request nests custom hostname under tracking_options`() {
val request =
SendMessageRequest.Builder(listOf(EmailName("recipient@example.com")))
.trackingOptions(TrackingOptions(links = true, domainName = "tracking.example.com"))
.build()

val json = JsonHelper.moshi().adapter(SendMessageRequest::class.java).toJson(request)

assertTrue(json.contains(""""tracking_options":{"links":true,"domain_name":"tracking.example.com"}"""))
assertFalse(json.contains("domainName"))
}
}
Loading
Loading