Initial import: eCW Charge Validation bot and results Portal
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>
This commit is contained in:
commit
ba15a574ab
205 files changed
+56772
No files matched your search
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
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())
|
||||
Reference in new issue
Block a user