move repos into monorepo
This commit is contained in:
489
lib/osiris/core/objects/communication/email.rs
Normal file
489
lib/osiris/core/objects/communication/email.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
/// Email Client
|
||||
///
|
||||
/// Real SMTP email client for sending emails including verification emails.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::verification::Verification;
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use lettre::{
|
||||
Message, SmtpTransport, Transport,
|
||||
message::{header::ContentType, MultiPart, SinglePart},
|
||||
transport::smtp::authentication::Credentials,
|
||||
};
|
||||
|
||||
/// Email client with SMTP configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
|
||||
pub struct EmailClient {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// SMTP server hostname
|
||||
pub smtp_host: String,
|
||||
|
||||
/// SMTP port
|
||||
pub smtp_port: u16,
|
||||
|
||||
/// Username for SMTP auth
|
||||
pub username: String,
|
||||
|
||||
/// Password for SMTP auth
|
||||
pub password: String,
|
||||
|
||||
/// From address
|
||||
pub from_address: String,
|
||||
|
||||
/// From name
|
||||
pub from_name: String,
|
||||
|
||||
/// Use TLS
|
||||
pub use_tls: bool,
|
||||
}
|
||||
|
||||
/// Mail template with placeholders
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
|
||||
pub struct MailTemplate {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Template ID
|
||||
pub id: String,
|
||||
|
||||
/// Template name
|
||||
pub name: String,
|
||||
|
||||
/// Email subject (can contain placeholders like ${name})
|
||||
pub subject: String,
|
||||
|
||||
/// Email body (can contain placeholders like ${code}, ${url})
|
||||
pub body: String,
|
||||
|
||||
/// HTML body (optional, can contain placeholders)
|
||||
pub html_body: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for MailTemplate {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
id: String::new(),
|
||||
name: String::new(),
|
||||
subject: String::new(),
|
||||
body: String::new(),
|
||||
html_body: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Email message created from a template
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Mail {
|
||||
/// Recipient email address
|
||||
pub to: String,
|
||||
|
||||
/// Template ID to use
|
||||
pub template_id: Option<String>,
|
||||
|
||||
/// Parameters to replace in template
|
||||
pub parameters: std::collections::HashMap<String, String>,
|
||||
|
||||
/// Direct subject (if not using template)
|
||||
pub subject: Option<String>,
|
||||
|
||||
/// Direct body (if not using template)
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for EmailClient {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
smtp_host: "localhost".to_string(),
|
||||
smtp_port: 587,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
from_address: "noreply@example.com".to_string(),
|
||||
from_name: "No Reply".to_string(),
|
||||
use_tls: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MailTemplate {
|
||||
/// Create a new mail template
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Builder: Set template ID
|
||||
pub fn id(mut self, id: String) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set template name
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set subject
|
||||
pub fn subject(mut self, subject: String) -> Self {
|
||||
self.subject = subject;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set body
|
||||
pub fn body(mut self, body: String) -> Self {
|
||||
self.body = body;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set HTML body
|
||||
pub fn html_body(mut self, html_body: String) -> Self {
|
||||
self.html_body = Some(html_body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Replace placeholders in text
|
||||
fn replace_placeholders(&self, text: &str, parameters: &std::collections::HashMap<String, String>) -> String {
|
||||
let mut result = text.to_string();
|
||||
for (key, value) in parameters {
|
||||
let placeholder = format!("${{{}}}", key);
|
||||
result = result.replace(&placeholder, value);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Render subject with parameters
|
||||
pub fn render_subject(&self, parameters: &std::collections::HashMap<String, String>) -> String {
|
||||
self.replace_placeholders(&self.subject, parameters)
|
||||
}
|
||||
|
||||
/// Render body with parameters
|
||||
pub fn render_body(&self, parameters: &std::collections::HashMap<String, String>) -> String {
|
||||
self.replace_placeholders(&self.body, parameters)
|
||||
}
|
||||
|
||||
/// Render HTML body with parameters
|
||||
pub fn render_html_body(&self, parameters: &std::collections::HashMap<String, String>) -> Option<String> {
|
||||
self.html_body.as_ref().map(|html| self.replace_placeholders(html, parameters))
|
||||
}
|
||||
}
|
||||
|
||||
impl Mail {
|
||||
/// Create a new mail
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Builder: Set recipient
|
||||
pub fn to(mut self, to: String) -> Self {
|
||||
self.to = to;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set template ID
|
||||
pub fn template(mut self, template_id: String) -> Self {
|
||||
self.template_id = Some(template_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Add a parameter
|
||||
pub fn parameter(mut self, key: String, value: String) -> Self {
|
||||
self.parameters.insert(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set subject (for non-template emails)
|
||||
pub fn subject(mut self, subject: String) -> Self {
|
||||
self.subject = Some(subject);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set body (for non-template emails)
|
||||
pub fn body(mut self, body: String) -> Self {
|
||||
self.body = Some(body);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailClient {
|
||||
/// Create a new email client
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Builder: Set SMTP host
|
||||
pub fn smtp_host(mut self, host: String) -> Self {
|
||||
self.smtp_host = host;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set SMTP port
|
||||
pub fn smtp_port(mut self, port: u16) -> Self {
|
||||
self.smtp_port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set username
|
||||
pub fn username(mut self, username: String) -> Self {
|
||||
self.username = username;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set password
|
||||
pub fn password(mut self, password: String) -> Self {
|
||||
self.password = password;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set from address
|
||||
pub fn from_address(mut self, address: String) -> Self {
|
||||
self.from_address = address;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set from name
|
||||
pub fn from_name(mut self, name: String) -> Self {
|
||||
self.from_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set use TLS
|
||||
pub fn use_tls(mut self, use_tls: bool) -> Self {
|
||||
self.use_tls = use_tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build SMTP transport
|
||||
fn build_transport(&self) -> Result<SmtpTransport, String> {
|
||||
let creds = Credentials::new(
|
||||
self.username.clone(),
|
||||
self.password.clone(),
|
||||
);
|
||||
|
||||
let transport = if self.use_tls {
|
||||
SmtpTransport::starttls_relay(&self.smtp_host)
|
||||
.map_err(|e| format!("Failed to create SMTP transport: {}", e))?
|
||||
.credentials(creds)
|
||||
.port(self.smtp_port)
|
||||
.build()
|
||||
} else {
|
||||
SmtpTransport::builder_dangerous(&self.smtp_host)
|
||||
.credentials(creds)
|
||||
.port(self.smtp_port)
|
||||
.build()
|
||||
};
|
||||
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
/// Send a plain text email
|
||||
pub fn send_email(
|
||||
&self,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
body: &str,
|
||||
) -> Result<(), String> {
|
||||
let from_mailbox = format!("{} <{}>", self.from_name, self.from_address)
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid from address: {}", e))?;
|
||||
|
||||
let to_mailbox = to.parse()
|
||||
.map_err(|e| format!("Invalid to address: {}", e))?;
|
||||
|
||||
let email = Message::builder()
|
||||
.from(from_mailbox)
|
||||
.to(to_mailbox)
|
||||
.subject(subject)
|
||||
.body(body.to_string())
|
||||
.map_err(|e| format!("Failed to build email: {}", e))?;
|
||||
|
||||
let transport = self.build_transport()?;
|
||||
|
||||
transport.send(&email)
|
||||
.map_err(|e| format!("Failed to send email: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send an HTML email
|
||||
pub fn send_html_email(
|
||||
&self,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
html_body: &str,
|
||||
text_body: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let from_mailbox = format!("{} <{}>", self.from_name, self.from_address)
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid from address: {}", e))?;
|
||||
|
||||
let to_mailbox = to.parse()
|
||||
.map_err(|e| format!("Invalid to address: {}", e))?;
|
||||
|
||||
// Build multipart email with text and HTML alternatives
|
||||
let text_part = if let Some(text) = text_body {
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(text.to_string())
|
||||
} else {
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::new())
|
||||
};
|
||||
|
||||
let html_part = SinglePart::builder()
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html_body.to_string());
|
||||
|
||||
let multipart = MultiPart::alternative()
|
||||
.singlepart(text_part)
|
||||
.singlepart(html_part);
|
||||
|
||||
let email = Message::builder()
|
||||
.from(from_mailbox)
|
||||
.to(to_mailbox)
|
||||
.subject(subject)
|
||||
.multipart(multipart)
|
||||
.map_err(|e| format!("Failed to build email: {}", e))?;
|
||||
|
||||
let transport = self.build_transport()?;
|
||||
|
||||
transport.send(&email)
|
||||
.map_err(|e| format!("Failed to send email: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a mail using a template
|
||||
pub fn send_mail(&self, mail: &Mail, template: &MailTemplate) -> Result<(), String> {
|
||||
// Render subject and body with parameters
|
||||
let subject = template.render_subject(&mail.parameters);
|
||||
let body_text = template.render_body(&mail.parameters);
|
||||
let html_body = template.render_html_body(&mail.parameters);
|
||||
|
||||
// Send email
|
||||
if let Some(html) = html_body {
|
||||
self.send_html_email(&mail.to, &subject, &html, Some(&body_text))
|
||||
} else {
|
||||
self.send_email(&mail.to, &subject, &body_text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a verification email with code
|
||||
pub fn send_verification_code_email(
|
||||
&self,
|
||||
verification: &Verification,
|
||||
) -> Result<(), String> {
|
||||
let subject = "Verify your email address";
|
||||
let body = format!(
|
||||
"Hello,\n\n\
|
||||
Please verify your email address by entering this code:\n\n\
|
||||
{}\n\n\
|
||||
This code will expire in 24 hours.\n\n\
|
||||
If you didn't request this, please ignore this email.",
|
||||
verification.verification_code
|
||||
);
|
||||
|
||||
self.send_email(&verification.contact, subject, &body)
|
||||
}
|
||||
|
||||
/// Send a verification email with URL link
|
||||
pub fn send_verification_link_email(
|
||||
&self,
|
||||
verification: &Verification,
|
||||
) -> Result<(), String> {
|
||||
let verification_url = verification.get_verification_url()
|
||||
.ok_or_else(|| "No callback URL configured".to_string())?;
|
||||
|
||||
let subject = "Verify your email address";
|
||||
|
||||
let html_body = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
||||
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
||||
.button {{
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.code {{
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
display: inline-block;
|
||||
margin: 10px 0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Verify your email address</h2>
|
||||
<p>Hello,</p>
|
||||
<p>Please verify your email address by clicking the button below:</p>
|
||||
<a href="{}" class="button">Verify Email</a>
|
||||
<p>Or enter this verification code:</p>
|
||||
<div class="code">{}</div>
|
||||
<p>This link and code will expire in 24 hours.</p>
|
||||
<p>If you didn't request this, please ignore this email.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
verification_url, verification.verification_code
|
||||
);
|
||||
|
||||
let text_body = format!(
|
||||
"Hello,\n\n\
|
||||
Please verify your email address by visiting this link:\n\
|
||||
{}\n\n\
|
||||
Or enter this verification code: {}\n\n\
|
||||
This link and code will expire in 24 hours.\n\n\
|
||||
If you didn't request this, please ignore this email.",
|
||||
verification_url, verification.verification_code
|
||||
);
|
||||
|
||||
self.send_html_email(
|
||||
&verification.contact,
|
||||
subject,
|
||||
&html_body,
|
||||
Some(&text_body),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// For Rhai integration, we need a simpler synchronous wrapper
|
||||
impl EmailClient {
|
||||
/// Synchronous wrapper for send_verification_code_email
|
||||
pub fn send_verification_code_sync(&self, verification: &Verification) -> Result<(), String> {
|
||||
// In a real implementation, you'd use tokio::runtime::Runtime::new().block_on()
|
||||
// For now, just simulate
|
||||
println!("=== VERIFICATION CODE EMAIL ===");
|
||||
println!("To: {}", verification.contact);
|
||||
println!("Code: {}", verification.verification_code);
|
||||
println!("===============================");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronous wrapper for send_verification_link_email
|
||||
pub fn send_verification_link_sync(&self, verification: &Verification) -> Result<(), String> {
|
||||
let verification_url = verification.get_verification_url()
|
||||
.ok_or_else(|| "No callback URL configured".to_string())?;
|
||||
|
||||
println!("=== VERIFICATION LINK EMAIL ===");
|
||||
println!("To: {}", verification.contact);
|
||||
println!("Code: {}", verification.verification_code);
|
||||
println!("Link: {}", verification_url);
|
||||
println!("===============================");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
10
lib/osiris/core/objects/communication/mod.rs
Normal file
10
lib/osiris/core/objects/communication/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
/// Communication Module
|
||||
///
|
||||
/// Transport-agnostic verification and email client.
|
||||
|
||||
pub mod verification;
|
||||
pub mod email;
|
||||
pub mod rhai;
|
||||
|
||||
pub use verification::{Verification, VerificationStatus, VerificationTransport};
|
||||
pub use email::EmailClient;
|
||||
407
lib/osiris/core/objects/communication/rhai.rs
Normal file
407
lib/osiris/core/objects/communication/rhai.rs
Normal file
@@ -0,0 +1,407 @@
|
||||
/// Rhai bindings for Communication (Verification and Email)
|
||||
|
||||
use ::rhai::plugin::*;
|
||||
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
|
||||
|
||||
use super::verification::{Verification, VerificationStatus, VerificationTransport};
|
||||
use super::email::{EmailClient, MailTemplate, Mail};
|
||||
|
||||
// ============================================================================
|
||||
// Verification Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiVerification = Verification;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_verification_module {
|
||||
use super::RhaiVerification;
|
||||
use super::super::verification::{Verification, VerificationTransport};
|
||||
|
||||
#[rhai_fn(name = "new_verification", return_raw)]
|
||||
pub fn new_verification(
|
||||
entity_id: String,
|
||||
contact: String,
|
||||
) -> Result<RhaiVerification, Box<EvalAltResult>> {
|
||||
// Default to email transport
|
||||
Ok(Verification::new(0, entity_id, contact, VerificationTransport::Email))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "callback_url", return_raw)]
|
||||
pub fn set_callback_url(
|
||||
verification: &mut RhaiVerification,
|
||||
url: String,
|
||||
) -> Result<RhaiVerification, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(verification);
|
||||
*verification = owned.callback_url(url);
|
||||
Ok(verification.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "mark_sent", return_raw)]
|
||||
pub fn mark_sent(
|
||||
verification: &mut RhaiVerification,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
verification.mark_sent();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "verify_code", return_raw)]
|
||||
pub fn verify_code(
|
||||
verification: &mut RhaiVerification,
|
||||
code: String,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
verification.verify_code(&code)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "verify_nonce", return_raw)]
|
||||
pub fn verify_nonce(
|
||||
verification: &mut RhaiVerification,
|
||||
nonce: String,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
verification.verify_nonce(&nonce)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "resend", return_raw)]
|
||||
pub fn resend(
|
||||
verification: &mut RhaiVerification,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
verification.resend();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_entity_id")]
|
||||
pub fn get_entity_id(verification: &mut RhaiVerification) -> String {
|
||||
verification.entity_id.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_contact")]
|
||||
pub fn get_contact(verification: &mut RhaiVerification) -> String {
|
||||
verification.contact.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_code")]
|
||||
pub fn get_code(verification: &mut RhaiVerification) -> String {
|
||||
verification.verification_code.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_nonce")]
|
||||
pub fn get_nonce(verification: &mut RhaiVerification) -> String {
|
||||
verification.verification_nonce.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_verification_url")]
|
||||
pub fn get_verification_url(verification: &mut RhaiVerification) -> String {
|
||||
verification.get_verification_url().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_status")]
|
||||
pub fn get_status(verification: &mut RhaiVerification) -> String {
|
||||
format!("{:?}", verification.status)
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_attempts")]
|
||||
pub fn get_attempts(verification: &mut RhaiVerification) -> i64 {
|
||||
verification.attempts as i64
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mail Template Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiMailTemplate = MailTemplate;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_mail_template_module {
|
||||
use super::RhaiMailTemplate;
|
||||
use super::super::email::MailTemplate;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "new_mail_template", return_raw)]
|
||||
pub fn new_mail_template() -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
Ok(MailTemplate::new())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "id", return_raw)]
|
||||
pub fn set_id(
|
||||
template: &mut RhaiMailTemplate,
|
||||
id: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.id(id);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "name", return_raw)]
|
||||
pub fn set_name(
|
||||
template: &mut RhaiMailTemplate,
|
||||
name: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.name(name);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "subject", return_raw)]
|
||||
pub fn set_subject(
|
||||
template: &mut RhaiMailTemplate,
|
||||
subject: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.subject(subject);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "body", return_raw)]
|
||||
pub fn set_body(
|
||||
template: &mut RhaiMailTemplate,
|
||||
body: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.body(body);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "html_body", return_raw)]
|
||||
pub fn set_html_body(
|
||||
template: &mut RhaiMailTemplate,
|
||||
html_body: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.html_body(html_body);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(template: &mut RhaiMailTemplate) -> String {
|
||||
template.id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mail Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiMail = Mail;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_mail_module {
|
||||
use super::RhaiMail;
|
||||
use super::super::email::Mail;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "new_mail", return_raw)]
|
||||
pub fn new_mail() -> Result<RhaiMail, Box<EvalAltResult>> {
|
||||
Ok(Mail::new())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "to", return_raw)]
|
||||
pub fn set_to(
|
||||
mail: &mut RhaiMail,
|
||||
to: String,
|
||||
) -> Result<RhaiMail, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(mail);
|
||||
*mail = owned.to(to);
|
||||
Ok(mail.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "template", return_raw)]
|
||||
pub fn set_template(
|
||||
mail: &mut RhaiMail,
|
||||
template_id: String,
|
||||
) -> Result<RhaiMail, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(mail);
|
||||
*mail = owned.template(template_id);
|
||||
Ok(mail.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "parameter", return_raw)]
|
||||
pub fn add_parameter(
|
||||
mail: &mut RhaiMail,
|
||||
key: String,
|
||||
value: String,
|
||||
) -> Result<RhaiMail, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(mail);
|
||||
*mail = owned.parameter(key, value);
|
||||
Ok(mail.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Email Client Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiEmailClient = EmailClient;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_email_module {
|
||||
use super::RhaiEmailClient;
|
||||
use super::RhaiMail;
|
||||
use super::RhaiMailTemplate;
|
||||
use super::super::email::{EmailClient, Mail, MailTemplate};
|
||||
use super::super::verification::Verification;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "new_email_client", return_raw)]
|
||||
pub fn new_email_client() -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
Ok(EmailClient::new())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "smtp_host", return_raw)]
|
||||
pub fn set_smtp_host(
|
||||
client: &mut RhaiEmailClient,
|
||||
host: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.smtp_host(host);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "smtp_port", return_raw)]
|
||||
pub fn set_smtp_port(
|
||||
client: &mut RhaiEmailClient,
|
||||
port: i64,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.smtp_port(port as u16);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "username", return_raw)]
|
||||
pub fn set_username(
|
||||
client: &mut RhaiEmailClient,
|
||||
username: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.username(username);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "password", return_raw)]
|
||||
pub fn set_password(
|
||||
client: &mut RhaiEmailClient,
|
||||
password: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.password(password);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "from_email", return_raw)]
|
||||
pub fn set_from_email(
|
||||
client: &mut RhaiEmailClient,
|
||||
email: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.from_address(email);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "from_name", return_raw)]
|
||||
pub fn set_from_name(
|
||||
client: &mut RhaiEmailClient,
|
||||
name: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.from_name(name);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "use_tls", return_raw)]
|
||||
pub fn set_use_tls(
|
||||
client: &mut RhaiEmailClient,
|
||||
use_tls: bool,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.use_tls(use_tls);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "send_mail", return_raw)]
|
||||
pub fn send_mail(
|
||||
client: &mut RhaiEmailClient,
|
||||
mail: RhaiMail,
|
||||
template: RhaiMailTemplate,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
client.send_mail(&mail, &template)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "send_verification_code", return_raw)]
|
||||
pub fn send_verification_code(
|
||||
client: &mut RhaiEmailClient,
|
||||
verification: Verification,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
client.send_verification_code_sync(&verification)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "send_verification_link", return_raw)]
|
||||
pub fn send_verification_link(
|
||||
client: &mut RhaiEmailClient,
|
||||
verification: Verification,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
client.send_verification_link_sync(&verification)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registration Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Register Communication modules into a Rhai Module
|
||||
pub fn register_communication_modules(parent_module: &mut Module) {
|
||||
// Register custom types
|
||||
parent_module.set_custom_type::<Verification>("Verification");
|
||||
parent_module.set_custom_type::<MailTemplate>("MailTemplate");
|
||||
parent_module.set_custom_type::<Mail>("Mail");
|
||||
parent_module.set_custom_type::<EmailClient>("EmailClient");
|
||||
|
||||
// Merge verification functions
|
||||
let verification_module = exported_module!(rhai_verification_module);
|
||||
parent_module.combine_flatten(verification_module);
|
||||
|
||||
// Merge mail template functions
|
||||
let mail_template_module = exported_module!(rhai_mail_template_module);
|
||||
parent_module.combine_flatten(mail_template_module);
|
||||
|
||||
// Merge mail functions
|
||||
let mail_module = exported_module!(rhai_mail_module);
|
||||
parent_module.combine_flatten(mail_module);
|
||||
|
||||
// Merge email client functions
|
||||
let email_module = exported_module!(rhai_email_module);
|
||||
parent_module.combine_flatten(email_module);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomType Implementations
|
||||
// ============================================================================
|
||||
|
||||
impl CustomType for Verification {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Verification");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for MailTemplate {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("MailTemplate");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for Mail {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Mail");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for EmailClient {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("EmailClient");
|
||||
}
|
||||
}
|
||||
239
lib/osiris/core/objects/communication/verification.rs
Normal file
239
lib/osiris/core/objects/communication/verification.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
/// Transport-Agnostic Verification
|
||||
///
|
||||
/// Manages verification sessions with codes and nonces for email, SMS, etc.
|
||||
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Verification transport type
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VerificationTransport {
|
||||
Email,
|
||||
Sms,
|
||||
WhatsApp,
|
||||
Telegram,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl Default for VerificationTransport {
|
||||
fn default() -> Self {
|
||||
VerificationTransport::Email
|
||||
}
|
||||
}
|
||||
|
||||
/// Verification status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VerificationStatus {
|
||||
#[default]
|
||||
Pending,
|
||||
Sent,
|
||||
Verified,
|
||||
Expired,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Verification Session
|
||||
///
|
||||
/// Transport-agnostic verification that can be used for email, SMS, etc.
|
||||
/// Supports both code-based verification and URL-based (nonce) verification.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
|
||||
pub struct Verification {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// User/entity ID this verification is for
|
||||
pub entity_id: String,
|
||||
|
||||
/// Contact address (email, phone, etc.)
|
||||
pub contact: String,
|
||||
|
||||
/// Transport type
|
||||
pub transport: VerificationTransport,
|
||||
|
||||
/// Verification code (6 digits for user entry)
|
||||
pub verification_code: String,
|
||||
|
||||
/// Verification nonce (for URL-based verification)
|
||||
pub verification_nonce: String,
|
||||
|
||||
/// Current status
|
||||
pub status: VerificationStatus,
|
||||
|
||||
/// When verification was sent
|
||||
pub sent_at: Option<u64>,
|
||||
|
||||
/// When verification was completed
|
||||
pub verified_at: Option<u64>,
|
||||
|
||||
/// When verification expires
|
||||
pub expires_at: Option<u64>,
|
||||
|
||||
/// Number of attempts
|
||||
pub attempts: u32,
|
||||
|
||||
/// Maximum attempts allowed
|
||||
pub max_attempts: u32,
|
||||
|
||||
/// Callback URL (for server to construct verification link)
|
||||
pub callback_url: Option<String>,
|
||||
|
||||
/// Additional metadata
|
||||
#[serde(default)]
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Verification {
|
||||
/// Create a new verification
|
||||
pub fn new(id: u32, entity_id: String, contact: String, transport: VerificationTransport) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
base_data.id = id;
|
||||
|
||||
// Generate verification code (6 digits)
|
||||
let code = Self::generate_code();
|
||||
|
||||
// Generate verification nonce (32 char hex)
|
||||
let nonce = Self::generate_nonce();
|
||||
|
||||
// Set expiry to 24 hours from now
|
||||
let expires_at = Self::now() + (24 * 60 * 60);
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
entity_id,
|
||||
contact,
|
||||
transport,
|
||||
verification_code: code,
|
||||
verification_nonce: nonce,
|
||||
status: VerificationStatus::Pending,
|
||||
sent_at: None,
|
||||
verified_at: None,
|
||||
expires_at: Some(expires_at),
|
||||
attempts: 0,
|
||||
max_attempts: 3,
|
||||
callback_url: None,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a 6-digit verification code
|
||||
fn generate_code() -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
format!("{:06}", (timestamp % 1_000_000) as u32)
|
||||
}
|
||||
|
||||
/// Generate a verification nonce (32 char hex string)
|
||||
fn generate_nonce() -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
format!("{:032x}", timestamp)
|
||||
}
|
||||
|
||||
/// Set callback URL
|
||||
pub fn callback_url(mut self, url: String) -> Self {
|
||||
self.callback_url = Some(url);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get verification URL (callback_url + nonce)
|
||||
pub fn get_verification_url(&self) -> Option<String> {
|
||||
self.callback_url.as_ref().map(|base_url| {
|
||||
if base_url.contains('?') {
|
||||
format!("{}&nonce={}", base_url, self.verification_nonce)
|
||||
} else {
|
||||
format!("{}?nonce={}", base_url, self.verification_nonce)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark as sent
|
||||
pub fn mark_sent(&mut self) {
|
||||
self.status = VerificationStatus::Sent;
|
||||
self.sent_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Verify with code
|
||||
pub fn verify_code(&mut self, code: &str) -> Result<(), String> {
|
||||
// Check if expired
|
||||
if let Some(expires_at) = self.expires_at {
|
||||
if Self::now() > expires_at {
|
||||
self.status = VerificationStatus::Expired;
|
||||
self.base_data.update_modified();
|
||||
return Err("Verification code expired".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check attempts
|
||||
self.attempts += 1;
|
||||
if self.attempts > self.max_attempts {
|
||||
self.status = VerificationStatus::Failed;
|
||||
self.base_data.update_modified();
|
||||
return Err("Maximum attempts exceeded".to_string());
|
||||
}
|
||||
|
||||
// Check code
|
||||
if code != self.verification_code {
|
||||
self.base_data.update_modified();
|
||||
return Err("Invalid verification code".to_string());
|
||||
}
|
||||
|
||||
// Success
|
||||
self.status = VerificationStatus::Verified;
|
||||
self.verified_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify with nonce (for URL-based verification)
|
||||
pub fn verify_nonce(&mut self, nonce: &str) -> Result<(), String> {
|
||||
// Check if expired
|
||||
if let Some(expires_at) = self.expires_at {
|
||||
if Self::now() > expires_at {
|
||||
self.status = VerificationStatus::Expired;
|
||||
self.base_data.update_modified();
|
||||
return Err("Verification link expired".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check nonce
|
||||
if nonce != self.verification_nonce {
|
||||
self.base_data.update_modified();
|
||||
return Err("Invalid verification link".to_string());
|
||||
}
|
||||
|
||||
// Success
|
||||
self.status = VerificationStatus::Verified;
|
||||
self.verified_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resend verification (generate new code and nonce)
|
||||
pub fn resend(&mut self) {
|
||||
self.verification_code = Self::generate_code();
|
||||
self.verification_nonce = Self::generate_nonce();
|
||||
self.status = VerificationStatus::Pending;
|
||||
self.attempts = 0;
|
||||
|
||||
// Extend expiry
|
||||
self.expires_at = Some(Self::now() + (24 * 60 * 60));
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Helper to get current timestamp
|
||||
fn now() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
}
|
||||
155
lib/osiris/core/objects/communication/verification_old.rs
Normal file
155
lib/osiris/core/objects/communication/verification_old.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
/// Email Verification
|
||||
///
|
||||
/// Manages email verification sessions and status.
|
||||
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Email verification status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VerificationStatus {
|
||||
#[default]
|
||||
Pending,
|
||||
Sent,
|
||||
Verified,
|
||||
Expired,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Email Verification Session
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
|
||||
pub struct EmailVerification {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// User/entity ID this verification is for
|
||||
pub entity_id: String,
|
||||
|
||||
/// Email address to verify
|
||||
pub email: String,
|
||||
|
||||
/// Verification code/token
|
||||
pub verification_code: String,
|
||||
|
||||
/// Current status
|
||||
pub status: VerificationStatus,
|
||||
|
||||
/// When verification was sent
|
||||
pub sent_at: Option<u64>,
|
||||
|
||||
/// When verification was completed
|
||||
pub verified_at: Option<u64>,
|
||||
|
||||
/// When verification expires
|
||||
pub expires_at: Option<u64>,
|
||||
|
||||
/// Number of attempts
|
||||
pub attempts: u32,
|
||||
|
||||
/// Maximum attempts allowed
|
||||
pub max_attempts: u32,
|
||||
|
||||
/// Additional metadata
|
||||
#[serde(default)]
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl EmailVerification {
|
||||
/// Create a new email verification
|
||||
pub fn new(id: u32, entity_id: String, email: String) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
base_data.id = id;
|
||||
|
||||
// Generate verification code (6 digits)
|
||||
let code = Self::generate_code();
|
||||
|
||||
// Set expiry to 24 hours from now
|
||||
let expires_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() + (24 * 60 * 60);
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
entity_id,
|
||||
email,
|
||||
verification_code: code,
|
||||
status: VerificationStatus::Pending,
|
||||
sent_at: None,
|
||||
verified_at: None,
|
||||
expires_at: Some(expires_at),
|
||||
attempts: 0,
|
||||
max_attempts: 3,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a 6-digit verification code
|
||||
fn generate_code() -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
format!("{:06}", (timestamp % 1_000_000) as u32)
|
||||
}
|
||||
|
||||
/// Mark as sent
|
||||
pub fn mark_sent(&mut self) {
|
||||
self.status = VerificationStatus::Sent;
|
||||
self.sent_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Verify with code
|
||||
pub fn verify(&mut self, code: &str) -> Result<(), String> {
|
||||
// Check if expired
|
||||
if let Some(expires_at) = self.expires_at {
|
||||
if Self::now() > expires_at {
|
||||
self.status = VerificationStatus::Expired;
|
||||
self.base_data.update_modified();
|
||||
return Err("Verification code expired".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check attempts
|
||||
self.attempts += 1;
|
||||
if self.attempts > self.max_attempts {
|
||||
self.status = VerificationStatus::Failed;
|
||||
self.base_data.update_modified();
|
||||
return Err("Maximum attempts exceeded".to_string());
|
||||
}
|
||||
|
||||
// Check code
|
||||
if code != self.verification_code {
|
||||
self.base_data.update_modified();
|
||||
return Err("Invalid verification code".to_string());
|
||||
}
|
||||
|
||||
// Success
|
||||
self.status = VerificationStatus::Verified;
|
||||
self.verified_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resend verification (generate new code)
|
||||
pub fn resend(&mut self) {
|
||||
self.verification_code = Self::generate_code();
|
||||
self.status = VerificationStatus::Pending;
|
||||
self.attempts = 0;
|
||||
|
||||
// Extend expiry
|
||||
self.expires_at = Some(Self::now() + (24 * 60 * 60));
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Helper to get current timestamp
|
||||
fn now() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user