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
2 changes: 2 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"Bash(./gradlew clean build:*)",
"Bash(java -version:*)",
"Bash(/usr/libexec/java_home:*)",
"Bash(./gradlew clean test:*)",
"Bash(./gradlew:*)",
"Bash(./gradlew build:*)"
]
}
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
- `activeCredentialId` field in `Connector` response model
- `activeCredentialId` field in `UpdateConnectorRequest` for setting the active credential on a Connector
* Enhanced `CredentialData.ConnectorOverride` to support optional `clientId` and `clientSecret` fields
* Support for `specific_time_availability` field in `AvailabilityParticipant` to override open hours configurations for specific dates and time ranges
* Support for `specific_time_availability` and `only_specific_time_availability` fields in `AvailabilityParticipant` to override open hours configurations for specific dates and time ranges
* `smtpRequired` field in `UrlForAuthenticationConfig` to ensure users enter their SMTP settings during hosted authentication

### Deprecated
* `CreateCredentialRequest.Override` - Use `CreateCredentialRequest.Connector` instead
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/com/nylas/models/AvailabilityParticipant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ data class AvailabilityParticipant(
*/
@Json(name = "specific_time_availability")
val specificTimeAvailability: List<SpecificTimeAvailability>? = null,
/**
* When set to true, only the times specified in [specificTimeAvailability] are considered available,
* ignoring the [openHours] configuration.
*/
@Json(name = "only_specific_time_availability")
val onlySpecificTimeAvailability: Boolean? = null,
) {
/**
* A builder for creating an [AvailabilityParticipant].
Expand All @@ -39,6 +45,7 @@ data class AvailabilityParticipant(
private var calendarIds: List<String>? = null
private var openHours: List<OpenHours>? = null
private var specificTimeAvailability: List<SpecificTimeAvailability>? = null
private var onlySpecificTimeAvailability: Boolean? = null

/**
* Set the calendar IDs associated with each participant's email address.
Expand All @@ -61,6 +68,14 @@ data class AvailabilityParticipant(
*/
fun specificTimeAvailability(specificTimeAvailability: List<SpecificTimeAvailability>) = apply { this.specificTimeAvailability = specificTimeAvailability }

/**
* Set whether only the times specified in [SpecificTimeAvailability] are considered available.
* When set to true, the [OpenHours] configuration is ignored.
* @param onlySpecificTimeAvailability Whether to only use specific time availability.
* @return The builder.
*/
fun onlySpecificTimeAvailability(onlySpecificTimeAvailability: Boolean) = apply { this.onlySpecificTimeAvailability = onlySpecificTimeAvailability }

/**
* Build the [AvailabilityParticipant].
* @return The [AvailabilityParticipant].
Expand All @@ -70,6 +85,7 @@ data class AvailabilityParticipant(
calendarIds,
openHours,
specificTimeAvailability,
onlySpecificTimeAvailability,
)
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/com/nylas/models/SpecificTimeAvailability.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,25 @@ data class SpecificTimeAvailability(
*/
@Json(name = "end")
val end: String,
/**
* IANA time zone database formatted string (e.g. America/Toronto).
* @see <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">List of tz database time zones</a>
*/
@Json(name = "timezone")
val timezone: String,
) {
/**
* A builder for creating a [SpecificTimeAvailability].
* @param date The date in YYYY-MM-DD format.
* @param start The start time in HH:MM format.
* @param end The end time in HH:MM format.
* @param timezone IANA time zone database formatted string (e.g. America/Toronto).
*/
data class Builder(
private val date: String,
private val start: String,
private val end: String,
private val timezone: String,
) {
/**
* Build the [SpecificTimeAvailability].
Expand All @@ -42,6 +50,7 @@ data class SpecificTimeAvailability(
date,
start,
end,
timezone,
)
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/com/nylas/models/UrlForAuthenticationConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ data class UrlForAuthenticationConfig(
*/
@Json(name = "credential_id")
val credentialId: String? = null,
/**
* If set to true, the user will be required to enter their SMTP settings during authentication.
* This is useful for IMAP-based providers to ensure SMTP credentials are collected for sending email.
*/
@Json(name = "smtp_required")
val smtpRequired: Boolean? = null,
) {
/**
* Builder for [UrlForAuthenticationConfig].
Expand All @@ -78,6 +84,7 @@ data class UrlForAuthenticationConfig(
private var state: String? = null
private var loginHint: String? = null
private var credentialId: String? = null
private var smtpRequired: Boolean? = null

/**
* Set the integration provider type that you already had set up with Nylas for this application.
Expand Down Expand Up @@ -138,6 +145,14 @@ data class UrlForAuthenticationConfig(
*/
fun credentialId(credentialId: String) = apply { this.credentialId = credentialId }

/**
* Set whether the user is required to enter their SMTP settings during authentication.
* This is useful for IMAP-based providers to ensure SMTP credentials are collected for sending email.
* @param smtpRequired Whether SMTP settings are required.
* @return This builder.
*/
fun smtpRequired(smtpRequired: Boolean) = apply { this.smtpRequired = smtpRequired }

/**
* Build the [UrlForAuthenticationConfig].
* @return The [UrlForAuthenticationConfig].
Expand All @@ -153,6 +168,7 @@ data class UrlForAuthenticationConfig(
state,
loginHint,
credentialId,
smtpRequired,
)
}
}
96 changes: 96 additions & 0 deletions src/test/kotlin/com/nylas/resources/AuthTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,102 @@ class AuthTests {
assertEquals(AuthProvider.GOOGLE, config.provider)
assertEquals("cred-456", config.credentialId)
}

@Test
fun `urlForOAuth2 with smtpRequired true should include smtp_required=true in URL`() {
val configWithSmtp = UrlForAuthenticationConfig(
clientId = "abc-123",
redirectUri = "https://example.com/oauth/callback",
provider = AuthProvider.IMAP,
smtpRequired = true,
)

val url = auth.urlForOAuth2(configWithSmtp)

assert(url.contains("smtp_required=true"))
assert(url.contains("response_type=code"))
}

@Test
fun `urlForOAuth2 with smtpRequired false should include smtp_required=false in URL`() {
val configWithSmtp = UrlForAuthenticationConfig(
clientId = "abc-123",
redirectUri = "https://example.com/oauth/callback",
provider = AuthProvider.IMAP,
smtpRequired = false,
)

val url = auth.urlForOAuth2(configWithSmtp)

assert(url.contains("smtp_required=false"))
assert(url.contains("response_type=code"))
}

@Test
fun `urlForOAuth2 without smtpRequired should not include smtp_required in URL`() {
val configWithoutSmtp = UrlForAuthenticationConfig(
clientId = "abc-123",
redirectUri = "https://example.com/oauth/callback",
provider = AuthProvider.IMAP,
)

val url = auth.urlForOAuth2(configWithoutSmtp)

assert(!url.contains("smtp_required"))
}

@Test
fun `urlForOAuth2PKCE with smtpRequired true should include smtp_required=true in URL`() {
val configWithSmtp = UrlForAuthenticationConfig(
clientId = "abc-123",
redirectUri = "https://example.com/oauth/callback",
provider = AuthProvider.IMAP,
smtpRequired = true,
)

val pkceAuthURL = auth.urlForOAuth2PKCE(configWithSmtp)

assert(pkceAuthURL.url.contains("smtp_required=true"))
assert(pkceAuthURL.url.contains("response_type=code"))
assert(pkceAuthURL.url.contains("code_challenge_method=s256"))
}

@Test
fun `urlForAdminConsent with smtpRequired true should include smtp_required=true in URL`() {
val configWithSmtp = UrlForAuthenticationConfig(
clientId = "abc-123",
redirectUri = "https://example.com/oauth/callback",
provider = AuthProvider.MICROSOFT,
smtpRequired = true,
)

val url = auth.urlForAdminConsent(configWithSmtp, "cred-789")

assert(url.contains("smtp_required=true"))
assert(url.contains("response_type=adminconsent"))
}

@Test
fun `UrlForAuthenticationConfig Builder with smtpRequired works correctly`() {
val config = UrlForAuthenticationConfig.Builder("client-123", "https://example.com/callback")
.provider(AuthProvider.IMAP)
.smtpRequired(true)
.build()

assertEquals("client-123", config.clientId)
assertEquals("https://example.com/callback", config.redirectUri)
assertEquals(AuthProvider.IMAP, config.provider)
assertEquals(true, config.smtpRequired)
}

@Test
fun `UrlForAuthenticationConfig Builder without smtpRequired defaults to null`() {
val config = UrlForAuthenticationConfig.Builder("client-123", "https://example.com/callback")
.provider(AuthProvider.IMAP)
.build()

assertNull(config.smtpRequired)
}
}

@Nested
Expand Down
Loading
Loading