mirror of
https://seed.flo-the.dev/z3gWc1qgaeZaoGwL4WTstLNoqjayM.git
synced 2025-12-06 04:47:35 +01:00
@@ -48,8 +48,10 @@
|
||||
black.enable = true;
|
||||
# sort imports
|
||||
isort.enable = true;
|
||||
# line length the same for black and isort
|
||||
isort.settings.flags = "-l 88";
|
||||
# look for code smell
|
||||
pylint.enable = true;
|
||||
# pylint.enable = true;
|
||||
detect-private-keys.enable = true;
|
||||
|
||||
|
||||
|
||||
118
smtprd.py
118
smtprd.py
@@ -13,19 +13,18 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from locale import setlocale, LC_ALL
|
||||
|
||||
from dataclasses import dataclass
|
||||
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 typing import List, Optional, Set, Tuple, Union
|
||||
|
||||
from aiosmtpd.controller import Controller, SMTP as Server
|
||||
from aiosmtpd.smtp import Session, Envelope
|
||||
from aiosmtplib import SMTP, SMTPException, SMTPRecipientsRefused, SMTPRecipientRefused
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Union, List, Tuple, Set
|
||||
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
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -53,7 +52,7 @@ class Config:
|
||||
client: ClientConfig
|
||||
|
||||
@classmethod
|
||||
def _from_config(cls, config: configparser.ConfigParser) -> 'Config':
|
||||
def _from_config(cls, config: configparser.ConfigParser) -> "Config":
|
||||
return cls(
|
||||
server=ServerConfig(
|
||||
hostname=config.get("server", "hostname", fallback="localhost"),
|
||||
@@ -63,17 +62,21 @@ 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(",")],
|
||||
recipients=[
|
||||
_.strip() for _ in config.get("client", "recipients").split(",")
|
||||
],
|
||||
username=config.get("client", "username"),
|
||||
password=config.get("client", "password"),
|
||||
set_reply_to=config.getboolean("client", "set_reply_to", fallback=False),
|
||||
set_reply_to=config.getboolean(
|
||||
"client", "set_reply_to", fallback=False
|
||||
),
|
||||
use_tls=config.getboolean("client", "use_tls", fallback=True),
|
||||
start_tls=config.getboolean("client", "start_tls", fallback=False),
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_ini(cls, filename: str) -> 'Config':
|
||||
def from_ini(cls, filename: str) -> "Config":
|
||||
try:
|
||||
config_parser: configparser.ConfigParser = configparser.ConfigParser()
|
||||
with open(filename, "r") as fp:
|
||||
@@ -88,19 +91,30 @@ class SMTPClient(SMTP):
|
||||
self._config: ClientConfig = config
|
||||
self._lock: asyncio.Lock = asyncio.Lock()
|
||||
super().__init__(
|
||||
hostname=self._config.hostname, port=self._config.port,
|
||||
username=self._config.username, password=self._config.password,
|
||||
use_tls=self._config.use_tls, start_tls=self._config.start_tls,
|
||||
hostname=self._config.hostname,
|
||||
port=self._config.port,
|
||||
username=self._config.username,
|
||||
password=self._config.password,
|
||||
use_tls=self._config.use_tls,
|
||||
start_tls=self._config.start_tls,
|
||||
)
|
||||
|
||||
async def _send_message(self, message: Message, sender: str, recipients: List[str]) -> None:
|
||||
failed_recipients: Set[str] = set((await self.send_message(
|
||||
message, sender=sender, recipients=recipients
|
||||
))[0].keys())
|
||||
async def _send_message(
|
||||
self, message: Message, sender: str, recipients: List[str]
|
||||
) -> None:
|
||||
failed_recipients: Set[str] = set(
|
||||
(await self.send_message(message, sender=sender, recipients=recipients))[
|
||||
0
|
||||
].keys()
|
||||
)
|
||||
if len(failed_recipients): # raise also when not all have been refused
|
||||
raise SMTPRecipientsRefused([SMTPRecipientRefused(0, "", _) for _ in failed_recipients])
|
||||
raise SMTPRecipientsRefused(
|
||||
[SMTPRecipientRefused(0, "", _) for _ in failed_recipients]
|
||||
)
|
||||
|
||||
async def forward_message(self, message: Message, sender: Optional[str], recipients: List[str]) -> None:
|
||||
async def forward_message(
|
||||
self, message: Message, sender: Optional[str], recipients: List[str]
|
||||
) -> None:
|
||||
del message["From"]
|
||||
del message["To"]
|
||||
del message["Reply-To"]
|
||||
@@ -108,19 +122,29 @@ class SMTPClient(SMTP):
|
||||
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:
|
||||
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']}) -> "
|
||||
print(
|
||||
f"{message['From']} ({message['Original-Sender']}) -> "
|
||||
f"{message['To']} ({message['Original-Recipient']}): "
|
||||
f"'{message.get('Subject', '')}'")
|
||||
f"'{message.get('Subject', '')}'"
|
||||
)
|
||||
|
||||
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)
|
||||
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
|
||||
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:
|
||||
@@ -131,11 +155,15 @@ class Handler:
|
||||
def __init__(self, client: SMTPClient) -> None:
|
||||
self._client: SMTPClient = client
|
||||
|
||||
async def handle_DATA(self, server: Server, session: Session, envelope: Envelope) -> str:
|
||||
async def handle_DATA(
|
||||
self, server: Server, session: Session, envelope: Envelope
|
||||
) -> str:
|
||||
try:
|
||||
await self._client.forward_message(self._prepare_message(server, envelope),
|
||||
await self._client.forward_message(
|
||||
self._prepare_message(server, envelope),
|
||||
sender=envelope.mail_from,
|
||||
recipients=envelope.rcpt_tos)
|
||||
recipients=envelope.rcpt_tos,
|
||||
)
|
||||
except (RuntimeError, ValueError) as e:
|
||||
print(f"Cannot forward: {str(e)}", file=sys.stderr)
|
||||
return f"550 {e.__cause__.__class__.__name__ if e.__cause__ is not None else e.__class__.__name__}"
|
||||
@@ -161,7 +189,9 @@ class Handler:
|
||||
host: str = server.hostname # getfqdn
|
||||
now: str = time.strftime("%a, %d %b %Y %H:%M:%S %z")
|
||||
|
||||
message["Received"] = f"from {peer[0]} ([{peer[0]}]) by {host} ([{sock[0]}]); {now}"
|
||||
message["Received"] = (
|
||||
f"from {peer[0]} ([{peer[0]}]) by {host} ([{sock[0]}]); {now}"
|
||||
)
|
||||
message["X-Peer"] = f"[{peer[0]}]:{peer[1]}"
|
||||
if "Date" not in message:
|
||||
message["Date"] = now
|
||||
@@ -173,7 +203,12 @@ class Handler:
|
||||
|
||||
class SMTPServer(Controller):
|
||||
def __init__(self, config: ServerConfig, handler: Handler) -> None:
|
||||
super().__init__(handler=handler, hostname=config.hostname, port=config.port, ready_timeout=10.0)
|
||||
super().__init__(
|
||||
handler=handler,
|
||||
hostname=config.hostname,
|
||||
port=config.port,
|
||||
ready_timeout=10.0,
|
||||
)
|
||||
|
||||
def run(self) -> bool:
|
||||
shutdown_requested: threading.Event = threading.Event()
|
||||
@@ -193,10 +228,17 @@ class SMTPServer(Controller):
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.strip(),
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("--config", metavar="CONFIG.INI", type=str, default="./config.ini",
|
||||
help="configuration file")
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__.strip(),
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
metavar="CONFIG.INI",
|
||||
type=str,
|
||||
default="./config.ini",
|
||||
help="configuration file",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
setlocale(LC_ALL, "C") # for strftime
|
||||
|
||||
@@ -206,7 +248,9 @@ def main() -> int:
|
||||
print(str(e), file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
controller = SMTPServer(config=config.server, handler=Handler(SMTPClient(config.client)))
|
||||
controller = SMTPServer(
|
||||
config=config.server, handler=Handler(SMTPClient(config.client))
|
||||
)
|
||||
return 0 if controller.run() else 1
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user