# SMTP forwarding relay daemon with signing and encryption # # Copyright (C) 2024 F. Brandes (additions to original code) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """ Tests for smtprd_ng """ # pylint: disable=protected-access import configparser import email import email.message from pathlib import Path import pytest from M2Crypto import BIO, SMIME, X509 from smtprd_ng import smtprd def config() -> smtprd.ClientConfig: """Return method for settings used here in test""" cfg = smtprd.ClientConfig( hostname="localhost", port=1234, username="user", password="pass", use_tls=False, start_tls=False, sender="sender", set_reply_to="", smime_cert=Path("tests", "signer.pem"), smime_cert_private=Path("tests", "privkey.pem"), ) return cfg def emails() -> smtprd.EmailConfig: """Return the email dict""" cfg = smtprd.EmailConfig( email_certs={"recipient": Path("tests", "signer.pem")}, ) return cfg def test_config_from_config(): """Test opening and reading the config file""" config_parser: configparser.ConfigParser = configparser.ConfigParser() with open(Path("config.example"), "r", encoding="utf8") as fp: config_parser.read_file(fp) cfg = smtprd.Config._from_config(config_parser) assert cfg.server.hostname == "localhost" assert cfg.client.port == 465 assert cfg.client.smime_cert == "" assert cfg.client.use_tls is True assert cfg.client.password == "" def test_config_from_ini(): """Test parsing the config file and using fallbacks. This is mostly redundant to the above test""" cfg = smtprd.Config.from_ini(Path("config.example")) assert cfg.server.hostname == "localhost" assert cfg.client.port == 465 assert cfg.client.smime_cert == "" assert cfg.client.use_tls is True def test_read_from_file(): """Test read_from_file function and the fallback""" existing_file = "config.example" non_existing_file = "abcde" empty_file = "tests/__init__.py" empty_string = "" assert smtprd.Config._read_from_file(existing_file) == "# [server]" with pytest.raises(Exception) as e_info: smtprd.Config._read_from_file(non_existing_file) assert e_info.typename == "OSError" assert str(e_info.value) == "File wasn't found: abcde" assert smtprd.Config._read_from_file(empty_string) == "" with pytest.raises(Exception) as e_info: smtprd.Config._read_from_file(empty_file) assert e_info.typename == "OSError" assert str(e_info.value) == "Empty file supplied: tests/__init__.py" def test_config_emailcerts(): """test retrieval of email/cert pair""" email_list = [("email1", "cert1"), ("email2", "cert2")] cfg = smtprd.Config._get_emails_and_certs(email_list) assert cfg["email2"] == "cert2" def test_client_encrypt(): """Test encryption. Keys were generated with openssl req -newkey rsa:1024 -nodes -x509 -days 365 -out signer.pem """ client = smtprd.SMTPClient(config(), emails()) encrypted = client._encrypt(b"abc", "subject", "recipient") lines = encrypted.decode().splitlines() # Test format of header assert lines[0] == "From: " + str(config().sender) assert lines[1] == "To: recipient" assert lines[2] == "Subject: " + "subject" assert lines[3] == "MIME-Version: 1.0" assert lines[4] == 'Content-Disposition: attachment; filename="smime.p7m"' assert ( lines[5] == 'Content-Type: application/x-pkcs7-mime; smime-type=enveloped-data; name="smime.p7m"' ) assert lines[6] == "Content-Transfer-Encoding: base64" # new line is important to seperate header from body assert lines[7] == "" assert ( lines[8] == "MIIBdgYJKoZIhvcNAQcDoIIBZzCCAWMCAQAxggEeMIIBGgIBADCBgjBqMQswCQYD" ) # test decryption s = SMIME.SMIME() s.load_key(config().smime_cert_private, emails().email_certs["recipient"]) buf = BIO.MemoryBuffer(encrypted) p7, _ = SMIME.smime_load_pkcs7_bio(buf) out = s.decrypt(p7) assert b"abc" in out def test_client_sign(): """Test signing""" client = smtprd.SMTPClient(config(), emails()) message = email.message.EmailMessage() message.set_content("Test to sign") # header are needed, because email.message.Emailmessage will add a # newline between the boundary and the content if no header is found message.add_header("From", config().sender) message.add_header("To", "recipient") message.add_header("Subject", "Test") signed = client._sign(message) assert "This is an S/MIME signed message" in signed.decode() assert "Test to sign" in signed.decode() s = SMIME.SMIME() # Load the signer's cert. x509 = X509.load_cert(Path(config().smime_cert)) sk = X509.X509_Stack() sk.push(x509) s.set_x509_stack(sk) # Load the signer's CA cert. In this case, because the signer's # cert is self-signed, it is the signer's cert itself. st = X509.X509_Store() st.load_info(str(config().smime_cert)) s.set_x509_store(st) # Load the data, verify it. buf = BIO.MemoryBuffer(signed) p7, data = SMIME.smime_load_pkcs7_bio(buf) v = s.verify(p7, data) assert "Test to sign" in v.decode() def test_cli_no_config_file(): """Test whether config file parameter is supplied""" test_args = ["--config", ""] with pytest.raises(Exception) as e_info: smtprd.main(test_args) assert e_info.typename == "OSError" assert str(e_info.value) == "No config file supplied" def test_cli_config_file_not_found(): """Test whether config file is found""" test_args = ["--config", "doesnotexist.conf"] with pytest.raises(Exception) as e_info: smtprd.main(test_args) assert e_info.typename == "OSError" assert str(e_info.value) == "Config file not found: doesnotexist.conf"