Skip to main content

atrg_email/
lib.rs

1#![deny(unsafe_code)]
2#![warn(missing_docs)]
3//! SMTP email sending and OTP verification for at-rust-go.
4//!
5//! Provides async email delivery via SMTP and a two-step OTP verification
6//! flow for institution/organization membership verification.
7//!
8//! When SMTP is not configured, OTPs are logged to stdout (dev mode).
9
10pub mod config;
11pub mod otp;
12pub mod send;
13
14pub use config::EmailConfig;
15pub use otp::{generate_otp, send_otp, verify_otp};
16pub use send::send_email;
17
18/// SQL for creating the OTP codes table (SQLite).
19pub const CREATE_OTP_TABLE_SQLITE: &str = r#"
20CREATE TABLE IF NOT EXISTS atrg_otp_codes (
21    id INTEGER PRIMARY KEY AUTOINCREMENT,
22    did TEXT NOT NULL,
23    email TEXT NOT NULL,
24    code TEXT NOT NULL,
25    expires_at INTEGER NOT NULL,
26    used INTEGER NOT NULL DEFAULT 0,
27    created_at INTEGER NOT NULL DEFAULT (unixepoch())
28);
29CREATE INDEX IF NOT EXISTS idx_atrg_otp_did_email ON atrg_otp_codes(did, email);
30"#;
31
32/// SQL for creating the OTP codes table (PostgreSQL).
33pub const CREATE_OTP_TABLE_POSTGRES: &str = r#"
34CREATE TABLE IF NOT EXISTS atrg_otp_codes (
35    id BIGSERIAL PRIMARY KEY,
36    did TEXT NOT NULL,
37    email TEXT NOT NULL,
38    code TEXT NOT NULL,
39    expires_at BIGINT NOT NULL,
40    used BOOLEAN NOT NULL DEFAULT FALSE,
41    created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::bigint
42);
43CREATE INDEX IF NOT EXISTS idx_atrg_otp_did_email ON atrg_otp_codes(did, email);
44"#;
45
46/// Validate that an email domain is in the allowed list.
47/// Returns `Ok(())` if allowed, `Err` with a message if not.
48pub fn validate_domain(email: &str, allowed_domains: &[String]) -> Result<(), String> {
49    if allowed_domains.is_empty() {
50        return Ok(()); // No restriction
51    }
52    let domain = email.split('@').nth(1).unwrap_or("").to_lowercase();
53    if allowed_domains.iter().any(|d| d.to_lowercase() == domain) {
54        Ok(())
55    } else {
56        Err(format!(
57            "Email domain '{}' is not allowed. Allowed: {}",
58            domain,
59            allowed_domains.join(", ")
60        ))
61    }
62}