diff --git a/.gitignore b/.gitignore index 0901798..e468498 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build/ app/build/ *.apk *.aab +output/ # Local sensitive configuration cookies.txt diff --git a/NOTICE.md b/NOTICE.md index 48349a5..1be8eec 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -9,3 +9,10 @@ inclusion in the upstream project's `main` branch. The upstream project is released under the MIT License. Its original copyright notice is retained in this repository's `LICENSE` file. + +## Apple interface asset + +`app/src/main/res/drawable/ic_hidemyemail_official.png` is the current public +Hide My Email icon served by iCloud.com and is used only as the Android launcher +icon. Apple and iCloud are trademarks of Apple Inc.; this asset is not covered +by this repository's MIT license. diff --git a/README.md b/README.md index 4259876..330f247 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ It is intentionally maintained as a separate project. It complements the upstrea - Validates the signed-in Apple account before actions are performed. - Generates Hide My Email addresses with labels and a configurable quantity. - Lists active and inactive Hide My Email addresses. +- Edits iCloud labels and notes, and can deactivate or reactivate an address. +- Repairs stale China-region captures that incorrectly contain a `.icloud.com` shard. - Maintains a local phone-side list with `unused`, `used`, and `trash` states. - Copies aliases to the Android clipboard with one tap. - Uses only the iCloud endpoints required for the selected operation; there is no companion server. diff --git a/app/build.gradle b/app/build.gradle index 6a6eebd..2769177 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId 'com.hidemyemail.generator' minSdk 23 targetSdk 34 - versionCode 2 - versionName '0.1.1' + versionCode 3 + versionName '0.1.2' } buildTypes { @@ -56,6 +56,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.11.0' testImplementation 'junit:junit:4.13.2' + testImplementation 'org.json:json:20230227' debugImplementation 'androidx.compose.ui:ui-tooling:1.4.3' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 260ebb7..8654b13 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,9 @@ (null) } var busy by remember { mutableStateOf(false) } var message by remember { mutableStateOf("准备就绪") } @@ -192,10 +216,30 @@ private fun HideMyEmailApp() { localAddresses.addAll(store.loadLocalAddresses()) } + fun applyRemoteAddresses(addresses: List) { + remoteAddresses.clear() + remoteAddresses.addAll(addresses) + store.upsertLocalAddresses(addresses.map { + LocalAddress( + email = it.email, + label = it.label, + state = if (it.active) "unused" else "trash", + updatedAt = System.currentTimeMillis(), + ) + }) + refreshLocal() + } + fun currentCookieContext(): CookieContext { - val parsed = HmeRepository.parseCookieContext(cookieInput, region) + val resolvedRegion = HmeRepository.detectRegion(cookieInput) ?: region + val parsed = HmeRepository.parseCookieContext(cookieInput, resolvedRegion) return if (maildomainHost.isNotBlank()) { - parsed.copy(maildomainHost = maildomainHost.trim()) + parsed.copy( + maildomainHost = HmeRepository.normalizeMaildomainHost( + maildomainHost, + resolvedRegion, + ), + ) } else { parsed } @@ -203,11 +247,56 @@ private fun HideMyEmailApp() { fun saveSettings() { val detectedRegion = HmeRepository.detectRegion(cookieInput) ?: region + val parsed = HmeRepository.parseCookieContext(cookieInput, detectedRegion) + val resolvedHost = if (maildomainHost.isNotBlank()) { + HmeRepository.normalizeMaildomainHost(maildomainHost, detectedRegion) + } else { + parsed.maildomainHost + } region = detectedRegion - store.saveSettings(detectedRegion, cookieInput, maildomainHost) + maildomainHost = resolvedHost + store.saveSettings(detectedRegion, cookieInput, resolvedHost) message = "已保存本地配置" } + suspend fun syncRemoteAddresses(requestRegion: ICloudRegion): Int { + val cookieContext = currentCookieContext() + val addresses = withContext(Dispatchers.IO) { + repository.listAddresses(cookieContext, requestRegion) + } + applyRemoteAddresses(addresses) + return addresses.size + } + + fun performAddressAction( + address: HmeAddress, + progress: String, + success: String, + action: (CookieContext, ICloudRegion, HmeAddress) -> Unit, + ) { + saveSettings() + val requestRegion = HmeRepository.detectRegion(cookieInput) ?: region + region = requestRegion + scope.launch { + busy = true + message = progress + val cookieContext = currentCookieContext() + val result = runCatching { + withContext(Dispatchers.IO) { + action(cookieContext, requestRegion, address) + } + syncRemoteAddresses(requestRegion) + } + result.onSuccess { + managedAddress = null + message = success + }.onFailure { + message = it.cleanMessage() + } + busy = false + } + } + LaunchedEffect(Unit) { refreshLocal() } @@ -326,6 +415,7 @@ private fun HideMyEmailApp() { showActive = showActive, onModeChange = { showActive = it }, addresses = remoteAddresses.filter { it.active == showActive }, + onManage = { managedAddress = it }, onRefresh = { saveSettings() val requestRegion = HmeRepository.detectRegion(cookieInput) ?: region @@ -375,6 +465,47 @@ private fun HideMyEmailApp() { } } } + + managedAddress?.let { address -> + HmeAddressManagementDialog( + address = address, + onDismiss = { managedAddress = null }, + onSave = { newLabel, newNote -> + performAddressAction( + address = address, + progress = "Updating address information... / 正在更新地址信息...", + success = "Address information updated / 地址信息已更新", + ) { cookieContext, requestRegion, item -> + repository.updateAddressMetadata( + cookieContext, + requestRegion, + item.anonymousId, + newLabel, + newNote, + ) + } + }, + onChangeActivity = { + if (address.active) { + performAddressAction( + address = address, + progress = "Deactivating address... / 正在停用地址...", + success = "Address deactivated / 地址已停用", + ) { cookieContext, requestRegion, item -> + repository.deactivateAddress(cookieContext, requestRegion, item.anonymousId) + } + } else { + performAddressAction( + address = address, + progress = "Reactivating address... / 正在重新启用地址...", + success = "Address reactivated / 地址已重新启用", + ) { cookieContext, requestRegion, item -> + repository.reactivateAddress(cookieContext, requestRegion, item.anonymousId) + } + } + }, + ) + } } @Composable @@ -512,6 +643,7 @@ private fun RemoteListScreen( showActive: Boolean, onModeChange: (Boolean) -> Unit, addresses: List, + onManage: (HmeAddress) -> Unit, onRefresh: () -> Unit, ) { Column(modifier = Modifier.fillMaxSize()) { @@ -532,7 +664,7 @@ private fun RemoteListScreen( verticalArrangement = Arrangement.spacedBy(10.dp), ) { items(addresses, key = { it.email }) { item -> - AddressCard(item) + AddressCard(item, onManage) } } } @@ -625,13 +757,130 @@ private fun OutlinedIconButtonText(text: String, icon: androidx.compose.ui.graph } @Composable -private fun AddressCard(address: HmeAddress) { +private fun AddressCard(address: HmeAddress, onManage: (HmeAddress) -> Unit) { Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = Color.White), ) { - AddressRow(address.email, address.label, address.createdAtText(), address.active) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + AddressRow(address.email, address.label, address.createdAtText(), address.active) + if (address.note.isNotBlank()) { + Text( + "Note / 备注: ${address.note}", + modifier = Modifier.padding(horizontal = 14.dp), + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF64748B), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Divider() + Row( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedButton( + onClick = { onManage(address) }, + enabled = address.anonymousId.isNotBlank(), + shape = RoundedCornerShape(8.dp), + ) { + Text("Manage / 管理") + } + } + } + } +} + +@Composable +private fun HmeAddressManagementDialog( + address: HmeAddress, + onDismiss: () -> Unit, + onSave: (String, String) -> Unit, + onChangeActivity: () -> Unit, +) { + var label by remember(address.anonymousId) { mutableStateOf(address.label) } + var note by remember(address.anonymousId) { mutableStateOf(address.note) } + var showDeactivateConfirmation by remember(address.anonymousId) { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Manage Hide My Email / 管理隐藏邮箱") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + address.email, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + Text( + if (address.active) "Active / 使用中" else "Inactive / 已停用", + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF64748B), + ) + OutlinedTextField( + value = label, + onValueChange = { label = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Label / 标签") }, + ) + OutlinedTextField( + value = note, + onValueChange = { note = it }, + modifier = Modifier.fillMaxWidth(), + minLines = 3, + label = { Text("Note / 备注") }, + ) + } + }, + confirmButton = { + Button( + onClick = { onSave(label.trim(), note.trim()) }, + enabled = label.trim().isNotBlank(), + ) { + Text("Save / 保存") + } + }, + dismissButton = { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton(onClick = onDismiss) { + Text("Close / 关闭") + } + OutlinedButton( + onClick = { + if (address.active) { + showDeactivateConfirmation = true + } else { + onChangeActivity() + } + }, + ) { + Text(if (address.active) "Deactivate / 停用" else "Reactivate / 重新启用") + } + } + }, + ) + + if (showDeactivateConfirmation) { + AlertDialog( + onDismissRequest = { showDeactivateConfirmation = false }, + title = { Text("Deactivate address? / 确认停用地址?") }, + text = { Text("Mail sent to this address will stop forwarding. / 该地址将停止转发邮件。") }, + confirmButton = { + Button(onClick = { + showDeactivateConfirmation = false + onChangeActivity() + }) { + Text("Deactivate / 停用") + } + }, + dismissButton = { + OutlinedButton(onClick = { showDeactivateConfirmation = false }) { + Text("Cancel / 取消") + } + }, + ) } } @@ -779,9 +1028,13 @@ class HmeRepository { val webservices = data.optJSONObject("webservices") ?: JSONObject() val maildomain = webservices.optJSONObject("maildomainws") val partition = data.optString("userPartition") - val detectedHost = context.maildomainHost.ifBlank { + val normalizedContextHost = normalizeMaildomainHost(context.maildomainHost, region) + val detectedHost = normalizedContextHost.ifBlank { when { - maildomain?.optString("url").orEmpty().isNotBlank() -> Uri.parse(maildomain?.optString("url")).host.orEmpty() + maildomain?.optString("url").orEmpty().isNotBlank() -> normalizeMaildomainHost( + Uri.parse(maildomain?.optString("url")).host.orEmpty(), + region, + ) partition.isNotBlank() -> "p$partition-maildomainws.icloud.${if (region == ICloudRegion.China) "com.cn" else "com"}" else -> region.defaultMaildomain } @@ -808,16 +1061,16 @@ class HmeRepository { repeat(count) { val generated = requestJson( method = "POST", - url = "https://$host/v1/hme/generate", + url = "https://$host${HmeEndpoints.generate}", region = region, cookie = context.cookie, - payload = JSONObject().put("langCode", "en-us"), + payload = JSONObject().put("langCode", region.generateLangCode), ) ensureSuccess(generated, "generate email") val email = generated.getJSONObject("result").getString("hme") val reserved = requestJson( method = "POST", - url = "https://$host/v1/hme/reserve", + url = "https://$host${HmeEndpoints.reserve}", region = region, cookie = context.cookie, payload = JSONObject() @@ -826,7 +1079,16 @@ class HmeRepository { .put("note", DefaultNote), ) ensureSuccess(reserved, "reserve email") - out.add(HmeAddress(email = email, label = label, active = true, createdAt = System.currentTimeMillis())) + out.add( + HmeAddress( + anonymousId = "", + email = email, + label = label, + note = DefaultNote, + active = true, + createdAt = System.currentTimeMillis(), + ), + ) } return out } @@ -836,25 +1098,57 @@ class HmeRepository { val host = resolveMaildomainHost(context, region) val data = requestJson( method = "GET", - url = "https://$host/v2/hme/list", + url = "https://$host${HmeEndpoints.list}", region = region, cookie = context.cookie, ) ensureSuccess(data, "list emails") - val emails = data.getJSONObject("result").optJSONArray("hmeEmails") ?: JSONArray() - return buildList { - for (index in 0 until emails.length()) { - val item = emails.optJSONObject(index) ?: continue - add( - HmeAddress( - email = item.optString("hme"), - label = item.optString("label"), - active = item.optBoolean("isActive"), - createdAt = item.optLong("createTimestamp"), - ) - ) - } - }.filter { it.email.isNotBlank() }.sortedByDescending { it.createdAt } + return parseHmeAddresses(data) + } + + fun updateAddressMetadata( + context: CookieContext, + region: ICloudRegion, + anonymousId: String, + label: String, + note: String, + ) { + require(context.cookie.isNotBlank()) { "Cookie is empty" } + require(anonymousId.isNotBlank()) { "Missing iCloud address identifier" } + val data = requestJson( + method = "POST", + url = "https://${resolveMaildomainHost(context, region)}${HmeEndpoints.updateMetadata}", + region = region, + cookie = context.cookie, + payload = updateMetadataPayload(anonymousId, label, note), + ) + ensureSuccess(data, "update email metadata") + } + + fun deactivateAddress(context: CookieContext, region: ICloudRegion, anonymousId: String) { + require(context.cookie.isNotBlank()) { "Cookie is empty" } + require(anonymousId.isNotBlank()) { "Missing iCloud address identifier" } + val data = requestJson( + method = "POST", + url = "https://${resolveMaildomainHost(context, region)}${HmeEndpoints.deactivate}", + region = region, + cookie = context.cookie, + payload = anonymousIdPayload(anonymousId), + ) + ensureSuccess(data, "deactivate email") + } + + fun reactivateAddress(context: CookieContext, region: ICloudRegion, anonymousId: String) { + require(context.cookie.isNotBlank()) { "Cookie is empty" } + require(anonymousId.isNotBlank()) { "Missing iCloud address identifier" } + val data = requestJson( + method = "POST", + url = "https://${resolveMaildomainHost(context, region)}${HmeEndpoints.reactivate}", + region = region, + cookie = context.cookie, + payload = anonymousIdPayload(anonymousId), + ) + ensureSuccess(data, "reactivate email") } private fun requestJson( @@ -865,8 +1159,8 @@ class HmeRepository { payload: JSONObject? = null, ): JSONObject { val httpUrl = url.toHttpUrl().newBuilder() - .addQueryParameter("clientBuildNumber", "2536Project32") - .addQueryParameter("clientMasteringNumber", "2536B20") + .addQueryParameter("clientBuildNumber", HmeRequestDefaults.clientBuildNumber) + .addQueryParameter("clientMasteringNumber", HmeRequestDefaults.clientMasteringNumber) .addQueryParameter("clientId", "") .addQueryParameter("dsid", "") .build() @@ -901,7 +1195,8 @@ class HmeRepository { } private fun resolveMaildomainHost(context: CookieContext, region: ICloudRegion): String { - return context.maildomainHost.ifBlank { region.defaultMaildomain } + return normalizeMaildomainHost(context.maildomainHost, region) + .ifBlank { region.defaultMaildomain } } private fun ensureSuccess(data: JSONObject, action: String) { @@ -915,6 +1210,36 @@ class HmeRepository { } companion object { + fun parseHmeAddresses(data: JSONObject): List { + val emails = data.optJSONObject("result")?.optJSONArray("hmeEmails") ?: JSONArray() + return buildList { + for (index in 0 until emails.length()) { + val item = emails.optJSONObject(index) ?: continue + add( + HmeAddress( + anonymousId = item.optString("anonymousId"), + email = item.optString("hme"), + label = item.optString("label"), + note = item.optString("note"), + active = item.optBoolean("isActive"), + createdAt = item.optLong("createTimestamp"), + ), + ) + } + }.filter { it.email.isNotBlank() }.sortedByDescending { it.createdAt } + } + + fun updateMetadataPayload(anonymousId: String, label: String, note: String): JSONObject { + return JSONObject() + .put("anonymousId", anonymousId) + .put("label", label) + .put("note", note) + } + + fun anonymousIdPayload(anonymousId: String): JSONObject { + return JSONObject().put("anonymousId", anonymousId) + } + fun parseCookieContext(raw: String, region: ICloudRegion): CookieContext { val rawContent = raw.lineSequence() .map { it.trim() } @@ -927,12 +1252,15 @@ class HmeRepository { if (rawContent.isBlank()) return CookieContext("") + val resolvedRegion = detectRegion(content) ?: region + val explicitHost = Regex("""(?m)^HIDEMYEMAIL_MAILDOMAIN_HOST=([A-Za-z0-9.-]+)\s*$""") .find(content) ?.groupValues ?.getOrNull(1) .orEmpty() .trim() + .let { normalizeMaildomainHost(it, resolvedRegion) } val base64Cookie = Regex("""(?m)^HIDEMYEMAIL_COOKIE_BASE64=([A-Za-z0-9+/=]+)\s*$""") .find(content) @@ -948,7 +1276,7 @@ class HmeRepository { .find(content) ?.groupValues ?.getOrNull(1) - ?.let { "$it-maildomainws.icloud.${if (region == ICloudRegion.China) "com.cn" else "com"}" } + ?.let { "$it-maildomainws.icloud.${if (resolvedRegion == ICloudRegion.China) "com.cn" else "com"}" } .orEmpty() val maildomainHost = explicitHost.ifBlank { shardHost } @@ -992,17 +1320,40 @@ class HmeRepository { return CookieContext(cookieHeader.trim(), maildomainHost) } - return CookieContext(content.lines().firstOrNull().orEmpty().trim(), maildomainHost) + val rawCookie = content.lines().firstOrNull().orEmpty().trim() + if (Regex("""^[^=;\s]+=""").containsMatchIn(rawCookie)) { + return CookieContext(rawCookie, maildomainHost) + } + + return CookieContext("", maildomainHost) + } + + fun normalizeMaildomainHost(host: String, region: ICloudRegion): String { + val cleanHost = host + .trim() + .removePrefix("https://") + .removePrefix("http://") + .substringBefore('/') + val shard = Regex("""^(p\d+)-maildomainws\.icloud\.com(?:\.cn)?$""") + .matchEntire(cleanHost) + ?.groupValues + ?.getOrNull(1) + return if (shard == null) { + cleanHost + } else { + "$shard-maildomainws.icloud.${if (region == ICloudRegion.China) "com.cn" else "com"}" + } } fun detectRegion(raw: String): ICloudRegion? { + if (raw.contains("icloud.com.cn", ignoreCase = true)) return ICloudRegion.China val declaredRegion = Regex("""(?m)^HIDEMYEMAIL_REGION=(global|china)\s*$""", RegexOption.IGNORE_CASE) .find(raw) ?.groupValues ?.getOrNull(1) ?.lowercase(Locale.ROOT) if (declaredRegion != null) return ICloudRegion.from(declaredRegion) - return if (raw.contains("icloud.com.cn", ignoreCase = true)) ICloudRegion.China else null + return null } } } diff --git a/app/src/main/res/drawable/ic_hidemyemail_official.png b/app/src/main/res/drawable/ic_hidemyemail_official.png new file mode 100644 index 0000000..c08269a Binary files /dev/null and b/app/src/main/res/drawable/ic_hidemyemail_official.png differ diff --git a/app/src/test/java/com/hidemyemail/generator/CookieParserTest.kt b/app/src/test/java/com/hidemyemail/generator/CookieParserTest.kt index 643528b..feee0b9 100644 --- a/app/src/test/java/com/hidemyemail/generator/CookieParserTest.kt +++ b/app/src/test/java/com/hidemyemail/generator/CookieParserTest.kt @@ -24,4 +24,40 @@ class CookieParserTest { HmeRepository.detectRegion("curl.exe https://www.icloud.com.cn/setup/ws/1/validate"), ) } + + @Test + fun normalizesStaleChinaShardHostFromCapturedCookieFile() { + val parsed = HmeRepository.parseCookieContext( + """ + HIDEMYEMAIL_REGION=global + HIDEMYEMAIL_REQUEST_URL=https://www.icloud.com.cn/applications/hidemyemail/2626Build17/zh-cn/index.html + HIDEMYEMAIL_MAILDOMAIN_HOST=p217-maildomainws.icloud.com + Cookie: X-APPLE-WEBAUTH-USER=valid; session=valid + """.trimIndent(), + ICloudRegion.Global, + ) + + assertEquals("X-APPLE-WEBAUTH-USER=valid; session=valid", parsed.cookie) + assertEquals("p217-maildomainws.icloud.com.cn", parsed.maildomainHost) + } + + @Test + fun rejectsJavaScriptBundleAsCookieInput() { + val parsed = HmeRepository.parseCookieContext( + "/*! iCloud application bundle */\nfunction app() { return '/v1/hme/generate' }", + ICloudRegion.China, + ) + + assertEquals("", parsed.cookie) + } + + @Test + fun rejectsJavaScriptAssignmentAsCookieInput() { + val parsed = HmeRepository.parseCookieContext( + "var session = 'not a Cookie header';", + ICloudRegion.China, + ) + + assertEquals("", parsed.cookie) + } } diff --git a/app/src/test/java/com/hidemyemail/generator/HmeManagementProtocolTest.kt b/app/src/test/java/com/hidemyemail/generator/HmeManagementProtocolTest.kt new file mode 100644 index 0000000..1a1cc75 --- /dev/null +++ b/app/src/test/java/com/hidemyemail/generator/HmeManagementProtocolTest.kt @@ -0,0 +1,56 @@ +package com.hidemyemail.generator + +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class HmeManagementProtocolTest { + @Test + fun parsesAnonymousIdAndNoteFromTheCurrentHmeListResponse() { + val addresses = HmeRepository.parseHmeAddresses( + JSONObject( + """ + { + "result": { + "hmeEmails": [ + { + "anonymousId": "anonymous-id", + "hme": "example@icloud.com", + "label": "Example", + "note": "Example note", + "isActive": true, + "createTimestamp": 123456789 + } + ] + } + } + """.trimIndent(), + ), + ) + + assertEquals(1, addresses.size) + assertEquals("anonymous-id", addresses.single().anonymousId) + assertEquals("Example note", addresses.single().note) + } + + @Test + fun exposesCurrentAppleManagementEndpointsAndPayloads() { + assertEquals("2626Build17", HmeRequestDefaults.clientBuildNumber) + assertEquals("2626Build17", HmeRequestDefaults.clientMasteringNumber) + assertEquals("/v1/hme/updateMetaData", HmeEndpoints.updateMetadata) + assertEquals("/v1/hme/deactivate", HmeEndpoints.deactivate) + assertEquals("/v1/hme/reactivate", HmeEndpoints.reactivate) + + val update = HmeRepository.updateMetadataPayload("anonymous-id", "New label", "New note") + val stateChange = HmeRepository.anonymousIdPayload("anonymous-id") + + assertEquals("anonymous-id", update.getString("anonymousId")) + assertEquals("New label", update.getString("label")) + assertEquals("New note", update.getString("note")) + assertEquals("anonymous-id", stateChange.getString("anonymousId")) + assertFalse(update.has("hme")) + assertTrue(stateChange.length() == 1) + } +} diff --git a/app/src/test/java/com/hidemyemail/generator/RegionProtocolTest.kt b/app/src/test/java/com/hidemyemail/generator/RegionProtocolTest.kt new file mode 100644 index 0000000..13094c7 --- /dev/null +++ b/app/src/test/java/com/hidemyemail/generator/RegionProtocolTest.kt @@ -0,0 +1,23 @@ +package com.hidemyemail.generator + +import org.junit.Assert.assertEquals +import org.junit.Test + +class RegionProtocolTest { + @Test + fun usesWebLocaleForHideMyEmailGeneration() { + assertEquals("en-us", ICloudRegion.Global.generateLangCode) + assertEquals("zh-cn", ICloudRegion.China.generateLangCode) + } + + @Test + fun chinaDomainWinsOverAStaleDeclaredGlobalRegion() { + assertEquals( + ICloudRegion.China, + HmeRepository.detectRegion( + "HIDEMYEMAIL_REGION=global\n" + + "HIDEMYEMAIL_REQUEST_URL=https://www.icloud.com.cn/applications/hidemyemail/2626Build17/zh-cn/index.html", + ), + ) + } +}