Android (Kotlin)
dev.frontmail:frontmail permet à une application Android (ou à n’importe quelle application
Kotlin/JVM) d’envoyer des e-mails à partir de vos modèles avec la clé publique – sans backend à vous.
C’est une simple bibliothèque Kotlin avec une API suspend ; ses seules dépendances sont
kotlinx-coroutines-core et kotlinx-serialization-json.
Installation
Section intitulée « Installation »dependencies { implementation("dev.frontmail:frontmail:0.1.0")}L’application nécessite Kotlin 2.2 ou plus récent et la permission INTERNET :
<uses-permission android:name="android.permission.INTERNET" />Code source et versions : github.com/frontmail-dev/frontmail-kotlin.
Autoriser les applications mobiles
Section intitulée « Autoriser les applications mobiles »Les applications natives n’envoient pas d’en-tête Origin ni Referer : les requêtes de
l’application sont donc refusées (que vous ayez défini des sites autorisés
ou non) avec 403 origin_not_allowed tant que vous
n’avez pas activé Sécurité → Applications mobiles → Autoriser les applications mobiles dans le
tableau de bord et cliqué sur Enregistrer. Voir
Paramètres de sécurité.
Les requêtes provenant de sites web restent comparées à la liste : l’option n’affaiblit donc pas la protection de vos formulaires web. L’option est nécessaire même avec une liste vide.
Créez un seul client et réutilisez-le (il est thread-safe). send est une fonction suspend :
appelez-la depuis une coroutine, p. ex. lifecycleScope ou 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…", // facultatif ) state.value = if (result.status == AcceptedStatus.HELD) "Bien reçu – il sera transmis sous peu." else "Merci !" } catch (e: FrontmailException) { state.value = e.message }}params accepte des chaînes, des nombres, des booléens, null, des maps et des listes imbriquées,
des tableaux et des JsonElement ; il existe aussi une surcharge qui prend un JsonObject. Sans
serviceId, c’est le service du modèle qui est utilisé.
result.status vaut QUEUED (accepté pour livraison) ou HELD (accepté et en attente de crédits).
result.statusToken vous permet de lire plus tard le statut de livraison.
Pour envoyer dans une langue du modèle, passez
SendOptions(locale = …) avec une balise BCP 47 – Locale.getDefault().toLanguageTag() donne la
langue de l’appareil. Une langue inconnue se rabat sur la même langue de base, puis sur la langue
par défaut du modèle ; result.locale est la langue utilisée :
val result = frontmail.send( "tpl_contact", mapOf("name" to name), options = SendOptions(locale = Locale.getDefault().toLanguageTag()),)Options du client :
| Option | Par défaut | Signification |
|---|---|---|
apiUrl |
https://api.frontmail.dev |
URL de base de l’API |
retry |
RetryPolicy() (3 nouvelles tentatives) |
RetryPolicy(retries, baseDelayMillis, maxDelayMillis) ; RetryPolicy.NONE désactive les nouvelles tentatives |
timeoutMillis |
15000 |
délai maximal d’une tentative |
Une clé privée (sk_…) lève une FrontmailException de code private_key_in_browser dès le
constructeur – une application ne peut contenir que la clé publique.
Chaque requête porte l’en-tête X-Frontmail-Client: frontmail-kotlin/<version>.
Pièces jointes
Section intitulée « Pièces jointes »Si le modèle autorise les pièces jointes dynamiques, passez-les dans
SendOptions :
frontmail.send( "tpl_job_application", mapOf("name" to name), options = SendOptions( attachments = listOf( Attachment.fromBytes("cv.pdf", "application/pdf", bytes), Attachment.Upload("upl_…"), // uploadé au préalable via POST /v1/uploads ), ),)Turnstile
Section intitulée « Turnstile »Si le modèle exige Turnstile, affichez le widget Cloudflare dans une
WebView et passez le jeton à send. Le SDK n’a pas de dépendance Android : il vous fournit les
briques et c’est vous qui câblez la WebView :
Turnstile.sharedWidget()– la clé mobile partagée de Frontmail. Vous n’avez pas besoin de clé de site : le SDK la récupère depuisGET /v1/public-config(mise en cache, elle n’est donc chargée qu’une fois) et le widget s’exécute dans une page HTML intégrée à l’URLhttps://mobile.frontmail.dev– rien n’est chargé depuis cette adresse.widget.html()– la page contenant le widget. Elle envoie des messages au pont JavaScriptFrontmailTurnstile(Turnstile.ANDROID_BRIDGE_NAME).Turnstile.parseMessage(data)– n’accepte que les messages exacts envoyés par la page ; renvoieToken,Expired,Errorounull.widget.navigationDecision(url, isForMainFrame)– le garde de navigation :ALLOW,OPEN_EXTERNALLY(liens à l’intérieur du widget) ouBLOCK.
@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)}Envoyez ensuite le jeton avec la turnstileKey du widget :
val widget = Turnstile.sharedWidget()webView.showTurnstile(widget, onToken = { token = it }, onExpire = { token = null })
// à l’envoi :try { frontmail.send( "tpl_contact", mapOf("email" to email, "message" to message), options = SendOptions(turnstileToken = token, turnstileKey = widget.turnstileKey), )} finally { // Un jeton ne sert qu’une fois – demandez-en un nouveau pour la tentative suivante. token = null webView.evaluateJavascript(Turnstile.RESET_SCRIPT, null)}Un jeton est valable environ 5 minutes et ne peut être utilisé qu’une fois : réinitialisez le
widget après chaque envoi, réussi ou non. Le widget normal nécessite une WebView d’environ
300 × 70 dp (compact : 150 × 140 dp).
Les requêtes de l’application n’ont pas d’en-tête Origin : Frontmail vérifie donc leurs jetons avec
sa clé secrète mobile partagée. Vous n’avez pas besoin de vos propres clés Turnstile pour cela – avec
Autoriser les applications mobiles activé, vous pouvez activer le CAPTCHA sur les modèles de
l’application même si votre organisation n’a pas encore de clés.
L’exemple complet (y compris Jetpack Compose et un pont limité à l’origine de la page avec
androidx.webkit) se trouve dans le README du SDK.
Votre propre clé de site
Section intitulée « Votre propre clé de site »Pour utiliser plutôt votre propre widget Turnstile, créez-le avec TurnstileWidget.own :
val widget = TurnstileWidget.own(siteKey = "YOUR_TURNSTILE_SITE_KEY", baseUrl = "https://example.com")widget.turnstileKey vaut alors TurnstileKey.ORG : le jeton est envoyé avec
turnstile_key: "org" et Frontmail le vérifie avec la clé secrète de
Sécurité → Protection contre les bots (Turnstile).
getStatus
Section intitulée « getStatus »val status = frontmail.getStatus(result) // ou getStatus(messageId, statusToken)status.status // MessageStatus.SENT, DELIVERED, BOUNCED, …status.events // [MessageEvent(type = "accepted", at = "…"), …]Le jeton circule dans l’en-tête X-Frontmail-Status-Token, jamais dans l’URL.
Nouvelles tentatives et idempotence
Section intitulée « Nouvelles tentatives et idempotence »Les erreurs réseau, les délais dépassés, les 5xx et les 429 sont relancés automatiquement (en
respectant Retry-After ; une attente de plus de 60 secondes n’est pas relancée), et toutes les
tentatives d’un même envoi réutilisent la même clé d’idempotence : une nouvelle tentative n’envoie
donc jamais l’e-mail deux fois. Les autres erreurs 4xx ne sont jamais relancées. Annuler la
coroutine arrête immédiatement la requête. Voir
Nouvelles tentatives et idempotence.
Chaque échec est une FrontmailException avec code, message, status (null pour les erreurs
réseau), docsUrl, details et retryAfter, ainsi que les tests isAuth, isValidation,
isRateLimit, isInsufficientCredits et isNetwork. Les codes côté client sont network_error,
timeout, invalid_response et private_key_in_browser. La façon de traiter chaque code est
décrite dans Gestion des erreurs.
Destinataires dynamiques
Section intitulée « Destinataires dynamiques »Si les champs To/CC/BCC d’un modèle utilisent des paramètres, un envoi avec la clé publique a un contenu verrouillé : le modèle doit exiger Turnstile, les champs d’expéditeur doivent être fixes, les paramètres ne peuvent pas contenir de HTML, les liens doivent pointer vers des domaines autorisés et chaque destinataire reçoit un nombre limité d’e-mails par jour.
Sécurité
Section intitulée « Sécurité »- Ne placez jamais que la clé publique (
pk_…) dans une application. Tout ce qui se trouve dans une application peut être extrait : une clé privée y serait comme publiée. Voir Clés publiques et privées. - Avec Autoriser les applications mobiles activé, les requêtes sans en-tête
Originsont acceptées de n’importe où – tout script qui connaît votre clé publique peut les envoyer. Appuyez-vous sur le rate limit par IP, sur Turnstile pour chaque modèle utilisé par l’application et sur la liste de blocage.