Repository: buildbot/buildbot

Issue:
MailNotifier fails with SMTP error if useTLS=False but buildbot[tls] has been installed.
If I install `buildbot` and then install the `buildbot[tls]` bundle, it pulls in dependencies `pyasn1 pyasn1-modules pyopenssl service-identity`.  However, after that, MailNotifier runs start failing due to failure to connect to SMTP server, even though useTLS is not set.

Using:
- Ubuntu LInux 18.04.5
- Python 3.6.9
- buildbot 2.8.4

Steps to reproduce:
1. Create a new virtual environment and activate it.
2. `pip install --upgrade pip wheel setuptools`
3. `pip install buildbot[bundle]`
4. `pip install buildbot[tls]`
5. `pip install requests` (I need this because I'm using Google Auth)
6. Start buildbot with a `master.cfg` that has a `MailNotifier` configured, and do something that triggers the `MailNotifier`.

Expected behavior:
- Email notification is sent

Actual behavior:
```
2020-11-02 14:45:46-0600 [-] Starting factory <twisted.mail.smtp.ESMTPSenderFactory object at 0x7f2dd58e5048>
2020-11-02 14:45:46-0600 [ESMTPSender,client] SMTP Client retrying server. Retry: 5
2020-11-02 14:45:46-0600 [ESMTPSender,client] SMTP Client retrying server. Retry: 4
2020-11-02 14:45:46-0600 [ESMTPSender,client] SMTP Client retrying server. Retry: 3
2020-11-02 14:45:46-0600 [ESMTPSender,client] SMTP Client retrying server. Retry: 2
2020-11-02 14:45:46-0600 [ESMTPSender,client] SMTP Client retrying server. Retry: 1
2020-11-02 14:45:46-0600 [-] Stopping factory <twisted.mail.smtp.ESMTPSenderFactory object at 0x7f2dd58e5048>
...
2020-11-02 14:46:25-0600 [-] Unhandled error in Deferred:
2020-11-02 14:46:25-0600 [-] Unhandled Error
    Traceback (most recent call last):
    --- <exception caught here> ---
      File "REDACTED/lib/python3.6/site-packages/buildbot/reporters/notifier.py", line 279, in buildMessage
        list(users), patches, logs)
      File "REDACTED/lib/python3.6/site-packages/buildbot/reporters/mail.py", line 280, in sendMessage
        yield self.sendMail(m, all_recipients)
    twisted.mail._except.SMTPConnectError: Unable to connect to server.
```

If I uninstall the above listed dependencies installed by `buildbot[tls]`, MailNotifier resumes normal operation.

Am I doing something wrong?  I encountered this issue [when trying to get the `buildbot-gitea` plugin to work](https://github.com/lab132/buildbot-gitea/issues/12), since it requires either `treq` or `txrequests`, and installing treq pulls in the same package dependencies that `buildbot[tls]` does.  Fortunately, installing `txrequests` does _not_ pull those packages in, so if I use `txrequests` instead I can get `buildbot-gitea` to work.  However, it seems odd that MailNotifier should break in this way just because those additional packages have been installed.

Thanks in advance for any suggestions...


Discussion:

Resolving patch:
diff --git a/master/buildbot/reporters/mail.py b/master/buildbot/reporters/mail.py
--- a/master/buildbot/reporters/mail.py
+++ b/master/buildbot/reporters/mail.py
@@ -332,11 +332,12 @@ def sendMail(self, m, recipients):
 
         s = unicode2bytes(s)
         recipients = [parseaddr(r)[1] for r in recipients]
+        hostname = self.relayhost if self.useTls or useAuth else None
         sender_factory = ESMTPSenderFactory(
             unicode2bytes(self.smtpUser), unicode2bytes(self.smtpPassword),
             parseaddr(self.fromaddr)[1], recipients, BytesIO(s),
             result, requireTransportSecurity=self.useTls,
-            requireAuthentication=useAuth, hostname=self.relayhost)
+            requireAuthentication=useAuth, hostname=hostname)
 
         if self.useSmtps:
             reactor.connectSSL(self.relayhost, self.smtpPort,


Test patch:
diff --git a/master/buildbot/test/unit/reporters/test_mail.py b/master/buildbot/test/unit/reporters/test_mail.py
--- a/master/buildbot/test/unit/reporters/test_mail.py
+++ b/master/buildbot/test/unit/reporters/test_mail.py
@@ -395,6 +395,82 @@ def test_sendMessageOverSsl(self):
         self.assertIn(('connectSSL', ('localhost', 25, None, fakereactor.connectSSL.call_args[
                       0][3]), {}), fakereactor.method_calls)
 
+    @ssl.skipUnless
+    @defer.inlineCallbacks
+    def test_hostname_for_client_context_for_tcp_over_tls(self):
+        fakeSenderFactory = Mock()
+        fakeSenderFactory.side_effect = lambda *args, **kwargs: args[
+            5].callback(True)
+        self.patch(mail, 'ESMTPSenderFactory', fakeSenderFactory)
+        fakereactor = Mock()
+        self.patch(mail, 'reactor', fakereactor)
+        msg = Mock()
+        msg.as_string = Mock(return_value='<email>')
+
+        mn = yield self.setupMailNotifier('John Doe <john.doe@domain.tld>',
+                                          useTls=True)
+        yield mn.sendMail(msg, ['Jane Doe <jane.doe@domain.tld>'])
+
+        self.assertEqual(mn.useTls, True)
+        self.assertEqual(1, len(fakereactor.method_calls))
+        self.assertIn(('connectTCP', ('localhost', 25, None), {}),
+                      fakereactor.method_calls)
+
+        self.assertEqual("localhost",
+                         fakeSenderFactory.call_args.kwargs.get("hostname"))
+
+    @ssl.skipUnless
+    @defer.inlineCallbacks
+    def test_hostname_for_client_context_for_tcp_over_tls_auth(self):
+        fakeSenderFactory = Mock()
+        fakeSenderFactory.side_effect = lambda *args, **kwargs: args[
+            5].callback(True)
+        self.patch(mail, 'ESMTPSenderFactory', fakeSenderFactory)
+        fakereactor = Mock()
+        self.patch(mail, 'reactor', fakereactor)
+        msg = Mock()
+        msg.as_string = Mock(return_value='<email>')
+
+        mn = yield self.setupMailNotifier('John Doe <john.doe@domain.tld>',
+                                          smtpUser="u$er",
+                                          smtpPassword="pa$$word",
+                                          useTls=False)
+        yield mn.sendMail(msg, ['Jane Doe <jane.doe@domain.tld>'])
+
+        self.assertEqual(mn.smtpUser, "u$er")
+        self.assertEqual(mn.smtpPassword, "pa$$word")
+        self.assertEqual(mn.useTls, False)
+        self.assertEqual(1, len(fakereactor.method_calls))
+        self.assertIn(('connectTCP', ('localhost', 25, None), {}),
+                      fakereactor.method_calls)
+
+        self.assertEqual("localhost",
+                         fakeSenderFactory.call_args.kwargs.get("hostname"))
+
+    @ssl.skipUnless
+    @defer.inlineCallbacks
+    def test_hostname_for_client_context_for_plan_tcp(self):
+        fakeSenderFactory = Mock()
+        fakeSenderFactory.side_effect = lambda *args, **kwargs: args[
+            5].callback(True)
+        self.patch(mail, 'ESMTPSenderFactory', fakeSenderFactory)
+        fakereactor = Mock()
+        self.patch(mail, 'reactor', fakereactor)
+        msg = Mock()
+        msg.as_string = Mock(return_value='<email>')
+
+        mn = yield self.setupMailNotifier('John Doe <john.doe@domain.tld>',
+                                          useTls=False)
+        yield mn.sendMail(msg, ['Jane Doe <jane.doe@domain.tld>'])
+
+        self.assertEqual(mn.useTls, False)
+        self.assertEqual(1, len(fakereactor.method_calls))
+        self.assertIn(('connectTCP', ('localhost', 25, None), {}),
+                      fakereactor.method_calls)
+
+        self.assertEqual(None,
+                         fakeSenderFactory.call_args.kwargs.get("hostname"))
+
 
 def create_msgdict(funny_chars='\u00E5\u00E4\u00F6'):
     unibody = f'Unicode body with non-ascii ({funny_chars}).'