124 lines
3.9 KiB
Rust
124 lines
3.9 KiB
Rust
use async_trait::async_trait;
|
|
|
|
pub use crate::smtp::error::SmtpError;
|
|
pub use crate::smtp::types::{AttachmentData, SendableMessage, SmtpCredentials, SmtpSendResult};
|
|
|
|
#[allow(clippy::double_must_use)]
|
|
#[async_trait]
|
|
pub trait SmtpClient: Send + Sync {
|
|
async fn send_message(
|
|
&self,
|
|
creds: &SmtpCredentials,
|
|
message: &SendableMessage,
|
|
) -> Result<SmtpSendResult, SmtpError>;
|
|
|
|
async fn check_connection(&self, _creds: &SmtpCredentials) -> Result<(), SmtpError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub struct RealSmtpClient;
|
|
|
|
fn smtp_transport(
|
|
creds: &SmtpCredentials,
|
|
) -> Result<lettre::AsyncSmtpTransport<lettre::Tokio1Executor>, SmtpError> {
|
|
use lettre::transport::smtp::authentication::Credentials;
|
|
use lettre::transport::smtp::client::{Tls, TlsParameters};
|
|
use lettre::{AsyncSmtpTransport, Tokio1Executor};
|
|
|
|
let smtp_creds = Credentials::new(creds.email.clone(), creds.password.clone());
|
|
let mut builder = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&creds.host)
|
|
.port(creds.port)
|
|
.credentials(smtp_creds);
|
|
if creds.tls {
|
|
let parameters = TlsParameters::builder(creds.host.clone())
|
|
.dangerous_accept_invalid_certs(!creds.tls_verify)
|
|
.dangerous_accept_invalid_hostnames(!creds.tls_verify)
|
|
.build()
|
|
.map_err(|e| SmtpError::ConnectionFailed(e.to_string()))?;
|
|
builder = builder.tls(if creds.port == 587 {
|
|
Tls::Required(parameters)
|
|
} else {
|
|
Tls::Wrapper(parameters)
|
|
});
|
|
} else {
|
|
builder = builder.tls(Tls::None);
|
|
}
|
|
Ok(builder.build())
|
|
}
|
|
|
|
#[async_trait]
|
|
impl SmtpClient for RealSmtpClient {
|
|
async fn send_message(
|
|
&self,
|
|
creds: &SmtpCredentials,
|
|
message: &SendableMessage,
|
|
) -> Result<SmtpSendResult, SmtpError> {
|
|
use lettre::AsyncTransport;
|
|
|
|
let message_id = format!(
|
|
"<{}.{}@{}>",
|
|
uuid::Uuid::new_v4(),
|
|
uuid::Uuid::new_v4(),
|
|
creds.host
|
|
);
|
|
let email = crate::smtp::builder::build_message(message, &message_id)
|
|
.map_err(SmtpError::SendFailed)?;
|
|
let rfc822 = email.formatted();
|
|
if message.max_message_size_bytes > 0 && rfc822.len() > message.max_message_size_bytes {
|
|
return Err(SmtpError::MessageTooLarge {
|
|
actual: rfc822.len(),
|
|
limit: message.max_message_size_bytes,
|
|
});
|
|
}
|
|
|
|
let transport = smtp_transport(creds)?;
|
|
|
|
transport.send(email).await.map_err(|e| {
|
|
let msg = e.to_string();
|
|
let lower = msg.to_lowercase();
|
|
if lower.contains("authentication")
|
|
|| lower.contains("credentials")
|
|
|| lower.contains("auth")
|
|
{
|
|
SmtpError::AuthenticationFailed
|
|
} else if lower.contains("connect")
|
|
|| lower.contains("dns")
|
|
|| lower.contains("resolve")
|
|
|| lower.contains("timeout")
|
|
|| lower.contains("tls")
|
|
{
|
|
SmtpError::ConnectionFailed(msg)
|
|
} else {
|
|
SmtpError::SendFailed(msg)
|
|
}
|
|
})?;
|
|
|
|
Ok(SmtpSendResult { message_id, rfc822 })
|
|
}
|
|
|
|
async fn check_connection(&self, creds: &SmtpCredentials) -> Result<(), SmtpError> {
|
|
let transport = smtp_transport(creds)?;
|
|
|
|
let accepted = transport.test_connection().await.map_err(|e| {
|
|
let message = e.to_string();
|
|
if message.to_ascii_lowercase().contains("auth") {
|
|
SmtpError::AuthenticationFailed
|
|
} else {
|
|
SmtpError::ConnectionFailed(message)
|
|
}
|
|
})?;
|
|
if accepted {
|
|
Ok(())
|
|
} else {
|
|
Err(SmtpError::ConnectionFailed(
|
|
"SMTP NOOP was not accepted".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "mock.rs"]
|
|
pub mod mock;
|