mirror of
https://github.com/chatmail/relay.git
synced 2026-09-11 09:53:14 +00:00
refactor: Use a custom, minimal SMTP client instead of lettre (#33)
This disables the Nagle's algorithm on re-insertion. Signed-off-by: Jagoda Ślązak <jslazak@jslazak.com>
This commit is contained in:
committed by
GitHub
parent
8d18b73681
commit
0ee1e9924c
@@ -10,4 +10,23 @@ pub enum Error {
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("OpenPGP packet header is truncated - can't validate!")]
|
||||
TruncatedHeader,
|
||||
#[error("Unable to send email, Error during {context}, server said: {raw_smtp_answer}")]
|
||||
MailSend {
|
||||
context: String,
|
||||
raw_smtp_answer: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Formats [`Error`] as an SMTP response.
|
||||
pub fn smtp_response(&self) -> String {
|
||||
match self {
|
||||
// We transparently pass postfix/milter errors reported on reinjection
|
||||
Error::MailSend {
|
||||
raw_smtp_answer, ..
|
||||
} => raw_smtp_answer.clone(),
|
||||
Error::TruncatedHeader => self.to_string(),
|
||||
_ => "451 Local error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@ use crate::config::Config;
|
||||
use crate::message::{check_encrypted, is_securejoin};
|
||||
use crate::smtp_server::SmtpHandler;
|
||||
use async_trait::async_trait;
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Tokio1Executor};
|
||||
use mailparse::{MailHeaderMap, parse_mail};
|
||||
|
||||
pub use crate::smtp_server::Envelope;
|
||||
use crate::utils::{extract_address, format_smtp_error};
|
||||
use crate::utils::extract_address;
|
||||
|
||||
/// Handler for incoming SMTP messages.
|
||||
pub struct IncomingBeforeQueueHandler {
|
||||
@@ -80,30 +79,12 @@ impl SmtpHandler for IncomingBeforeQueueHandler {
|
||||
async fn reinject_mail(&self, envelope: &Envelope) -> Result<(), String> {
|
||||
log::debug!("Re-injecting the mail that passed checks");
|
||||
|
||||
let mailer = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous("localhost")
|
||||
.port(self.config.postfix_reinject_port_incoming)
|
||||
.build();
|
||||
|
||||
let envelope_data = lettre::address::Envelope::new(
|
||||
Some(
|
||||
envelope
|
||||
.mail_from
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid from address: {}", e))?,
|
||||
),
|
||||
envelope
|
||||
.rcpt_to
|
||||
.iter()
|
||||
.map(|addr| addr.parse())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Invalid to address: {}", e))?,
|
||||
)
|
||||
.map_err(|e| format!("Failed to create envelope: {}", e))?;
|
||||
|
||||
mailer
|
||||
.send_raw(&envelope_data, &envelope.data)
|
||||
crate::smtp_client::send(self.config.postfix_reinject_port_incoming, envelope)
|
||||
.await
|
||||
.map_err(format_smtp_error)?;
|
||||
.map_err(|e| {
|
||||
log::warn!("Failed to re-inject mail: {}", e);
|
||||
e.smtp_response()
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ pub(crate) mod inbound;
|
||||
pub(crate) mod message;
|
||||
pub(crate) mod openpgp;
|
||||
pub(crate) mod outbound;
|
||||
pub(crate) mod smtp_client;
|
||||
pub(crate) mod smtp_server;
|
||||
pub(crate) mod utils;
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@ use crate::config::Config;
|
||||
use crate::message::{check_encrypted, is_securejoin, recipient_matches_passthrough};
|
||||
pub use crate::smtp_server::Envelope;
|
||||
use crate::smtp_server::SmtpHandler;
|
||||
use crate::utils::{extract_address, format_smtp_error};
|
||||
use crate::utils::extract_address;
|
||||
use async_trait::async_trait;
|
||||
use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter};
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Tokio1Executor};
|
||||
use mailparse::{MailHeaderMap, parse_mail};
|
||||
|
||||
/// Handler for outgoing SMTP messages.
|
||||
@@ -127,30 +126,12 @@ impl SmtpHandler for OutgoingBeforeQueueHandler {
|
||||
async fn reinject_mail(&self, envelope: &Envelope) -> Result<(), String> {
|
||||
log::debug!("Re-injecting the mail that passed checks");
|
||||
|
||||
let mailer = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous("localhost")
|
||||
.port(self.config.postfix_reinject_port)
|
||||
.build();
|
||||
|
||||
let envelope_data = lettre::address::Envelope::new(
|
||||
Some(
|
||||
envelope
|
||||
.mail_from
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid from address: {}", e))?,
|
||||
),
|
||||
envelope
|
||||
.rcpt_to
|
||||
.iter()
|
||||
.map(|addr| addr.parse())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Invalid to address: {}", e))?,
|
||||
)
|
||||
.map_err(|e| format!("Failed to create envelope: {}", e))?;
|
||||
|
||||
mailer
|
||||
.send_raw(&envelope_data, &envelope.data)
|
||||
crate::smtp_client::send(self.config.postfix_reinject_port, envelope)
|
||||
.await
|
||||
.map_err(format_smtp_error)?;
|
||||
.map_err(|e| {
|
||||
log::warn!("Failed to re-inject mail: {}", e);
|
||||
e.smtp_response()
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::smtp_server::Envelope;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufStream};
|
||||
use tokio::net::TcpSocket;
|
||||
|
||||
const LOCALHOST: IpAddr = IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1));
|
||||
|
||||
/// Sends an email using an SMTP server at `localhost:<smtp_port>`.
|
||||
pub async fn send(smtp_port: u16, envelope: &Envelope) -> Result<(), crate::error::Error> {
|
||||
let socket = TcpSocket::new_v4()?;
|
||||
|
||||
// Disable Nagle's algorithm.
|
||||
socket.set_nodelay(true)?;
|
||||
|
||||
let stream = socket
|
||||
.connect(SocketAddr::new(LOCALHOST, smtp_port))
|
||||
.await?;
|
||||
|
||||
let mut buf_stream = BufStream::new(stream);
|
||||
let mut response = String::new();
|
||||
|
||||
macro_rules! smtp_write {
|
||||
($command: expr) => {
|
||||
buf_stream.write_all($command).await?;
|
||||
buf_stream.flush().await?;
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! smtp_read {
|
||||
($context:expr, $expected_code:expr) => {
|
||||
buf_stream.read_line(&mut response).await?;
|
||||
if !response.starts_with($expected_code) {
|
||||
return Err(crate::error::Error::MailSend {
|
||||
context: $context.to_string(),
|
||||
raw_smtp_answer: response.clone(),
|
||||
});
|
||||
}
|
||||
response.clear();
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! smtp_cmd {
|
||||
($command:expr, $context:expr, $expected_code:expr) => {
|
||||
smtp_write!($command);
|
||||
smtp_read!($context, $expected_code);
|
||||
};
|
||||
}
|
||||
|
||||
// Read initial greeting
|
||||
smtp_read!("initial greeting", "220");
|
||||
|
||||
// Greet (Using HELO as we don't want to deal with extended SMTP anyway.)
|
||||
smtp_cmd!(b"HELO localhost\r\n", "HELO", "250");
|
||||
|
||||
// MAIL FROM
|
||||
smtp_cmd!(
|
||||
format!("MAIL FROM:<{}>\r\n", envelope.mail_from).as_bytes(),
|
||||
"MAIL FROM",
|
||||
"250"
|
||||
);
|
||||
|
||||
// RCPT TO
|
||||
for rcpt in &envelope.rcpt_to {
|
||||
smtp_cmd!(
|
||||
format!("RCPT TO:<{}>\r\n", rcpt).as_bytes(),
|
||||
"RCPT TO",
|
||||
"250"
|
||||
);
|
||||
}
|
||||
|
||||
// DATA
|
||||
smtp_cmd!(b"DATA\r\n", "DATA", "354");
|
||||
smtp_write!(&envelope.data);
|
||||
smtp_write!(b".\r\n");
|
||||
smtp_read!("end of DATA", "250");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use mailparse::MailAddr;
|
||||
use std::error::Error;
|
||||
|
||||
/// Extracts the first email address found in SMTP command or email header.
|
||||
///
|
||||
@@ -27,29 +26,6 @@ pub fn extract_address(input: &str) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Formats SMTP error to be able to send it back to postfix.
|
||||
pub fn format_smtp_error(error: lettre::transport::smtp::Error) -> String {
|
||||
if let Some(code) = error.status() {
|
||||
format!(
|
||||
"{} {}",
|
||||
code,
|
||||
error
|
||||
.source()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or("Unknown error".to_string())
|
||||
)
|
||||
} else {
|
||||
// Default to 451, most probably means some internal service error (e.g. milter)
|
||||
format!(
|
||||
"451 {}",
|
||||
error
|
||||
.source()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or("Unknown error".to_string())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user