55bca92691
Core features: - Class 1 T.30 protocol: full send/receive implementation - HDLC: DLE-stuffing, FCS strip, USR5637 bit-reversal handling - T.4 MH encoder/decoder (1728px A4 standard) - Document pipeline: PDF (Ghostscript), PNG, TIFF input - Width clamping: US Letter 1734px → 1728px fax standard - Cover page: CJK rasterization (TW/CN/JP/EN), TIFF + HTML output - OCR verification: Tesseract 5 with eng+chi_tra, CJK space-tolerant - API server (axum): health, send, jobs, cover, retry, cancel - Background worker: auto-poll queue, speed fallback, retry policy - Modem detection, pool management Real-world test results (2026-07-23): - V90 → 25153038: 4 pages, V.17 12000 bps, 2:33 ✅ - USR5637 → 25153038: 4 pages, V.17 12000 bps, 2:26 ✅ - Both faxes confirmed received on remote machine Tested: loopback (100% pixel match), multi-page, all input formats, cover pages, OCR verify, API endpoints, worker processing. 13 unit tests pass, 0 new clippy warnings.
111 lines
3.1 KiB
Rust
111 lines
3.1 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use lettre::{
|
|
message::Message,
|
|
transport::smtp::authentication::Credentials,
|
|
SmtpTransport, Transport,
|
|
};
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct EmailConfig {
|
|
pub smtp_host: String,
|
|
pub smtp_port: u16,
|
|
pub smtp_username: String,
|
|
pub smtp_password: String,
|
|
pub from_address: String,
|
|
pub from_name: Option<String>,
|
|
#[serde(default)]
|
|
pub use_tls: bool,
|
|
}
|
|
|
|
pub struct EmailSender {
|
|
config: EmailConfig,
|
|
}
|
|
|
|
impl EmailSender {
|
|
pub fn new(config: EmailConfig) -> Self {
|
|
Self { config }
|
|
}
|
|
|
|
pub fn send_email(
|
|
&self,
|
|
to: &str,
|
|
subject: &str,
|
|
body: &str,
|
|
) -> crate::error::Result<()> {
|
|
let email = Message::builder()
|
|
.from(
|
|
format!(
|
|
"{} <{}>",
|
|
self.config.from_name.as_deref().unwrap_or("Telfax"),
|
|
self.config.from_address
|
|
)
|
|
.parse()
|
|
.unwrap(),
|
|
)
|
|
.to(to.parse().unwrap())
|
|
.subject(subject)
|
|
.body(body.to_string())
|
|
.unwrap();
|
|
|
|
let creds = Credentials::new(
|
|
self.config.smtp_username.clone(),
|
|
self.config.smtp_password.clone(),
|
|
);
|
|
|
|
let mailer = SmtpTransport::relay(&self.config.smtp_host)
|
|
.unwrap()
|
|
.credentials(creds)
|
|
.port(self.config.smtp_port)
|
|
.build();
|
|
|
|
mailer.send(&email)
|
|
.map_err(|e| crate::error::FaxError::Other(format!("Failed to send email: {}", e)))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn send_success_notification(
|
|
&self,
|
|
to: &str,
|
|
fax_number: &str,
|
|
job_id: &str,
|
|
) -> crate::error::Result<()> {
|
|
let subject = format!("Fax sent successfully to {}", fax_number);
|
|
let body = format!(
|
|
"Your fax to {} has been sent successfully.\n\nJob ID: {}\n\nThis is an automated message from Telfax Fax Server.",
|
|
fax_number, job_id
|
|
);
|
|
|
|
self.send_email(to, &subject, &body)
|
|
}
|
|
|
|
pub fn send_failure_notification(
|
|
&self,
|
|
to: &str,
|
|
fax_number: &str,
|
|
job_id: &str,
|
|
error: &str,
|
|
) -> crate::error::Result<()> {
|
|
let subject = format!("Fax failed to {}", fax_number);
|
|
let body = format!(
|
|
"Your fax to {} has failed.\n\nJob ID: {}\nError: {}\n\nPlease check the fax number and try again.\n\nThis is an automated message from Telfax Fax Server.",
|
|
fax_number, job_id, error
|
|
);
|
|
|
|
self.send_email(to, &subject, &body)
|
|
}
|
|
|
|
pub fn send_received_notification(
|
|
&self,
|
|
to: &str,
|
|
from_number: &str,
|
|
) -> crate::error::Result<()> {
|
|
let subject = format!("Fax received from {}", from_number);
|
|
let body = format!(
|
|
"You have received a fax from {}.\n\nPlease check the fax server for the attachment.\n\nThis is an automated message from Telfax Fax Server.",
|
|
from_number
|
|
);
|
|
|
|
self.send_email(to, &subject, &body)
|
|
}
|
|
} |