use crate::pdf::ExecutionReport;
use crate::{ReportingError, Result};
use lettre::message::{header, Attachment, MultiPart, SinglePart};
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
use serde::{Deserialize, Serialize};
use std::fs;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailConfig {
pub smtp_host: String,
pub smtp_port: u16,
pub username: String,
pub password: String,
pub from_address: String,
pub from_name: String,
}
#[derive(Debug, Clone)]
pub struct EmailReport {
pub subject: String,
pub recipients: Vec<String>,
pub html_body: String,
pub text_body: String,
pub pdf_attachment: Option<Vec<u8>>,
}
pub struct EmailNotifier {
config: EmailConfig,
transport: SmtpTransport,
}
impl EmailNotifier {
pub fn new(config: EmailConfig) -> Result<Self> {
let creds = Credentials::new(config.username.clone(), config.password.clone());
let transport = SmtpTransport::relay(&config.smtp_host)
.map_err(|e| ReportingError::Email(e.to_string()))?
.credentials(creds)
.port(config.smtp_port)
.build();
Ok(Self { config, transport })
}
pub fn send(&self, email_report: &EmailReport) -> Result<()> {
let from = format!("{} <{}>", self.config.from_name, self.config.from_address);
let mut message_builder = Message::builder()
.from(
from.parse()
.map_err(|e| ReportingError::Email(format!("Invalid from address: {}", e)))?,
)
.subject(&email_report.subject);
for recipient in &email_report.recipients {
message_builder = message_builder.to(recipient
.parse()
.map_err(|e| ReportingError::Email(format!("Invalid recipient: {}", e)))?);
}
let mut multipart = MultiPart::alternative()
.singlepart(
SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.body(email_report.text_body.clone()),
)
.singlepart(
SinglePart::builder()
.header(header::ContentType::TEXT_HTML)
.body(email_report.html_body.clone()),
);
if let Some(ref pdf_data) = email_report.pdf_attachment {
let attachment = Attachment::new("report.pdf".to_string())
.body(pdf_data.clone(), "application/pdf".parse().unwrap());
multipart = MultiPart::mixed().multipart(multipart).singlepart(attachment);
}
let email = message_builder
.multipart(multipart)
.map_err(|e| ReportingError::Email(e.to_string()))?;
self.transport.send(&email).map_err(|e| ReportingError::Email(e.to_string()))?;
Ok(())
}
pub fn send_execution_report(
&self,
report: &ExecutionReport,
recipients: Vec<String>,
include_pdf: bool,
) -> Result<()> {
let subject =
format!("Chaos Test Report: {} - {}", report.orchestration_name, report.status);
let html_body = self.generate_html_report(report);
let text_body = self.generate_text_report(report);
let pdf_attachment = if include_pdf {
Some(self.generate_pdf_attachment(report)?)
} else {
None
};
let email_report = EmailReport {
subject,
recipients,
html_body,
text_body,
pdf_attachment,
};
self.send(&email_report)
}
fn generate_html_report(&self, report: &ExecutionReport) -> String {
format!(
r#"
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.header {{ background: #2c3e50; color: white; padding: 20px; }}
.content {{ padding: 20px; }}
.status-badge {{ padding: 5px 10px; border-radius: 3px; font-weight: bold; }}
.success {{ background: #27ae60; color: white; }}
.failure {{ background: #e74c3c; color: white; }}
.metrics {{ display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin: 20px 0; }}
.metric-card {{ border: 1px solid #ddd; padding: 15px; border-radius: 5px; }}
.metric-value {{ font-size: 24px; font-weight: bold; color: #2c3e50; }}
.metric-label {{ font-size: 12px; color: #7f8c8d; }}
table {{ width: 100%; border-collapse: collapse; margin: 20px 0; }}
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
th {{ background: #f8f9fa; font-weight: bold; }}
</style>
</head>
<body>
<div class="header">
<h1>🌩️ Chaos Orchestration Report</h1>
<p>{}</p>
</div>
<div class="content">
<h2>Summary</h2>
<p>
<span class="status-badge {}">{}</span>
</p>
<p>
<strong>Duration:</strong> {}s<br>
<strong>Started:</strong> {}<br>
<strong>Ended:</strong> {}
</p>
<h2>Execution Metrics</h2>
<div class="metrics">
<div class="metric-card">
<div class="metric-value">{}</div>
<div class="metric-label">Total Steps</div>
</div>
<div class="metric-card">
<div class="metric-value">{}</div>
<div class="metric-label">Completed Steps</div>
</div>
<div class="metric-card">
<div class="metric-value">{:.2}%</div>
<div class="metric-label">Error Rate</div>
</div>
<div class="metric-card">
<div class="metric-value">{:.2}ms</div>
<div class="metric-label">Avg Latency</div>
</div>
</div>
{}
{}
<hr>
<p style="font-size: 12px; color: #7f8c8d;">
Generated by MockForge on {}<br>
<a href="https://github.com/your-org/mockforge">View Documentation</a>
</p>
</div>
</body>
</html>
"#,
report.orchestration_name,
if report.failed_steps == 0 {
"success"
} else {
"failure"
},
report.status,
report.duration_seconds,
report.start_time.format("%Y-%m-%d %H:%M:%S UTC"),
report.end_time.format("%Y-%m-%d %H:%M:%S UTC"),
report.total_steps,
report.completed_steps,
report.metrics.error_rate * 100.0,
report.metrics.avg_latency_ms,
self.generate_failures_html(&report.failures),
self.generate_recommendations_html(&report.recommendations),
chrono::Utc::now().format("%Y-%m-%d %H:%M UTC")
)
}
fn generate_failures_html(&self, failures: &[crate::pdf::FailureDetail]) -> String {
if failures.is_empty() {
return String::new();
}
let mut html = String::from(
"<h2>Failures</h2><table><tr><th>Step</th><th>Error</th><th>Time</th></tr>",
);
for failure in failures {
html.push_str(&format!(
"<tr><td>{}</td><td>{}</td><td>{}</td></tr>",
failure.step_name,
failure.error_message,
failure.timestamp.format("%H:%M:%S")
));
}
html.push_str("</table>");
html
}
fn generate_recommendations_html(&self, recommendations: &[String]) -> String {
if recommendations.is_empty() {
return String::new();
}
let mut html = String::from("<h2>Recommendations</h2><ul>");
for rec in recommendations {
html.push_str(&format!("<li>{}</li>", rec));
}
html.push_str("</ul>");
html
}
fn generate_text_report(&self, report: &ExecutionReport) -> String {
format!(
"CHAOS ORCHESTRATION REPORT\n\
========================\n\n\
Orchestration: {}\n\
Status: {}\n\
Duration: {}s\n\
Started: {}\n\
Ended: {}\n\n\
EXECUTION SUMMARY\n\
-----------------\n\
Total Steps: {}\n\
Completed: {}\n\
Failed: {}\n\n\
METRICS\n\
-------\n\
Total Requests: {}\n\
Error Rate: {:.2}%\n\
Avg Latency: {:.2}ms\n\
P95 Latency: {:.2}ms\n\n\
Generated by MockForge\n",
report.orchestration_name,
report.status,
report.duration_seconds,
report.start_time.format("%Y-%m-%d %H:%M:%S UTC"),
report.end_time.format("%Y-%m-%d %H:%M:%S UTC"),
report.total_steps,
report.completed_steps,
report.failed_steps,
report.metrics.total_requests,
report.metrics.error_rate * 100.0,
report.metrics.avg_latency_ms,
report.metrics.p95_latency_ms
)
}
fn generate_pdf_attachment(&self, report: &ExecutionReport) -> Result<Vec<u8>> {
use crate::pdf::{PdfConfig, PdfReportGenerator};
let config = PdfConfig::default();
let generator = PdfReportGenerator::new(config);
let temp_path = "/tmp/mockforge_report.pdf";
generator.generate(report, temp_path)?;
let pdf_data = fs::read(temp_path)?;
fs::remove_file(temp_path)?;
Ok(pdf_data)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_html_generation() {
let config = EmailConfig {
smtp_host: "smtp.example.com".to_string(),
smtp_port: 587,
username: "user".to_string(),
password: "pass".to_string(),
from_address: "noreply@example.com".to_string(),
from_name: "MockForge".to_string(),
};
assert_eq!(config.smtp_port, 587);
}
}