keplars 1.11.0

Official Rust SDK for Keplars Email API
Documentation
use std::time::Duration;
use hmac::{Hmac, Mac};
use sha2::Sha256;

pub fn exponential_backoff(retry_count: u32, base_delay: Duration) -> Duration {
    let base_ms = base_delay.as_millis() as f64;
    let exponential = base_ms * 2_f64.powi(retry_count as i32);
    let jitter = rand_jitter(exponential * 0.3);
    let total = (exponential + jitter) as u64;
    Duration::from_millis(total.min(30_000))
}

fn rand_jitter(max: f64) -> f64 {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};
    use std::time::SystemTime;

    let mut hasher = DefaultHasher::new();
    SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap_or_default()
        .subsec_nanos()
        .hash(&mut hasher);
    let hash = hasher.finish();
    (hash as f64 / u64::MAX as f64) * max
}

pub fn validate_email(email: &str) -> bool {
    let parts: Vec<&str> = email.splitn(2, '@').collect();
    if parts.len() != 2 || parts[0].is_empty() {
        return false;
    }
    let domain_parts: Vec<&str> = parts[1].splitn(2, '.').collect();
    domain_parts.len() == 2 && !domain_parts[0].is_empty() && !domain_parts[1].is_empty()
}

pub fn verify_webhook_signature(
    payload: &str,
    signature: &str,
    timestamp: &str,
    secret: &str,
    tolerance_seconds: Option<i64>,
) -> bool {
    let tolerance = tolerance_seconds.unwrap_or(300);
    let now = std::time::SystemTime::now()
        .duration_since(std::time::SystemTime::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs() as i64;

    let payload_ts: i64 = match timestamp.parse() {
        Ok(t) => t,
        Err(_) => return false,
    };

    if (now - payload_ts).abs() > tolerance {
        return false;
    }

    let signed_payload = format!("{}.{}", timestamp, payload);
    let mut mac = match Hmac::<Sha256>::new_from_slice(secret.as_bytes()) {
        Ok(m) => m,
        Err(_) => return false,
    };
    mac.update(signed_payload.as_bytes());
    let expected = hex::encode(mac.finalize().into_bytes());

    constant_time_eq(signature.as_bytes(), expected.as_bytes())
}

fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
}