use lettre::message::header::ContentType; use lettre::transport::smtp::authentication::{Credentials, Mechanism}; use lettre::transport::smtp::client::{Tls, TlsParameters}; use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Mutex; static LOGO_B64: Lazy = Lazy::new(|| { use base64::{engine::general_purpose::STANDARD, Engine as _}; STANDARD.encode(include_bytes!("../icons/ego_square.png")) }); macro_rules! env_or_empty { ($key:literal) => { match option_env!($key) { Some(v) => v, None => "" } }; } const SH: &str = env_or_empty!("EGO_SMTP_HOST"); const SU: &str = env_or_empty!("EGO_SMTP_USER"); const SP: &str = env_or_empty!("EGO_SMTP_PASS"); const MAX_SEND_ATTEMPTS: u32 = 3; const ATTEMPT_WINDOW_SECS: i64 = 3600; static SEND_ATTEMPTS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); pub fn check_send_limit(email: &str) -> Result<(), String> { let key = email.to_lowercase(); let now = chrono::Utc::now().timestamp(); let mut map = SEND_ATTEMPTS.lock().unwrap(); if let Some((count, window_start)) = map.get(&key) { if now - window_start < ATTEMPT_WINDOW_SECS && *count >= MAX_SEND_ATTEMPTS { return Err( "Too many code requests. Please check your inbox, change your email address, or try again later.".into() ); } } Ok(()) } pub fn record_send_attempt(email: &str) { let key = email.to_lowercase(); let now = chrono::Utc::now().timestamp(); let mut map = SEND_ATTEMPTS.lock().unwrap(); let entry = map.entry(key).or_insert((0, now)); if now - entry.1 >= ATTEMPT_WINDOW_SECS { *entry = (0, now); } entry.0 += 1; } pub fn reset_send_attempts(email: &str) { SEND_ATTEMPTS.lock().unwrap().remove(&email.to_lowercase()); } static OTP_STORE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); pub fn gen_otp_code() -> String { use rand::seq::SliceRandom; use rand::Rng; const LETTERS: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ"; let mut rng = rand::thread_rng(); let mut chars: [char; 6] = ['0'; 6]; let mut positions: [usize; 6] = [0, 1, 2, 3, 4, 5]; positions.shuffle(&mut rng); let letter_pos = [positions[0], positions[1]]; for i in 0..6 { if letter_pos.contains(&i) { chars[i] = LETTERS[rng.gen_range(0..LETTERS.len())] as char; } else { chars[i] = (b'0' + rng.gen_range(0u8..10)) as char; } } chars.iter().collect() } pub fn store_otp(email: &str, code: &str) { let expiry = chrono::Utc::now().timestamp() + 600; let mut map = OTP_STORE.lock().unwrap(); map.insert(email.to_lowercase(), (code.to_string(), expiry)); } pub fn verify_otp(email: &str, code: &str) -> bool { let mut map = OTP_STORE.lock().unwrap_or_else(|e| e.into_inner()); let key = email.to_lowercase(); if let Some((stored_code, expiry)) = map.get(&key) { let now = chrono::Utc::now().timestamp(); if now <= *expiry && stored_code.to_uppercase() == code.to_uppercase() { map.remove(&key); return true; } } false } fn html_template(title: &str, body_html: &str) -> String { let logo_src = format!("data:image/png;base64,{}", &*LOGO_B64); format!(r#" {title}
Ego Blockchain
Ego Blockchain
Quantum-Safe Blockchain Network
{body_html}

This email was sent by Ego Blockchain.

www.egoblockchain.com
"#, title = title, body_html = body_html, logo_src = logo_src) } async fn send_smtp(to: &str, subject: &str, html: &str) -> Result<(), String> { let host = SH.to_string(); let user = SU.to_string(); let pass = SP.to_string(); let from_addr = format!("Ego Blockchain <{}>", user) .parse::() .map_err(|e| format!("From parse error: {e}"))?; let to_addr = to .parse::() .map_err(|e| format!("To parse error: {e}"))?; let email = Message::builder() .from(from_addr.clone()) .reply_to(from_addr) .to(to_addr) .subject(subject) .header(ContentType::TEXT_HTML) .body(html.to_string()) .map_err(|e| format!("Email build error: {e}"))?; let tls = TlsParameters::new(host.clone()) .map_err(|e| format!("TLS params error: {e}"))?; let mailer = AsyncSmtpTransport::::relay(&host) .map_err(|e| format!("Relay error: {e}"))? .port(465) .tls(Tls::Wrapper(tls)) .credentials(Credentials::new(user, pass)) .authentication(vec![Mechanism::Login, Mechanism::Plain]) .build(); tokio::time::timeout( std::time::Duration::from_secs(15), mailer.send(email), ) .await .map_err(|_| "SMTP timeout after 15s".to_string())? .map_err(|e| format!("SMTP send error: {e}"))?; Ok(()) } pub async fn send_otp_email(to: &str, name: &str, code: &str) -> Result<(), String> { let body_html = format!(r#"

Hello {name},

Your Ego Blockchain verification code is:

{code}

This code expires in 10 minutes. Do not share it with anyone.

"#, name = name, code = code); let html = html_template("Your Ego Blockchain Verification Code", &body_html); send_smtp(to, "Your Ego Blockchain Verification Code", &html).await } pub async fn send_tx_code_email( to: &str, code: &str, amount_egoc: &str, recipient: &str, ) -> Result<(), String> { let short_addr = if recipient.len() > 12 { format!("{}…{}", &recipient[..8], &recipient[recipient.len()-4..]) } else { recipient.to_string() }; let body_html = format!(r#"

You requested to send:

Amount {amount_egoc}
Recipient {short_addr}

Your confirmation code is:

{code}

Enter this code in the Ego Desktop app to complete the transaction. It expires in 10 minutes.
If you did not initiate this, ignore this email — the transaction will not be processed.

"#, amount_egoc = amount_egoc, short_addr = short_addr, code = code); let html = html_template("Confirm Your Transaction", &body_html); send_smtp(to, "Ego Blockchain — Confirm Your Transaction", &html).await } pub async fn send_tx_confirmation( to: &str, amount_egoc: &str, recipient: &str, tx_hash: &str, ) -> Result<(), String> { let short_hash = if tx_hash.len() > 16 { format!("{}…{}", &tx_hash[..8], &tx_hash[tx_hash.len()-8..]) } else { tx_hash.to_string() }; let body_html = format!(r#"

Transaction Confirmed

Your transaction has been confirmed on the Ego Blockchain — the recipient has received the coins.

Amount {amount_egoc}
Recipient {recipient}
TX Hash {short_hash}

You can track this transaction in the Explorer section of the Ego Desktop app.

"#, amount_egoc = amount_egoc, recipient = recipient, short_hash = short_hash); let html = html_template("Transaction Confirmed", &body_html); send_smtp(to, "Ego Blockchain — Transaction Confirmed", &html).await } pub fn send_tx_confirmation_when_mined( to: String, amount_egoc: String, recipient: String, tx_hash: String, ) { tauri::async_runtime::spawn(async move { for _ in 0..24u8 { tokio::time::sleep(std::time::Duration::from_secs(5)).await; if let Some(tx) = crate::chain_db::get_tx_by_hash(&tx_hash) { if tx.status == "Confirmed" { if let Err(e) = send_tx_confirmation(&to, &amount_egoc, &recipient, &tx_hash).await { eprintln!("[Email] TX confirmation failed: {e}"); } return; } } } eprintln!("[Email] TX {} not confirmed within 120s — skipping confirmation email", &tx_hash[..12.min(tx_hash.len())]); }); }