Taifa MailTaifa Mail Docs
SDKs

Rust SDK

Async Rust SDK for Taifa Mail. Send transactional and bulk email, manage domains, contacts, suppressions, templates, and webhooks.

The Taifa Mail Rust SDK is published as the taifa-mail crate on crates.io. It is an async client built on reqwest and serde, with a single transport layer that owns bearer authentication, JSON encoding, retries on 429 and 5xx (honouring Retry-After), and error mapping to a typed TaifaMailError.

Source lives in the taifa-mail-sdks repository.


Installation

Add the crate with cargo:

cargo add taifa-mail

The client is async, so you also need a runtime. We use tokio throughout the examples:

cargo add tokio --features full

Every SDK call is a future. You must .await it inside an async runtime such as tokio or async-std. The examples below use #[tokio::main].


Client setup

Construct a client with your API key. Keys start with tfm_k_ and are passed as a bearer token on every request.

use taifa_mail::Taifa Mail;
 
let client = Taifa Mail::new(std::env::var("TAIFA_MAIL_API_KEY").unwrap())?;

Taifa Mail::new uses the defaults: base URL https://govconnect.ke, 3 total attempts on retryable failures, and a 30 second per-request timeout. To override any of these, use the builder:

use std::time::Duration;
use taifa_mail::Taifa Mail;
 
let client = Taifa Mail::builder(std::env::var("TAIFA_MAIL_API_KEY").unwrap())
    .base_url("https://govconnect.ke")
    .max_retries(5)
    .timeout(Duration::from_secs(60))
    .build()?;
OptionMethodDefaultDescription
Base URL.base_url(...)https://govconnect.keOverride the API host (trailing slashes are trimmed).
Max retries.max_retries(u32)3Total attempts on 429 / 5xx, including the first. Floored at 1.
Timeout.timeout(Duration)30 secondsPer-request timeout.

Resources are reached through accessor methods on the client: client.emails(), client.domains(), client.contacts(), client.suppressions(), client.templates(), and client.webhooks(). The client is Clone and cheap to share across tasks.


Send an email

A full, runnable example. SendEmail::builder takes the sender, recipients, and subject, then chained setters add the body and optional fields. Every send returns Result<SendEmailResponse, TaifaMailError>.

use taifa_mail::{Taifa Mail, SendEmail};
 
#[tokio::main]
async fn main() -> Result<(), taifa_mail::TaifaMailError> {
    let client = Taifa Mail::new(std::env::var("TAIFA_MAIL_API_KEY").unwrap())?;
 
    let message = SendEmail::builder(
        ("hello@yourdomain.com", "Your Company"),
        "customer@example.com",
        "Your receipt",
    )
    .html("<h1>Thanks for your order.</h1>")
    .text("Thanks for your order.")
    .tags(vec!["receipts".to_string()])
    .build();
 
    let res = client.emails().send(&message).await?;
    println!("queued message {} ({})", res.id, res.status);
 
    Ok(())
}

The sender and each recipient are an Address. There is sugar so you rarely build one by hand:

  • A bare &str or String becomes { email } (via From<&str>).
  • A (&str, &str) tuple becomes { email, name }.
  • Anywhere a recipient list is expected (to, cc, bcc), a single address or a Vec of them both work, thanks to IntoAddressList.
use taifa_mail::{Address, SendEmail};
 
let message = SendEmail::builder(
    Address::named("hello@yourdomain.com", "Your Company"),
    vec!["a@example.com", "b@example.com"],
    "Weekly digest",
)
.cc("manager@example.com")
.reply_to("support@yourdomain.com")
.send_at("2026-07-01T09:00:00Z") // ISO 8601, Starter plan and up
.build();

On the wire the sender field is serialized as from_ (with a trailing underscore). The SDK hides that quirk: you always work with a clean from argument and Address.


Emails

The emails() resource sends, looks up, searches, schedules, and inspects messages.

// Send a batch (Starter plan and up).
let batch = client.emails().send_batch(&[message_a, message_b]).await?;
println!("sent {}/{}", batch.sent, batch.total);
 
// Dry-run validation: would this send, without sending it?
let check = client.emails().validate(&message).await?;
if !check.can_send {
    for issue in &check.issues {
        eprintln!("{}: {}", issue.field, issue.error);
    }
}
 
// List recent emails (page is zero-based).
let emails = client.emails().list(Some("delivered"), Some(0), Some(20)).await?;
 
// Fetch one email with its bodies and events.
let detail = client.emails().get("a1b2c3d4-...").await?;
 
// Event timeline for a message.
let events = client.emails().events("a1b2c3d4-...").await?;
 
// Re-send a bounced / rejected / failed message as a new one.
let retried = client.emails().retry("a1b2c3d4-...").await?;
 
// Search with inline tokens (to:, from:, status:, domain:, tag:).
let hits = client.emails()
    .search(Some("welcome status:delivered"), None, None, Some(0), Some(20))
    .await?;
 
// Scheduled sends.
let scheduled = client.emails().list_scheduled().await?;
client.emails().cancel_scheduled("b2c3d4e5-...").await?;
client.emails().send_scheduled_now("b2c3d4e5-...").await?;
 
// Poll for status changes since a timestamp (capped at 50 rows).
let updates = client.emails().updates("2026-06-13T00:00:00Z").await?;

Domains

The domains() resource registers, verifies, inspects, and transfers sending domains.

use taifa_mail::TransferDomain;
 
// List your sending domains and their status.
let domains = client.domains().list().await?;
 
// Register a domain; the response carries the DNS records to publish.
let domain = client.domains().create("yourdomain.com").await?;
for record in &domain.dns_records {
    println!("{} {} -> {}", record.record_type, record.host, record.value);
}
 
// Re-check DNS and verify.
let verified = client.domains().verify(&domain.id).await?;
 
// Live health checks (DKIM, SPF, DMARC, return-path, MX).
let health = client.domains().health(&domain.id).await?;
println!("{} ok, {} warn", health.summary.ok, health.summary.warn);
 
// Diagnose and score configuration issues.
let diagnosis = client.domains().diagnose(&domain.id).await?;
 
// Rotate the DKIM key.
let rotation = client.domains().rotate_dkim(&domain.id).await?;
 
// Transfer the domain to another Taifa Mail account.
let transfer = client.domains()
    .transfer(&domain.id, &TransferDomain::new("new-owner@example.com").note("handover"))
    .await?;
 
// Check availability or existence before adding.
let availability = client.domains().check_availability("yourdomain.com").await?;
let existing = client.domains().check("yourdomain.com").await?;

Contacts

The contacts() resource manages subscriber lists, individual contacts, CSV imports, and templated bulk sends.

use taifa_mail::{AddContact, BulkSend, CreateList, UpdateList};
 
// Create and list subscriber lists.
let list = client.contacts()
    .create_list(&CreateList::new("Newsletter").description("Monthly updates"))
    .await?;
let lists = client.contacts().list_lists().await?;
 
// Get a list with a page of its contacts (zero-based page).
let detail = client.contacts().get_list(&list.id, Some(0), Some(50)).await?;
 
// Update or delete a list.
client.contacts().update_list(&list.id, &UpdateList::default().name("Weekly")).await?;
client.contacts().delete_list(&list.id).await?;
 
// Add and remove individual contacts.
let contact = client.contacts()
    .add_contact(&list.id, &AddContact::new("jane@example.com").name("Jane Doe"))
    .await?;
client.contacts().remove_contact(&list.id, &contact.id).await?;
 
// Import contacts from a CSV file (header row required).
let csv = std::fs::read("contacts.csv").unwrap();
let imported = client.contacts().upload_csv(&list.id, csv, "contacts.csv").await?;
println!("{} imported, {} skipped", imported.imported, imported.skipped);
 
// Templated bulk send to every contact on the list.
let result = client.contacts()
    .bulk_send(
        &list.id,
        &BulkSend::new("sender-address-id", "Hello {{name}}")
            .html("<p>Hi {{name}}!</p>"),
    )
    .await?;
println!("{} queued, {} skipped", result.queued, result.skipped);

Suppressions

The suppressions() resource manages the do-not-send list. Its list endpoint returns a paginated Page<Suppression> envelope rather than a bare array.

use taifa_mail::AddSuppression;
 
// List suppressed addresses (paginated; zero-based page).
let page = client.suppressions().list(Some(0), Some(50), None).await?;
println!("{} of {} suppressed", page.items.len(), page.total);
 
// Suppress a single address.
client.suppressions()
    .add(&AddSuppression::new("bounced@example.com").reason("hard_bounce"))
    .await?;
 
// Bulk-import from a file (one email per line).
let file = std::fs::read("suppressions.txt").unwrap();
let result = client.suppressions().bulk_upload(file, "suppressions.txt").await?;
println!("{} added, {} skipped", result.added, result.skipped);
 
// Remove an address from the list.
client.suppressions().remove("suppression-id").await?;

Templates

The templates() resource manages reusable templates (Starter plan and up). On the wire html and text map to html_body and text_body; the SDK exposes the clean names.

use taifa_mail::{CreateTemplate, UpdateTemplate};
 
// Create a template; variables are derived server-side from {{placeholders}}.
let template = client.templates()
    .create(
        &CreateTemplate::new("Welcome")
            .subject("Welcome, {{name}}")
            .html("<p>Hi {{name}}, thanks for joining.</p>"),
    )
    .await?;
 
// List, fetch, update, duplicate, delete.
let templates = client.templates().list().await?;
let one = client.templates().get(&template.id).await?;
client.templates().update(&template.id, &UpdateTemplate::default().subject("Welcome aboard")).await?;
let copy = client.templates().duplicate(&template.id).await?;
client.templates().delete(&template.id).await?;

Webhooks

The webhooks() resource manages event subscriptions and inspects deliveries. The delivery list returns a paginated Page<WebhookDelivery> envelope.

use taifa_mail::{CreateWebhook, UpdateWebhook};
 
// Create a webhook; the signing secret is generated and returned.
let webhook = client.webhooks()
    .create(&CreateWebhook::new(
        "https://example.com/hooks/taifamail",
        vec!["email.delivered".to_string(), "email.bounced".to_string()],
    ))
    .await?;
println!("signing secret: {}", webhook.secret);
 
// List, update, delete.
let webhooks = client.webhooks().list().await?;
client.webhooks().update(&webhook.id, &UpdateWebhook::default().is_active(false)).await?;
client.webhooks().delete(&webhook.id).await?;
 
// Queue a test delivery.
let test = client.webhooks().test(&webhook.id).await?;
 
// Inspect deliveries (paginated), then drill into one.
let deliveries = client.webhooks().list_deliveries(&webhook.id, Some(0), Some(20), None).await?;
let delivery = client.webhooks().get_delivery(&webhook.id, "delivery-id").await?;

Error handling

Every fallible call returns Result<T, TaifaMailError> (the crate also exports a Result<T> alias). TaifaMailError is raised for any non-2xx response or for a transport failure that survives all retries. It implements std::error::Error and Display, so it composes with ? and the wider error ecosystem.

pub struct TaifaMailError {
    pub status: u16,            // HTTP status; 0 means a transport/network failure
    pub code: Option<String>,  // machine-readable code from the API body, if present
    pub message: String,       // human-readable message
}

Branch on status and code to handle specific failures:

match client.emails().send(&message).await {
    Ok(res) => println!("queued {}", res.id),
    Err(err) => match err.status {
        0 => eprintln!("network error: {}", err.message),
        422 => eprintln!("invalid request ({:?}): {}", err.code, err.message),
        429 => eprintln!("rate limited: {}", err.message),
        _ => eprintln!("taifamail error {}: {}", err.status, err.message),
    },
}

A status of 0 indicates a transport or network failure with no HTTP response. The transport already retries 429 and 5xx responses (honouring Retry-After) up to max_retries, so an error reaching your code means the retries were exhausted.


Configuration

SettingWhereDefaultNotes
API keyTaifa Mail::new(key) / Taifa Mail::builder(key)requiredMust be a valid tfm_k_ key; passed as a bearer token.
Base URL.base_url(...)https://govconnect.keTrailing slashes trimmed.
Max retries.max_retries(u32)3Total attempts on 429 / 5xx. Floored at 1.
Timeout.timeout(Duration)30 secondsPer-request timeout via reqwest.

Read the API key from the environment rather than hard-coding it:

let client = Taifa Mail::new(std::env::var("TAIFA_MAIL_API_KEY").expect("TAIFA_MAIL_API_KEY not set"))?;

Never commit API keys. Load them from environment variables or a secrets manager, and scope each key to the minimum permissions it needs.


Next steps

On this page