🚨(lint) lint subprojects

This commit is contained in:
Sylvain Zimmer
2026-07-06 16:20:51 +02:00
committed by jbpenrath
parent 632f38da2d
commit d8fb367cfe
15 changed files with 133 additions and 102 deletions
@@ -48,14 +48,20 @@ def main() -> None:
parsed = parse_email(raw)
inline = next(
(a for a in parsed.get("attachments") or [] if a.get("disposition") == "inline"),
(
a
for a in parsed.get("attachments") or []
if a.get("disposition") == "inline"
),
None,
)
assert inline is not None, "expected an inline attachment on round-trip"
assert inline.get("cid") == CID, (
f"cid did not round-trip: composed={CID!r}, parsed={inline.get('cid')!r}"
)
print(f"OK: inline image '{inline['name']}' round-tripped with cid <{inline['cid']}>")
print(
f"OK: inline image '{inline['name']}' round-tripped with cid <{inline['cid']}>"
)
if __name__ == "__main__":
+3
View File
@@ -109,6 +109,9 @@ select = [
# and embed control characters in defense-matrix fixtures (PLE2502).
"tests/**" = ["S", "SLF", "PLC0415", "PLE2502"]
# CLI examples use print() for output and assert for self-checking.
"examples/**" = ["T20", "S101"]
# ──────────────────────────── ty (PEP 561 typecheck) ────────────────────────────
# Static typecheck via Astral's ``ty`` — single Rust binary, no Node,
# no Python deps. The shape contracts (``types.py``) and the public
@@ -296,9 +296,7 @@ def main() -> int:
_ok("cross-realm role_id (master 'admin' role) → 404")
# ────────────────────── authentication ──────────────────────
r = _post_with_token(
None, {"role_id": created_role_id, "usernames": usernames}
)
r = _post_with_token(None, {"role_id": created_role_id, "usernames": usernames})
assert r.status_code == 401
_ok("no Authorization header → 401")
+6 -5
View File
@@ -19,7 +19,7 @@ def wait_for_rspamd():
max_retries = 200 # Increase retries (40 seconds total)
base_url = RSPAMD_URL.replace("/_api", "")
last_error = None
for attempt in range(max_retries):
# Try checkv2 endpoint first (more reliable than ping through nginx)
try:
@@ -34,7 +34,9 @@ def wait_for_rspamd():
)
# If we get a response (even if it's an error about empty message), rspamd is up
if response.status_code in (200, 400, 401, 403):
logger.info(f"Rspamd is ready (checkv2 check returned {response.status_code})")
logger.info(
f"Rspamd is ready (checkv2 check returned {response.status_code})"
)
return
last_error = f"Unexpected status code: {response.status_code}"
except requests.exceptions.ConnectionError as e:
@@ -49,7 +51,7 @@ def wait_for_rspamd():
last_error = f"Request error: {e}"
if attempt % 30 == 0:
logger.debug(f"Checkv2 check error: {e}")
# Also try ping endpoint as backup
try:
response = requests.get(f"{base_url}/ping", timeout=2)
@@ -59,7 +61,7 @@ def wait_for_rspamd():
except requests.exceptions.RequestException:
# Ignore ping errors, we prefer checkv2
pass
if attempt == max_retries - 1:
raise RuntimeError(
f"Rspamd did not become ready after {max_retries} attempts. "
@@ -72,4 +74,3 @@ def wait_for_rspamd():
f"last error: {last_error}, retrying..."
)
time.sleep(0.2)
+14 -15
View File
@@ -1,11 +1,9 @@
"""Simple tests for rspamd API."""
import os
import pytest
import requests
from conftest import RSPAMD_URL, RSPAMD_AUTH
def test_rspamd_health(wait_for_rspamd):
"""Test that rspamd is running and accessible."""
# Use the controller endpoint for health check
@@ -19,34 +17,36 @@ def test_rspamd_check_empty_message(wait_for_rspamd):
"""Test rspamd checkv2 API with an empty message."""
# Empty email message
empty_email = b""
headers = {"Content-Type": "message/rfc822"}
if RSPAMD_AUTH:
# RSPAMD_AUTH is used directly as Authorization header value
headers["Authorization"] = RSPAMD_AUTH
response = requests.post(
f"{RSPAMD_URL}/checkv2",
data=empty_email,
headers=headers,
timeout=10,
)
assert response.status_code == 200
result = response.json()
# Verify response structure
assert "action" in result
assert "score" in result
assert "required_score" in result
assert "is_skipped" in result
# Empty message may be marked as spam depending on rspamd configuration
# At minimum, verify we got a valid response with the expected structure
assert result["action"] in ("reject", "add header", "greylist", "no action")
assert isinstance(result["score"], (int, float))
# required_score can be None in some rspamd configurations
assert result["required_score"] is None or isinstance(result["required_score"], (int, float))
assert result["required_score"] is None or isinstance(
result["required_score"], (int, float)
)
def test_rspamd_check_simple_message(wait_for_rspamd):
@@ -59,27 +59,27 @@ Date: Mon, 1 Jan 2024 12:00:00 +0000
This is a test email body.
"""
headers = {"Content-Type": "message/rfc822"}
if RSPAMD_AUTH:
# RSPAMD_AUTH is used directly as Authorization header value
headers["Authorization"] = RSPAMD_AUTH
response = requests.post(
f"{RSPAMD_URL}/checkv2",
data=simple_email,
headers=headers,
timeout=10,
)
assert response.status_code == 200
result = response.json()
# Verify response structure
assert "action" in result
assert "score" in result
assert "required_score" in result
# Simple valid message should not be rejected
# If required_score is None, just check that action is not reject
if result["required_score"] is not None:
@@ -87,4 +87,3 @@ This is a test email body.
else:
# If no required_score, just verify action is valid
assert result["action"] in ("reject", "add header", "greylist", "no action")
+8 -8
View File
@@ -85,7 +85,7 @@ class DeliveryMilter(Milter.Base):
self.rcpttos.append(clean_to)
return Milter.CONTINUE
except Exception:
except Exception: # noqa: BLE001
# Exception during validation - temporary failure
return Milter.TEMPFAIL
@@ -150,7 +150,7 @@ class DeliveryMilter(Milter.Base):
else:
return Milter.TEMPFAIL
except Exception:
except Exception: # noqa: BLE001
return Milter.TEMPFAIL
def close(self):
@@ -165,7 +165,7 @@ class DeliveryMilter(Milter.Base):
def main():
"""Run the milter server"""
print("Starting delivery milter...")
print("Starting delivery milter...") # noqa: T201
# Set the socket for milter communication
# Use Unix socket for better performance and security
@@ -179,7 +179,7 @@ def main():
try:
os.setgid(grp.getgrnam("postfix").gr_gid)
except (KeyError, OSError) as e:
print(f"Warning: could not set gid to postfix: {e}", file=sys.stderr)
print(f"Warning: could not set gid to postfix: {e}", file=sys.stderr) # noqa: T201
os.umask(0o117)
# Register our milter class
@@ -189,15 +189,15 @@ def main():
flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
Milter.set_flags(flags)
print(f"Milter listening on {socket_path}")
print(f"Milter listening on {socket_path}") # noqa: T201
try:
# Start the milter
Milter.runmilter("delivery_milter", socket_path, timeout=240)
except KeyboardInterrupt:
print("Milter shutting down...")
except Exception as e:
print(f"Milter error: {e}")
print("Milter shutting down...") # noqa: T201
except Exception as e: # noqa: BLE001
print(f"Milter error: {e}") # noqa: T201
sys.exit(1)
+5 -4
View File
@@ -84,7 +84,7 @@ def test_simple_email_delivery_with_multiple_recipients(mock_api_server, smtp_cl
assert len(mock_api_server.received_emails) == 1
email = mock_api_server.received_emails[0]
assert set(email["metadata"]["original_recipients"]) == set(["test@example.com"])
assert set(email["metadata"]["original_recipients"]) == {"test@example.com"}
assert email["metadata"]["sender"] == "sender@example.com"
assert email["email"]["subject"] == "Simple Test Email"
assert email["email"]["from"] == "sender@example.com"
@@ -112,9 +112,10 @@ def test_simple_email_delivery_with_multiple_recipients(mock_api_server, smtp_cl
assert len(mock_api_server.received_emails) == 1
email = mock_api_server.received_emails[0]
assert set(email["metadata"]["original_recipients"]) == set(
["test@example.com", "test2@example.com"]
)
assert set(email["metadata"]["original_recipients"]) == {
"test@example.com",
"test2@example.com",
}
assert email["metadata"]["sender"] == "sender@example.com"
assert email["email"]["subject"] == "Simple Test Email"
assert email["email"]["from"] == "sender@example.com"
+5 -4
View File
@@ -1,12 +1,13 @@
import pytest
import smtplib
import time
import logging
import os
import smtplib
import time
from email.parser import BytesParser
import pytest
from aiosmtpd.controller import Controller
from aiosmtpd.handlers import Message
from aiosmtpd.smtp import AuthResult, LoginPassword
from email.parser import BytesParser
# Set up logging
logging.basicConfig(level=logging.INFO)
+3 -3
View File
@@ -1,9 +1,9 @@
import logging
import base64
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import logging
from email.mime.application import MIMEApplication
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
logger = logging.getLogger(__name__)
+3 -3
View File
@@ -1,8 +1,8 @@
import logging
import os
import time
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
logger = logging.getLogger(__name__)
@@ -30,7 +30,7 @@ def test_send_simple_text_email(smtp_client, mock_smtp_server):
# Give some time for the message to be relayed and received by the mock server
max_retries = 10
for attempt in range(max_retries):
for _attempt in range(max_retries):
if len(mock_smtp_server.get_messages()) > 0:
break
time.sleep(0.5)
@@ -65,7 +65,7 @@ def test_send_simple_text_email_localhost(smtp_client, mock_smtp_server):
# Give some time for the message to be relayed and received by the mock server
max_retries = 10
for attempt in range(max_retries):
for _attempt in range(max_retries):
if len(mock_smtp_server.get_messages()) > 0:
break
time.sleep(0.5)
+4 -3
View File
@@ -1,10 +1,11 @@
import pytest
import time
import logging
import time
from email.message import EmailMessage
from email.parser import BytesParser
from email.policy import default as default_policy
import pytest
logger = logging.getLogger(__name__)
# Define a sample raw MIME message
@@ -49,7 +50,7 @@ def test_mime_message_unmodified(smtp_client, mock_smtp_server):
# sendmail expects bytes for the message
smtp_client.sendmail(sender, recipient, original_bytes)
logger.info("Raw message sent successfully via smtp_client.")
except Exception as e:
except Exception as e: # noqa: BLE001
logger.error(f"Failed to send raw message: {e}")
pytest.fail(f"SMTP sendmail failed: {e}")
+3 -2
View File
@@ -1,7 +1,8 @@
import pytest
import smtplib
import logging
import os
import smtplib
import pytest
logger = logging.getLogger(__name__)
+45 -29
View File
@@ -1,24 +1,19 @@
import pytest
import smtplib
import time
import logging
import os
import socket
import subprocess
import ssl
import struct
import socks
from aiosmtpd.controller import Controller
from aiosmtpd.handlers import Message
from urllib.parse import urlparse
from dataclasses import dataclass
from email.parser import BytesParser
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Parse SOCKS proxy environment variables
from urllib.parse import urlparse
from dataclasses import dataclass
@dataclass
class ProxyConfig:
@@ -27,29 +22,31 @@ class ProxyConfig:
host: str = "localhost"
port: int = 1080
def parse_proxy_env(proxy_env):
"""Parse SOCKS_PROXY1 or SOCKS_PROXY2 environment variable
Format: username:password@host:port
"""
if not proxy_env:
return ProxyConfig()
try:
# Add scheme to make it a valid URL for urlparse
if not proxy_env.startswith(('http://', 'https://', 'socks://')):
if not proxy_env.startswith(("http://", "https://", "socks://")):
proxy_env = f"socks://{proxy_env}"
parsed = urlparse(proxy_env)
return ProxyConfig(
username=parsed.username,
password=parsed.password,
host=parsed.hostname or "localhost",
port=parsed.port or 1080
port=parsed.port or 1080,
)
except Exception:
return ProxyConfig()
# Parse both proxy configurations
PROXY1_CONFIG = parse_proxy_env(os.getenv("SOCKS_PROXY1"))
PROXY2_CONFIG = parse_proxy_env(os.getenv("SOCKS_PROXY2"))
@@ -58,15 +55,16 @@ PROXY2_CONFIG = parse_proxy_env(os.getenv("SOCKS_PROXY2"))
def get_container_ip():
"""Get the container's IP address automatically"""
try:
result = subprocess.run(['hostname', '-I'], capture_output=True, text=True)
result = subprocess.run(["hostname", "-I"], capture_output=True, text=True)
# hostname -I returns space-separated IPs, first one is usually the main one
return result.stdout.strip().split()[0]
except:
except Exception:
return "127.0.0.1" # fallback
class MessageStore:
"""Simple storage for received email messages"""
def __init__(self):
self.messages = []
@@ -78,13 +76,13 @@ class MessageStore:
def get_messages(self):
return self.messages
def get_last_connection_info(self):
"""Get connection info from the last received message"""
if self.messages:
return self.messages[-1].get("connection_info", {})
return {}
def get_connection_info_for_subject(self, subject):
"""Get connection info for a specific message subject"""
for message in self.messages:
@@ -95,6 +93,7 @@ class MessageStore:
class MockSMTPHandler(Message):
"""Handle SMTP messages and store them"""
def __init__(self, message_store):
super().__init__()
self.message_store = message_store
@@ -123,6 +122,7 @@ class MockSMTPHandler(Message):
class MockSMTPServer:
"""Mock SMTP server for testing"""
def __init__(self, host="0.0.0.0", port=2525):
self.host = host
self.port = port
@@ -143,18 +143,35 @@ class MockSMTPServer:
return self.message_store.get_messages()
def create_proxied_socket(proxy_host, proxy_port, target_host, target_port, username=None, password=None, timeout=5):
def create_proxied_socket(
proxy_host,
proxy_port,
target_host,
target_port,
username=None,
password=None,
timeout=5,
):
"""Create a socket connected through a SOCKS proxy"""
proxy = socks.socksocket()
if type(timeout) in {int, float}:
proxy.settimeout(timeout)
proxy.set_proxy(socks.PROXY_TYPE_SOCKS5, proxy_host, proxy_port, rdns=False, username=username, password=password)
proxy.set_proxy(
socks.PROXY_TYPE_SOCKS5,
proxy_host,
proxy_port,
rdns=False,
username=username,
password=password,
)
proxy.connect((target_host, target_port))
return proxy
class SOCKSClient:
"""SOCKS client for testing"""
def __init__(self, proxy_host, proxy_port, username=None, password=None):
self.proxy_host = proxy_host
self.proxy_port = proxy_port
@@ -170,7 +187,7 @@ class SOCKSClient:
target_port,
self.username,
self.password,
timeout
timeout,
)
sock.close()
return True
@@ -192,7 +209,7 @@ def socks_client():
proxy_host=PROXY1_CONFIG.host,
proxy_port=PROXY1_CONFIG.port,
username=PROXY1_CONFIG.username,
password=PROXY1_CONFIG.password
password=PROXY1_CONFIG.password,
)
@@ -203,7 +220,7 @@ def socks_client_proxy2():
proxy_host=PROXY2_CONFIG.host,
proxy_port=PROXY2_CONFIG.port,
username=PROXY2_CONFIG.username,
password=PROXY2_CONFIG.password
password=PROXY2_CONFIG.password,
)
@@ -216,7 +233,7 @@ def smtp_client_direct():
try:
client.quit()
except:
except Exception:
pass
@@ -230,9 +247,9 @@ class ProxySMTP(smtplib.SMTP):
# This makes it simpler for SMTP_SSL to use the SMTP connect code
# and just alter the socket connection bit.
if timeout is not None and not timeout:
raise ValueError('Non-blocking socket (timeout=0) is not supported')
raise ValueError("Non-blocking socket (timeout=0) is not supported")
if self.debuglevel > 0:
self._print_debug('connect: to', (host, port), self.source_address)
self._print_debug("connect: to", (host, port), self.source_address)
return create_proxied_socket(
self.socks_client.proxy_host,
@@ -241,7 +258,7 @@ class ProxySMTP(smtplib.SMTP):
port,
self.socks_client.username,
self.socks_client.password,
timeout
timeout,
)
@@ -254,11 +271,10 @@ def smtp_client_via_proxy(socks_client):
client = ProxySMTP(container_ip, 2525, socks_client=socks_client)
client.set_debuglevel(2)
yield client
try:
client.quit()
except:
except Exception:
pass
+13 -10
View File
@@ -12,22 +12,22 @@ def test_smtp_connection_direct(smtp_client_direct, mock_smtp_server):
smtp_client_direct.ehlo()
assert smtp_client_direct.noop()[0] == 250, "Direct SMTP connection should work"
message = MIMEText("Test direct connection email")
message["From"] = "sender@example.com"
message["To"] = "recipient@localhost"
message["Subject"] = "Test Direct Connection"
mock_smtp_server.clear_messages()
response = smtp_client_direct.send_message(message)
assert not response, "Sending should succeed"
# Wait for message and get connection info
time.sleep(1)
messages = mock_smtp_server.get_messages()
assert len(messages) == 1, "Message should be received"
assert messages[0]["subject"] == "Test Direct Connection"
# Log connection info for debugging
connection_info = messages[0].get("connection_info", {})
logger.info(f"Direct SMTP connection info: {connection_info}")
@@ -41,26 +41,29 @@ def test_smtp_connection_via_proxy(smtp_client_via_proxy, mock_smtp_server):
smtp_client_via_proxy.ehlo()
assert smtp_client_via_proxy.noop()[0] == 250, "Proxy SMTP connection should work"
message = MIMEText("Test proxy connection email")
message["From"] = "sender@example.com"
message["To"] = "recipient@localhost"
message["Subject"] = "Test Proxy Connection"
mock_smtp_server.clear_messages()
response = smtp_client_via_proxy.send_message(message)
assert not response, "Sending should succeed"
# Wait for message and get connection info
time.sleep(1)
messages = mock_smtp_server.get_messages()
assert len(messages) == 1, "Message should be received"
assert messages[0]["subject"] == "Test Proxy Connection"
# Log connection info for debugging
connection_info = messages[0].get("connection_info", {})
logger.info(f"Proxy SMTP connection info: {connection_info}")
assert connection_info["peer_host"] != "127.0.0.1", "Proxy SMTP connection should not be direct"
assert connection_info["peer_host"] != "127.0.0.1", (
"Proxy SMTP connection should not be direct"
)
# TODO: stress test with https://pypi.org/project/pytest-run-parallel/ ?
# TODO: stress test with https://pypi.org/project/pytest-run-parallel/ ?
+12 -11
View File
@@ -19,9 +19,9 @@ def test_socks_authentication_invalid_password(socks_client):
proxy_host=socks_client.proxy_host,
proxy_port=socks_client.proxy_port,
username=socks_client.username,
password="wrong_password"
password="wrong_password",
)
result = client.test_connection("8.8.8.8", 53)
assert not result, "SOCKS connection should fail with invalid password"
@@ -33,9 +33,9 @@ def test_socks_authentication_invalid_username(socks_client):
proxy_host=socks_client.proxy_host,
proxy_port=socks_client.proxy_port,
username="wrong_username",
password=socks_client.password
password=socks_client.password,
)
result = client.test_connection("8.8.8.8", 53)
assert not result, "SOCKS connection should fail with invalid username"
@@ -44,10 +44,9 @@ def test_socks_authentication_no_credentials(socks_client):
"""Test SOCKS connection without authentication (should fail)"""
# Create client without credentials using existing fixture
client = SOCKSClient(
proxy_host=socks_client.proxy_host,
proxy_port=socks_client.proxy_port
proxy_host=socks_client.proxy_host, proxy_port=socks_client.proxy_port
)
result = client.test_connection("8.8.8.8", 53)
assert not result, "SOCKS connection should fail without credentials"
@@ -59,9 +58,9 @@ def test_socks_authentication_failure_handling(socks_client):
proxy_host=socks_client.proxy_host,
proxy_port=socks_client.proxy_port,
username="invalid_user",
password="invalid_pass"
password="invalid_pass",
)
result = client.test_connection("8.8.8.8", 53)
assert not result, "Connection should fail with invalid credentials"
@@ -69,7 +68,9 @@ def test_socks_authentication_failure_handling(socks_client):
# Connection Tests
def test_socks_proxy_connection_establishment(socks_client):
"""Test that SOCKS proxy connection can be established"""
assert socks_client.test_connection("8.8.8.8", 53), "SOCKS connection should be established successfully"
assert socks_client.test_connection("8.8.8.8", 53), (
"SOCKS connection should be established successfully"
)
def test_socks_proxy_connection_refused(socks_client):
@@ -86,5 +87,5 @@ def test_socks_proxy_connection_timeout(socks_client):
def test_socks_proxy_error_handling(socks_client):
"""Test SOCKS proxy error handling"""
assert not socks_client.test_connection("nonexistent.invalid", 80)