piknik
piknik copied to clipboard
`AsyncSmtpTransport` triggers high number of DNS queries to `smtp.gmail.com`
Describe the bug
Since I've added lettre to my backend service, which is deployed locally on my home server, I've noticed that my system makes DNS requests to smtp.gmail.com every minute. I don't expect this to be normal behavior.
To Reproduce This is how I use it:
use std::error::Error;
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncTransport,
Message,
};
type AsyncSmtpTransport = lettre::AsyncSmtpTransport<lettre::Tokio1Executor>;
#[derive(Debug, Clone)]
pub struct EmailClient {
mailer: AsyncSmtpTransport,
}
impl EmailClient {
pub fn new(username: String, password: String) -> Self {
let mailer = AsyncSmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(Credentials::new(username, password))
.build();
Self { mailer }
}
async fn send(
&self,
from: &str,
to: &str,
subject: &str,
body: String,
) -> Result<(), Box<dyn Error>> {
let msg = Message::builder()
.from(from.parse().unwrap())
.to(to.parse().unwrap())
.subject(subject)
.header(ContentType::TEXT_HTML)
.body(body)
.unwrap();
self.mailer.send(msg).await.map_err(Box::new)?;
Ok(())
}
}
I call this send method inside an axum handler which rarely gets called.
Expected behavior Emails are delivered and everything looks good, but there must be something going on in the background to make my system issue this many DNS queries.
Environment (please complete the following information):
lettre = { version = "0.11", default-features = false, features = [
"tokio1",
"builder",
"smtp-transport",
"pool",
"tokio1-rustls-tls",
"tracing",
] }
- OS
Linux rpi-s2 6.6.47+rpt-rpi-v8 #1 SMP PREEMPT Debian 1:6.6.47-1+rpt1 (2024-09-02) aarch64 GNU/Linux
Additional context I've discovered this by looking at my AdGuardHome
I think there are several issues that could be at play here:
By default, if you don't send email for 1 minute straight, lettre will close the connection ^1. This is a conservative default, and you may want to increase it. However, setting it too high and then going idle may cause the SMTP server to disconnect you. lettre should handle this correctly so it could be an option.
smtp.gmail.com only has a 5 minute TTL, so if you disconnect and reconnect frequently, even if the system has a DNS cache, it will expire every 5 minutes if not less (because of other DNS caches in the middle that for various reasons shorten the TTL).
dig smtp.gmail.com @ns1.google.com
[...]
smtp.gmail.com. 300 IN A 64.233.184.109
[...]
To add up to this: I'm currently sending at maximum 2 emails per day, in this scenario I'd expect to see a maximum of 2 DNS queries to smtp.google.com if they are more than 5 minutes apart.
Ah wow. I found out very quickly by looking at my code from 3.5 years ago :laughing:. Could you try building your project with lettre from #1012 and letting me know if that fixes it for you?
Will try that later today, grazie!
Working like a charm now, I've sent an email and DNS is only been polled once 👍
Fixed by #1012
Released in v0.11.11
I think I spoke too early 🥲 woke up this morning and realised that now it makes 2 requests, one after the other, every minute.
I'll try and investigate this further as it didn't poll DNS in the first 10m window when the system started and sent an email.
Should this be re-opened?
Were you able to determine the source of the DNS queries? I ran this very, very basic test and the connection was only opened once at the beginning and closed once, nothing else happened since.
cargo run --features tokio1-native-tls --example tokio1_smtp_tls
diff --git a/examples/tokio1_smtp_tls.rs b/examples/tokio1_smtp_tls.rs
index 8329b34..764d079 100644
--- a/examples/tokio1_smtp_tls.rs
+++ b/examples/tokio1_smtp_tls.rs
@@ -1,3 +1,5 @@
+use std::future::pending;
+
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
@@ -11,27 +13,19 @@ use tokio1_crate as tokio;
async fn main() {
tracing_subscriber::fmt::init();
- let email = Message::builder()
- .from("NoBody <[email protected]>".parse().unwrap())
- .reply_to("Yuin <[email protected]>".parse().unwrap())
- .to("Hei <[email protected]>".parse().unwrap())
- .subject("Happy new async year")
- .header(ContentType::TEXT_PLAIN)
- .body(String::from("Be happy with async!"))
- .unwrap();
-
- let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
+ let creds = Credentials::new(
+ "[redacted]".to_owned(),
+ "[redacted]".to_owned(),
+ );
// Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio1Executor> =
- AsyncSmtpTransport::<Tokio1Executor>::relay("smtp.gmail.com")
+ AsyncSmtpTransport::<Tokio1Executor>::relay("smtp.[redacted].com")
.unwrap()
.credentials(creds)
.build();
- // Send the email
- match mailer.send(email).await {
- Ok(_) => println!("Email sent successfully!"),
- Err(e) => panic!("Could not send email: {e:?}"),
- }
+ mailer.test_connection().await.unwrap();
+
+ pending::<()>().await;
}
diff --git a/src/transport/smtp/client/async_net.rs b/src/transport/smtp/client/async_net.rs
index b9e89c5..0458c01 100644
--- a/src/transport/smtp/client/async_net.rs
+++ b/src/transport/smtp/client/async_net.rs
@@ -137,6 +137,7 @@ impl AsyncNetworkStream {
tls_parameters: Option<TlsParameters>,
local_addr: Option<IpAddr>,
) -> Result<AsyncNetworkStream, Error> {
+ println!("CONNECT");
async fn try_connect<T: Tokio1ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
@@ -644,3 +645,9 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
}
}
}
+
+impl Drop for AsyncNetworkStream {
+ fn drop(&mut self) {
+ println!("DROPPED");
+ }
+}
Just to add more to this, I'm using the following struct in an Axum state, wrapped in an Arc.
#[derive(Debug, Clone)]
pub struct EmailClient {
mailer: AsyncSmtpTransport,
}
struct State {
...
emailer: Arc<EmailClient>
...
}
I think that deriving Clone here is a mistake, that seemed to clone the entire EmailClient for each Axum worker (4) and that spiked even further the number of requests. By removing the Clone statement I'm back to a quite frequent but single dns lookup.
- #[derive(Debug, Clone)]
+ #[derive(Debug)]
pub struct EmailClient {
mailer: AsyncSmtpTransport,
}
The interesting thing is that when my service is deployed on a raspberry pi zero 2w deployed on my LAN the dns lookup happens every 25-30s, if on the other hand I run the service locally on my machine (Mac M2) the dns lookup is fired ~~every 2 minutes~~ much less frequently (varying from every 2m to every 4-5m sometimes).
Could this be affected by build targets? The one running on the raspberry pi is the armv7-unknown-linux-gnueabihf
I don't see how cloning it has anything to do with it because the internal implementation of AsyncSmtpTransport is using Arcs, unless the pool feature is disabled. Running an AsyncSmtpTransport with the default configuration I see it connects when I first call test_connection, it disconnects after a while and stays disconnected.
I don't see how cloning it has anything to do with it because the internal implementation of
AsyncSmtpTransportis usingArcs, unless thepoolfeature is disabled.
You're right, just saw that the other day.
I've now disabled the pool feature and that seems to drastically reduce the DNS lookups to smpt.gmail.com but I guess that's expected.