Preskočiť na obsah

Android (Kotlin)

dev.frontmail:frontmail umožní aplikácii pre Android (alebo akejkoľvek aplikácii v Kotline na JVM) odosielať e-maily cez vaše šablóny s verejným kľúčom – bez vlastného backendu. Je to obyčajná knižnica v Kotline so suspend API; jediné závislosti sú kotlinx-coroutines-core a kotlinx-serialization-json.

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

Aplikácia potrebuje Kotlin 2.2 alebo novší a oprávnenie INTERNET:

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

Zdrojový kód a vydania: github.com/frontmail-dev/frontmail-kotlin.

Natívne aplikácie neposielajú hlavičku Origin ani Referer, takže požiadavky z aplikácie (bez ohľadu na to, či máte nastavené povolené weby) skončia chybou 403 origin_not_allowed, kým v dashboarde nezapnete Zabezpečenie → Mobilné aplikácie → Povoliť mobilné aplikácie a nekliknete na Uložiť. Pozrite Nastavenia zabezpečenia.

Požiadavky z webov sa naďalej kontrolujú podľa zoznamu, takže prepínač ochranu vašich webových formulárov neoslabí. Prepínač je potrebný aj pri prázdnom zozname.

Vytvorte jedného klienta a používajte ho opakovane (je bezpečný pre viac vlákien). send je suspend funkcia, takže ju volajte z korutiny, napr. z lifecycleScope alebo 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) "Prijaté – doručíme čo najskôr." else "Ďakujeme!"
} catch (e: FrontmailException) {
state.value = e.message
}
}

params prijíma reťazce, čísla, booleany, null, vnorené mapy a zoznamy, polia a JsonElement; existuje aj variant, ktorý berie JsonObject. Bez serviceId sa použije služba šablóny.

result.status je QUEUED (prijaté na doručenie) alebo HELD (prijaté, čaká na kredity). result.statusToken slúži na neskoršie zistenie stavu doručenia.

Ak chcete e-mail poslať v niektorom jazyku šablóny, odovzdajte SendOptions(locale = …) so značkou BCP 47 – jazyk zariadenia vráti Locale.getDefault().toLanguageTag(). Pri neznámom jazyku sa použije rovnaký základný jazyk a potom predvolený jazyk šablóny; result.locale je použitý jazyk:

val result = frontmail.send(
"tpl_contact",
mapOf("name" to name),
options = SendOptions(locale = Locale.getDefault().toLanguageTag()),
)

Voľby klienta:

Voľba Predvolená hodnota Význam
apiUrl https://api.frontmail.dev základná URL API
retry RetryPolicy() (3 opakovania) RetryPolicy(retries, baseDelayMillis, maxDelayMillis), RetryPolicy.NONE opakovanie vypne
timeoutMillis 15000 časový limit jedného pokusu

Súkromný kľúč (sk_…) vyhodí FrontmailException s kódom private_key_in_browser už v konštruktore – v aplikácii smie byť len verejný kľúč.

Každá požiadavka nesie hlavičku X-Frontmail-Client: frontmail-kotlin/<verzia>.

Ak šablóna povoľuje dynamické prílohy, odovzdajte ich 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_…"), // nahrané vopred cez POST /v1/uploads
),
),
)

Ak šablóna vyžaduje Turnstile, zobrazte widget Cloudflaru vo WebView a token odovzdajte do send. SDK nemá žiadnu závislosť od Androidu, takže vám dá jednotlivé diely a WebView si zapojíte sami:

  • Turnstile.sharedWidget() – zdieľaný mobilný kľúč Frontmailu. Site key nepotrebujete: SDK ho načíta z GET /v1/public-config (s cache, takže len raz) a widget pobeží ako vložená HTML stránka s adresou https://mobile.frontmail.dev – z tej adresy sa nič nesťahuje.
  • widget.html() – stránka s widgetom. Správy posiela do JavaScriptového mosta FrontmailTurnstile (Turnstile.ANDROID_BRIDGE_NAME).
  • Turnstile.parseMessage(data) – prijme len presne tie správy, ktoré stránka posiela; vráti Token, Expired, Error alebo null.
  • widget.navigationDecision(url, isForMainFrame) – stráženie navigácie: ALLOW, OPEN_EXTERNALLY (odkazy vo widgete) alebo 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 potom pošlite spolu s turnstileKey widgetu:

val widget = Turnstile.sharedWidget()
webView.showTurnstile(widget, onToken = { token = it }, onExpire = { token = null })
// pri odoslaní:
try {
frontmail.send(
"tpl_contact",
mapOf("email" to email, "message" to message),
options = SendOptions(turnstileToken = token, turnstileKey = widget.turnstileKey),
)
} finally {
// Token sa dá použiť len raz – na ďalší pokus si vyžiadajte nový.
token = null
webView.evaluateJavascript(Turnstile.RESET_SCRIPT, null)
}

Token platí približne 5 minút a dá sa použiť len raz: po každom odoslaní, úspešnom aj neúspešnom, widget resetujte. Bežný widget potrebuje WebView približne 300 × 70 dp (kompaktný 150 × 140 dp).

Požiadavky z aplikácie nemajú hlavičku Origin, takže ich tokeny Frontmail overí svojím zdieľaným mobilným tajným kľúčom. Vlastné kľúče Turnstile na to nepotrebujete – so zapnutým Povoliť mobilné aplikácie môžete CAPTCHA pri šablónach aplikácie zapnúť, aj keď organizácia kľúče zatiaľ nemá.

Kompletný príklad (vrátane Jetpack Compose a mosta obmedzeného na origin stránky cez androidx.webkit) nájdete v README SDK.

Ak chcete namiesto toho použiť vlastný widget Turnstile, vytvorte ho cez TurnstileWidget.own:

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

widget.turnstileKey je potom TurnstileKey.ORG, takže sa token pošle s turnstile_key: "org" a Frontmail ho overí secret key zo Zabezpečenie → Ochrana pred botmi (Turnstile).

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

Token sa posiela v hlavičke X-Frontmail-Status-Token, nikdy v URL.

Sieťové chyby, vypršané časové limity, 5xx a 429 sa automaticky opakujú (s ohľadom na Retry-After; čakanie dlhšie ako 60 sekúnd sa už neopakuje) a všetky pokusy jedného odoslania zdieľajú rovnaký idempotency key, takže opakovanie e-mail nikdy nepošle dvakrát. Ostatné chyby 4xx sa neopakujú nikdy. Zrušenie korutiny požiadavku okamžite zastaví. Pozrite Opakovanie a idempotencia.

Každé zlyhanie je FrontmailException s vlastnosťami code, message, status (pri sieťových chybách null), docsUrl, details a retryAfter a s kontrolami isAuth, isValidation, isRateLimit, isInsufficientCredits a isNetwork. Kódy z klienta sú network_error, timeout, invalid_response a private_key_in_browser. Ako reagovať na jednotlivé kódy, opisuje Spracovanie chýb.

Ak To/CC/BCC šablóny používajú parametre, má odoslanie s verejným kľúčom uzamknutý obsah: šablóna musí vyžadovať Turnstile, polia odosielateľa musia byť pevné, parametre nesmú obsahovať HTML, odkazy musia smerovať na povolené domény a každý príjemca dostane za deň len obmedzený počet e-mailov.

  • Do aplikácie patrí len verejný kľúč (pk_…). Čokoľvek v aplikácii sa dá vytiahnuť, takže súkromný kľúč v nej je ako zverejnený. Pozrite Verejný a súkromný kľúč.
  • So zapnutým Povoliť mobilné aplikácie prijmeme požiadavky bez hlavičky Origin odkiaľkoľvek – poslať ich môže akýkoľvek skript, ktorý pozná váš verejný kľúč. Spoľahnite sa na limit na IP adresu, Turnstile pri všetkých šablónach, ktoré aplikácia používa, a na zoznam blokovaných hodnôt.