""" 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.` 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())