Harden live mail OAuth and immutable releases
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-30 02:00:09 +02:00
parent 1397f085bd
commit 67be350283
23 changed files with 429 additions and 20 deletions
+6 -2
View File
@@ -9,7 +9,10 @@ class MailboxConnectionForm(forms.ModelForm):
password = forms.CharField(
required=False,
label="App-wachtwoord",
help_text="Laat leeg bij wijzigen om het bestaande app-wachtwoord te behouden.",
help_text=(
"Alleen voor Gmail of een aangepaste provider. Microsoft 365 gebruikt "
"de centrale OAuth-configuratie."
),
widget=forms.PasswordInput(
attrs={"autocomplete": "new-password", "placeholder": "App-wachtwoord"},
render_value=False,
@@ -50,7 +53,8 @@ class MailboxConnectionForm(forms.ModelForm):
data = super().clean()
if data.get("provider") != MailboxConnection.Provider.CUSTOM:
data["custom_host"] = ""
if not self.instance.pk and not data.get("password"):
uses_oauth = data.get("provider") == MailboxConnection.Provider.OUTLOOK
if not self.instance.pk and not uses_oauth and not data.get("password"):
self.add_error("password", "Een app-wachtwoord is verplicht voor een nieuwe koppeling.")
return data
@@ -0,0 +1,13 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("sources", "0007_alter_mailboxconnection_platform")]
operations = [
migrations.AlterField(
model_name="mailboxconnection",
name="encrypted_password",
field=models.TextField(blank=True),
)
]
+1 -1
View File
@@ -357,7 +357,7 @@ class MailboxConnection(TimeStampedModel):
custom_host = models.CharField(max_length=255, blank=True)
port = models.PositiveIntegerField(default=993)
username = models.CharField(max_length=320)
encrypted_password = models.TextField()
encrypted_password = models.TextField(blank=True)
mailbox = models.CharField(max_length=255, default="INBOX")
enabled = models.BooleanField(default=True)
poll_interval_minutes = models.PositiveIntegerField(
+7 -1
View File
@@ -74,14 +74,19 @@ def _validate_connected_peer(
*,
before: ValidatedUrl,
after: ValidatedUrl,
require_peer: bool,
) -> None:
peer_ip = _connected_peer_ip(response)
if peer_ip is not None:
if not peer_ip.is_global:
raise FetchError(f"Niet-publiek verbonden IP-adres geblokkeerd: {peer_ip}")
return
if require_peer:
raise FetchError(
"Verbonden IP-adres kon niet veilig worden vastgesteld; aanvraag geblokkeerd."
)
# Mock/custom transports do not always expose the peer socket. Keep the conservative
# DNS-overlap fallback for those transports.
# DNS-overlap fallback only for explicitly injected test/custom clients.
if not set(before.addresses).intersection(after.addresses):
raise FetchError("DNS-rebindcontrole faalde bij het benaderen van bron.")
@@ -175,6 +180,7 @@ def fetch_url(
response,
before=validation,
after=post_validation,
require_peer=own_client,
)
if response.status_code in {301, 302, 303, 307, 308}:
location = response.headers.get("location")
+13 -2
View File
@@ -7,6 +7,7 @@ from typing import Any
from django.conf import settings
from apps.core.services.microsoft365 import acquire_exchange_access_token, xoauth2_bytes
from apps.sources.models import EmailMessageRecord, MailboxConnection
from apps.sources.services.email_import import ingest_email, message_identity
from apps.sources.services.mailbox_connections import decrypt_mailbox_password
@@ -30,7 +31,11 @@ def poll_imap_mailbox(
if client_factory is None:
client_factory = imaplib.IMAP4_SSL
host_validator(f"https://{connection.imap_host}")
password = decrypt_mailbox_password(connection)
password = (
""
if connection.provider == MailboxConnection.Provider.OUTLOOK
else decrypt_mailbox_password(connection)
)
client_options = {"timeout": settings.IMAP_CONNECT_TIMEOUT_SECONDS}
if client_factory is imaplib.IMAP4_SSL:
client_options["ssl_context"] = trusted_tls_context()
@@ -42,7 +47,13 @@ def poll_imap_mailbox(
"failed": 0,
}
try:
client.login(connection.username, password)
if connection.provider == MailboxConnection.Provider.OUTLOOK:
access_token = acquire_exchange_access_token()
client.authenticate(
"XOAUTH2", lambda _challenge: xoauth2_bytes(connection.username, access_token)
)
else:
client.login(connection.username, password)
status, _ = client.select(connection.mailbox, readonly=True)
if status != "OK":
raise RuntimeError("IMAP-mailbox kon niet worden geopend")
+7 -2
View File
@@ -48,7 +48,9 @@ def decrypt_mailbox_password(connection: MailboxConnection) -> str:
def rotate_mailbox_credentials() -> int:
"""Re-encrypt every mailbox password with the first configured key."""
rotated = 0
for connection in MailboxConnection.objects.select_for_update().all():
for connection in MailboxConnection.objects.select_for_update().exclude(
provider=MailboxConnection.Provider.OUTLOOK
):
password = decrypt_mailbox_password(connection)
connection.encrypted_password = encrypt_mailbox_password(password)
connection.save(update_fields=["encrypted_password", "updated_at"])
@@ -79,7 +81,10 @@ def save_mailbox_connection(
"poll_interval_minutes",
):
setattr(connection, field, cleaned_data[field])
if password:
uses_oauth = connection.provider == MailboxConnection.Provider.OUTLOOK
if uses_oauth:
connection.encrypted_password = ""
elif password:
connection.encrypted_password = encrypt_mailbox_password(password)
elif not connection.encrypted_password:
raise MailboxCredentialError("Een app-wachtwoord is verplicht.")