Rust · Email breach checks
Check email breaches in Rust with reqwest and serde
Breach records arrive with PascalCase keys like Title, BreachDate and DataClasses. In Rust you keep idiomatic snake_case field names and add #[serde(rename_all = "PascalCase")] to the Breach struct, and serde maps breach_date to BreachDate and data_classes to DataClasses for you. Declare only the fields you need; serde ignores unknown keys by default, so the struct stays small and focused on what your code reads.
The main function retries up to three times inside a loop and matches on res.status(). A 429 reads the Retry-After header before consuming the body, then inspects error.code so a quota_exceeded response ends the program instead of sleeping. Both 400 and 401 return descriptive errors through Box<dyn Error>, and any other status stops without retrying, since a 5xx lookup may already have been charged.
Steps
- 1
Create a binary with cargo new breach-check and add reqwest, serde, serde_json and tokio.
- 2
Replace src/main.rs with the example and export FINOWL_API_KEY.
- 3
Run cargo run -- someone@example.com to check an address.
- 4
Use is_verified and data_classes on each Breach to decide what to tell the user.
- 5
Move the request into a shared client stored in your web framework's application state.
cargo add reqwest --features json
cargo add serde --features derive
cargo add serde_json thiserror
cargo add tokio --features fulluse reqwest::{header::RETRY_AFTER, Client, StatusCode};
use serde::Deserialize;
use serde_json::{json, Value};
use std::{env, error::Error, time::Duration};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Breach {
title: String,
breach_date: String,
data_classes: Vec<String>,
is_verified: bool,
}
#[derive(Debug, Deserialize)]
struct EmailBreaches {
email: String,
breached: bool,
breaches: Vec<Breach>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let api_key = env::var("FINOWL_API_KEY")?;
let email = env::args().nth(1).unwrap_or_else(|| "alex@example.com".to_string());
let client = Client::builder().timeout(Duration::from_secs(15)).build()?;
for _ in 0..3 {
let res = client
.post("https://api.finowl.co/v1/breaches/email")
.bearer_auth(&api_key)
.json(&json!({ "email": email }))
.send()
.await?;
match res.status() {
StatusCode::OK => {
let body: EmailBreaches = res.json().await?;
if !body.breached {
println!("No known breaches for {}", body.email);
}
for b in &body.breaches {
let flag = if b.is_verified { "" } else { " (unverified)" };
println!("{} ({}){}: {}", b.title, b.breach_date, flag, b.data_classes.join(", "));
}
return Ok(());
}
StatusCode::BAD_REQUEST => return Err("Finowl rejected the email as invalid_input".into()),
StatusCode::UNAUTHORIZED => return Err("FINOWL_API_KEY is missing or invalid".into()),
StatusCode::TOO_MANY_REQUESTS => {
let secs = res.headers().get(RETRY_AFTER)
.and_then(|v| v.to_str().ok()?.parse::<u64>().ok())
.unwrap_or(5);
let body: Value = res.json().await.unwrap_or_default();
if body["error"]["code"] == "quota_exceeded" {
return Err("out of Finowl credits".into());
}
tokio::time::sleep(Duration::from_secs(secs)).await;
}
other => return Err(format!("Finowl returned {other}; not retrying").into()),
}
}
Err("still rate limited after 3 attempts".into())
}IN PRODUCTION
Production tips
Add a field to Breach only when you read it. A small struct documents exactly which parts of the response your code relies on and keeps allocations down for addresses with many breaches.
Parse breach_date with chrono or the time crate if you sort or filter by date. Finowl sends it as a plain YYYY-MM-DD string, which NaiveDate or time::Date represents exactly.
When checking many addresses, bound concurrency with a tokio Semaphore or buffer_unordered so you stay under 60 requests per minute instead of relying on 429 responses to slow you down.
Each lookup costs 1 credit, with 100 free credits every month. See pricing and error codes.
A FEW GOOD QUESTIONS
Common questions
Related guides
- Check breached passwords in Rust with reqwest
- Batch gender predictions in Rust with serde
- Check email breaches in Python with requests
- Check email breaches in Node.js with fetch
- Check email breaches in TypeScript with typed results
- Check email breaches in Go with net/http
- Email breach checks for fintech signups and logins
- Email breach checks for online stores and shopper logins
- Email breach checks for SaaS logins and admins
Check email breaches in Rust with reqwest and serde
Grab an API key, paste the example and run it. 100 free credits every month.