mirror of
https://seed.flo-the.dev/z3gWc1qgaeZaoGwL4WTstLNoqjayM.git
synced 2025-12-06 04:47:35 +01:00
first working signing version
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
This commit is contained in:
99
smtprd.py
99
smtprd.py
@@ -18,13 +18,22 @@ from email import message_from_bytes, message_from_string
|
||||
from email.message import Message
|
||||
from email.policy import default as EmailMessagePolicy
|
||||
from locale import LC_ALL, setlocale
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Set, Tuple, Union
|
||||
|
||||
import envelope
|
||||
from aiosmtpd.controller import SMTP as Server
|
||||
from aiosmtpd.controller import Controller
|
||||
from aiosmtpd.smtp import Envelope, Session
|
||||
from aiosmtplib import SMTP, SMTPException, SMTPRecipientRefused, SMTPRecipientsRefused
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
Encoding,
|
||||
load_pem_private_key,
|
||||
pkcs7,
|
||||
)
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from envelope import Envelope
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -44,6 +53,9 @@ class ClientConfig:
|
||||
set_reply_to: bool
|
||||
use_tls: bool
|
||||
start_tls: bool
|
||||
smime_cert: str
|
||||
smime_cert_private: str
|
||||
smime_to_cert: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -52,7 +64,7 @@ class Config:
|
||||
client: ClientConfig
|
||||
|
||||
@classmethod
|
||||
def _from_config(cls, config: configparser.ConfigParser) -> "Config":
|
||||
def _from_config(cls, config: configparser.RawConfigParser) -> "Config":
|
||||
return cls(
|
||||
server=ServerConfig(
|
||||
hostname=config.get("server", "hostname", fallback="localhost"),
|
||||
@@ -72,6 +84,11 @@ class Config:
|
||||
),
|
||||
use_tls=config.getboolean("client", "use_tls", fallback=True),
|
||||
start_tls=config.getboolean("client", "start_tls", fallback=False),
|
||||
smime_cert=config.get("client", "smime_cert", fallback=""),
|
||||
smime_cert_private=config.get(
|
||||
"client", "smime_cert_private", fallback=""
|
||||
),
|
||||
smime_to_cert=config.get("client", "smime_to_cert", fallback=""),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -99,13 +116,68 @@ class SMTPClient(SMTP):
|
||||
start_tls=self._config.start_tls,
|
||||
)
|
||||
|
||||
def _encrypt_and_sign(self, message: Message, recipients: List[str]) -> Message:
|
||||
# Currently does NOT work
|
||||
new_message = (
|
||||
Envelope()
|
||||
.smime()
|
||||
.load(message)
|
||||
.sign(
|
||||
key=Path(self._config.smime_cert_private),
|
||||
cert=Path(self._config.smime_cert),
|
||||
)
|
||||
.as_message()
|
||||
)
|
||||
new_message["Original-Sender"] = message["Original-Sender"]
|
||||
new_message["Original-Recipient"] = message["Original-Recipient"]
|
||||
new_message["From"] = message["From"]
|
||||
new_message["To"] = message["To"]
|
||||
return new_message
|
||||
|
||||
def _sign(self, message: Message) -> bytes:
|
||||
"""Sign the message
|
||||
|
||||
This function signs the message. All existing headers will be moved
|
||||
inside the signature boundary.
|
||||
Args:
|
||||
message (Message): message object
|
||||
|
||||
Returns:
|
||||
bytes: The signed message including the From and To Header
|
||||
"""
|
||||
with open(Path(self._config.smime_cert_private), "rb") as key_data:
|
||||
key = load_pem_private_key(key_data.read(), password=None)
|
||||
with open(self._config.smime_cert, "rb") as cert_data:
|
||||
cert = load_pem_x509_certificate(cert_data.read())
|
||||
|
||||
# sign
|
||||
output = (
|
||||
pkcs7.PKCS7SignatureBuilder()
|
||||
.set_data(message.as_bytes())
|
||||
.add_signer(cert, key, hashes.SHA512(), rsa_padding=padding.PKCS1v15())
|
||||
.sign(Encoding.SMIME, [pkcs7.PKCS7Options.DetachedSignature])
|
||||
)
|
||||
# Add correct headers
|
||||
new = b"From: " + self._config.sender.encode() + b"\r\n" + output
|
||||
new = b"To: " + ", ".join(self._config.recipients).encode() + b"\r\n" + new
|
||||
new = b"Subject: " + message.get("Subject", "").encode() + b"\r\n" + new
|
||||
return new
|
||||
|
||||
async def _send_message(
|
||||
self, message: Message, sender: str, recipients: List[str]
|
||||
self, message: bytes, sender: str, recipients: List[str]
|
||||
) -> None:
|
||||
# if we use send_message, it expects a message object
|
||||
# This will add a newline after the signing process. The message object is fine
|
||||
# but as soon as _as_bytes or _as_string is called, it will add a new line between the boundary of the
|
||||
# signed message, which will ruin the signature
|
||||
failed_recipients: Set[str] = set(
|
||||
(await self.send_message(message, sender=sender, recipients=recipients))[
|
||||
0
|
||||
].keys()
|
||||
(
|
||||
await self.sendmail(
|
||||
sender,
|
||||
recipients,
|
||||
message,
|
||||
)
|
||||
)[0].keys()
|
||||
)
|
||||
if len(failed_recipients): # raise also when not all have been refused
|
||||
raise SMTPRecipientsRefused(
|
||||
@@ -120,21 +192,14 @@ class SMTPClient(SMTP):
|
||||
del message["Reply-To"]
|
||||
message["Original-Sender"] = sender if sender is not None else ""
|
||||
message["Original-Recipient"] = ", ".join(recipients)
|
||||
message["From"] = self._config.sender
|
||||
message["To"] = ", ".join(self._config.recipients)
|
||||
if (
|
||||
self._config.set_reply_to
|
||||
and sender is not None
|
||||
and sender != self._config.sender
|
||||
):
|
||||
message["Reply-To"] = sender
|
||||
|
||||
print(
|
||||
f"{message['From']} ({message['Original-Sender']}) -> "
|
||||
f"{message['To']} ({message['Original-Recipient']}): "
|
||||
f"{self._config.sender} ({message['Original-Sender']}) -> "
|
||||
f"{', '.join(self._config.recipients)} ({message['Original-Recipient']}): "
|
||||
f"'{message.get('Subject', '')}'"
|
||||
)
|
||||
|
||||
if self._config.smime_cert:
|
||||
message = self._sign(message)
|
||||
async with self._lock: # TODO: consumer task from spool queue, reusing connections
|
||||
try:
|
||||
await self.connect()
|
||||
|
||||
Reference in New Issue
Block a user