Release v0.1.2
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""split mail profile usernames from server config
|
||||
|
||||
Revision ID: 0a1b2c3d4e6f
|
||||
Revises: f5a6b7c8d9e0
|
||||
Create Date: 2026-06-25 15:20:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0a1b2c3d4e6f"
|
||||
down_revision = "f5a6b7c8d9e0"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _profiles_table() -> sa.Table:
|
||||
return sa.table(
|
||||
"mail_server_profiles",
|
||||
sa.column("id", sa.String(length=36)),
|
||||
sa.column("smtp_config", sa.JSON()),
|
||||
sa.column("smtp_username", sa.String(length=320)),
|
||||
sa.column("imap_config", sa.JSON()),
|
||||
sa.column("imap_username", sa.String(length=320)),
|
||||
)
|
||||
|
||||
|
||||
def _without_username(value):
|
||||
if not isinstance(value, dict):
|
||||
return value, None
|
||||
data = dict(value)
|
||||
username = data.pop("username", None)
|
||||
data.pop("enabled", None)
|
||||
return data, username
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
if "mail_server_profiles" not in inspector.get_table_names():
|
||||
return
|
||||
columns = {column["name"] for column in inspector.get_columns("mail_server_profiles")}
|
||||
if "smtp_username" not in columns:
|
||||
op.add_column("mail_server_profiles", sa.Column("smtp_username", sa.String(length=320), nullable=True))
|
||||
if "imap_username" not in columns:
|
||||
op.add_column("mail_server_profiles", sa.Column("imap_username", sa.String(length=320), nullable=True))
|
||||
|
||||
profiles = _profiles_table()
|
||||
rows = bind.execute(sa.select(profiles.c.id, profiles.c.smtp_config, profiles.c.smtp_username, profiles.c.imap_config, profiles.c.imap_username)).mappings().all()
|
||||
for row in rows:
|
||||
smtp_config, smtp_username = _without_username(row["smtp_config"] or {})
|
||||
imap_config, imap_username = _without_username(row["imap_config"] or {}) if row["imap_config"] is not None else (None, None)
|
||||
values = {
|
||||
"smtp_config": smtp_config,
|
||||
"imap_config": imap_config,
|
||||
}
|
||||
if row["smtp_username"] in (None, "") and smtp_username not in (None, ""):
|
||||
values["smtp_username"] = str(smtp_username)
|
||||
if row["imap_username"] in (None, "") and imap_username not in (None, ""):
|
||||
values["imap_username"] = str(imap_username)
|
||||
bind.execute(sa.update(profiles).where(profiles.c.id == row["id"]).values(**values))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
if "mail_server_profiles" not in inspector.get_table_names():
|
||||
return
|
||||
columns = {column["name"] for column in inspector.get_columns("mail_server_profiles")}
|
||||
profiles = _profiles_table()
|
||||
if {"smtp_username", "imap_username"}.issubset(columns):
|
||||
rows = bind.execute(sa.select(profiles.c.id, profiles.c.smtp_config, profiles.c.smtp_username, profiles.c.imap_config, profiles.c.imap_username)).mappings().all()
|
||||
for row in rows:
|
||||
smtp_config = dict(row["smtp_config"] or {})
|
||||
if row["smtp_username"] not in (None, ""):
|
||||
smtp_config["username"] = row["smtp_username"]
|
||||
imap_config = dict(row["imap_config"] or {}) if row["imap_config"] is not None else None
|
||||
if imap_config is not None and row["imap_username"] not in (None, ""):
|
||||
imap_config["username"] = row["imap_username"]
|
||||
bind.execute(sa.update(profiles).where(profiles.c.id == row["id"]).values(smtp_config=smtp_config, imap_config=imap_config))
|
||||
if "imap_username" in columns:
|
||||
op.drop_column("mail_server_profiles", "imap_username")
|
||||
if "smtp_username" in columns:
|
||||
op.drop_column("mail_server_profiles", "smtp_username")
|
||||
@@ -0,0 +1,108 @@
|
||||
"""drop mail credential override policy fields
|
||||
|
||||
Revision ID: 1b2c3d4e5f70
|
||||
Revises: 0a1b2c3d4e6f
|
||||
Create Date: 2026-06-25 18:30:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "1b2c3d4e5f70"
|
||||
down_revision = "0a1b2c3d4e6f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
_MAIL_POLICY_KEY = "mail_profile_policy"
|
||||
_CREDENTIAL_KEYS = ("smtp_credentials", "imap_credentials")
|
||||
_DEPRECATED_LIMIT_KEYS = ("smtp_credentials.allow_override", "imap_credentials.allow_override")
|
||||
|
||||
|
||||
def _json_table(table_name: str, column_name: str) -> sa.Table:
|
||||
return sa.table(
|
||||
table_name,
|
||||
sa.column("id", sa.String(length=36)),
|
||||
sa.column(column_name, sa.JSON()),
|
||||
)
|
||||
|
||||
|
||||
def _as_dict(value: Any) -> dict[str, Any] | None:
|
||||
if isinstance(value, dict):
|
||||
return dict(value)
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return dict(parsed) if isinstance(parsed, dict) else None
|
||||
return None
|
||||
|
||||
|
||||
def _scrub_policy(value: Any) -> tuple[dict[str, Any] | None, bool]:
|
||||
policy = _as_dict(value)
|
||||
if policy is None:
|
||||
return None, False
|
||||
changed = False
|
||||
|
||||
for key in _CREDENTIAL_KEYS:
|
||||
credential = _as_dict(policy.get(key))
|
||||
if credential is not None and "allow_override" in credential:
|
||||
credential.pop("allow_override", None)
|
||||
policy[key] = credential
|
||||
changed = True
|
||||
|
||||
limits = _as_dict(policy.get("allow_lower_level_limits"))
|
||||
if limits is not None:
|
||||
for key in _DEPRECATED_LIMIT_KEYS:
|
||||
if key in limits:
|
||||
limits.pop(key, None)
|
||||
changed = True
|
||||
if changed:
|
||||
policy["allow_lower_level_limits"] = limits
|
||||
|
||||
return policy, changed
|
||||
|
||||
|
||||
def _scrub_settings_table(table_name: str) -> None:
|
||||
bind = op.get_bind()
|
||||
table = _json_table(table_name, "settings")
|
||||
rows = bind.execute(sa.select(table.c.id, table.c.settings)).mappings().all()
|
||||
for row in rows:
|
||||
settings = _as_dict(row["settings"])
|
||||
if settings is None:
|
||||
continue
|
||||
policy, changed = _scrub_policy(settings.get(_MAIL_POLICY_KEY))
|
||||
if changed and policy is not None:
|
||||
settings[_MAIL_POLICY_KEY] = policy
|
||||
bind.execute(sa.update(table).where(table.c.id == row["id"]).values(settings=settings))
|
||||
|
||||
|
||||
def _scrub_policy_column(table_name: str) -> None:
|
||||
bind = op.get_bind()
|
||||
table = _json_table(table_name, "mail_profile_policy")
|
||||
rows = bind.execute(sa.select(table.c.id, table.c.mail_profile_policy)).mappings().all()
|
||||
for row in rows:
|
||||
policy, changed = _scrub_policy(row["mail_profile_policy"])
|
||||
if changed and policy is not None:
|
||||
bind.execute(sa.update(table).where(table.c.id == row["id"]).values(mail_profile_policy=policy))
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
tables = set(inspector.get_table_names())
|
||||
for table_name in ("system_settings", "tenants"):
|
||||
if table_name in tables and "settings" in {column["name"] for column in inspector.get_columns(table_name)}:
|
||||
_scrub_settings_table(table_name)
|
||||
for table_name in ("users", "groups", "campaigns"):
|
||||
if table_name in tables and "mail_profile_policy" in {column["name"] for column in inspector.get_columns(table_name)}:
|
||||
_scrub_policy_column(table_name)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# The removed fields are redundant with the surviving lower-level limit
|
||||
# switches, so they cannot be reconstructed safely.
|
||||
pass
|
||||
Reference in New Issue
Block a user