mirror of
https://seed.flo-the.dev/z3gWc1qgaeZaoGwL4WTstLNoqjayM.git
synced 2025-12-06 12:57:35 +01:00
190 lines
6.4 KiB
Python
190 lines
6.4 KiB
Python
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
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] == "MIIBTgYJKoZIhvcNAQcDoIIBPzCCATsCAQAxgfcwgfQCAQAwXTBFMQswCQYDVQQG"
|
|
)
|
|
|
|
# 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"
|