piknik icon indicating copy to clipboard operation
piknik copied to clipboard

`AsyncSmtpTransport` triggers high number of DNS queries to `smtp.gmail.com`

Open mattrighetti opened this issue 11 months ago • 13 comments
trafficstars

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

mattrighetti avatar Dec 02 '24 23:12 mattrighetti

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
[...]

paolobarbolini avatar Dec 05 '24 07:12 paolobarbolini

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.

mattrighetti avatar Dec 05 '24 11:12 mattrighetti

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?

paolobarbolini avatar Dec 05 '24 13:12 paolobarbolini

Will try that later today, grazie!

mattrighetti avatar Dec 05 '24 16:12 mattrighetti

Working like a charm now, I've sent an email and DNS is only been polled once 👍

mattrighetti avatar Dec 05 '24 17:12 mattrighetti

Fixed by #1012

paolobarbolini avatar Dec 05 '24 19:12 paolobarbolini

Released in v0.11.11

paolobarbolini avatar Dec 05 '24 19:12 paolobarbolini

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.

mattrighetti avatar Dec 06 '24 10:12 mattrighetti

Should this be re-opened?

mattrighetti avatar Dec 13 '24 18:12 mattrighetti

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");
+    }
+}

paolobarbolini avatar Dec 16 '24 22:12 paolobarbolini

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

mattrighetti avatar Feb 16 '25 17:02 mattrighetti

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.

paolobarbolini avatar Feb 17 '25 09:02 paolobarbolini

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.

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.

mattrighetti avatar Mar 07 '25 10:03 mattrighetti