Repository: lettre/lettre (language: rust)

Issue:
Possible bug in transparency encoding of mail body
**Describe the bug**
According to the [spec](https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.2): 

> Before sending a line of mail text, the SMTP client checks the first character of the line.  If it is a period, one additional period is inserted at the beginning of the line.

But the [function](https://github.com/lettre/lettre/blob/master/src/transport/smtp/client/mod.rs#L64) seems to only apply to lines that only contain a "." and nothing else.

**To Reproduce**
The [test](https://github.com/lettre/lettre/blob/master/src/transport/smtp/client/mod.rs#L129) in the module has an example that seems incorrect.

**Expected behavior**
It seems to me that b".test\n" should be converted to b"..test\n".

**Environment (please complete the following information):**
- Lettre version - master



Discussion / hints:

Fix patch:
diff --git a/src/transport/smtp/client/connection.rs b/src/transport/smtp/client/connection.rs
index f145be9e7..657cb94e3 100644
--- a/src/transport/smtp/client/connection.rs
+++ b/src/transport/smtp/client/connection.rs
@@ -243,11 +243,12 @@ impl SmtpConnection {
 
     /// Sends the message content
     pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
-        let mut out_buf: Vec<u8> = vec![];
         let mut codec = ClientCodec::new();
+        let mut out_buf = Vec::with_capacity(message.len());
         codec.encode(message, &mut out_buf);
         self.write(out_buf.as_slice())?;
         self.write(b"\r\n.\r\n")?;
+
         self.read_response()
     }
 
diff --git a/src/transport/smtp/client/mod.rs b/src/transport/smtp/client/mod.rs
index 557d2e609..fcfcbd8d5 100644
--- a/src/transport/smtp/client/mod.rs
+++ b/src/transport/smtp/client/mod.rs
@@ -48,60 +48,57 @@ mod net;
 mod tls;
 
 /// The codec used for transparency
-#[derive(Default, Clone, Copy, Debug)]
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Debug)]
 struct ClientCodec {
-    escape_count: u8,
+    status: CodecStatus,
 }
 
 impl ClientCodec {
     /// Creates a new client codec
     pub fn new() -> Self {
-        ClientCodec::default()
+        Self {
+            status: CodecStatus::StartOfNewLine,
+        }
     }
 
     /// Adds transparency
     fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
-        match frame.len() {
-            0 => {
-                match self.escape_count {
-                    0 => buf.extend_from_slice(b"\r\n.\r\n"),
-                    1 => buf.extend_from_slice(b"\n.\r\n"),
-                    2 => buf.extend_from_slice(b".\r\n"),
-                    _ => unreachable!(),
+        for &b in frame {
+            buf.push(b);
+            match (b, self.status) {
+                (b'\r', _) => {
+                    self.status = CodecStatus::StartingNewLine;
                 }
-                self.escape_count = 0;
-            }
-            _ => {
-                let mut start = 0;
-                for (idx, byte) in frame.iter().enumerate() {
-                    match self.escape_count {
-                        0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
-                        1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
-                        2 => {
-                            self.escape_count = if *byte == b'.' {
-                                3
-                            } else if *byte == b'\r' {
-                                1
-                            } else {
-                                0
-                            }
-                        }
-                        _ => unreachable!(),
-                    }
-                    if self.escape_count == 3 {
-                        self.escape_count = 0;
-                        buf.extend_from_slice(&frame[start..idx]);
-                        buf.extend_from_slice(b".");
-                        start = idx;
-                    }
+                (b'\n', CodecStatus::StartingNewLine) => {
+                    self.status = CodecStatus::StartOfNewLine;
+                }
+                (_, CodecStatus::StartingNewLine) => {
+                    self.status = CodecStatus::MiddleOfLine;
+                }
+                (b'.', CodecStatus::StartOfNewLine) => {
+                    self.status = CodecStatus::MiddleOfLine;
+                    buf.push(b'.');
+                }
+                (_, CodecStatus::StartOfNewLine) => {
+                    self.status = CodecStatus::MiddleOfLine;
                 }
-                buf.extend_from_slice(&frame[start..]);
+                _ => {}
             }
         }
     }
 }
 
+#[derive(Debug, Copy, Clone)]
+#[allow(clippy::enum_variant_names)]
+enum CodecStatus {
+    /// We are past the first character of the current line
+    MiddleOfLine,
+    /// We just read a `\r` character
+    StartingNewLine,
+    /// We are at the start of a new line
+    StartOfNewLine,
+}
+
 /// Returns the string replacing all the CRLF with "\<CRLF\>"
 /// Used for debug displays
 #[cfg(feature = "tracing")]
@@ -115,9 +112,10 @@ mod test {
 
     #[test]
     fn test_codec() {
+        let mut buf = Vec::new();
         let mut codec = ClientCodec::new();
-        let mut buf: Vec<u8> = vec![];
 
+        codec.encode(b".\r\n", &mut buf);
         codec.encode(b"test\r\n", &mut buf);
         codec.encode(b"test\r\n\r\n", &mut buf);
         codec.encode(b".\r\n", &mut buf);
@@ -128,9 +126,13 @@ mod test {
         codec.encode(b"test\n", &mut buf);
         codec.encode(b".test\n", &mut buf);
         codec.encode(b"test", &mut buf);
+        codec.encode(b"test", &mut buf);
+        codec.encode(b"test\r\n", &mut buf);
+        codec.encode(b".test\r\n", &mut buf);
+        codec.encode(b"test.\r\n", &mut buf);
         assert_eq!(
             String::from_utf8(buf).unwrap(),
-            "test\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
+            "..\r\ntest\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntesttesttest\r\n..test\r\ntest.\r\n"
         );
     }
 


Test patch: