Preskočiť na obsah

iOS a platformy Apple (Swift)

Swift balík Frontmail umožní aplikácii pre iOS (aj pre ostatné platformy Apple) odosielať e-maily cez vaše šablóny s verejným kľúčom – bez vlastného backendu. Nemá žiadne závislosti, používa async/await a prekladá sa v jazykovom režime Swift 6.

Požiadavky: Swift 5.9+ (Xcode 15+); iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1.

Xcode: File → Add Package Dependencies… → zadajte https://github.com/frontmail-dev/frontmail-swift → knižnicu Frontmail pridajte do targetu aplikácie.

Package.swift:

dependencies: [
.package(url: "https://github.com/frontmail-dev/frontmail-swift", from: "0.1.0"),
],
targets: [
.target(name: "MyApp", dependencies: [.product(name: "Frontmail", package: "frontmail-swift")]),
]

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.

Klienta vytvorte raz (je nemenný a dá sa zdieľať medzi vláknami) a volajte send:

import Frontmail
let frontmail = try Frontmail(publicKey: "pk_4f2a…")
let result = try await frontmail.send(
templateID: "tpl_contact",
params: ["email": "jan@example.com", "message": "Dobrý deň!", "qty": 2, "items": [["sku": "A-1"]]],
serviceID: "svc_01J9…" // voliteľné – nil použije predvolenú službu šablóny
)
if result.status == .held {
// Prijaté – doručí sa o chvíľu (organizácia čaká na kredity).
}

send vracia SendResult s vlastnosťami messageID, status (.queued alebo .held) a statusToken. Keď odovzdáte súkromný kľúč, inicializátor vyhodí chybu (pozrite Zabezpečenie).

Voľby inicializátora:

let frontmail = try Frontmail(
publicKey: "pk_4f2a…",
apiURL: URL(string: "https://api.frontmail.dev")!, // predvolené
retry: RetryPolicy(maxRetries: 3, baseDelay: 0.3, maxDelay: 10), // predvolené; .disabled = bez opakovania
timeout: 15, // sekundy na jeden pokus (predvolené)
session: .shared // URLSession (predvolené)
)

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

Parametre sú [String: FrontmailValue] – hodnota JSON, ktorú zapíšete bežnými literálmi (reťazce, čísla, booleany, nil, polia a slovníky). Hodnoty z premenných zabaľte do príslušného prípadu: ["email": .string(email)].

Poslúži aj ľubovoľná hodnota Encodable, z ktorej vznikne objekt JSON. Názvy vlastností sa pošlú bez zmeny (premenujete ich cez CodingKeys), dátumy ako reťazce ISO-8601:

struct Contact: Encodable {
let email: String
let message: String
}
try await frontmail.send(templateID: "tpl_contact", params: Contact(email: email, message: message))

SendOptions nesie token Turnstile, prílohy, prípadne vlastný idempotency key a jazyk šablóny:

try await frontmail.send(
templateID: "tpl_order",
params: ["order": "1042"],
options: SendOptions(
turnstileToken: token,
attachments: [
.data(pdfData, filename: "faktura.pdf", contentType: "application/pdf"),
.upload(id: "upl_…"), // nahrané skôr cez POST /v1/uploads
]
)
)

SendOptions(locale: …) pošle e-mail v niektorom jazyku šablóny (značka BCP 47). Pre jazyk zariadenia použite Locale.preferredLanguages.first (napr. de-AT) – Locale.current.identifier obsahuje _ a odmietne sa ako invalid_body. Pri neznámom jazyku sa použije rovnaký základný jazyk a potom predvolený jazyk šablóny; result.locale je použitý jazyk:

let result = try await frontmail.send(
templateID: "tpl_contact",
params: ["name": "Jan"],
options: SendOptions(locale: Locale.preferredLanguages.first)
)
print(result.locale ?? "default") // "de"

So statusToken z výsledku send si aplikácia prečíta stav danej správy. SDK token posiela v hlavičke X-Frontmail-Status-Token, nikdy v URL:

let status = try await frontmail.status(messageID: result.messageID, statusToken: result.statusToken)
print(status.status) // .queued, .sending, .sent, .delivered, .bounced, … alebo .unknown("…")
print(status.events.map(\.type)) // ["accepted", "sent", …]

Ak šablóna vyžaduje Turnstile, zobrazte widget Cloudflare vo WKWebView a token odovzdajte do send. Site key nepotrebujete: v predvolenom stave sa použije zdieľaný mobilný kľúč Frontmailu. Turnstile.widget(client:) ho načíta z GET /v1/public-config (s cache v pamäti, takže len raz) a widget beží ako vložená HTML stránka s adresou https://mobile.frontmail.dev – z tej adresy sa nič nesťahuje.

SDK dodáva jednotlivé diely, samotné WebView patrí do vašej aplikácie:

API Čo robí
Turnstile.widget(client:) site key + adresa stránky + turnstileKey, s ktorým sa tokeny posielajú
Turnstile.html(siteKey:theme:size:action:language:) stránka s widgetom; správy posiela do script message handlera frontmailTurnstile
Turnstile.parseMessage(_:) overí správu → .token(String), .expired, .error(code:)
Turnstile.navigationAction(url:baseURL:isTopFrame:) stráženie navigácie: .allow, .cancel, .openExternally(URL)
Turnstile.isTrustedMessageOrigin(_:baseURL:) prijme správy len zo stránky widgetu
Turnstile.resetScript JavaScript, ktorý widget resetuje a vyžiada nový token

Kompletné view pre SwiftUI – skopírujte si ho do aplikácie:

import SwiftUI
import WebKit
import Frontmail
struct TurnstileView: UIViewRepresentable {
let widget: Turnstile.Widget
var theme: Turnstile.Theme = .auto
var size: Turnstile.Size = .normal
/// Zmenou hodnoty widget resetujete a získate nový token (tokeny sa dajú použiť len raz).
var resetID = 0
let onMessage: (Turnstile.Message) -> Void
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeUIView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
config.websiteDataStore = .nonPersistent()
config.preferences.javaScriptCanOpenWindowsAutomatically = false
config.userContentController.add(context.coordinator, name: Turnstile.messageHandlerName)
let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = context.coordinator
webView.uiDelegate = context.coordinator
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.isScrollEnabled = false
context.coordinator.resetID = resetID
webView.loadHTMLString(Turnstile.html(siteKey: widget.siteKey, theme: theme, size: size), baseURL: widget.baseURL)
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
context.coordinator.parent = self
if context.coordinator.resetID != resetID {
context.coordinator.resetID = resetID
webView.evaluateJavaScript(Turnstile.resetScript)
}
}
static func dismantleUIView(_ webView: WKWebView, coordinator: Coordinator) {
webView.configuration.userContentController.removeScriptMessageHandler(forName: Turnstile.messageHandlerName)
}
@MainActor
final class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate {
var parent: TurnstileView
var resetID = 0
init(_ parent: TurnstileView) { self.parent = parent }
// S aplikáciou smie hovoriť len vložená stránka (hlavný rámec s adresou baseURL).
func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.frameInfo.isMainFrame,
Turnstile.isTrustedMessageOrigin(message.frameInfo.request.url, baseURL: parent.widget.baseURL),
let parsed = Turnstile.parseMessage(message.body) else { return }
parent.onMessage(parsed)
}
// Hlavný rámec zostane na widgete, odkazy z neho sa otvoria v systémovom prehliadači.
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void
) {
switch Turnstile.navigationAction(
url: navigationAction.request.url,
baseURL: parent.widget.baseURL,
isTopFrame: navigationAction.targetFrame?.isMainFrame
) {
case .allow:
decisionHandler(.allow)
case .cancel:
decisionHandler(.cancel)
case let .openExternally(url):
decisionHandler(.cancel)
UIApplication.shared.open(url)
}
}
// window.open / target="_blank": druhé WebView nikdy nevytvárať.
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
if case let .openExternally(url) = Turnstile.navigationAction(
url: navigationAction.request.url, baseURL: parent.widget.baseURL, isTopFrame: true
) {
UIApplication.shared.open(url)
}
return nil
}
}
}

Použitie vo formulári: získať token, odoslať a widget resetovať:

struct ContactView: View {
let frontmail: Frontmail
@State private var widget: Turnstile.Widget?
@State private var token: String?
@State private var resetID = 0
@State private var email = ""
@State private var message = ""
@State private var info = ""
var body: some View {
Form {
TextField("E-mail", text: $email)
TextField("Správa", text: $message)
if let widget {
TurnstileView(widget: widget, size: .flexible, resetID: resetID) { event in
switch event {
case let .token(t): token = t
case .expired: token = nil
case let .error(code): info = "Chyba Turnstile \(code ?? "")"
}
}
.frame(height: Turnstile.Size.flexible.recommendedHeight)
}
Button("Odoslať") { Task { await submit() } }.disabled(token == nil)
Text(info)
}
.task {
do { widget = try await Turnstile.widget(client: frontmail) } catch { info = "\(error)" }
}
}
private func submit() async {
guard let widget, let token else { return }
// Token sa dá použiť len raz – nový si vyžiadajte bez ohľadu na výsledok.
defer { self.token = nil; resetID += 1 }
do {
let result = try await frontmail.send(
templateID: "tpl_contact",
params: ["email": .string(email), "message": .string(message)],
options: SendOptions(turnstileToken: token, turnstileKey: widget.turnstileKey)
)
info = result.status == .held ? "Prijaté – doručíme o chvíľu." : "Ďakujeme!"
} catch let error as FrontmailError {
info = error.message
} catch {
info = "\(error)"
}
}
}

WebView je uzamknuté na widget: načíta len vloženú stránku (baseURL) a výzvu Cloudflaru, odkazy vo widgete sa otvoria v systémovom prehliadači, druhé okno sa otvoriť nedá a do vášho kódu sa dostanú len správy zo stránky widgetu. Rovnaký Coordinator funguje aj v UIKite.

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á.

Ak chcete namiesto toho použiť vlastný widget Turnstile, vytvorte si widget sami:

let widget = Turnstile.Widget.custom(siteKey: "YOUR_TURNSTILE_SITE_KEY", baseURL: URL(string: "https://example.com")!)

Jeho turnstileKey je .org, takže SendOptions(turnstileToken: token, turnstileKey: widget.turnstileKey) pošle turnstile_key: "org" a Frontmail token overí secret key zo Zabezpečenie → Ochrana pred botmi (Turnstile).

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.

Sieťové chyby, vypršanie časového limitu, 5xx a 429 sa automaticky opakujú (v predvolenom stave trikrát, s exponenciálnym čakaním a náhodným rozptylom). SDK rešpektuje hlavičku Retry-After; ak žiada viac než 60 sekúnd, chybu vyhodí hneď. Ostatné odpovede 4xx sa nikdy neopakujú. Všetky pokusy jedného odoslania zdieľajú rovnaký idempotency key (náhodné UUID, ak neodovzdáte vlastný idempotencyKey), takže opakovanie e-mail nikdy nepošle dvakrát. Zrušenie volajúceho Task požiadavku okamžite ukončí s kódom aborted. Pozrite Opakovanie a idempotencia.

Všetko vyhadzuje FrontmailError s vlastnosťami code, message, status (nil pri chybách na strane klienta), docsURL, details a retryAfter. Klientske kódy sú network_error, timeout, aborted, invalid_response a private_key_in_browser; kontroly isAuth, isValidation, isRateLimit, isInsufficientCredits, isNetwork a isCancelled ich zoskupujú:

do {
try await frontmail.send(templateID: "tpl_contact", params: params)
} catch let error as FrontmailError where error.isRateLimit {
show("Príliš veľa správ – skúste to znova o \(Int(error.retryAfter ?? 60)) s.")
} catch let error as FrontmailError where error.code == .originNotAllowed {
assertionFailure("V dashboarde zapnite Zabezpečenie → Mobilné aplikácie → Povoliť mobilné aplikácie.")
} catch let error as FrontmailError {
show(error.message)
}

Ako reagovať na jednotlivé kódy, opisuje Spracovanie chýb.

  • Do aplikácie patrí len verejný kľúč (pk_…). Čokoľvek v balíku aplikácie sa dá vytiahnuť, takže súkromný kľúč v nej je ako zverejnený. Kľúče začínajúce sk_ SDK odmietne s chybou private_key_in_browser. 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.
  • Šablóna, ktorá berie príjemcu z parametrov (napríklad potvrdenie na adresu, ktorú používateľ zadal), je uzamknutá: potrebuje Turnstile, pevného odosielateľa, krátke parametre bez HTML a odkazy len na vaše domény. Pozrite Povolené odkazy a zámok obsahu.

SDK používateľov nesleduje, samo žiadne údaje nezbiera a nepoužíva API, pri ktorých Apple vyžaduje uviesť dôvod – to isté uvádza jeho PrivacyInfo.xcprivacy. To, čo posielajú vaše šablóny (napríklad e-mailová adresa), sú údaje, ktoré zbiera vaša aplikácia: uveďte ich v jej privacy manifeste a v údajoch o súkromí v App Store.