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
|
||||
!README.md
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@ -110,13 +110,33 @@ def random_password():
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
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):
|
||||
localpart, domain = email.split("@", 1)
|
||||
if domain.lower() != MAILU_DOMAIN.lower():
|
||||
return
|
||||
hashed = bcrypt_sha256.hash(password)
|
||||
now = datetime.datetime.utcnow()
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO "user" (
|
||||
@ -167,30 +187,29 @@ def main():
|
||||
|
||||
for user in users:
|
||||
attrs = user.get("attributes", {}) or {}
|
||||
app_pw_value = attrs.get("mailu_app_password")
|
||||
if isinstance(app_pw_value, list):
|
||||
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
|
||||
app_pw = get_attribute_value(attrs, "mailu_app_password")
|
||||
mailu_email = resolve_mailu_email(user, attrs)
|
||||
|
||||
email = user.get("email")
|
||||
if not email:
|
||||
email = f"{user['username']}@{MAILU_DOMAIN}"
|
||||
needs_update = False
|
||||
if not get_attribute_value(attrs, "mailu_email"):
|
||||
attrs["mailu_email"] = [mailu_email]
|
||||
needs_update = True
|
||||
|
||||
if not app_pw:
|
||||
app_pw = random_password()
|
||||
attrs["mailu_app_password"] = [app_pw]
|
||||
needs_update = True
|
||||
|
||||
if needs_update:
|
||||
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(
|
||||
part for part in [user.get("firstName"), user.get("lastName")] if part
|
||||
).strip()
|
||||
|
||||
ensure_mailu_user(cursor, email, app_pw, display_name)
|
||||
log(f"Synced mailbox for {email}")
|
||||
ensure_mailu_user(cursor, mailu_email, app_pw, display_name)
|
||||
log(f"Synced mailbox for {mailu_email}")
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
@ -5,6 +5,7 @@ KC_BASE="${KC_BASE:?}"
|
||||
KC_REALM="${KC_REALM:?}"
|
||||
KC_ADMIN_USER="${KC_ADMIN_USER:?}"
|
||||
KC_ADMIN_PASS="${KC_ADMIN_PASS:?}"
|
||||
MAILU_DOMAIN="${MAILU_DOMAIN:?}"
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
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
|
||||
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)')
|
||||
[[ -z "${email}" || -z "${app_pw}" ]] && continue
|
||||
if account_exists "${username}" "${email}"; then
|
||||
echo "Skipping ${email}, already exists"
|
||||
|
||||
if [[ -z "${mailu_email}" ]]; then
|
||||
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
|
||||
fi
|
||||
echo "Syncing ${email}"
|
||||
echo "Syncing ${mailu_email}"
|
||||
/usr/sbin/runuser -u www-data -- php occ mail:account:create \
|
||||
"${username}" "${username}" "${email}" \
|
||||
mail.bstein.dev 993 ssl "${email}" "${app_pw}" \
|
||||
mail.bstein.dev 587 tls "${email}" "${app_pw}" || true
|
||||
"${username}" "${username}" "${mailu_email}" \
|
||||
mail.bstein.dev 993 ssl "${mailu_email}" "${app_pw}" \
|
||||
mail.bstein.dev 587 tls "${mailu_email}" "${app_pw}" || true
|
||||
done
|
||||
|
||||
@ -102,7 +102,8 @@ def test_kc_get_users_paginates(monkeypatch):
|
||||
sync.SESSION = _PagedSession()
|
||||
users = sync.kc_get_users("tok")
|
||||
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):
|
||||
@ -119,6 +120,7 @@ def test_ensure_mailu_user_skips_foreign_domain(monkeypatch):
|
||||
|
||||
def test_ensure_mailu_user_upserts(monkeypatch):
|
||||
sync = load_sync_module(monkeypatch)
|
||||
monkeypatch.setattr(sync.bcrypt_sha256, "hash", lambda password: f"hash:{password}")
|
||||
captured = {}
|
||||
|
||||
class _Cursor:
|
||||
@ -134,6 +136,7 @@ def test_ensure_mailu_user_upserts(monkeypatch):
|
||||
|
||||
def test_main_generates_password_and_upserts(monkeypatch):
|
||||
sync = load_sync_module(monkeypatch)
|
||||
monkeypatch.setattr(sync.bcrypt_sha256, "hash", lambda password: f"hash:{password}")
|
||||
users = [
|
||||
{"id": "u1", "username": "user1", "email": "user1@example.com", "attributes": {}},
|
||||
{"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()
|
||||
|
||||
# Should attempt two inserts (third user skipped due to domain mismatch)
|
||||
assert len(updated) == 1 # only one missing attr was backfilled
|
||||
assert conns and len(conns[0]._cursor.executions) == 2
|
||||
# Always backfill mailu_email, even if Keycloak recovery email is external.
|
||||
assert len(updated) == 3
|
||||
assert conns and len(conns[0]._cursor.executions) == 3
|
||||
|
||||
@ -25,6 +25,8 @@ spec:
|
||||
value: https://sso.bstein.dev
|
||||
- name: KC_REALM
|
||||
value: atlas
|
||||
- name: MAILU_DOMAIN
|
||||
value: bstein.dev
|
||||
- name: KC_ADMIN_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user