Files
telfax/src/email/smtp.rs
T
Warren 55bca92691 V1.0: Class 1 fax — real-world 4-page send to external number confirmed
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.
2026-07-24 18:47:15 +08:00

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)
}
}