Repository: awdeorio/mailmerge

Issue:
Load system default SSL cert
This issue applies to the SMTP SSL security mode.  Use the system's default SSL certificate chain.

From https://docs.python.org/3/library/smtplib.html :

> Please use [ssl.SSLContext.load_cert_chain()](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain) instead, or let [ssl.create_default_context()](https://docs.python.org/3/library/ssl.html#ssl.create_default_context) select the system’s trusted CA certificates for you.

Discussion:

Patch:
diff --git a/mailmerge/sendmail_client.py b/mailmerge/sendmail_client.py
index 80d0a89..d0bde46 100644
--- a/mailmerge/sendmail_client.py
+++ b/mailmerge/sendmail_client.py
@@ -10,6 +10,7 @@ import configparser
 import getpass
 import datetime
 import base64
+import ssl
 from . import exceptions
 
 # Type to store info read from config file
@@ -118,7 +119,12 @@ class SendmailClient:
     def sendmail_ssltls(self, sender, recipients, message):
         """Send email message with SSL/TLS security."""
         message_flattened = str(message)
-        with smtplib.SMTP_SSL(self.config.host, self.config.port) as smtp:
+        try:
+            ctx = ssl.create_default_context()
+        except ssl.SSLError as err:
+            raise exceptions.MailmergeError(f"SSL Error: {err}")
+        host, port = (self.config.host, self.config.port)
+        with smtplib.SMTP_SSL(host, port, context=ctx) as smtp:
             smtp.login(self.config.username, self.password)
             smtp.sendmail(sender, recipients, message_flattened)
 


Test patch:
diff --git a/tests/test_sendmail_client.py b/tests/test_sendmail_client.py
index 3b0e47f..cad7600 100644
--- a/tests/test_sendmail_client.py
+++ b/tests/test_sendmail_client.py
@@ -9,6 +9,7 @@ import smtplib
 import email
 import email.parser
 import base64
+import ssl
 import pytest
 from mailmerge import SendmailClient, MailmergeError
 
@@ -378,7 +379,7 @@ def test_security_plain(mocker, tmp_path):
 
 
 def test_security_ssl(mocker, tmp_path):
-    """Verify open (Never) security configuration."""
+    """Verify SSL/TLS security configuration."""
     # Config for SSL SMTP server
     config_path = tmp_path/"server.conf"
     config_path.write_text(textwrap.dedent("""\
@@ -397,6 +398,10 @@ def test_security_ssl(mocker, tmp_path):
     mock_smtp = mocker.patch('smtplib.SMTP')
     mock_smtp_ssl = mocker.patch('smtplib.SMTP_SSL')
 
+    # Mock SSL
+    mock_ssl_create_default_context = \
+        mocker.patch('ssl.create_default_context')
+
     # Mock the password entry
     mock_getpass = mocker.patch('getpass.getpass')
     mock_getpass.return_value = "password"
@@ -412,6 +417,8 @@ def test_security_ssl(mocker, tmp_path):
     assert mock_getpass.call_count == 1
     assert mock_smtp.call_count == 0
     assert mock_smtp_ssl.call_count == 1
+    assert mock_ssl_create_default_context.called
+    assert "context" in mock_smtp_ssl.call_args[1]  # SSL cert chain
     smtp = mock_smtp_ssl.return_value.__enter__.return_value
     assert smtp.ehlo.call_count == 0
     assert smtp.starttls.call_count == 0
@@ -419,6 +426,44 @@ def test_security_ssl(mocker, tmp_path):
     assert smtp.sendmail.call_count == 1
 
 
+def test_ssl_error(mocker, tmp_path):
+    """Verify SSL/TLS with an SSL error."""
+    # Config for SSL SMTP server
+    config_path = tmp_path/"server.conf"
+    config_path.write_text(textwrap.dedent("""\
+        [smtp_server]
+        host = smtp.mail.umich.edu
+        port = 465
+        security = SSL/TLS
+        username = YOUR_USERNAME_HERE
+    """))
+
+    # Simple template
+    sendmail_client = SendmailClient(config_path, dry_run=False)
+    message = email.message_from_string("Hello world")
+
+    # Mock ssl.create_default_context() to raise an exception
+    mocker.patch(
+        'ssl.create_default_context',
+        side_effect=ssl.SSLError(1, "CERTIFICATE_VERIFY_FAILED")
+    )
+
+    # Mock the password entry
+    mock_getpass = mocker.patch('getpass.getpass')
+    mock_getpass.return_value = "password"
+
+    # Send a message
+    with pytest.raises(MailmergeError) as err:
+        sendmail_client.sendmail(
+            sender="test@test.com",
+            recipients=["test@test.com"],
+            message=message,
+        )
+
+    # Verify exception string
+    assert "CERTIFICATE_VERIFY_FAILED" in str(err.value)
+
+
 def test_missing_username(tmp_path):
     """Verify exception on missing username."""
     config_path = tmp_path/"server.conf"