feat(mail): add governed JMAP mailbox sync and search
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-22 17:09:05 +02:00
parent fd808336bc
commit 1cbf4acaf4
29 changed files with 2481 additions and 183 deletions
+73
View File
@@ -1,5 +1,8 @@
from __future__ import annotations
import urllib.parse
from typing import Literal
from pydantic import Field, model_validator
from govoplan_core.mail.config import (
@@ -51,10 +54,80 @@ class Pop3Config(Pop3ServerConfig):
username: str | None = None
password: str | None = None
class JmapServerConfig(StrictModel):
"""Server-only settings for an RFC 8620/8621 mailbox endpoint."""
session_url: str
account_id: str | None = Field(default=None, max_length=255)
auth_scheme: Literal["bearer", "basic"] = "bearer"
timeout_seconds: int = Field(default=20, ge=1, le=120)
max_response_bytes: int = Field(
default=5 * 1024 * 1024,
ge=64 * 1024,
le=25 * 1024 * 1024,
)
max_body_value_bytes: int = Field(
default=1 * 1024 * 1024,
ge=1_024,
le=5 * 1024 * 1024,
)
allowed_api_origins: list[str] = Field(default_factory=list, max_length=10)
@model_validator(mode="after")
def validate_urls(self) -> "JmapServerConfig":
self.session_url = _absolute_http_url(self.session_url, label="JMAP session URL")
session_origin = _http_origin(self.session_url)
origins: list[str] = []
for value in self.allowed_api_origins:
normalized = _http_origin(
_absolute_http_url(value, label="JMAP allowed API origin")
)
if normalized != session_origin and normalized not in origins:
origins.append(normalized)
self.allowed_api_origins = origins
if self.account_id is not None:
self.account_id = self.account_id.strip() or None
return self
class JmapConfig(JmapServerConfig):
username: str | None = Field(default=None, max_length=320)
password: str | None = None
@model_validator(mode="after")
def validate_credentials(self) -> "JmapConfig":
if self.auth_scheme == "basic" and not (self.username and self.password):
raise ValueError("JMAP Basic authentication requires username and password")
if self.auth_scheme == "bearer" and not self.password:
raise ValueError("JMAP Bearer authentication requires an access token")
return self
def _absolute_http_url(value: str, *, label: str) -> str:
parsed = urllib.parse.urlsplit(str(value or "").strip())
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError(f"{label} must be an absolute HTTP(S) URL")
if parsed.username or parsed.password:
raise ValueError(f"{label} must not include embedded credentials")
if parsed.fragment:
raise ValueError(f"{label} must not include a fragment")
return urllib.parse.urlunsplit(parsed)
def _http_origin(value: str) -> str:
parsed = urllib.parse.urlsplit(value)
port = parsed.port or (443 if parsed.scheme == "https" else 80)
default_port = 443 if parsed.scheme == "https" else 80
suffix = "" if port == default_port else f":{port}"
return f"{parsed.scheme.lower()}://{(parsed.hostname or '').lower()}{suffix}"
__all__ = [
"ImapConfig",
"ImapFolderMappings",
"ImapServerConfig",
"JmapConfig",
"JmapServerConfig",
"Pop3Config",
"Pop3ServerConfig",
"SmtpConfig",