"""IMAP mailbox handling and SMTP sending for chancen@destengs.com.""" from __future__ import annotations import email import email.utils import imaplib import smtplib import ssl from email.message import EmailMessage from email.policy import default as default_policy IMAP_HOST, IMAP_PORT = "mail.destengs.com", 993 SMTP_HOST, SMTP_PORT = "mail.destengs.com", 587 KEYWORD = "$ProjektChecked" class MailBox: def __init__(self, user, password, conn=None): self.user, self.password = user, password self.conn = conn or self._connect() self.conn.select("INBOX") self._trash = None def _connect(self): conn = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) conn.login(self.user, self.password) return conn def ensure_alive(self): """Reconnect if the server dropped the connection (idle timeout).""" try: typ, _ = self.conn.noop() if typ == "OK": return except Exception: # noqa: BLE001 pass try: self.conn.logout() except Exception: # noqa: BLE001 pass self.conn = self._connect() self.conn.select("INBOX") def unchecked_uids(self): typ, data = self.conn.uid("SEARCH", None, f"UNKEYWORD {KEYWORD}") if typ != "OK" or not data or not data[0]: return [] return [u.decode() for u in data[0].split()] def fetch(self, uid): typ, data = self.conn.uid("FETCH", uid, "(BODY.PEEK[])") if typ != "OK" or not data or not data[0] or not isinstance(data[0], tuple): raise RuntimeError(f"FETCH {uid} fehlgeschlagen: {typ}") return email.message_from_bytes(data[0][1], policy=default_policy) def flag_checked(self, uid): self.conn.uid("STORE", uid, "+FLAGS", f"({KEYWORD})") def trash_folder(self): if self._trash: return self._trash typ, data = self.conn.list() candidates = [] for raw in data or []: line = raw.decode() if isinstance(raw, bytes) else str(raw) name = line.rsplit(" ", 1)[-1].strip('"') if "\\Trash" in line.split(")")[0]: self._trash = name return name candidates.append(name) for cand in ("Trash", "INBOX.Trash"): if cand in candidates: self._trash = cand return cand self._trash = "Trash" return self._trash def move_to_trash(self, uid): folder = self.trash_folder() typ, _ = self.conn.uid("MOVE", uid, folder) if typ != "OK": self.conn.uid("COPY", uid, folder) self.conn.uid("STORE", uid, "+FLAGS", "(\\Deleted)") self.conn.expunge() def close(self): try: self.conn.logout() except Exception: pass def _sent_folder(conn): """Find the server's Sent folder via special-use flag, fallback names.""" typ, data = conn.list() candidates = [] for raw in data or []: line = raw.decode() if isinstance(raw, bytes) else str(raw) name = line.rsplit(" ", 1)[-1].strip('"') if "\\Sent" in line.split(")")[0]: return name candidates.append(name) for cand in ("Sent", "INBOX.Sent"): if cand in candidates: return cand return "Sent" def append_sent(user, password, msg, conn=None): """Store a copy of an outgoing message in the Sent folder (best effort).""" own = conn is None conn = conn or imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) try: if own: conn.login(user, password) folder = _sent_folder(conn) conn.append(folder, "(\\Seen)", None, msg.as_bytes()) finally: if own: try: conn.logout() except Exception: pass def send_mail(user, password, to, subject, body): msg = EmailMessage() msg["From"], msg["To"], msg["Subject"] = user, to, subject msg["Date"] = email.utils.formatdate(localtime=True) msg["Message-ID"] = email.utils.make_msgid(domain="destengs.com") msg.set_content(body) with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as server: server.starttls(context=ssl.create_default_context()) server.login(user, password) server.send_message(msg) try: append_sent(user, password, msg) except Exception: # noqa: BLE001 — copy is best effort; the mail went out pass