mailu: sync via mailu_email attribute
This commit is contained in:
parent
10e322e853
commit
e6eff8165a
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
|||||||
*.md
|
*.md
|
||||||
!README.md
|
!README.md
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|||||||
@ -110,13 +110,33 @@ def random_password():
|
|||||||
alphabet = string.ascii_letters + string.digits
|
alphabet = string.ascii_letters + string.digits
|
||||||
return "".join(secrets.choice(alphabet) for _ in range(24))
|
return "".join(secrets.choice(alphabet) for _ in range(24))
|
||||||
|
|
||||||
|
def get_attribute_value(attributes, key):
|
||||||
|
raw = (attributes or {}).get(key)
|
||||||
|
if isinstance(raw, list):
|
||||||
|
return raw[0] if raw else None
|
||||||
|
if isinstance(raw, str):
|
||||||
|
return raw
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_mailu_email(user, attributes):
|
||||||
|
explicit = get_attribute_value(attributes, "mailu_email")
|
||||||
|
if explicit:
|
||||||
|
return explicit
|
||||||
|
|
||||||
|
email = user.get("email") or ""
|
||||||
|
if "@" in email and email.lower().endswith(f"@{MAILU_DOMAIN.lower()}"):
|
||||||
|
return email
|
||||||
|
|
||||||
|
return f"{user['username']}@{MAILU_DOMAIN}"
|
||||||
|
|
||||||
|
|
||||||
def ensure_mailu_user(cursor, email, password, display_name):
|
def ensure_mailu_user(cursor, email, password, display_name):
|
||||||
localpart, domain = email.split("@", 1)
|
localpart, domain = email.split("@", 1)
|
||||||
if domain.lower() != MAILU_DOMAIN.lower():
|
if domain.lower() != MAILU_DOMAIN.lower():
|
||||||
return
|
return
|
||||||
hashed = bcrypt_sha256.hash(password)
|
hashed = bcrypt_sha256.hash(password)
|
||||||
now = datetime.datetime.utcnow()
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO "user" (
|
INSERT INTO "user" (
|
||||||
@ -167,30 +187,29 @@ def main():
|
|||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
attrs = user.get("attributes", {}) or {}
|
attrs = user.get("attributes", {}) or {}
|
||||||
app_pw_value = attrs.get("mailu_app_password")
|
app_pw = get_attribute_value(attrs, "mailu_app_password")
|
||||||
if isinstance(app_pw_value, list):
|
mailu_email = resolve_mailu_email(user, attrs)
|
||||||
app_pw = app_pw_value[0] if app_pw_value else None
|
|
||||||
elif isinstance(app_pw_value, str):
|
|
||||||
app_pw = app_pw_value
|
|
||||||
else:
|
|
||||||
app_pw = None
|
|
||||||
|
|
||||||
email = user.get("email")
|
needs_update = False
|
||||||
if not email:
|
if not get_attribute_value(attrs, "mailu_email"):
|
||||||
email = f"{user['username']}@{MAILU_DOMAIN}"
|
attrs["mailu_email"] = [mailu_email]
|
||||||
|
needs_update = True
|
||||||
|
|
||||||
if not app_pw:
|
if not app_pw:
|
||||||
app_pw = random_password()
|
app_pw = random_password()
|
||||||
attrs["mailu_app_password"] = [app_pw]
|
attrs["mailu_app_password"] = [app_pw]
|
||||||
|
needs_update = True
|
||||||
|
|
||||||
|
if needs_update:
|
||||||
kc_update_attributes(token, user, attrs)
|
kc_update_attributes(token, user, attrs)
|
||||||
log(f"Set mailu_app_password for {email}")
|
log(f"Updated Mailu attributes for {mailu_email}")
|
||||||
|
|
||||||
display_name = " ".join(
|
display_name = " ".join(
|
||||||
part for part in [user.get("firstName"), user.get("lastName")] if part
|
part for part in [user.get("firstName"), user.get("lastName")] if part
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
ensure_mailu_user(cursor, email, app_pw, display_name)
|
ensure_mailu_user(cursor, mailu_email, app_pw, display_name)
|
||||||
log(f"Synced mailbox for {email}")
|
log(f"Synced mailbox for {mailu_email}")
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@ -5,6 +5,7 @@ KC_BASE="${KC_BASE:?}"
|
|||||||
KC_REALM="${KC_REALM:?}"
|
KC_REALM="${KC_REALM:?}"
|
||||||
KC_ADMIN_USER="${KC_ADMIN_USER:?}"
|
KC_ADMIN_USER="${KC_ADMIN_USER:?}"
|
||||||
KC_ADMIN_PASS="${KC_ADMIN_PASS:?}"
|
KC_ADMIN_PASS="${KC_ADMIN_PASS:?}"
|
||||||
|
MAILU_DOMAIN="${MAILU_DOMAIN:?}"
|
||||||
|
|
||||||
if ! command -v jq >/dev/null 2>&1; then
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
apt-get update && apt-get install -y jq curl >/dev/null
|
apt-get update && apt-get install -y jq curl >/dev/null
|
||||||
@ -45,16 +46,26 @@ users=$(curl -s -H "Authorization: Bearer ${token}" \
|
|||||||
|
|
||||||
echo "${users}" | jq -c '.[]' | while read -r user; do
|
echo "${users}" | jq -c '.[]' | while read -r user; do
|
||||||
username=$(echo "${user}" | jq -r '.username')
|
username=$(echo "${user}" | jq -r '.username')
|
||||||
email=$(echo "${user}" | jq -r '.email // empty')
|
keycloak_email=$(echo "${user}" | jq -r '.email // empty')
|
||||||
|
mailu_email=$(echo "${user}" | jq -r '(.attributes.mailu_email[0] // .attributes.mailu_email // empty)')
|
||||||
app_pw=$(echo "${user}" | jq -r '(.attributes.mailu_app_password[0] // .attributes.mailu_app_password // empty)')
|
app_pw=$(echo "${user}" | jq -r '(.attributes.mailu_app_password[0] // .attributes.mailu_app_password // empty)')
|
||||||
[[ -z "${email}" || -z "${app_pw}" ]] && continue
|
|
||||||
if account_exists "${username}" "${email}"; then
|
if [[ -z "${mailu_email}" ]]; then
|
||||||
echo "Skipping ${email}, already exists"
|
if [[ -n "${keycloak_email}" && "${keycloak_email,,}" == *"@${MAILU_DOMAIN,,}" ]]; then
|
||||||
|
mailu_email="${keycloak_email}"
|
||||||
|
else
|
||||||
|
mailu_email="${username}@${MAILU_DOMAIN}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -z "${mailu_email}" || -z "${app_pw}" ]] && continue
|
||||||
|
if account_exists "${username}" "${mailu_email}"; then
|
||||||
|
echo "Skipping ${mailu_email}, already exists"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
echo "Syncing ${email}"
|
echo "Syncing ${mailu_email}"
|
||||||
/usr/sbin/runuser -u www-data -- php occ mail:account:create \
|
/usr/sbin/runuser -u www-data -- php occ mail:account:create \
|
||||||
"${username}" "${username}" "${email}" \
|
"${username}" "${username}" "${mailu_email}" \
|
||||||
mail.bstein.dev 993 ssl "${email}" "${app_pw}" \
|
mail.bstein.dev 993 ssl "${mailu_email}" "${app_pw}" \
|
||||||
mail.bstein.dev 587 tls "${email}" "${app_pw}" || true
|
mail.bstein.dev 587 tls "${mailu_email}" "${app_pw}" || true
|
||||||
done
|
done
|
||||||
|
|||||||
@ -102,7 +102,8 @@ def test_kc_get_users_paginates(monkeypatch):
|
|||||||
sync.SESSION = _PagedSession()
|
sync.SESSION = _PagedSession()
|
||||||
users = sync.kc_get_users("tok")
|
users = sync.kc_get_users("tok")
|
||||||
assert [u["id"] for u in users] == ["u1", "u2"]
|
assert [u["id"] for u in users] == ["u1", "u2"]
|
||||||
assert sync.SESSION.calls == 2
|
# Pagination stops when results < page size.
|
||||||
|
assert sync.SESSION.calls == 1
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_mailu_user_skips_foreign_domain(monkeypatch):
|
def test_ensure_mailu_user_skips_foreign_domain(monkeypatch):
|
||||||
@ -119,6 +120,7 @@ def test_ensure_mailu_user_skips_foreign_domain(monkeypatch):
|
|||||||
|
|
||||||
def test_ensure_mailu_user_upserts(monkeypatch):
|
def test_ensure_mailu_user_upserts(monkeypatch):
|
||||||
sync = load_sync_module(monkeypatch)
|
sync = load_sync_module(monkeypatch)
|
||||||
|
monkeypatch.setattr(sync.bcrypt_sha256, "hash", lambda password: f"hash:{password}")
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
class _Cursor:
|
class _Cursor:
|
||||||
@ -134,6 +136,7 @@ def test_ensure_mailu_user_upserts(monkeypatch):
|
|||||||
|
|
||||||
def test_main_generates_password_and_upserts(monkeypatch):
|
def test_main_generates_password_and_upserts(monkeypatch):
|
||||||
sync = load_sync_module(monkeypatch)
|
sync = load_sync_module(monkeypatch)
|
||||||
|
monkeypatch.setattr(sync.bcrypt_sha256, "hash", lambda password: f"hash:{password}")
|
||||||
users = [
|
users = [
|
||||||
{"id": "u1", "username": "user1", "email": "user1@example.com", "attributes": {}},
|
{"id": "u1", "username": "user1", "email": "user1@example.com", "attributes": {}},
|
||||||
{"id": "u2", "username": "user2", "email": "user2@example.com", "attributes": {"mailu_app_password": ["keepme"]}},
|
{"id": "u2", "username": "user2", "email": "user2@example.com", "attributes": {"mailu_app_password": ["keepme"]}},
|
||||||
@ -176,6 +179,6 @@ def test_main_generates_password_and_upserts(monkeypatch):
|
|||||||
|
|
||||||
sync.main()
|
sync.main()
|
||||||
|
|
||||||
# Should attempt two inserts (third user skipped due to domain mismatch)
|
# Always backfill mailu_email, even if Keycloak recovery email is external.
|
||||||
assert len(updated) == 1 # only one missing attr was backfilled
|
assert len(updated) == 3
|
||||||
assert conns and len(conns[0]._cursor.executions) == 2
|
assert conns and len(conns[0]._cursor.executions) == 3
|
||||||
|
|||||||
@ -25,6 +25,8 @@ spec:
|
|||||||
value: https://sso.bstein.dev
|
value: https://sso.bstein.dev
|
||||||
- name: KC_REALM
|
- name: KC_REALM
|
||||||
value: atlas
|
value: atlas
|
||||||
|
- name: MAILU_DOMAIN
|
||||||
|
value: bstein.dev
|
||||||
- name: KC_ADMIN_USER
|
- name: KC_ADMIN_USER
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user