mirror of
https://seed.flo-the.dev/z3gWc1qgaeZaoGwL4WTstLNoqjayM.git
synced 2025-12-06 04:47:35 +01:00
add a configurable list of recipients/cert dict
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
This commit is contained in:
@@ -70,7 +70,6 @@ class ClientConfig:
|
||||
hostname: str
|
||||
port: int
|
||||
sender: str
|
||||
recipients: List[str]
|
||||
username: Optional[str]
|
||||
password: Optional[str]
|
||||
set_reply_to: bool
|
||||
@@ -78,7 +77,13 @@ class ClientConfig:
|
||||
start_tls: bool
|
||||
smime_cert: str
|
||||
smime_cert_private: str
|
||||
smime_to_cert: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmailConfig:
|
||||
"""email to cert matching"""
|
||||
|
||||
email_certs: dict
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -92,9 +97,19 @@ class Config:
|
||||
|
||||
server: ServerConfig
|
||||
client: ClientConfig
|
||||
emails: EmailConfig
|
||||
|
||||
@classmethod
|
||||
def _get_emails_and_certs(cls, emailcertlist: list[tuple[str, str]]) -> dict:
|
||||
"""Take the List from the INI file and convert to dict"""
|
||||
email_certs = {}
|
||||
for email, cert in emailcertlist:
|
||||
email_certs[email] = cert
|
||||
return email_certs
|
||||
|
||||
@classmethod
|
||||
def _from_config(cls, config: configparser.RawConfigParser) -> "Config":
|
||||
email_certs = cls._get_emails_and_certs(config.items("emails"))
|
||||
return cls(
|
||||
server=ServerConfig(
|
||||
hostname=config.get("server", "hostname", fallback="localhost"),
|
||||
@@ -104,9 +119,6 @@ class Config:
|
||||
hostname=config.get("client", "hostname"),
|
||||
port=config.getint("client", "port"),
|
||||
sender=config.get("client", "sender"),
|
||||
recipients=[
|
||||
_.strip() for _ in config.get("client", "recipients").split(",")
|
||||
],
|
||||
username=config.get("client", "username"),
|
||||
password=config.get("client", "password"),
|
||||
set_reply_to=config.getboolean(
|
||||
@@ -118,8 +130,8 @@ class Config:
|
||||
smime_cert_private=config.get(
|
||||
"client", "smime_cert_private", fallback=""
|
||||
),
|
||||
smime_to_cert=config.get("client", "smime_to_cert", fallback=""),
|
||||
),
|
||||
emails=EmailConfig(email_certs=email_certs),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -151,8 +163,9 @@ class SMTPClient(SMTP):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config: ClientConfig) -> None:
|
||||
def __init__(self, config: ClientConfig, emails: EmailConfig) -> None:
|
||||
self._config: ClientConfig = config
|
||||
self._emails: EmailConfig = emails
|
||||
self._lock: asyncio.Lock = asyncio.Lock()
|
||||
super().__init__(
|
||||
hostname=self._config.hostname,
|
||||
@@ -163,11 +176,13 @@ class SMTPClient(SMTP):
|
||||
start_tls=self._config.start_tls,
|
||||
)
|
||||
|
||||
def _encrypt(self, message: bytes, subject: str) -> bytes:
|
||||
def _encrypt(self, message: bytes, subject: str, recipient: str) -> bytes:
|
||||
"""Encrypt the message
|
||||
|
||||
Args:
|
||||
message (bytes): message in bytes format (can/should be signed)
|
||||
subject (str): Subject of the message
|
||||
recipient: Recipient of the message
|
||||
|
||||
Returns:
|
||||
bytes: Encrypted message
|
||||
@@ -176,7 +191,7 @@ class SMTPClient(SMTP):
|
||||
s = SMIME.SMIME()
|
||||
|
||||
# Load target cert to encrypt to.
|
||||
x509 = X509.load_cert(self._config.smime_to_cert)
|
||||
x509 = X509.load_cert(self._emails.email_certs[recipient])
|
||||
sk = X509.X509_Stack()
|
||||
sk.push(x509)
|
||||
s.set_x509_stack(sk)
|
||||
@@ -191,7 +206,7 @@ class SMTPClient(SMTP):
|
||||
|
||||
# Add header
|
||||
out.write("From: " + self._config.sender + "\r\n")
|
||||
out.write("To: " + ", ".join(self._config.recipients) + "\r\n")
|
||||
out.write("To: " + recipient + "\r\n")
|
||||
out.write("Subject: " + subject + "\r\n")
|
||||
s.write(out, p7)
|
||||
|
||||
@@ -222,7 +237,7 @@ class SMTPClient(SMTP):
|
||||
)
|
||||
return output
|
||||
|
||||
def _encrypt_and_sign(self, message: Message) -> bytes:
|
||||
def _encrypt_and_sign(self, message: Message, recipient: str) -> bytes:
|
||||
"""Sign and encrypt the message
|
||||
|
||||
Args:
|
||||
@@ -232,7 +247,7 @@ class SMTPClient(SMTP):
|
||||
bytes: The signed and encrypted message including the From and To Header
|
||||
"""
|
||||
signed = self._sign(message)
|
||||
encrypt = self._encrypt(signed, message.get("Subject", ""))
|
||||
encrypt = self._encrypt(signed, message.get("Subject", ""), recipient)
|
||||
return encrypt
|
||||
|
||||
async def _send_message(
|
||||
@@ -276,30 +291,28 @@ class SMTPClient(SMTP):
|
||||
del message["Reply-To"]
|
||||
message["Original-Sender"] = sender if sender is not None else ""
|
||||
message["Original-Recipient"] = ", ".join(recipients)
|
||||
|
||||
print(
|
||||
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._encrypt_and_sign(message)
|
||||
async with (
|
||||
self._lock
|
||||
): # TODO: consumer task from spool queue, reusing connections
|
||||
try:
|
||||
await self.connect()
|
||||
await self._send_message(
|
||||
message, self._config.sender, self._config.recipients
|
||||
)
|
||||
except SMTPRecipientsRefused as e:
|
||||
raise RuntimeError(
|
||||
f"Recipients refused: {', '.join(_.recipient for _ in e.recipients)}"
|
||||
) from e
|
||||
except (SMTPException, OSError) as e:
|
||||
raise RuntimeError(str(e)) from e
|
||||
finally:
|
||||
self.close()
|
||||
for recipient in self._emails.email_certs:
|
||||
print(
|
||||
f"{self._config.sender} ({message['Original-Sender']}) -> "
|
||||
f"{recipient} ({message['Original-Recipient']}): "
|
||||
f"'{message.get('Subject', '')}'"
|
||||
)
|
||||
if self._config.smime_cert and self._emails.email_certs[recipient] != "":
|
||||
message = self._encrypt_and_sign(message, recipient)
|
||||
async with (
|
||||
self._lock
|
||||
): # TODO: consumer task from spool queue, reusing connections
|
||||
try:
|
||||
await self.connect()
|
||||
await self._send_message(message, self._config.sender, recipient)
|
||||
except SMTPRecipientsRefused as e:
|
||||
raise RuntimeError(
|
||||
f"Recipients refused: {', '.join(_.recipient for _ in e.recipients)}"
|
||||
) from e
|
||||
except (SMTPException, OSError) as e:
|
||||
raise RuntimeError(str(e)) from e
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
@@ -443,7 +456,7 @@ def main(args=None) -> int:
|
||||
print(str(e), file=sys.stderr)
|
||||
return 1
|
||||
controller = SMTPServer(
|
||||
config=config.server, handler=Handler(SMTPClient(config.client))
|
||||
config=config.server, handler=Handler(SMTPClient(config.client, config.emails))
|
||||
)
|
||||
return 0 if controller.run() else 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user