Android (Kotlin)
dev.frontmail:frontmail pozwala aplikacji na Androida (albo dowolnej aplikacji w Kotlinie/JVM)
wysyłać e-maile przez twoje szablony z kluczem publicznym – bez własnego backendu. To zwykła
biblioteka w Kotlinie z API opartym na suspend; jej jedyne zależności to kotlinx-coroutines-core
i kotlinx-serialization-json.
Instalacja
Dział zatytułowany „Instalacja”dependencies { implementation("dev.frontmail:frontmail:0.1.0")}Aplikacja wymaga Kotlina 2.2 lub nowszego oraz uprawnienia INTERNET:
<uses-permission android:name="android.permission.INTERNET" />Kod źródłowy i wydania: github.com/frontmail-dev/frontmail-kotlin.
Zezwól na aplikacje mobilne
Dział zatytułowany „Zezwól na aplikacje mobilne”Aplikacje natywne nie wysyłają nagłówka Origin ani Referer, więc żądania z aplikacji są
odrzucane (niezależnie od tego, czy masz ustawione dozwolone domeny)
z błędem 403 origin_not_allowed, dopóki nie włączysz
w panelu Bezpieczeństwo → Aplikacje mobilne → Zezwól na aplikacje mobilne i nie klikniesz
Zapisz. Zobacz Ustawienia bezpieczeństwa.
Żądania ze stron są nadal sprawdzane z listą, więc przełącznik nie osłabia ochrony twoich formularzy na stronach. Przełącznik jest wymagany nawet przy pustej liście.
Wysyłka
Dział zatytułowany „Wysyłka”Utwórz jednego klienta i używaj go wielokrotnie (jest bezpieczny wątkowo). send to funkcja
suspend, więc wywołuj ją z korutyny, np. lifecycleScope albo viewModelScope:
import dev.frontmail.AcceptedStatusimport dev.frontmail.Frontmailimport 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…", // opcjonalne ) state.value = if (result.status == AcceptedStatus.HELD) "Otrzymaliśmy – wkrótce ją dostarczymy." else "Dziękujemy!" } catch (e: FrontmailException) { state.value = e.message }}params przyjmuje ciągi znaków, liczby, wartości logiczne, null, zagnieżdżone mapy i listy,
tablice oraz JsonElementy; jest też przeciążenie przyjmujące JsonObject. Bez serviceId
zostanie użyta usługa szablonu.
result.status to QUEUED (przyjęta do doręczenia) albo HELD (przyjęta i czeka na kredyty).
result.statusToken pozwala później odczytać status doręczenia.
Żeby wysłać e-mail w jednym z języków szablonu, przekaż
SendOptions(locale = …) ze znacznikiem BCP 47 – Locale.getDefault().toLanguageTag() zwraca język
urządzenia. Nieznany język przechodzi na ten sam język bazowy, a potem na język domyślny szablonu;
result.locale to użyty język:
val result = frontmail.send( "tpl_contact", mapOf("name" to name), options = SendOptions(locale = Locale.getDefault().toLanguageTag()),)Opcje klienta:
| Opcja | Domyślnie | Znaczenie |
|---|---|---|
apiUrl |
https://api.frontmail.dev |
Bazowy URL API |
retry |
RetryPolicy() (3 ponowienia) |
RetryPolicy(retries, baseDelayMillis, maxDelayMillis), RetryPolicy.NONE wyłącza ponawianie |
timeoutMillis |
15000 |
timeout pojedynczej próby |
Klucz prywatny (sk_…) powoduje FrontmailException z kodem private_key_in_browser już
w konstruktorze – aplikacja może zawierać tylko klucz publiczny.
Każde żądanie niesie nagłówek X-Frontmail-Client: frontmail-kotlin/<version>.
Załączniki
Dział zatytułowany „Załączniki”Jeśli szablon dopuszcza dynamiczne załączniki, przekaż je w SendOptions:
frontmail.send( "tpl_job_application", mapOf("name" to name), options = SendOptions( attachments = listOf( Attachment.fromBytes("cv.pdf", "application/pdf", bytes), Attachment.Upload("upl_…"), // przesłane wcześniej przez POST /v1/uploads ), ),)Turnstile
Dział zatytułowany „Turnstile”Jeśli szablon wymaga Turnstile, pokaż widget Cloudflare w WebView
i przekaż token do send. SDK nie zależy od Androida, więc daje ci gotowe elementy, a WebView
podłączasz sam:
Turnstile.sharedWidget()– wspólny mobilny klucz Frontmail. Nie potrzebujesz site key: SDK pobiera go zGET /v1/public-config(z cache, więc tylko raz), a widget działa jako osadzona strona HTML z adresemhttps://mobile.frontmail.dev– nic nie jest pobierane z tego adresu.widget.html()– strona z widgetem. Wysyła wiadomości do mostka JavaScriptFrontmailTurnstile(Turnstile.ANDROID_BRIDGE_NAME).Turnstile.parseMessage(data)– przyjmuje wyłącznie dokładnie te wiadomości, które wysyła strona; zwracaToken,Expired,Erroralbonull.widget.navigationDecision(url, isForMainFrame)– strażnik nawigacji:ALLOW,OPEN_EXTERNALLY(linki wewnątrz widgetu) alboBLOCK.
@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)}Następnie wyślij token razem z turnstileKey widgetu:
val widget = Turnstile.sharedWidget()webView.showTurnstile(widget, onToken = { token = it }, onExpire = { token = null })
// przy wysyłce:try { frontmail.send( "tpl_contact", mapOf("email" to email, "message" to message), options = SendOptions(turnstileToken = token, turnstileKey = widget.turnstileKey), )} finally { // Token można użyć tylko raz – przy następnej próbie potrzebny jest nowy. token = null webView.evaluateJavascript(Turnstile.RESET_SCRIPT, null)}Token jest ważny około 5 minut i można go użyć tylko raz: po każdej wysyłce, udanej lub nie,
zresetuj widget. Zwykły widget potrzebuje WebView o rozmiarze około 300 × 70 dp (kompaktowy
150 × 140 dp).
Żądania z aplikacji nie mają nagłówka Origin, więc Frontmail weryfikuje ich tokeny wspólnym
mobilnym kluczem tajnym. Nie potrzebujesz do tego własnych kluczy Turnstile – przy włączonym
Zezwól na aplikacje mobilne możesz włączyć CAPTCHA w szablonach używanych przez aplikację, nawet
jeśli twoja organizacja nie ma jeszcze kluczy.
Pełny przykład (także z Jetpack Compose i mostkiem ograniczonym do originu strony przez
androidx.webkit) znajdziesz w README SDK.
Własny site key
Dział zatytułowany „Własny site key”Żeby zamiast tego użyć własnego widgetu Turnstile, utwórz go przez TurnstileWidget.own:
val widget = TurnstileWidget.own(siteKey = "YOUR_TURNSTILE_SITE_KEY", baseUrl = "https://example.com")widget.turnstileKey ma wtedy wartość TurnstileKey.ORG, więc token jest wysyłany
z turnstile_key: "org", a Frontmail weryfikuje go kluczem tajnym z
Bezpieczeństwo → Ochrona przed botami (Turnstile).
getStatus
Dział zatytułowany „getStatus”val status = frontmail.getStatus(result) // albo getStatus(messageId, statusToken)status.status // MessageStatus.SENT, DELIVERED, BOUNCED, …status.events // [MessageEvent(type = "accepted", at = "…"), …]Token jest przesyłany w nagłówku X-Frontmail-Status-Token, nigdy w URL-u.
Ponawianie i idempotencja
Dział zatytułowany „Ponawianie i idempotencja”Błędy sieci, timeouty, 5xx i 429 są ponawiane automatycznie (z uwzględnieniem Retry-After;
oczekiwanie dłuższe niż 60 sekund nie jest ponawiane), a wszystkie próby jednej wysyłki używają tego
samego klucza idempotencji, więc ponowienie nigdy nie wyśle e-maila dwa razy. Pozostałe błędy 4xx
nigdy nie są ponawiane. Anulowanie korutyny natychmiast przerywa żądanie. Zobacz
Ponawianie i idempotencja.
Każdy błąd to FrontmailException z code, message, status (null dla błędów sieci),
docsUrl, details i retryAfter, a do tego sprawdzeniami isAuth, isValidation,
isRateLimit, isInsufficientCredits i isNetwork. Kody po stronie klienta to network_error,
timeout, invalid_response i private_key_in_browser. Jak obsłużyć poszczególne kody, opisuje
strona Obsługa błędów.
Dynamiczni odbiorcy
Dział zatytułowany „Dynamiczni odbiorcy”Jeśli pola Do/DW/UDW szablonu korzystają z parametrów, wysyłka z kluczem publicznym ma zablokowaną treść: szablon musi wymagać Turnstile, pola nadawcy muszą być stałe, parametry nie mogą zawierać HTML, linki muszą prowadzić do dozwolonych domen, a każdy odbiorca dostaje ograniczoną liczbę e-maili dziennie.
Bezpieczeństwo
Dział zatytułowany „Bezpieczeństwo”- Do aplikacji wkładaj tylko klucz publiczny (
pk_…). Wszystko w aplikacji da się wyciągnąć, więc klucz prywatny w niej jest tak dobry jak opublikowany. Zobacz Klucze publiczne i prywatne. - Przy włączonym Zezwól na aplikacje mobilne żądania bez nagłówka
Originsą przyjmowane skądkolwiek – może je wysłać każdy skrypt, który zna twój klucz publiczny. Polegaj na limicie na IP, Turnstile w każdym szablonie używanym przez aplikację i liście blokad.