Repository: lettre/lettre (language: rust)

Issue:
IPv6 DNS resolution on IPv4-Only systems
**Describe the bug**
While I cant be certain lettre is the culprid, my limited bug trace lead me to conclude the following bug might be an upstream issue:
https://github.com/dani-garcia/bitwarden_rs/issues/569

The issue is SMTP send resolving a smtp server domain to a ipv6 adress, on a system not supporting ipv6 (aka not having a working ipv6 connection)

**To Reproduce**
See above

**Expected behavior**
If connecting using the ipv6 lookup fails, it should retry using ipv4, no mater if a ipv6 connection is available or not. So it shouldn't mater if there is a ipv6 or ipv4 connection, yet it seems to mater.

**Environment (please complete the following information):**
- Latest (0.9.2)
- FreeBSD (and possibly others)

**Other Notes**
This might turn out to be a downstream error or even an upstream error in the rust DNS resolver.
However: Looking at the lettre code, it doesn't seem to retry using ipv4 if ipv6 fails.


Discussion / hints:
Solution to this is using any sort of override (either a custom DNS server or host file), to not-send any AAAA reponses.
Just so we know, as this medium priority bug is getting ignored and last update was 3 months ago...
Can we just assume lettre is abandonware?
lettre is maintained by a single person for now (i.e. me) and I work on it when I have time and motivation.

DNS resolution should indeed be improved.

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index a98df950d..8cbb96385 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,7 +24,6 @@ hyperx = { version = "1", optional = true, features = ["headers"] }
 idna = "0.2"
 line-wrap = "0.1"
 log = "0.4"
-uuid = { version = "0.8", features = ["v4"] }
 mime = { version = "0.3", optional = true }
 native-tls = { version = "0.2", optional = true }
 nom = { version = "5", optional = true }
@@ -36,6 +35,7 @@ rustls = { version = "0.17", optional = true }
 serde = { version = "1", optional = true, features = ["derive"] }
 serde_json = { version = "1", optional = true }
 textnonce = { version = "0.7", optional = true }
+uuid = { version = "0.8", features = ["v4"] }
 webpki = { version = "0.21", optional = true }
 webpki-roots = { version = "0.19", optional = true }
 
@@ -51,7 +51,7 @@ name = "transport_smtp"
 
 [features]
 builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
-default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "rustls-tls", "builder", "r2d2"]
+default = ["file-transport", "smtp-transport", "rustls-tls", "hostname", "sendmail-transport", "builder"]
 file-transport = ["serde", "serde_json"]
 rustls-tls = ["webpki", "webpki-roots", "rustls"]
 sendmail-transport = []
diff --git a/src/transport/smtp/client/mod.rs b/src/transport/smtp/client/mod.rs
index 7cde3f753..d32ca8479 100644
--- a/src/transport/smtp/client/mod.rs
+++ b/src/transport/smtp/client/mod.rs
@@ -186,13 +186,14 @@ impl SmtpConnection {
             && self.server_info.supports_feature(Extension::StartTls)
     }
 
+    #[allow(unused_variables)]
     pub fn starttls(
         &mut self,
         tls_parameters: &TlsParameters,
         hello_name: &ClientId,
     ) -> Result<(), Error> {
         if self.server_info.supports_feature(Extension::StartTls) {
-            #[cfg(any(feature = "native-tls", feature = "rustls"))]
+            #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
             {
                 try_smtp!(self.command(Starttls), self);
                 try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self);
@@ -201,7 +202,7 @@ impl SmtpConnection {
                 try_smtp!(self.ehlo(hello_name), self);
                 Ok(())
             }
-            #[cfg(not(any(feature = "native-tls", feature = "rustls")))]
+            #[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
             // This should never happen as `Tls` can only be created
             // when a TLS library is enabled
             unreachable!("TLS support required but not supported");
diff --git a/src/transport/smtp/client/net.rs b/src/transport/smtp/client/net.rs
index f82e9abf5..5dcf5538b 100644
--- a/src/transport/smtp/client/net.rs
+++ b/src/transport/smtp/client/net.rs
@@ -3,15 +3,15 @@
 use crate::transport::smtp::{client::mock::MockStream, error::Error};
 #[cfg(feature = "native-tls")]
 use native_tls::{TlsConnector, TlsStream};
-#[cfg(feature = "rustls")]
+#[cfg(feature = "rustls-tls")]
 use rustls::{ClientConfig, ClientSession};
 #[cfg(feature = "native-tls")]
 use std::io::ErrorKind;
-#[cfg(feature = "rustls")]
+#[cfg(feature = "rustls-tls")]
 use std::sync::Arc;
 use std::{
     io::{self, Read, Write},
-    net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream},
+    net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
     time::Duration,
 };
 
@@ -23,7 +23,7 @@ pub struct TlsParameters {
     #[cfg(feature = "native-tls")]
     connector: TlsConnector,
     /// A client from `rustls`
-    #[cfg(feature = "rustls")]
+    #[cfg(feature = "rustls-tls")]
     // TODO use the same in all transports of the client
     connector: Box<ClientConfig>,
     /// The domain name which is expected in the TLS certificate from the server
@@ -38,7 +38,7 @@ impl TlsParameters {
     }
 
     /// Creates a `TlsParameters`
-    #[cfg(feature = "rustls")]
+    #[cfg(feature = "rustls-tls")]
     pub fn new(domain: String, connector: ClientConfig) -> Self {
         Self {
             connector: Box::new(connector),
@@ -54,7 +54,7 @@ pub enum NetworkStream {
     /// Encrypted TCP stream
     #[cfg(feature = "native-tls")]
     Tls(Box<TlsStream<TcpStream>>),
-    #[cfg(feature = "rustls")]
+    #[cfg(feature = "rustls-tls")]
     Tls(Box<rustls::StreamOwned<ClientSession, TcpStream>>),
     /// Mock stream
     Mock(MockStream),
@@ -67,7 +67,7 @@ impl NetworkStream {
             NetworkStream::Tcp(ref s) => s.peer_addr(),
             #[cfg(feature = "native-tls")]
             NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
-            #[cfg(feature = "rustls")]
+            #[cfg(feature = "rustls-tls")]
             NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
             NetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
                 Ipv4Addr::new(127, 0, 0, 1),
@@ -82,20 +82,34 @@ impl NetworkStream {
             NetworkStream::Tcp(ref s) => s.shutdown(how),
             #[cfg(feature = "native-tls")]
             NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
-            #[cfg(feature = "rustls")]
+            #[cfg(feature = "rustls-tls")]
             NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
             NetworkStream::Mock(_) => Ok(()),
         }
     }
 
-    pub fn connect(
-        addr: &SocketAddr,
+    pub fn connect<T: ToSocketAddrs>(
+        server: T,
         timeout: Option<Duration>,
         tls_parameters: Option<&TlsParameters>,
     ) -> Result<NetworkStream, Error> {
+        fn try_connect_timeout<T: ToSocketAddrs>(
+            server: T,
+            timeout: Duration,
+        ) -> Result<TcpStream, Error> {
+            let addrs = server.to_socket_addrs()?;
+            for addr in addrs {
+                let result = TcpStream::connect_timeout(&addr, timeout);
+                if result.is_ok() {
+                    return result.map_err(|e| e.into());
+                }
+            }
+            Err(Error::Client("Could not connect"))
+        }
+
         let tcp_stream = match timeout {
-            Some(t) => TcpStream::connect_timeout(addr, t)?,
-            None => TcpStream::connect(addr)?,
+            Some(t) => try_connect_timeout(server, t)?,
+            None => TcpStream::connect(server)?,
         };
 
         match tls_parameters {
@@ -105,7 +119,7 @@ impl NetworkStream {
                 .connect(context.domain.as_ref(), tcp_stream)
                 .map(|tls| NetworkStream::Tls(Box::new(tls)))
                 .map_err(|e| Error::Io(io::Error::new(ErrorKind::Other, e))),
-            #[cfg(feature = "rustls")]
+            #[cfg(feature = "rustls-tls")]
             Some(context) => {
                 let domain = webpki::DNSNameRef::try_from_ascii_str(&context.domain)?;
 
@@ -114,10 +128,13 @@ impl NetworkStream {
                     tcp_stream,
                 ))))
             }
+            #[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
+            Some(_) => panic!("TLS configuration without support"),
             None => Ok(NetworkStream::Tcp(tcp_stream)),
         }
     }
 
+    #[allow(unused_variables, unreachable_code)]
     pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
         *self = match *self {
             #[cfg(feature = "native-tls")]
@@ -128,7 +145,7 @@ impl NetworkStream {
                 Ok(tls_stream) => NetworkStream::Tls(Box::new(tls_stream)),
                 Err(err) => return Err(Error::Io(io::Error::new(ErrorKind::Other, err))),
             },
-            #[cfg(feature = "rustls")]
+            #[cfg(feature = "rustls-tls")]
             NetworkStream::Tcp(ref mut stream) => {
                 let domain = webpki::DNSNameRef::try_from_ascii_str(&tls_parameters.domain)?;
 
@@ -137,7 +154,11 @@ impl NetworkStream {
                     stream.try_clone().unwrap(),
                 )))
             }
-            NetworkStream::Tls(_) | NetworkStream::Mock(_) => return Ok(()),
+            #[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
+            NetworkStream::Tcp(_) => panic!("STARTTLS without TLS support"),
+            #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
+            NetworkStream::Tls(_) => return Ok(()),
+            NetworkStream::Mock(_) => return Ok(()),
         };
 
         Ok(())
@@ -146,6 +167,7 @@ impl NetworkStream {
     pub fn is_encrypted(&self) -> bool {
         match *self {
             NetworkStream::Tcp(_) | NetworkStream::Mock(_) => false,
+            #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
             NetworkStream::Tls(_) => true,
         }
     }
@@ -153,6 +175,7 @@ impl NetworkStream {
     pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
         match *self {
             NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
+            #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
             NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
             NetworkStream::Mock(_) => Ok(()),
         }
@@ -162,6 +185,7 @@ impl NetworkStream {
     pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
         match *self {
             NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
+            #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
             NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
             NetworkStream::Mock(_) => Ok(()),
         }
@@ -174,7 +198,7 @@ impl Read for NetworkStream {
             NetworkStream::Tcp(ref mut s) => s.read(buf),
             #[cfg(feature = "native-tls")]
             NetworkStream::Tls(ref mut s) => s.read(buf),
-            #[cfg(feature = "rustls")]
+            #[cfg(feature = "rustls-tls")]
             NetworkStream::Tls(ref mut s) => s.read(buf),
             NetworkStream::Mock(ref mut s) => s.read(buf),
         }
@@ -187,7 +211,7 @@ impl Write for NetworkStream {
             NetworkStream::Tcp(ref mut s) => s.write(buf),
             #[cfg(feature = "native-tls")]
             NetworkStream::Tls(ref mut s) => s.write(buf),
-            #[cfg(feature = "rustls")]
+            #[cfg(feature = "rustls-tls")]
             NetworkStream::Tls(ref mut s) => s.write(buf),
             NetworkStream::Mock(ref mut s) => s.write(buf),
         }
@@ -198,7 +222,7 @@ impl Write for NetworkStream {
             NetworkStream::Tcp(ref mut s) => s.flush(),
             #[cfg(feature = "native-tls")]
             NetworkStream::Tls(ref mut s) => s.flush(),
-            #[cfg(feature = "rustls")]
+            #[cfg(feature = "rustls-tls")]
             NetworkStream::Tls(ref mut s) => s.flush(),
             NetworkStream::Mock(ref mut s) => s.flush(),
         }
diff --git a/src/transport/smtp/commands.rs b/src/transport/smtp/commands.rs
index a4eb49823..6cee1eba8 100644
--- a/src/transport/smtp/commands.rs
+++ b/src/transport/smtp/commands.rs
@@ -169,7 +169,6 @@ pub struct Vrfy {
 
 impl Display for Vrfy {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        #[allow(clippy::write_with_newline)]
         write!(f, "VRFY {}\r\n", self.argument)
     }
 }
@@ -190,7 +189,6 @@ pub struct Expn {
 
 impl Display for Expn {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        #[allow(clippy::write_with_newline)]
         write!(f, "EXPN {}\r\n", self.argument)
     }
 }
diff --git a/src/transport/smtp/error.rs b/src/transport/smtp/error.rs
index 4fad19a46..0652f11e4 100644
--- a/src/transport/smtp/error.rs
+++ b/src/transport/smtp/error.rs
@@ -69,6 +69,7 @@ impl Display for Error {
             Parsing(ref err) => fmt.write_str(err.description()),
             #[cfg(feature = "rustls-tls")]
             InvalidDNSName(ref err) => err.fmt(fmt),
+            #[cfg(feature = "r2d2")]
             Pool(ref err) => err.fmt(fmt),
         }
     }
diff --git a/src/transport/smtp/mod.rs b/src/transport/smtp/mod.rs
index 70d7c987c..59360039b 100644
--- a/src/transport/smtp/mod.rs
+++ b/src/transport/smtp/mod.rs
@@ -12,10 +12,12 @@
 //! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
 //!
 
+#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
+use crate::transport::smtp::client::net::TlsParameters;
 use crate::{
     transport::smtp::{
         authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
-        client::{net::TlsParameters, SmtpConnection},
+        client::SmtpConnection,
         error::{Error, SmtpResult},
         extension::ClientId,
     },
@@ -25,11 +27,12 @@ use crate::{
 use native_tls::{Protocol, TlsConnector};
 #[cfg(feature = "r2d2")]
 use r2d2::Pool;
-#[cfg(feature = "rustls")]
+#[cfg(feature = "rustls-tls")]
 use rustls::ClientConfig;
+#[cfg(feature = "r2d2")]
 use std::ops::DerefMut;
 use std::time::Duration;
-#[cfg(feature = "rustls")]
+#[cfg(feature = "rustls-tls")]
 use webpki_roots::TLS_SERVER_ROOTS;
 
 pub mod authentication;
@@ -61,18 +64,18 @@ const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
 
 /// How to apply TLS to a client connection
 #[derive(Clone)]
-#[allow(missing_debug_implementations)]
+#[allow(missing_debug_implementations, missing_copy_implementations)]
 pub enum Tls {
     /// Insecure connection only (for testing purposes)
     None,
     /// Start with insecure connection and use `STARTTLS` when available
-    #[cfg(any(feature = "native-tls", feature = "rustls"))]
+    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
     Opportunistic(TlsParameters),
     /// Start with insecure connection and require `STARTTLS`
-    #[cfg(any(feature = "native-tls", feature = "rustls"))]
+    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
     Required(TlsParameters),
     /// Use TLS wrapped connection
-    #[cfg(any(feature = "native-tls", feature = "rustls"))]
+    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
     Wrapper(TlsParameters),
 }
 
@@ -128,6 +131,7 @@ impl SmtpTransport {
     /// Simple and secure transport, should be used when possible.
     /// Creates an encrypted transport over submissions port, using the provided domain
     /// to validate TLS certificates.
+    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
     pub fn relay(relay: &str) -> Result<Self, Error> {
         #[cfg(feature = "native-tls")]
         let mut tls_builder = TlsConnector::builder();
@@ -136,22 +140,25 @@ impl SmtpTransport {
         #[cfg(feature = "native-tls")]
         let tls_parameters = TlsParameters::new(relay.to_string(), tls_builder.build().unwrap());
 
-        #[cfg(feature = "rustls")]
+        #[cfg(feature = "rustls-tls")]
         let mut tls = ClientConfig::new();
-        #[cfg(feature = "rustls")]
+        #[cfg(feature = "rustls-tls")]
         tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
-        #[cfg(feature = "rustls")]
+        #[cfg(feature = "rustls-tls")]
         let tls_parameters = TlsParameters::new(relay.to_string(), tls);
 
-        let new = Self::new(relay)
+        #[allow(unused_mut)]
+        let mut new = Self::new(relay)
             .port(SUBMISSIONS_PORT)
             .tls(Tls::Wrapper(tls_parameters));
 
         #[cfg(feature = "r2d2")]
-        // Pool with default configuration
-        // FIXME avoid clone
-        let tpool = new.clone();
-        let new = new.pool(Pool::new(tpool)?);
+        {
+            // Pool with default configuration
+            // FIXME avoid clone
+            let tpool = new.clone();
+            new = new.pool(Pool::new(tpool)?);
+        }
         Ok(new)
     }
 
@@ -193,6 +200,7 @@ impl SmtpTransport {
     }
 
     /// Set the TLS settings to use
+    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
     pub fn tls(mut self, tls: Tls) -> Self {
         self.tls = tls;
         self
@@ -213,20 +221,23 @@ impl SmtpTransport {
             (self.server.as_ref(), self.port),
             self.timeout,
             &self.hello_name,
+            #[allow(clippy::match_single_binding)]
             match self.tls {
+                #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
                 Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
                 _ => None,
             },
         )?;
 
+        #[allow(clippy::match_single_binding)]
         match self.tls {
-            #[cfg(any(feature = "native-tls", feature = "rustls"))]
+            #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
             Tls::Opportunistic(ref tls_parameters) => {
                 if conn.can_starttls() {
                     conn.starttls(tls_parameters, &self.hello_name)?;
                 }
             }
-            #[cfg(any(feature = "native-tls", feature = "rustls"))]
+            #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
             Tls::Required(ref tls_parameters) => {
                 conn.starttls(tls_parameters, &self.hello_name)?;
             }


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2dce1eccb..ce4f282d6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -29,6 +29,11 @@ jobs:
       - uses: actions-rs/cargo@v1
         with:
           command: test
+      - run: rm target/debug/deps/liblettre-*
+      - uses: actions-rs/cargo@v1
+        with:
+          command: test
+          args: --no-default-features --features=builder,smtp-transport,file-transport,sendmail-transport
 
   check:
     name: Check
@@ -78,28 +83,28 @@ jobs:
           command: clippy
           args: -- -D warnings
 
-  coverage:
-    name: Coverage
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v1
-      - uses: actions-rs/toolchain@v1
-        with:
-          toolchain: nightly
-          override: true
-      - run: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install postfix
-      - run: smtp-sink 2525 1000&
-      - uses: actions-rs/cargo@v1
-        with:
-          command: test
-          args: --no-fail-fast
-        env:
-          CARGO_INCREMENTAL: "0"
-          RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads"
-      - id: coverage
-        uses: actions-rs/grcov@v0.1
-      - name: Coveralls upload
-        uses: coverallsapp/github-action@master
-        with:
-          github-token: ${{ secrets.GITHUB_TOKEN }}
-          path-to-lcov: ${{ steps.coverage.outputs.report }}
+#  coverage:
+#    name: Coverage
+#    runs-on: ubuntu-latest
+#    steps:
+#      - uses: actions/checkout@v1
+#      - uses: actions-rs/toolchain@v1
+#        with:
+#          toolchain: nightly
+#          override: true
+#      - run: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install postfix
+#      - run: smtp-sink 2525 1000&
+#      - uses: actions-rs/cargo@v1
+#        with:
+#          command: test
+#          args: --no-fail-fast
+#        env:
+#          CARGO_INCREMENTAL: "0"
+#          RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads"
+#      - id: coverage
+#        uses: actions-rs/grcov@v0.1
+#      - name: Coveralls upload
+#        uses: coverallsapp/github-action@master
+#        with:
+#          github-token: ${{ secrets.GITHUB_TOKEN }}
+#          path-to-lcov: ${{ steps.coverage.outputs.report }}