email-cli 0.6.1

Agent-friendly email CLI for Resend
use anyhow::{Context, Result, bail};
use reqwest::blocking::Response;
use reqwest::header::HeaderMap;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::thread::sleep;
use std::time::Duration;

pub fn backoff(attempt: usize) -> Duration {
    let millis = 700_u64.saturating_mul((attempt as u64) + 1);
    Duration::from_millis(millis)
}

pub fn retry_delay(headers: &HeaderMap, attempt: usize) -> Duration {
    headers
        .get("retry-after")
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.parse::<u64>().ok())
        .map(Duration::from_secs)
        .unwrap_or_else(|| backoff(attempt))
}

pub fn should_retry_error(err: &reqwest::Error) -> bool {
    err.is_timeout() || err.is_connect() || err.is_request()
}

pub fn decode_json<T: DeserializeOwned>(response: Response) -> Result<T> {
    let status = response.status();
    let text = response.text().context("failed to read http response")?;
    if !status.is_success() {
        bail!("Resend API {}: {}", status, extract_error_message(&text));
    }
    serde_json::from_str(&text).context("failed to decode json response")
}

pub fn decode_bytes(response: Response) -> Result<Vec<u8>> {
    let status = response.status();
    if !status.is_success() {
        let text = response.text().unwrap_or_default();
        bail!(
            "download failed {}: {}",
            status,
            extract_error_message(&text)
        );
    }
    response
        .bytes()
        .map(|body| body.to_vec())
        .context("failed to read body")
}

pub fn extract_error_message(body: &str) -> String {
    serde_json::from_str::<Value>(body)
        .ok()
        .and_then(|value| {
            value
                .get("message")
                .and_then(|message| message.as_str())
                .map(|message| message.to_string())
        })
        .unwrap_or_else(|| body.to_string())
}

pub fn fetch_sent_detail(client: &crate::resend::ResendClient, id: &str) -> Option<crate::models::SentEmail> {
    for attempt in 0..3 {
        if let Ok(detail) = client.get_sent_email(id) {
            return Some(detail);
        }
        sleep(Duration::from_millis(300 * ((attempt as u64) + 1)));
    }
    None
}