fix(mail): rotate-creds wrong subcommand — silently broke for 35h
`mail.droplet.rotate_creds` and `seed_mailboxes` called `maddy creds set --password X user@host`, but maddy has no `set` subcommand — the actual one is `password`. Effect: the ssh exec returned non-zero (Error: No help topic for 'set'); fall-through to `creds create` errored on the existing account; local credentials.json got the new password but the droplet kept the OLD one. The IMAP poller then looped forever on Invalid credentials. Discovered when investigating why mail-poller had been unhealthy for 35 hours straight (zero CMS emails ingested into Zotero). - Use `creds password` for existing accounts; fall through to `creds create` + `imap-acct create` only when password fails. - Save under both short cache key (back-compat) and full address. - Same fix applied to seed_mailboxes' in-sync path. Manual recovery already done out-of-band: - maddy creds password applied directly on droplet - credentials.json updated locally; mail-poller restarted - 45 items now tagged source:email in bib.sqlite Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -421,12 +421,25 @@ def _addr_to_key(addr: str) -> str:
|
|||||||
|
|
||||||
def rotate_creds(user_or_addr: str) -> str:
|
def rotate_creds(user_or_addr: str) -> str:
|
||||||
"""Mint new password and push to droplet. ``user_or_addr`` may be
|
"""Mint new password and push to droplet. ``user_or_addr`` may be
|
||||||
``user`` (assumed at apex) or full ``user@host``."""
|
``user`` (assumed at apex) or full ``user@host``.
|
||||||
|
|
||||||
|
maddy subcommands:
|
||||||
|
creds password → change password on existing account
|
||||||
|
creds create → create new account (also needs imap-acct create)
|
||||||
|
Earlier versions of this function called ``creds set``, which doesn't
|
||||||
|
exist in maddy — it silently failed and fell through to ``creds
|
||||||
|
create`` which then errored on the existing account. Net result:
|
||||||
|
local credentials.json got the new password but the droplet kept
|
||||||
|
the old one. The poller then loops forever on Invalid credentials.
|
||||||
|
"""
|
||||||
addr = user_or_addr if "@" in user_or_addr else f"{user_or_addr}@{DOMAIN}"
|
addr = user_or_addr if "@" in user_or_addr else f"{user_or_addr}@{DOMAIN}"
|
||||||
meta = discover_droplet(_do_client())
|
meta = discover_droplet(_do_client())
|
||||||
if not meta:
|
if not meta:
|
||||||
raise RuntimeError("no mail droplet exists")
|
raise RuntimeError("no mail droplet exists")
|
||||||
new_pw = _gen_password()
|
new_pw = _gen_password()
|
||||||
|
|
||||||
|
# Try password change first (account exists). If the account
|
||||||
|
# doesn't exist yet, create it + the matching IMAP account.
|
||||||
r = _ssh(
|
r = _ssh(
|
||||||
meta["public_ip"],
|
meta["public_ip"],
|
||||||
"docker",
|
"docker",
|
||||||
@@ -434,7 +447,7 @@ def rotate_creds(user_or_addr: str) -> str:
|
|||||||
"mail",
|
"mail",
|
||||||
"maddy",
|
"maddy",
|
||||||
"creds",
|
"creds",
|
||||||
"set",
|
"password",
|
||||||
"--password",
|
"--password",
|
||||||
new_pw,
|
new_pw,
|
||||||
addr,
|
addr,
|
||||||
@@ -464,7 +477,10 @@ def rotate_creds(user_or_addr: str) -> str:
|
|||||||
addr,
|
addr,
|
||||||
)
|
)
|
||||||
creds = _load_json(CREDS_JSON)
|
creds = _load_json(CREDS_JSON)
|
||||||
|
# Save under both forms so lookup works whether the caller passes
|
||||||
|
# the bare key or the full address.
|
||||||
creds[_addr_to_key(addr)] = new_pw
|
creds[_addr_to_key(addr)] = new_pw
|
||||||
|
creds[addr] = new_pw
|
||||||
_save_json(CREDS_JSON, creds)
|
_save_json(CREDS_JSON, creds)
|
||||||
CREDS_JSON.chmod(0o600)
|
CREDS_JSON.chmod(0o600)
|
||||||
ok(f"rotated {addr}")
|
ok(f"rotated {addr}")
|
||||||
@@ -488,6 +504,8 @@ def seed_mailboxes(addrs: tuple[str, ...] = DEFAULT_MAILBOXES) -> None:
|
|||||||
key = _addr_to_key(addr)
|
key = _addr_to_key(addr)
|
||||||
pw = creds.get(key)
|
pw = creds.get(key)
|
||||||
if pw:
|
if pw:
|
||||||
|
# `creds password` updates an existing account; falls
|
||||||
|
# through to create+imap-acct for first-time provision.
|
||||||
r = _ssh(
|
r = _ssh(
|
||||||
ip,
|
ip,
|
||||||
"docker",
|
"docker",
|
||||||
@@ -495,7 +513,7 @@ def seed_mailboxes(addrs: tuple[str, ...] = DEFAULT_MAILBOXES) -> None:
|
|||||||
"mail",
|
"mail",
|
||||||
"maddy",
|
"maddy",
|
||||||
"creds",
|
"creds",
|
||||||
"set",
|
"password",
|
||||||
"--password",
|
"--password",
|
||||||
pw,
|
pw,
|
||||||
addr,
|
addr,
|
||||||
|
|||||||
Reference in New Issue
Block a user