Prepare v0.1.0 release
This commit is contained in:
@@ -198,6 +198,40 @@ def _policy_from_attr(value: Any) -> dict[str, Any]:
|
||||
return normalize_mail_profile_policy(value if isinstance(value, dict) else {})
|
||||
|
||||
|
||||
|
||||
def _mail_policy_source_fields(policy: dict[str, Any], *, baseline: bool = False) -> list[str]:
|
||||
fields: list[str] = []
|
||||
if _meaningful_allow_patterns(policy.get("allowed_profile_ids") or []):
|
||||
fields.append("allowed_profile_ids")
|
||||
for key in ("allow_user_profiles", "allow_group_profiles", "allow_campaign_profiles"):
|
||||
if isinstance(policy.get(key), bool):
|
||||
fields.append(key)
|
||||
for key in ("smtp_credentials", "imap_credentials"):
|
||||
credential = policy.get(key) or {}
|
||||
if isinstance(credential, dict):
|
||||
if isinstance(credential.get("inherit"), bool):
|
||||
fields.append(f"{key}.inherit")
|
||||
if isinstance(credential.get("allow_override"), bool):
|
||||
fields.append(f"{key}.allow_override")
|
||||
for kind in ("whitelist", "blacklist"):
|
||||
rules = policy.get(kind) or {}
|
||||
if isinstance(rules, dict):
|
||||
for key in PROFILE_PATTERN_KEYS:
|
||||
if _clean_string_list(rules.get(key, [])):
|
||||
fields.append(f"{kind}.{key}")
|
||||
if baseline and not fields:
|
||||
fields.append("defaults")
|
||||
return fields
|
||||
|
||||
|
||||
def _mail_policy_source_step(source: str, source_id: str | None, label: str, policy: dict[str, Any], *, baseline: bool = False) -> dict[str, Any]:
|
||||
return {
|
||||
"scope_type": source,
|
||||
"scope_id": source_id,
|
||||
"label": label,
|
||||
"applied_fields": _mail_policy_source_fields(policy, baseline=baseline),
|
||||
}
|
||||
|
||||
def _merge_credential_policy(current: EffectiveCredentialPolicy, data: dict[str, Any], *, source: str) -> None:
|
||||
inherit = data.get("inherit")
|
||||
if isinstance(inherit, bool):
|
||||
@@ -216,9 +250,9 @@ def _merge_credential_policy(current: EffectiveCredentialPolicy, data: dict[str,
|
||||
current.allow_override = False
|
||||
|
||||
|
||||
def _merge_policy(policy: EffectiveMailProfilePolicy, data: dict[str, Any], *, source: str) -> None:
|
||||
def _merge_policy(policy: EffectiveMailProfilePolicy, data: dict[str, Any], *, source: str, source_id: str | None = None, label: str | None = None, baseline: bool = False) -> None:
|
||||
normalized = normalize_mail_profile_policy(data)
|
||||
policy.source_policies.append(normalized)
|
||||
policy.source_policies.append(_mail_policy_source_step(source, source_id, label or source.capitalize(), normalized, baseline=baseline))
|
||||
allowed_ids = _meaningful_allow_patterns(normalized.get("allowed_profile_ids") or [])
|
||||
if allowed_ids:
|
||||
policy.allowed_profile_id_sets.append(set(allowed_ids))
|
||||
@@ -280,18 +314,18 @@ def effective_mail_profile_policy(
|
||||
|
||||
policy = EffectiveMailProfilePolicy()
|
||||
system_settings = get_system_settings(session)
|
||||
_merge_policy(policy, _policy_from_settings(system_settings.settings or {}), source="system")
|
||||
_merge_policy(policy, _policy_from_settings(tenant.settings or {}), source="tenant")
|
||||
_merge_policy(policy, _policy_from_settings(system_settings.settings or {}), source="system", label="System", baseline=True)
|
||||
_merge_policy(policy, _policy_from_settings(tenant.settings or {}), source="tenant", source_id=tenant.id, label="Tenant")
|
||||
if owner_user_id:
|
||||
user = session.get(User, owner_user_id)
|
||||
if user and user.tenant_id == tenant_id:
|
||||
_merge_policy(policy, _policy_from_attr(user.mail_profile_policy), source="user")
|
||||
_merge_policy(policy, _policy_from_attr(user.mail_profile_policy), source="user", source_id=user.id, label="Owner user")
|
||||
if owner_group_id:
|
||||
group = session.get(Group, owner_group_id)
|
||||
if group and group.tenant_id == tenant_id:
|
||||
_merge_policy(policy, _policy_from_attr(group.mail_profile_policy), source="group")
|
||||
_merge_policy(policy, _policy_from_attr(group.mail_profile_policy), source="group", source_id=group.id, label="Owner group")
|
||||
if campaign is not None:
|
||||
_merge_policy(policy, _policy_from_attr(campaign.mail_profile_policy), source="campaign")
|
||||
_merge_policy(policy, _policy_from_attr(campaign.mail_profile_policy), source="campaign", source_id=campaign.id, label="Campaign")
|
||||
return policy
|
||||
|
||||
|
||||
@@ -309,11 +343,11 @@ def parent_mail_profile_policy(
|
||||
|
||||
policy = EffectiveMailProfilePolicy()
|
||||
system_settings = get_system_settings(session)
|
||||
_merge_policy(policy, _policy_from_settings(system_settings.settings or {}), source="system")
|
||||
_merge_policy(policy, _policy_from_settings(system_settings.settings or {}), source="system", label="System", baseline=True)
|
||||
if clean_scope == "tenant":
|
||||
return policy
|
||||
|
||||
_merge_policy(policy, _policy_from_settings(tenant.settings or {}), source="tenant")
|
||||
_merge_policy(policy, _policy_from_settings(tenant.settings or {}), source="tenant", source_id=tenant.id, label="Tenant")
|
||||
if clean_scope in {"user", "group"}:
|
||||
return policy
|
||||
|
||||
@@ -325,11 +359,11 @@ def parent_mail_profile_policy(
|
||||
if campaign.owner_user_id:
|
||||
user = session.get(User, campaign.owner_user_id)
|
||||
if user and user.tenant_id == tenant_id:
|
||||
_merge_policy(policy, _policy_from_attr(user.mail_profile_policy), source="user")
|
||||
_merge_policy(policy, _policy_from_attr(user.mail_profile_policy), source="user", source_id=user.id, label="Owner user")
|
||||
if campaign.owner_group_id:
|
||||
group = session.get(Group, campaign.owner_group_id)
|
||||
if group and group.tenant_id == tenant_id:
|
||||
_merge_policy(policy, _policy_from_attr(group.mail_profile_policy), source="group")
|
||||
_merge_policy(policy, _policy_from_attr(group.mail_profile_policy), source="group", source_id=group.id, label="Owner group")
|
||||
return policy
|
||||
|
||||
|
||||
@@ -454,7 +488,7 @@ def _ensure_scope_allows_profile_creation(
|
||||
if scope_type == "campaign":
|
||||
policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=scope_id)
|
||||
if not policy.allow_campaign_profiles:
|
||||
raise MailProfileError("Campaign-scoped mail-server profiles are disabled by policy")
|
||||
raise MailProfileError("Campaign-local mail profiles are disabled by policy")
|
||||
return
|
||||
if scope_type == "user":
|
||||
policy = effective_mail_profile_policy(session, tenant_id=tenant_id, owner_user_id=scope_id)
|
||||
@@ -694,6 +728,17 @@ def _assert_profile_credentials_allowed_for_server(profile: MailServerProfile, p
|
||||
raise MailProfileError(f"{protocol.upper()} credentials must be supplied by this campaign or scope policy")
|
||||
|
||||
|
||||
def _inline_transport_configured(value: Any) -> bool:
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
if value.get("enabled") is True:
|
||||
return True
|
||||
for key in ("host", "username", "password"):
|
||||
if str(value.get(key) or "").strip():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def assert_campaign_mail_policy_allows_json(
|
||||
session: Session,
|
||||
*,
|
||||
@@ -737,6 +782,12 @@ def assert_campaign_mail_policy_allows_json(
|
||||
return
|
||||
smtp = server.get("smtp") if isinstance(server, dict) and isinstance(server.get("smtp"), dict) else None
|
||||
imap = server.get("imap") if isinstance(server, dict) and isinstance(server.get("imap"), dict) else None
|
||||
if campaign_id:
|
||||
policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign_id)
|
||||
if not policy.allow_campaign_profiles and (_inline_transport_configured(smtp) or _inline_transport_configured(imap)):
|
||||
raise MailProfileError("Campaign-local inline mail settings are disabled by policy")
|
||||
_assert_transport_values_allowed(policy, smtp, imap)
|
||||
return
|
||||
assert_mail_policy_allows_transport(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
|
||||
@@ -30,6 +30,7 @@ from govoplan_mail.backend.mail_profiles import (
|
||||
get_mail_server_profile,
|
||||
imap_config_from_profile,
|
||||
list_mail_server_profiles,
|
||||
parent_mail_profile_policy,
|
||||
profile_response_payload,
|
||||
set_mail_profile_policy,
|
||||
smtp_config_from_profile,
|
||||
@@ -138,6 +139,12 @@ def _imap_config_for_profile(session: Session, *, tenant_id: str, profile_id: st
|
||||
return imap
|
||||
|
||||
|
||||
def _parent_mail_profile_policy_payload(session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None):
|
||||
if scope_type == "system":
|
||||
return None
|
||||
return parent_mail_profile_policy(session, tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id).as_dict()
|
||||
|
||||
|
||||
@router.get("/profiles", response_model=MailServerProfileListResponse)
|
||||
def list_profiles(
|
||||
include_inactive: bool = False,
|
||||
@@ -267,11 +274,19 @@ def read_mail_profile_policy(
|
||||
try:
|
||||
policy = get_mail_profile_policy(session, tenant_id=principal.tenant_id, scope_type=scope_type, scope_id=scope_id)
|
||||
effective = None
|
||||
effective_sources = []
|
||||
if campaign_id:
|
||||
effective = effective_mail_profile_policy(session, tenant_id=principal.tenant_id, campaign_id=campaign_id).as_dict()
|
||||
effective_policy = effective_mail_profile_policy(session, tenant_id=principal.tenant_id, campaign_id=campaign_id)
|
||||
effective = effective_policy.as_dict()
|
||||
effective_sources = effective_policy.source_policies
|
||||
elif scope_type == "campaign" and scope_id:
|
||||
effective = effective_mail_profile_policy(session, tenant_id=principal.tenant_id, campaign_id=scope_id).as_dict()
|
||||
return MailProfilePolicyResponse(scope_type=scope_type, scope_id=scope_id, policy=policy, effective_policy=effective)
|
||||
effective_policy = effective_mail_profile_policy(session, tenant_id=principal.tenant_id, campaign_id=scope_id)
|
||||
effective = effective_policy.as_dict()
|
||||
effective_sources = effective_policy.source_policies
|
||||
parent_policy = parent_mail_profile_policy(session, tenant_id=principal.tenant_id, scope_type=scope_type, scope_id=scope_id) if scope_type != "system" else None
|
||||
parent = parent_policy.as_dict() if parent_policy else None
|
||||
parent_sources = parent_policy.source_policies if parent_policy else []
|
||||
return MailProfilePolicyResponse(scope_type=scope_type, scope_id=scope_id, policy=policy, effective_policy=effective, parent_policy=parent, effective_policy_sources=effective_sources, parent_policy_sources=parent_sources)
|
||||
except MailProfileError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
|
||||
@@ -296,9 +311,15 @@ def write_mail_profile_policy(
|
||||
)
|
||||
session.commit()
|
||||
effective = None
|
||||
effective_sources = []
|
||||
if scope_type == "campaign" and scope_id:
|
||||
effective = effective_mail_profile_policy(session, tenant_id=principal.tenant_id, campaign_id=scope_id).as_dict()
|
||||
return MailProfilePolicyResponse(scope_type=scope_type, scope_id=scope_id, policy=policy, effective_policy=effective)
|
||||
effective_policy = effective_mail_profile_policy(session, tenant_id=principal.tenant_id, campaign_id=scope_id)
|
||||
effective = effective_policy.as_dict()
|
||||
effective_sources = effective_policy.source_policies
|
||||
parent_policy = parent_mail_profile_policy(session, tenant_id=principal.tenant_id, scope_type=scope_type, scope_id=scope_id) if scope_type != "system" else None
|
||||
parent = parent_policy.as_dict() if parent_policy else None
|
||||
parent_sources = parent_policy.source_policies if parent_policy else []
|
||||
return MailProfilePolicyResponse(scope_type=scope_type, scope_id=scope_id, policy=policy, effective_policy=effective, parent_policy=parent, effective_policy_sources=effective_sources, parent_policy_sources=parent_sources)
|
||||
except MailProfileError as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
@@ -404,6 +425,7 @@ def list_profile_mailbox_messages(
|
||||
host=result.host,
|
||||
port=result.port,
|
||||
security=result.security,
|
||||
total_count=result.total_count,
|
||||
messages=[_mailbox_summary_response(message) for message in result.messages],
|
||||
)
|
||||
except MailProfileError as exc:
|
||||
|
||||
@@ -47,11 +47,21 @@ class MailProfilePolicyUpdateRequest(BaseModel):
|
||||
policy: MailProfilePolicyPayload = Field(default_factory=MailProfilePolicyPayload)
|
||||
|
||||
|
||||
class PolicySourceStepResponse(BaseModel):
|
||||
scope_type: str
|
||||
scope_id: str | None = None
|
||||
label: str
|
||||
applied_fields: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MailProfilePolicyResponse(BaseModel):
|
||||
scope_type: MailProfileScope
|
||||
scope_id: str | None = None
|
||||
policy: dict[str, Any]
|
||||
effective_policy: dict[str, Any] | None = None
|
||||
parent_policy: dict[str, Any] | None = None
|
||||
effective_policy_sources: list[PolicySourceStepResponse] = Field(default_factory=list)
|
||||
parent_policy_sources: list[PolicySourceStepResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MailServerProfileCreateRequest(BaseModel):
|
||||
@@ -160,6 +170,7 @@ class MailMailboxMessageListResponse(BaseModel):
|
||||
host: str | None = None
|
||||
port: int | None = None
|
||||
security: str | None = None
|
||||
total_count: int = 0
|
||||
messages: list[MailMailboxMessageSummaryResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ class ImapMailboxMessageListResult:
|
||||
security: str
|
||||
folder: str
|
||||
messages: list[ImapMailboxMessageSummary]
|
||||
total_count: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -512,7 +513,7 @@ def list_imap_messages(*, imap_config: ImapConfig, folder: str = "INBOX", limit:
|
||||
)
|
||||
for record in records[:limit]
|
||||
]
|
||||
return ImapMailboxMessageListResult(host=host, port=port, security=imap_config.security.value, folder=folder, messages=messages)
|
||||
return ImapMailboxMessageListResult(host=host, port=port, security=imap_config.security.value, folder=folder, messages=messages, total_count=len(records))
|
||||
|
||||
client = _open_imap(imap_config)
|
||||
try:
|
||||
@@ -526,7 +527,7 @@ def list_imap_messages(*, imap_config: ImapConfig, folder: str = "INBOX", limit:
|
||||
for uid in reversed(uids[-limit:]):
|
||||
fetched_uid, flags, size_bytes, raw = _fetch_message_by_uid(client, uid)
|
||||
messages.append(_message_summary_from_raw(uid=fetched_uid, folder=folder, raw=raw, flags=flags, size_bytes=size_bytes))
|
||||
return ImapMailboxMessageListResult(host=host, port=port, security=imap_config.security.value, folder=folder, messages=messages)
|
||||
return ImapMailboxMessageListResult(host=host, port=port, security=imap_config.security.value, folder=folder, messages=messages, total_count=len(uids))
|
||||
finally:
|
||||
try:
|
||||
client.logout()
|
||||
|
||||
Reference in New Issue
Block a user