Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one commit with no earlier history: the tracked files were de-identified first (synthetic claim ids and patient data only; see Context.md section 7). Credentials, run logs, screenshots, claim reports and client workbooks are gitignored and not included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
6.7 KiB
Python
170 lines
6.7 KiB
Python
"""
|
|
Mailbox probe — run this before trusting `OTP_MODE=imap`.
|
|
|
|
IMAP_HOST in `.env` is a guess. All we were handed is a Roundcube webmail link
|
|
on `cpanel1.methodhosting.net`; cPanel serves IMAP from the cPanel host on some
|
|
setups and from `mail.<domain>` on others, and the only way to know which is to
|
|
try. This connects to each candidate in turn, logs in, and prints the newest
|
|
subjects so it is obvious the eCW code actually lands in this inbox.
|
|
|
|
Nothing is modified — mail is opened with readonly=True, so nothing gets marked
|
|
as read.
|
|
|
|
Run:
|
|
python "D:\\Ecw Process\\src\\check_imap.py"
|
|
|
|
Then copy the host it reports as WORKING into IMAP_HOST in `.env`.
|
|
"""
|
|
|
|
# Run standalone from the project root: python tools\check_imap.py
|
|
# The package lives under src\ , which is not on the path for a loose script.
|
|
import sys as _sys
|
|
from pathlib import Path as _Path
|
|
_sys.path.insert(0, str(_Path(__file__).resolve().parent.parent / "src"))
|
|
# Windows hands these scripts a cp1252 console, which cannot encode the box
|
|
# characters and arrows below — check_imap.py died on its own output with
|
|
# UnicodeEncodeError. See runlog._force_utf8_console.
|
|
for _s in (_sys.stdout, _sys.stderr):
|
|
try:
|
|
_s.reconfigure(encoding="utf-8", errors="replace")
|
|
except Exception:
|
|
pass
|
|
|
|
import ssl
|
|
import sys
|
|
import email
|
|
import socket
|
|
import imaplib
|
|
from email.header import decode_header, make_header
|
|
from ecw_bot import settings as cfg
|
|
from ecw_bot.auth.mailbox import _extract_code
|
|
# Order matters — the configured value is tried first so a already-correct
|
|
# .env passes on the first attempt instead of hammering the other hosts.
|
|
def _candidates() -> list:
|
|
domain = cfg.IMAP_USERNAME.partition("@")[2].strip()
|
|
hosts = [cfg.IMAP_HOST]
|
|
if domain:
|
|
hosts += [f"mail.{domain}", domain]
|
|
# cpanel1.methodhosting.net → methodhosting.net is not a mail host, but
|
|
# mail.cpanel1... occasionally is. Cheap to include, harmless if it fails.
|
|
if cfg.IMAP_HOST:
|
|
hosts.append(f"mail.{cfg.IMAP_HOST}")
|
|
seen, out = set(), []
|
|
for h in hosts:
|
|
h = (h or "").strip().rstrip("/")
|
|
if h and h not in seen:
|
|
seen.add(h)
|
|
out.append(h)
|
|
return out
|
|
def _subject(msg) -> str:
|
|
try:
|
|
return str(make_header(decode_header(msg.get("Subject") or "")))
|
|
except Exception:
|
|
return str(msg.get("Subject") or "")
|
|
def _connect(host: str, port: int, use_ssl: bool, timeout: int = 15):
|
|
# imaplib has no timeout parameter before 3.9 and ignores it for IMAP4_SSL
|
|
# on some builds, so set it globally — otherwise a wrong host hangs for the
|
|
# full TCP retry window instead of failing in a few seconds.
|
|
socket.setdefaulttimeout(timeout)
|
|
if use_ssl:
|
|
return imaplib.IMAP4_SSL(host, port, ssl_context=ssl.create_default_context())
|
|
conn = imaplib.IMAP4(host, port)
|
|
conn.starttls(ssl_context=ssl.create_default_context())
|
|
return conn
|
|
def _try_host(host: str) -> bool:
|
|
"""Connect, log in, and print recent subjects. True if the host worked."""
|
|
label = f"{host}:{cfg.IMAP_PORT} ssl={cfg.IMAP_SSL}"
|
|
print(f"\n── trying {label}")
|
|
conn = None
|
|
try:
|
|
conn = _connect(host, cfg.IMAP_PORT, cfg.IMAP_SSL)
|
|
except Exception as exc:
|
|
print(f" connect failed: {type(exc).__name__}: {exc}")
|
|
return False
|
|
|
|
try:
|
|
conn.login(cfg.IMAP_USERNAME, cfg.IMAP_PASSWORD)
|
|
except Exception as exc:
|
|
# Reaching the server but failing the login is a different problem from
|
|
# a wrong host — say so, because it means the host is probably right.
|
|
print(f" connected, but LOGIN failed: {exc}")
|
|
print(f" → host looks right; check IMAP_USERNAME / IMAP_PASSWORD")
|
|
# Specifically NOT the webmail 2FA. cPanel's Two-Factor Authentication
|
|
# guards the cPanel and Roundcube web interfaces only; IMAP/POP3/SMTP
|
|
# are exempt, verified on this account (CAPABILITY offers AUTH=PLAIN and
|
|
# AUTH=LOGIN, no 2FA mechanism). So a failure here is the password.
|
|
print(f" → not the webmail security code: cPanel 2FA does not apply "
|
|
f"to IMAP")
|
|
try:
|
|
conn.logout()
|
|
except Exception:
|
|
pass
|
|
return False
|
|
print(" login OK")
|
|
try:
|
|
typ, data = conn.list()
|
|
if typ == "OK":
|
|
names = [l.decode(errors="replace").split(' "." ')[-1].strip('"')
|
|
for l in (data or [])]
|
|
print(f" folders: {', '.join(names[:12])}")
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
typ, data = conn.select(cfg.IMAP_FOLDER, readonly=True)
|
|
if typ != "OK":
|
|
print(f" cannot select folder {cfg.IMAP_FOLDER!r}: {data}")
|
|
return False
|
|
total = int(data[0]) if data and data[0] else 0
|
|
print(f" {cfg.IMAP_FOLDER}: {total} message(s)")
|
|
|
|
typ, data = conn.search(None, "ALL")
|
|
ids = data[0].split() if typ == "OK" and data and data[0] else []
|
|
if not ids:
|
|
print(" inbox is empty — trigger a login so eCW mails a code, then rerun")
|
|
else:
|
|
print(" newest messages:")
|
|
for num in reversed(ids[-10:]):
|
|
typ, raw = conn.fetch(num, "(RFC822.HEADER)")
|
|
if typ != "OK" or not raw or not raw[0]:
|
|
continue
|
|
msg = email.message_from_bytes(raw[0][1])
|
|
subj = _subject(msg)
|
|
code = _extract_code(subj)
|
|
hit = f" [code in subject: {code}]" if code else ""
|
|
print(f" {msg.get('Date', '?')[:31]:<32} {subj[:60]}{hit}")
|
|
return True
|
|
finally:
|
|
try:
|
|
conn.logout()
|
|
except Exception:
|
|
pass
|
|
def main() -> int:
|
|
if not (cfg.IMAP_USERNAME and cfg.IMAP_PASSWORD):
|
|
print("IMAP_USERNAME / IMAP_PASSWORD not set in .env — nothing to probe")
|
|
return 2
|
|
|
|
print(f"mailbox : {cfg.IMAP_USERNAME}")
|
|
print(f"port : {cfg.IMAP_PORT} (ssl={cfg.IMAP_SSL})")
|
|
print(f"folder : {cfg.IMAP_FOLDER}")
|
|
print(f"configured IMAP_HOST: {cfg.IMAP_HOST or '(blank)'}")
|
|
|
|
for host in _candidates():
|
|
if _try_host(host):
|
|
print(f"\n✓ WORKING host: {host}")
|
|
if host != cfg.IMAP_HOST:
|
|
print(f" → set IMAP_HOST={host} in .env")
|
|
else:
|
|
print(" → .env is already correct, OTP_MODE=imap is good to go")
|
|
return 0
|
|
|
|
print("\n✗ No candidate host worked.")
|
|
print(" Ask hosting for the IMAP server name, or read it from Roundcube:")
|
|
print(" Settings → Mail Accounts / Configure → IMAP server.")
|
|
print(" Port 143 with IMAP_SSL=false is worth a try if 993 is blocked.")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|