feat(mail): add governed JMAP mailbox sync and search
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user