Skip to content

Commit fd0165d

Browse files
authored
TW-4630: add support for HostedAuth "smtp_required" field (#305)
1 parent 93bbd40 commit fd0165d

File tree

7 files changed

+179
-15
lines changed

7 files changed

+179
-15
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"Bash(./gradlew clean build:*)",
1111
"Bash(java -version:*)",
1212
"Bash(/usr/libexec/java_home:*)",
13+
"Bash(./gradlew clean test:*)",
14+
"Bash(./gradlew:*)",
1315
"Bash(./gradlew build:*)"
1416
]
1517
}

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
- `activeCredentialId` field in `Connector` response model
1212
- `activeCredentialId` field in `UpdateConnectorRequest` for setting the active credential on a Connector
1313
* Enhanced `CredentialData.ConnectorOverride` to support optional `clientId` and `clientSecret` fields
14-
* Support for `specific_time_availability` field in `AvailabilityParticipant` to override open hours configurations for specific dates and time ranges
14+
* Support for `specific_time_availability` and `only_specific_time_availability` fields in `AvailabilityParticipant` to override open hours configurations for specific dates and time ranges
15+
* `smtpRequired` field in `UrlForAuthenticationConfig` to ensure users enter their SMTP settings during hosted authentication
1516

1617
### Deprecated
1718
* `CreateCredentialRequest.Override` - Use `CreateCredentialRequest.Connector` instead

src/main/kotlin/com/nylas/models/AvailabilityParticipant.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ data class AvailabilityParticipant(
2828
*/
2929
@Json(name = "specific_time_availability")
3030
val specificTimeAvailability: List<SpecificTimeAvailability>? = null,
31+
/**
32+
* When set to true, only the times specified in [specificTimeAvailability] are considered available,
33+
* ignoring the [openHours] configuration.
34+
*/
35+
@Json(name = "only_specific_time_availability")
36+
val onlySpecificTimeAvailability: Boolean? = null,
3137
) {
3238
/**
3339
* A builder for creating an [AvailabilityParticipant].
@@ -39,6 +45,7 @@ data class AvailabilityParticipant(
3945
private var calendarIds: List<String>? = null
4046
private var openHours: List<OpenHours>? = null
4147
private var specificTimeAvailability: List<SpecificTimeAvailability>? = null
48+
private var onlySpecificTimeAvailability: Boolean? = null
4249

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

71+
/**
72+
* Set whether only the times specified in [SpecificTimeAvailability] are considered available.
73+
* When set to true, the [OpenHours] configuration is ignored.
74+
* @param onlySpecificTimeAvailability Whether to only use specific time availability.
75+
* @return The builder.
76+
*/
77+
fun onlySpecificTimeAvailability(onlySpecificTimeAvailability: Boolean) = apply { this.onlySpecificTimeAvailability = onlySpecificTimeAvailability }
78+
6479
/**
6580
* Build the [AvailabilityParticipant].
6681
* @return The [AvailabilityParticipant].
@@ -70,6 +85,7 @@ data class AvailabilityParticipant(
7085
calendarIds,
7186
openHours,
7287
specificTimeAvailability,
88+
onlySpecificTimeAvailability,
7389
)
7490
}
7591
}

src/main/kotlin/com/nylas/models/SpecificTimeAvailability.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,25 @@ data class SpecificTimeAvailability(
2222
*/
2323
@Json(name = "end")
2424
val end: String,
25+
/**
26+
* IANA time zone database formatted string (e.g. America/Toronto).
27+
* @see <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">List of tz database time zones</a>
28+
*/
29+
@Json(name = "timezone")
30+
val timezone: String,
2531
) {
2632
/**
2733
* A builder for creating a [SpecificTimeAvailability].
2834
* @param date The date in YYYY-MM-DD format.
2935
* @param start The start time in HH:MM format.
3036
* @param end The end time in HH:MM format.
37+
* @param timezone IANA time zone database formatted string (e.g. America/Toronto).
3138
*/
3239
data class Builder(
3340
private val date: String,
3441
private val start: String,
3542
private val end: String,
43+
private val timezone: String,
3644
) {
3745
/**
3846
* Build the [SpecificTimeAvailability].
@@ -42,6 +50,7 @@ data class SpecificTimeAvailability(
4250
date,
4351
start,
4452
end,
53+
timezone,
4554
)
4655
}
4756
}

src/main/kotlin/com/nylas/models/UrlForAuthenticationConfig.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ data class UrlForAuthenticationConfig(
6060
*/
6161
@Json(name = "credential_id")
6262
val credentialId: String? = null,
63+
/**
64+
* If set to true, the user will be required to enter their SMTP settings during authentication.
65+
* This is useful for IMAP-based providers to ensure SMTP credentials are collected for sending email.
66+
*/
67+
@Json(name = "smtp_required")
68+
val smtpRequired: Boolean? = null,
6369
) {
6470
/**
6571
* Builder for [UrlForAuthenticationConfig].
@@ -78,6 +84,7 @@ data class UrlForAuthenticationConfig(
7884
private var state: String? = null
7985
private var loginHint: String? = null
8086
private var credentialId: String? = null
87+
private var smtpRequired: Boolean? = null
8188

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

148+
/**
149+
* Set whether the user is required to enter their SMTP settings during authentication.
150+
* This is useful for IMAP-based providers to ensure SMTP credentials are collected for sending email.
151+
* @param smtpRequired Whether SMTP settings are required.
152+
* @return This builder.
153+
*/
154+
fun smtpRequired(smtpRequired: Boolean) = apply { this.smtpRequired = smtpRequired }
155+
141156
/**
142157
* Build the [UrlForAuthenticationConfig].
143158
* @return The [UrlForAuthenticationConfig].
@@ -153,6 +168,7 @@ data class UrlForAuthenticationConfig(
153168
state,
154169
loginHint,
155170
credentialId,
171+
smtpRequired,
156172
)
157173
}
158174
}

src/test/kotlin/com/nylas/resources/AuthTests.kt

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,102 @@ class AuthTests {
146146
assertEquals(AuthProvider.GOOGLE, config.provider)
147147
assertEquals("cred-456", config.credentialId)
148148
}
149+
150+
@Test
151+
fun `urlForOAuth2 with smtpRequired true should include smtp_required=true in URL`() {
152+
val configWithSmtp = UrlForAuthenticationConfig(
153+
clientId = "abc-123",
154+
redirectUri = "https://example.com/oauth/callback",
155+
provider = AuthProvider.IMAP,
156+
smtpRequired = true,
157+
)
158+
159+
val url = auth.urlForOAuth2(configWithSmtp)
160+
161+
assert(url.contains("smtp_required=true"))
162+
assert(url.contains("response_type=code"))
163+
}
164+
165+
@Test
166+
fun `urlForOAuth2 with smtpRequired false should include smtp_required=false in URL`() {
167+
val configWithSmtp = UrlForAuthenticationConfig(
168+
clientId = "abc-123",
169+
redirectUri = "https://example.com/oauth/callback",
170+
provider = AuthProvider.IMAP,
171+
smtpRequired = false,
172+
)
173+
174+
val url = auth.urlForOAuth2(configWithSmtp)
175+
176+
assert(url.contains("smtp_required=false"))
177+
assert(url.contains("response_type=code"))
178+
}
179+
180+
@Test
181+
fun `urlForOAuth2 without smtpRequired should not include smtp_required in URL`() {
182+
val configWithoutSmtp = UrlForAuthenticationConfig(
183+
clientId = "abc-123",
184+
redirectUri = "https://example.com/oauth/callback",
185+
provider = AuthProvider.IMAP,
186+
)
187+
188+
val url = auth.urlForOAuth2(configWithoutSmtp)
189+
190+
assert(!url.contains("smtp_required"))
191+
}
192+
193+
@Test
194+
fun `urlForOAuth2PKCE with smtpRequired true should include smtp_required=true in URL`() {
195+
val configWithSmtp = UrlForAuthenticationConfig(
196+
clientId = "abc-123",
197+
redirectUri = "https://example.com/oauth/callback",
198+
provider = AuthProvider.IMAP,
199+
smtpRequired = true,
200+
)
201+
202+
val pkceAuthURL = auth.urlForOAuth2PKCE(configWithSmtp)
203+
204+
assert(pkceAuthURL.url.contains("smtp_required=true"))
205+
assert(pkceAuthURL.url.contains("response_type=code"))
206+
assert(pkceAuthURL.url.contains("code_challenge_method=s256"))
207+
}
208+
209+
@Test
210+
fun `urlForAdminConsent with smtpRequired true should include smtp_required=true in URL`() {
211+
val configWithSmtp = UrlForAuthenticationConfig(
212+
clientId = "abc-123",
213+
redirectUri = "https://example.com/oauth/callback",
214+
provider = AuthProvider.MICROSOFT,
215+
smtpRequired = true,
216+
)
217+
218+
val url = auth.urlForAdminConsent(configWithSmtp, "cred-789")
219+
220+
assert(url.contains("smtp_required=true"))
221+
assert(url.contains("response_type=adminconsent"))
222+
}
223+
224+
@Test
225+
fun `UrlForAuthenticationConfig Builder with smtpRequired works correctly`() {
226+
val config = UrlForAuthenticationConfig.Builder("client-123", "https://example.com/callback")
227+
.provider(AuthProvider.IMAP)
228+
.smtpRequired(true)
229+
.build()
230+
231+
assertEquals("client-123", config.clientId)
232+
assertEquals("https://example.com/callback", config.redirectUri)
233+
assertEquals(AuthProvider.IMAP, config.provider)
234+
assertEquals(true, config.smtpRequired)
235+
}
236+
237+
@Test
238+
fun `UrlForAuthenticationConfig Builder without smtpRequired defaults to null`() {
239+
val config = UrlForAuthenticationConfig.Builder("client-123", "https://example.com/callback")
240+
.provider(AuthProvider.IMAP)
241+
.build()
242+
243+
assertNull(config.smtpRequired)
244+
}
149245
}
150246

151247
@Nested

0 commit comments

Comments
 (0)