Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
223dc69
TW-5374 Add missing admin API resources
AaronDDM Jun 11, 2026
ff68117
TW-5374 Fix CI lint
AaronDDM Jun 11, 2026
026beb2
TW-5374 Align workspace schema with source
AaronDDM Jun 11, 2026
f9bafaa
TW-5374 Fix rules list test stubs
AaronDDM Jun 11, 2026
071c6dd
TW-5374 Validate workspace auto-group domain
AaronDDM Jun 12, 2026
607da36
Add lists admin API support
AaronDDM Jun 12, 2026
4192f40
Fix rules list response parsing
AaronDDM Jun 12, 2026
69f4a06
Add service account signing for domains
AaronDDM Jun 12, 2026
5ce8bbf
Add workspace list pagination params
AaronDDM Jun 12, 2026
66d868c
Support callback URIs in application updates
AaronDDM Jun 12, 2026
a157d87
Canonicalize signed domain request bodies
AaronDDM Jun 12, 2026
20cbcfd
Address Java admin API review feedback
AaronDDM Jun 12, 2026
13f4293
Make workspace policy detach explicit
AaronDDM Jun 12, 2026
7f23105
Unwrap nested rules list response
AaronDDM Jun 12, 2026
2f155cc
Support flat and nested rules list responses
AaronDDM Jun 12, 2026
7f3f68e
Address Java list and callback URI feedback
AaronDDM Jun 12, 2026
6047079
Complete Java domains parity fixes
AaronDDM Jun 12, 2026
2869c28
Split domain verification request types
AaronDDM Jun 12, 2026
999315c
Keep rules list wrapper internal
AaronDDM Jun 12, 2026
0e5ae03
Remove unsupported domain list filters
AaronDDM Jun 12, 2026
cd53b18
Restore RequestOverrides JVM constructor
AaronDDM Jun 12, 2026
ee9b088
Keep RequestOverrides data class ABI stable
AaronDDM Jun 12, 2026
0a4114a
Address application and workspace review feedback
AaronDDM Jun 12, 2026
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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
## [Unreleased]

### Added
* Application administration updates
- `Applications.update()` for `PATCH /v3/applications`
- Application updates support sparse branding fields and `callback_uris`, including callback URI IDs for preserving existing callback URIs
- Redirect URI updates use `PATCH /v3/applications/redirect-uris/{id}`
- Manage Domains admin CRUD and verification endpoints on `client.domains()` via `/v3/admin/domains`; these support `ServiceAccountSigner` for Nylas Service Account request-signing auth without Bearer auth, canonical signed wire bodies, manually signed headers in `RequestOverrides.headers`, base64-encoded PEM service-account keys, and request-only verification types
- `Workspaces` resource via `client.workspaces()`: CRUD, paginated listing with `limit` and `page_token`, `autoGroup`, `manualAssign`, `default`, `policyId`, explicit `clearPolicyId`, and `ruleIds`; `CreateWorkspaceRequest` validates that `domain` is present when `autoGroup` is true; `WorkspaceAutoGroupRequest.invalidAlso` includes invalid grants in auto-grouping when enabled
* Transactional email support via `Domains.sendTransactionalEmail()`
- `SendTransactionalEmailRequest` model (and fluent `Builder`) for composing transactional messages from a verified domain — supports `to`, `from`, `cc`, `bcc`, `reply_to`, `subject`, `body`, `send_at`, `reply_to_message_id`, `tracking_options`, `use_draft`, `custom_headers`, and `is_plaintext`
- `NylasClient.domains()` accessor returning the new `Domains` resource
- Automatic multipart/form-data upload when the total attachment size exceeds the JSON limit
- Examples: `TransactionalEmailExample.java` and `KotlinTransactionalEmailExample.kt`
* Administration API — Policies, Rules, and Lists (app-level, `nylas` provider only)
- `Policies` resource via `client.policies()`: full CRUD (`list`, `find`, `create`, `update`, `destroy`) with `CreatePolicyRequest` / `UpdatePolicyRequest` and supporting models (`Policy`, `PolicyLimits`, `PolicyOptions`, `PolicySpamDetection`)
- `Rules` resource via `client.rules()`: full CRUD with `CreateRuleRequest` / `UpdateRuleRequest` and supporting models (`Rule`, `RuleAction`, `RuleActionType`, `RuleCondition`, `RuleConditionOperator`, `RuleMatch`, `RuleMatchOperator`, `RuleTrigger`)
- `Rules` resource via `client.rules()`: full CRUD plus `listEvaluations` for grant rule-evaluation audit records; handles the nested `/v3/rules` list envelope returned by the API
- `NylasLists` resource via `client.lists()`: full CRUD plus `listItems`, `addItems`, and `removeItems` for managing list contents; `NylasList`, `NylasListItem`, `NylasListType`, `ListItemsRequest` models
- `NylasLists.create()` for `POST /v3/lists` with `CreateNylasListRequest` (`name`, `type`, and optional `description`)

## [v2.16.1] - Release 2026-05-21

Expand Down
12 changes: 10 additions & 2 deletions src/main/kotlin/com/nylas/NylasClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ open class NylasClient(
*/
open fun rules(): Rules = Rules(this)

/**
* Access the Workspaces API
* @return The Workspaces API
*/
open fun workspaces(): Workspaces = Workspaces(this)

/**
* Access the Lists API
* @return The Lists API
Expand Down Expand Up @@ -355,8 +361,10 @@ open class NylasClient(
val builder = Request.Builder().url(url.build())

// Override the API key if it is provided in the override
val apiKey = overrides?.apiKey ?: this.apiKey
builder.addHeader(HttpHeaders.AUTHORIZATION.headerName, "Bearer $apiKey")
if (overrides?.omitAuthorization != true) {
val apiKey = overrides?.apiKey ?: this.apiKey
builder.addHeader(HttpHeaders.AUTHORIZATION.headerName, "Bearer $apiKey")
}

// Add additional headers
if (overrides?.headers != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class HttpLoggingInterceptor : Interceptor {
for (i in 0 until headers.size) {
val name = headers.name(i)
var value = headers.value(i)
if (!isLogAuthHeader && "Authorization" == name) {
if ((!isLogAuthHeader && "Authorization" == name) || shouldRedactHeader(name)) {
value = "<not logged>"
}
headersLog.append(" ").append(name).append(": ").append(value).append("\n")
Expand Down Expand Up @@ -166,5 +166,9 @@ class HttpLoggingInterceptor : Interceptor {
private val requestLogs = LoggerFactory.getLogger("com.nylas.http.Summary")
private val headersLogs = LoggerFactory.getLogger("com.nylas.http.Headers")
private val bodyLogs = LoggerFactory.getLogger("com.nylas.http.Body")

internal fun shouldRedactHeader(name: String): Boolean {
return "X-Nylas-Signature".equals(name, ignoreCase = true)
}
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/com/nylas/models/CreateDomainRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.nylas.models

import com.squareup.moshi.Json

/**
* Class representation of a Nylas create domain request.
*/
data class CreateDomainRequest(
@Json(name = "name")
val name: String,
@Json(name = "domain_address")
val domainAddress: String,
)
2 changes: 1 addition & 1 deletion src/main/kotlin/com/nylas/models/CreateNylasListRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.squareup.moshi.Json
*/
data class CreateNylasListRequest(
/**
* Name of the list (1–256 characters).
* Name of the list.
*/
@Json(name = "name")
val name: String,
Expand Down
38 changes: 38 additions & 0 deletions src/main/kotlin/com/nylas/models/CreateWorkspaceRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.nylas.models

import com.squareup.moshi.Json

/**
* Class representation of a Nylas create workspace request.
*/
data class CreateWorkspaceRequest(
@Json(name = "name")
val name: String,
@Json(name = "domain")
val domain: String? = null,
Comment thread
AaronDDM marked this conversation as resolved.
@Json(name = "auto_group")
val autoGroup: Boolean? = null,
@Json(name = "policy_id")
val policyId: String? = null,
@Json(name = "rule_ids")
val ruleIds: List<String>? = null,
) {
init {
require(autoGroup != true || !domain.isNullOrBlank()) {
"domain is required when autoGroup is true"
}
}

data class Builder(private val name: String) {
private var domain: String? = null
private var autoGroup: Boolean? = null
private var policyId: String? = null
private var ruleIds: List<String>? = null

fun domain(domain: String?) = apply { this.domain = domain }
fun autoGroup(autoGroup: Boolean?) = apply { this.autoGroup = autoGroup }
fun policyId(policyId: String?) = apply { this.policyId = policyId }
fun ruleIds(ruleIds: List<String>?) = apply { this.ruleIds = ruleIds }
fun build() = CreateWorkspaceRequest(name, domain, autoGroup, policyId, ruleIds)
}
}
39 changes: 39 additions & 0 deletions src/main/kotlin/com/nylas/models/Domain.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.nylas.models

import com.squareup.moshi.Json

/**
* Class representation of a Nylas managed email domain.
*/
data class Domain(
@Json(name = "id")
val id: String? = null,
@Json(name = "name")
val name: String? = null,
@Json(name = "domain_address")
val domainAddress: String? = null,
@Json(name = "organization_id")
val organizationId: String? = null,
@Json(name = "branded")
val branded: Boolean? = null,
@Json(name = "region")
val region: String? = null,
@Json(name = "verified_ownership")
val verifiedOwnership: Boolean? = null,
@Json(name = "verified_mx")
val verifiedMx: Boolean? = null,
@Json(name = "verified_spf")
val verifiedSpf: Boolean? = null,
@Json(name = "verified_feedback")
val verifiedFeedback: Boolean? = null,
@Json(name = "verified_dkim")
val verifiedDkim: Boolean? = null,
@Json(name = "verified_dmarc")
val verifiedDmarc: Boolean? = null,
@Json(name = "verified_arc")
val verifiedArc: Boolean? = null,
@Json(name = "created_at")
val createdAt: Long? = null,
@Json(name = "updated_at")
val updatedAt: Long? = null,
)
123 changes: 123 additions & 0 deletions src/main/kotlin/com/nylas/models/DomainVerification.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.nylas.models

import com.squareup.moshi.Json

/**
* DNS verification types accepted by the Manage Domains API.
*/
enum class DomainVerificationType {
@Json(name = "ownership")
OWNERSHIP,

@Json(name = "mx")
MX,

@Json(name = "spf")
SPF,

@Json(name = "dkim")
DKIM,

@Json(name = "feedback")
FEEDBACK,

@Json(name = "dmarc")
DMARC,

@Json(name = "arc")
ARC,
Comment thread
AaronDDM marked this conversation as resolved.
}

/**
* DNS verification types accepted by Manage Domains verification requests.
*/
enum class DomainVerificationRequestType {
Comment thread
AaronDDM marked this conversation as resolved.
@Json(name = "ownership")
OWNERSHIP,

@Json(name = "mx")
MX,

@Json(name = "spf")
SPF,

@Json(name = "dkim")
DKIM,

@Json(name = "feedback")
FEEDBACK,
}

/**
* Status values returned by domain DNS verification attempts.
*/
enum class DomainVerificationStatus {
@Json(name = "pending")
PENDING,

@Json(name = "done")
DONE,

@Json(name = "failed")
FAILED,
}

/**
* Class representation of a domain DNS verification request.
*/
data class DomainVerificationRequest(
@Json(name = "type")
val type: DomainVerificationRequestType,
@Json(name = "options")
val options: Map<String, Any>? = null,
) {
/**
* Builder for [DomainVerificationRequest].
*/
data class Builder(private val type: DomainVerificationRequestType) {
private var options: Map<String, Any>? = null

/**
* Set verification options.
* @param options Verification options.
* @return This builder.
*/
fun options(options: Map<String, Any>?) = apply { this.options = options }

/**
* Build the [DomainVerificationRequest].
* @return The built [DomainVerificationRequest].
*/
fun build() = DomainVerificationRequest(type, options)
}
}

/**
* Class representation of a verification attempt returned by the API.
*/
data class DomainVerificationAttempt(
@Json(name = "type")
val type: DomainVerificationType? = null,
@Json(name = "options")
val options: Map<String, Any>? = null,
)

/**
* Class representation of a domain verification result.
*/
data class DomainVerificationResult(
@Json(name = "domain_id")
val domainId: String? = null,
@Json(name = "attempt")
val attempt: DomainVerificationAttempt? = null,
@Json(name = "status")
val status: DomainVerificationStatus? = null,
@Json(name = "created_at")
val createdAt: Long? = null,
@Json(name = "expires_at")
val expiresAt: Long? = null,
@Json(name = "details")
val details: Map<String, Any>? = null,
@Json(name = "message")
val message: String? = null,
)
22 changes: 22 additions & 0 deletions src/main/kotlin/com/nylas/models/ListDomainsQueryParams.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.nylas.models

import com.squareup.moshi.Json

/**
* Class representation of the query parameters for listing domains.
*/
data class ListDomainsQueryParams(
@Json(name = "limit")
val limit: Int? = null,
@Json(name = "page_token")
val pageToken: String? = null,
) : IQueryParams {
class Builder {
private var limit: Int? = null
private var pageToken: String? = null

fun limit(limit: Int?) = apply { this.limit = limit }
fun pageToken(pageToken: String?) = apply { this.pageToken = pageToken }
fun build() = ListDomainsQueryParams(limit, pageToken)
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/com/nylas/models/ListRuleEvaluationsQueryParams.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.nylas.models

import com.squareup.moshi.Json

/**
* Class representation of the query parameters for listing rule evaluations.
*/
data class ListRuleEvaluationsQueryParams(
@Json(name = "limit")
val limit: Int? = null,
@Json(name = "page_token")
val pageToken: String? = null,
) : IQueryParams {
class Builder {
private var limit: Int? = null
private var pageToken: String? = null

fun limit(limit: Int?) = apply { this.limit = limit }
fun pageToken(pageToken: String?) = apply { this.pageToken = pageToken }
fun build() = ListRuleEvaluationsQueryParams(limit, pageToken)
}
}
47 changes: 47 additions & 0 deletions src/main/kotlin/com/nylas/models/ListWorkspacesQueryParams.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.nylas.models

import com.squareup.moshi.Json

/**
* Class representation of the query parameters for listing workspaces.
*/
data class ListWorkspacesQueryParams(
/**
* The maximum number of objects to return.
*/
@Json(name = "limit")
val limit: Int? = null,
/**
* Cursor for pagination. Pass the value of [ListResponse.nextCursor] to get the next page.
*/
@Json(name = "page_token")
val pageToken: String? = null,
) : IQueryParams {
/**
* Builder for [ListWorkspacesQueryParams].
*/
class Builder {
private var limit: Int? = null
private var pageToken: String? = null

/**
* Set the maximum number of objects to return.
* @param limit The maximum number of objects to return.
* @return The builder.
*/
fun limit(limit: Int) = apply { this.limit = limit }

/**
* Set the pagination cursor.
* @param pageToken Cursor for pagination. Pass the value of [ListResponse.nextCursor].
* @return The builder.
*/
fun pageToken(pageToken: String) = apply { this.pageToken = pageToken }

/**
* Build the [ListWorkspacesQueryParams].
* @return A [ListWorkspacesQueryParams] with the provided values.
*/
fun build() = ListWorkspacesQueryParams(limit, pageToken)
}
}
Loading
Loading