Přeskočit na obsah

Android (Kotlin)

dev.frontmail:frontmail umožní aplikaci pro Android (nebo jakékoli aplikaci v Kotlinu na JVM) odesílat e-maily přes vaše šablony s public key – bez vlastního backendu. Je to obyčejná knihovna v Kotlinu se suspend API; jediné závislosti jsou kotlinx-coroutines-core a kotlinx-serialization-json.

app/build.gradle.kts
dependencies {
implementation("dev.frontmail:frontmail:0.1.0")
}

Aplikace potřebuje Kotlin 2.2 nebo novější a oprávnění INTERNET:

<uses-permission android:name="android.permission.INTERNET" />

Zdrojový kód a vydání: github.com/frontmail-dev/frontmail-kotlin.

Povolte mobilní aplikace

Sekce “Povolte mobilní aplikace”

Nativní aplikace neposílají hlavičku Origin ani Referer, takže požadavky z aplikace (bez ohledu na to, jestli máte nastavené povolené weby) skončí chybou 403 origin_not_allowed, dokud v dashboardu nezapnete Zabezpečení → Mobilní aplikace → Povolit mobilní aplikace a nekliknete na Uložit. Viz Nastavení zabezpečení.

Požadavky z webů se dál kontrolují podle seznamu, takže přepínač ochranu vašich webových formulářů neoslabí. Přepínač je potřeba i s prázdným seznamem.

Vytvořte jednoho klienta a používejte ho opakovaně (je bezpečný pro více vláken). send je suspend funkce, takže ji volejte z korutiny, např. z lifecycleScope nebo viewModelScope:

import dev.frontmail.AcceptedStatus
import dev.frontmail.Frontmail
import dev.frontmail.FrontmailException
val frontmail = Frontmail("pk_4f2a…")
viewModelScope.launch {
try {
val result = frontmail.send(
templateId = "tpl_contact",
params = mapOf("email" to email, "message" to message),
serviceId = "svc_01J9…", // nepovinné
)
state.value = if (result.status == AcceptedStatus.HELD) "Přijato – doručíme co nejdřív." else "Děkujeme!"
} catch (e: FrontmailException) {
state.value = e.message
}
}

params přijímá řetězce, čísla, booleany, null, vnořené mapy a seznamy, pole a JsonElement; existuje i varianta, která bere JsonObject. Bez serviceId se použije služba šablony.

result.status je QUEUED (přijato k doručení) nebo HELD (přijato, čeká na kredity). result.statusToken slouží k pozdějšímu zjištění stavu doručení.

Volby klienta:

Volba Výchozí hodnota Význam
apiUrl https://api.frontmail.dev základní URL API
retry RetryPolicy() (3 opakování) RetryPolicy(retries, baseDelayMillis, maxDelayMillis), RetryPolicy.NONE opakování vypne
timeoutMillis 15000 časový limit jednoho pokusu

Private key (sk_…) vyhodí FrontmailException s kódem private_key_in_browser už v konstruktoru – v aplikaci smí být jen public key.

Každý požadavek nese hlavičku X-Frontmail-Client: frontmail-kotlin/<verze>.

Pokud šablona povoluje dynamické přílohy, předejte je v SendOptions:

frontmail.send(
"tpl_job_application",
mapOf("name" to name),
options = SendOptions(
attachments = listOf(
Attachment.fromBytes("cv.pdf", "application/pdf", bytes),
Attachment.Upload("upl_…"), // nahráno předem přes POST /v1/uploads
),
),
)

Pokud šablona vyžaduje Turnstile, zobrazte widget Cloudflaru ve WebView a token předejte do send. SDK nemá žádnou závislost na Androidu, takže vám dá jednotlivé díly a WebView si zapojíte sami:

  • Turnstile.sharedWidget() – sdílený mobilní klíč Frontmailu. Site key nepotřebujete: SDK ho načte z GET /v1/public-config (s cache, takže jen jednou) a widget poběží jako vložená HTML stránka s adresou https://mobile.frontmail.dev – z té adresy se nic nestahuje.
  • widget.html() – stránka s widgetem. Zprávy posílá do JavaScriptového mostu FrontmailTurnstile (Turnstile.ANDROID_BRIDGE_NAME).
  • Turnstile.parseMessage(data) – přijme jen přesně ty zprávy, které stránka posílá; vrátí Token, Expired, Error nebo null.
  • widget.navigationDecision(url, isForMainFrame) – hlídání navigace: ALLOW, OPEN_EXTERNALLY (odkazy ve widgetu) nebo BLOCK.
@SuppressLint("SetJavaScriptEnabled")
fun WebView.showTurnstile(widget: TurnstileWidget, onToken: (String) -> Unit, onExpire: () -> Unit) {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.allowFileAccess = false
settings.allowContentAccess = false
settings.javaScriptCanOpenWindowsAutomatically = false
settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
setBackgroundColor(Color.TRANSPARENT)
val main = Handler(Looper.getMainLooper())
addJavascriptInterface(
object {
@JavascriptInterface
fun postMessage(data: String?) {
main.post {
when (val m = Turnstile.parseMessage(data)) {
is TurnstileMessage.Token -> onToken(m.token)
TurnstileMessage.Expired -> onExpire()
is TurnstileMessage.Error -> Log.w("Turnstile", "error ${m.code}")
null -> Unit
}
}
}
},
Turnstile.ANDROID_BRIDGE_NAME,
)
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean =
when (widget.navigationDecision(request.url.toString(), request.isForMainFrame)) {
Turnstile.NavigationDecision.ALLOW -> false
Turnstile.NavigationDecision.OPEN_EXTERNALLY -> {
runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, request.url)) }
true
}
Turnstile.NavigationDecision.BLOCK -> true
}
}
loadDataWithBaseURL(widget.baseUrl, widget.html(), "text/html", "utf-8", null)
}

Token pak pošlete spolu s turnstileKey widgetu:

val widget = Turnstile.sharedWidget()
webView.showTurnstile(widget, onToken = { token = it }, onExpire = { token = null })
// při odeslání:
try {
frontmail.send(
"tpl_contact",
mapOf("email" to email, "message" to message),
options = SendOptions(turnstileToken = token, turnstileKey = widget.turnstileKey),
)
} finally {
// Token jde použít jen jednou – pro další pokus si vyžádejte nový.
token = null
webView.evaluateJavascript(Turnstile.RESET_SCRIPT, null)
}

Token platí asi 5 minut a jde použít jen jednou: po každém odeslání, úspěšném i neúspěšném, widget resetujte. Běžný widget potřebuje WebView zhruba 300 × 70 dp (kompaktní 150 × 140 dp).

Požadavky z aplikace nemají hlavičku Origin, takže jejich tokeny Frontmail ověří svým sdíleným mobilním tajným klíčem. Vlastní klíče Turnstile k tomu nepotřebujete – se zapnutým Povolit mobilní aplikace můžete CAPTCHA u šablon aplikace zapnout, i když organizace klíče zatím nemá.

Kompletní příklad (včetně Jetpack Compose a mostu omezeného na origin stránky přes androidx.webkit) najdete v README SDK.

Pokud chcete místo toho použít vlastní widget Turnstile, vytvořte ho přes TurnstileWidget.own:

val widget = TurnstileWidget.own(siteKey = "YOUR_TURNSTILE_SITE_KEY", baseUrl = "https://example.com")

widget.turnstileKey je pak TurnstileKey.ORG, takže se token pošle s turnstile_key: "org" a Frontmail ho ověří secret key ze Zabezpečení → Ochrana proti botům (Turnstile).

val status = frontmail.getStatus(result) // nebo getStatus(messageId, statusToken)
status.status // MessageStatus.SENT, DELIVERED, BOUNCED, …
status.events // [MessageEvent(type = "accepted", at = "…"), …]

Token se posílá v hlavičce X-Frontmail-Status-Token, nikdy v URL.

Opakování a idempotence

Sekce “Opakování a idempotence”

Chyby sítě, vypršené časové limity, 5xx a 429 se automaticky opakují (s ohledem na Retry-After; čekání delší než 60 sekund se už neopakuje) a všechny pokusy jednoho odeslání sdílejí stejný idempotency key, takže opakování e-mail nikdy nepošle dvakrát. Ostatní chyby 4xx se neopakují nikdy. Zrušení korutiny požadavek okamžitě zastaví. Viz Retry a idempotence.

Každé selhání je FrontmailException s vlastnostmi code, message, status (u chyb sítě null), docsUrl, details a retryAfter a s kontrolami isAuth, isValidation, isRateLimit, isInsufficientCredits a isNetwork. Kódy z klienta jsou network_error, timeout, invalid_response a private_key_in_browser. Jak reagovat na jednotlivé kódy, popisuje Zpracování chyb.

Pokud To/CC/BCC šablony používají parametry, má odeslání s public key uzamčený obsah: šablona musí vyžadovat Turnstile, pole odesílatele musí být pevná, parametry nesmí obsahovat HTML, odkazy musí mířit na povolené domény a každý příjemce dostane za den jen omezený počet e-mailů.

  • Do aplikace patří jen public key (pk_…). Cokoli v aplikaci se dá vytáhnout, takže private key v ní je jako zveřejněný. Viz Public key a private key.
  • Se zapnutým Povolit mobilní aplikace přijmeme požadavky bez hlavičky Origin odkudkoli – poslat je může jakýkoli skript, který zná váš public key. Spolehněte se na limit na IP adresu, Turnstile u všech šablon, které aplikace používá, a na seznam blokovaných hodnot.