Repository: twisted/twisted

Issue:
MaildirMessage cannot work with a file in text mode nor in binary mode
|[<img alt="vmario's avatar" src="https://avatars.githubusercontent.com/u/62034?s=50" width="50" height="50">](https://github.com/vmario)| @vmario reported|
|-|-|
|Trac ID|trac#10244|
|Type|defect|
|Created|2021-07-30 10:16:47Z|

I've tried to use `MaildirMessage` object (Twisted v21.7.0) with file opened in text mode but I've got this:

```
2021-07-30T12:05:59+0200 [ESMTP (TLSMemoryBIOProtocol),0,10.127.1.50] Unhandled Error
        Traceback (most recent call last):
          File "/home/user/.local/lib/python3.8/site-packages/twisted/protocols/policies.py", line 109, in dataReceived
            self.wrappedProtocol.dataReceived(data)
          File "/home/user/.local/lib/python3.8/site-packages/twisted/protocols/basic.py", line 439, in dataReceived
            self.lineReceived(line)
          File "/home/user/.local/lib/python3.8/site-packages/twisted/mail/smtp.py", line 501, in lineReceived
            return getattr(self, "state_" + self.mode)(line)
          File "/home/user/.local/lib/python3.8/site-packages/twisted/mail/smtp.py", line 515, in state_COMMAND
            method(b"")
        --- <exception caught here> ---
          File "/home/user/.local/lib/python3.8/site-packages/twisted/mail/smtp.py", line 690, in do_DATA
            msg.lineReceived(rcvdhdr)
          File "/home/user/.local/lib/python3.8/site-packages/twisted/mail/maildir.py", line 143, in lineReceived
            mail.FileMessage.lineReceived(self, line)
          File "/home/user/.local/lib/python3.8/site-packages/twisted/mail/mail.py", line 434, in lineReceived
            self.fp.write(line + b"\n")
        builtins.TypeError: write() argument must be str, not bytes
```

This is clear because you cannot write bytes to the text file. Unfortunately with file in binary mode I've got this:

```
2021-07-30T12:03:10+0200 [ESMTP (TLSMemoryBIOProtocol),0,10.127.1.50] Unhandled Error
        Traceback (most recent call last):
          File "/home/user/.local/lib/python3.8/site-packages/twisted/protocols/policies.py", line 109, in dataReceived
            self.wrappedProtocol.dataReceived(data)
          File "/home/user/.local/lib/python3.8/site-packages/twisted/protocols/basic.py", line 439, in dataReceived
            self.lineReceived(line)
          File "/home/user/.local/lib/python3.8/site-packages/twisted/mail/smtp.py", line 501, in lineReceived
            return getattr(self, "state_" + self.mode)(line)
          File "/home/user/.local/lib/python3.8/site-packages/twisted/mail/smtp.py", line 515, in state_COMMAND
            method(b"")
        --- <exception caught here> ---
          File "/home/user/.local/lib/python3.8/site-packages/twisted/mail/smtp.py", line 687, in do_DATA
            msg = msgFunc()
          File "/home/user/Kodowanie/Git/RKPProxy/smtp/client.py", line 80, in <lambda>
            return lambda: LocalMessageFactory(user, self.usersDirPath).message()
          File "/home/user/Kodowanie/Git/RKPProxy/smtp/client.py", line 217, in message
            return maildir.MaildirMessage(
          File "/home/user/.local/lib/python3.8/site-packages/twisted/mail/maildir.py", line 132, in __init__
            fp.write(header)
        builtins.TypeError: a bytes-like object is required, not 'str'
```

`MaildirMessage` writes header to the file but this header is string not bytes.

<details><summary>Searchable metadata</summary>

```
trac-id__10244 10244
type__defect defect
reporter__vmario vmario
priority__normal normal
milestone__None None
branch__ 
branch_author__ 
status__new new
resolution__None None
component__mail mail
keywords__None None
time__1627640207974143 1627640207974143
changetime__1656140124886537 1656140124886537
version__None None
owner__vmario vmario

```
</details>



Discussion:

Resolving patch:
diff --git a/src/twisted/mail/maildir.py b/src/twisted/mail/maildir.py
--- a/src/twisted/mail/maildir.py
+++ b/src/twisted/mail/maildir.py
@@ -126,7 +126,7 @@ def __init__(self, address, fp, *a, **kw):
         @type kw: L{dict}
         @param kw: Keyword arguments for L{FileMessage.__init__}.
         """
-        header = "Delivered-To: %s\n" % address
+        header = b"Delivered-To: %s\n" % address
         fp.write(header)
         self.size = len(header)
         mail.FileMessage.__init__(self, fp, *a, **kw)


Test patch:
diff --git a/src/twisted/mail/test/test_mail.py b/src/twisted/mail/test/test_mail.py
--- a/src/twisted/mail/test/test_mail.py
+++ b/src/twisted/mail/test/test_mail.py
@@ -8,6 +8,7 @@
 import email.message
 import email.parser
 import errno
+import glob
 import io
 import os
 import pickle
@@ -220,25 +221,12 @@ def test_rejected(self):
 @skipIf(platformType != "posix", "twisted.mail only works on posix")
 class FileMessageTests(TestCase):
     def setUp(self):
-        self.name = "fileMessage.testFile"
-        self.final = "final.fileMessage.testFile"
-        self.f = open(self.name, "w")
+        self.name = self.mktemp()
+        self.final = self.mktemp()
+        self.f = open(self.name, "wb")
+        self.addCleanup(self.f.close)
         self.fp = mail.mail.FileMessage(self.f, self.name, self.final)
 
-    def tearDown(self):
-        try:
-            self.f.close()
-        except BaseException:
-            pass
-        try:
-            os.remove(self.name)
-        except BaseException:
-            pass
-        try:
-            os.remove(self.final)
-        except BaseException:
-            pass
-
     def testFinalName(self):
         return self.fp.eomReceived().addCallback(self._cbFinalName)
 
@@ -247,18 +235,16 @@ def _cbFinalName(self, result):
         self.assertTrue(self.f.closed)
         self.assertFalse(os.path.exists(self.name))
 
-    @skipIf(sys.version_info >= (3,), "not ported to Python 3")
     def testContents(self):
-        contents = "first line\nsecond line\nthird line\n"
+        contents = b"first line\nsecond line\nthird line\n"
         for line in contents.splitlines():
             self.fp.lineReceived(line)
         self.fp.eomReceived()
-        with open(self.final) as f:
+        with open(self.final, "rb") as f:
             self.assertEqual(f.read(), contents)
 
-    @skipIf(sys.version_info >= (3,), "not ported to Python 3")
     def testInterrupted(self):
-        contents = "first line\nsecond line\n"
+        contents = b"first line\nsecond line\n"
         for line in contents.splitlines():
             self.fp.lineReceived(line)
         self.fp.connectionLost()
@@ -266,6 +252,71 @@ def testInterrupted(self):
         self.assertFalse(os.path.exists(self.final))
 
 
+@skipIf(platformType != "posix", "twisted.mail only works on posix")
+class MaildirMessageTests(TestCase):
+    """
+    Tests for the file creating by the L{mail.maildir.MaildirMessage}.
+    """
+
+    def setUp(self):
+        """
+        Create and open a temporary file.
+        """
+        self.name = self.mktemp()
+        self.final = self.mktemp()
+        self.address = b"user@example.com"
+        self.f = open(self.name, "wb")
+        self.addCleanup(self.f.close)
+        self.fp = mail.maildir.MaildirMessage(
+            self.address, self.f, self.name, self.final
+        )
+
+    def _finalName(self):
+        """
+        Search for the final file path.
+
+        @rtype: L{str}
+        @return: Final file path.
+        """
+        return glob.glob(f"{self.final},S=[0-9]*")[0]
+
+    def test_finalName(self):
+        """
+        Send the EOM to the message and check that the final file name contains
+        the correct file size and the temporary file has been closed and removed.
+        """
+        final = self.successResultOf(self.fp.eomReceived())
+        self.assertEqual(final, f"{self.final},S={os.path.getsize(final)}")
+        self.assertTrue(self.f.closed)
+        self.assertFalse(os.path.exists(self.name))
+
+    def test_contents(self):
+        """
+        Send a message contents and the EOM to the message and check that the
+        final file contains the correct header and the message contents.
+        """
+        contents = b"first line\nsecond line\nthird line\n"
+        for line in contents.splitlines():
+            self.fp.lineReceived(line)
+        final = self.successResultOf(self.fp.eomReceived())
+        with open(final, "rb") as f:
+            self.assertEqual(
+                f.read(), b"Delivered-To: %s\n%s" % (self.address, contents)
+            )
+
+    def test_interrupted(self):
+        """
+        Check that the interrupted message transfer removes the temporary file
+        and a doesn't create a final file.
+        """
+        contents = b"first line\nsecond line\n"
+        for line in contents.splitlines():
+            self.fp.lineReceived(line)
+        self.fp.connectionLost()
+        self.assertFalse(os.path.exists(self.name))
+        self.assertRaises(IndexError, self._finalName)
+
+
 @skipIf(platformType != "posix", "twisted.mail only works on posix")
 class MailServiceTests(TestCase):
     def setUp(self):