mirror of
https://seed.flo-the.dev/z3gWc1qgaeZaoGwL4WTstLNoqjayM.git
synced 2025-12-06 21:07:36 +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.message import Message
|
||||||
from email.policy import default as EmailMessagePolicy
|
from email.policy import default as EmailMessagePolicy
|
||||||
from locale import LC_ALL, setlocale
|
from locale import LC_ALL, setlocale
|
||||||
|
from pathlib import Path
|
||||||
from typing import List, Optional, Set, Tuple, Union
|
from typing import List, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
import envelope
|
|
||||||
from aiosmtpd.controller import SMTP as Server
|
from aiosmtpd.controller import SMTP as Server
|
||||||
from aiosmtpd.controller import Controller
|
from aiosmtpd.controller import Controller
|
||||||
from aiosmtpd.smtp import Envelope, Session
|
from aiosmtpd.smtp import Envelope, Session
|
||||||
from aiosmtplib import SMTP, SMTPException, SMTPRecipientRefused, SMTPRecipientsRefused
|
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)
|
@dataclass(frozen=True)
|
||||||
@@ -44,6 +53,9 @@ class ClientConfig:
|
|||||||
set_reply_to: bool
|
set_reply_to: bool
|
||||||
use_tls: bool
|
use_tls: bool
|
||||||
start_tls: bool
|
start_tls: bool
|
||||||
|
smime_cert: str
|
||||||
|
smime_cert_private: str
|
||||||
|
smime_to_cert: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -52,7 +64,7 @@ class Config:
|
|||||||
client: ClientConfig
|
client: ClientConfig
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_config(cls, config: configparser.ConfigParser) -> "Config":
|
def _from_config(cls, config: configparser.RawConfigParser) -> "Config":
|
||||||
return cls(
|
return cls(
|
||||||
server=ServerConfig(
|
server=ServerConfig(
|
||||||
hostname=config.get("server", "hostname", fallback="localhost"),
|
hostname=config.get("server", "hostname", fallback="localhost"),
|
||||||
@@ -72,6 +84,11 @@ class Config:
|
|||||||
),
|
),
|
||||||
use_tls=config.getboolean("client", "use_tls", fallback=True),
|
use_tls=config.getboolean("client", "use_tls", fallback=True),
|
||||||
start_tls=config.getboolean("client", "start_tls", fallback=False),
|
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,
|
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(
|
async def _send_message(
|
||||||
self, message: Message, sender: str, recipients: List[str]
|
self, message: bytes, sender: str, recipients: List[str]
|
||||||
) -> None:
|
) -> 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(
|
failed_recipients: Set[str] = set(
|
||||||
(await self.send_message(message, sender=sender, recipients=recipients))[
|
(
|
||||||
0
|
await self.sendmail(
|
||||||
].keys()
|
sender,
|
||||||
|
recipients,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
)[0].keys()
|
||||||
)
|
)
|
||||||
if len(failed_recipients): # raise also when not all have been refused
|
if len(failed_recipients): # raise also when not all have been refused
|
||||||
raise SMTPRecipientsRefused(
|
raise SMTPRecipientsRefused(
|
||||||
@@ -120,21 +192,14 @@ class SMTPClient(SMTP):
|
|||||||
del message["Reply-To"]
|
del message["Reply-To"]
|
||||||
message["Original-Sender"] = sender if sender is not None else ""
|
message["Original-Sender"] = sender if sender is not None else ""
|
||||||
message["Original-Recipient"] = ", ".join(recipients)
|
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(
|
print(
|
||||||
f"{message['From']} ({message['Original-Sender']}) -> "
|
f"{self._config.sender} ({message['Original-Sender']}) -> "
|
||||||
f"{message['To']} ({message['Original-Recipient']}): "
|
f"{', '.join(self._config.recipients)} ({message['Original-Recipient']}): "
|
||||||
f"'{message.get('Subject', '')}'"
|
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
|
async with self._lock: # TODO: consumer task from spool queue, reusing connections
|
||||||
try:
|
try:
|
||||||
await self.connect()
|
await self.connect()
|
||||||
|
|||||||
Reference in New Issue
Block a user