""" Pobieranie wiadomosci tekstowych od klientow z poczty IMAP. Bliznaczy modul do `email_photo_fetcher.py`, ale zamiast zalacznikow graficznych zwraca tresc tekstowa wiadomosci (body) od podanego nadawcy. Uzywany przez silnik `/wygeneruj-projekty` aby wykryc dodatkowe poprawki/uwagi ktore klient przysylal mailem juz po zlozeniu zamowienia. Skrzynka: EMAIL_01, folder: INBOX (na stale — zgodnie z wymaganiem). Wymagane zmienne srodowiskowe (.env w katalogu projektu): EMAIL_01_HOST EMAIL_01_USERNAME EMAIL_01_PASSWORD EMAIL_01_IMAP_PORT - opcjonalnie, domyslnie 993 Uzycie programowe: from email_message_fetcher import fetch_customer_messages items = fetch_customer_messages("klient@example.com", days_back=60) # items: list[dict] z kluczami: date, subject, body Uzycie z CLI: python email_message_fetcher.py --email klient@example.com --days 60 """ import argparse import email import imaplib import json import sys from datetime import datetime, timedelta from email.header import decode_header from email_photo_fetcher import _imap_config # reuse env loader def _decode_header_value(raw): if not raw: return "" parts = decode_header(raw) out = [] for chunk, enc in parts: if isinstance(chunk, bytes): try: out.append(chunk.decode(enc or "utf-8", errors="replace")) except LookupError: out.append(chunk.decode("utf-8", errors="replace")) else: out.append(chunk) return "".join(out).strip() def _extract_text_body(msg): """Zwraca text/plain z wiadomosci; gdy brak — fallback do text/html (bez tagow).""" plain_parts = [] html_parts = [] for part in msg.walk(): if part.get_content_maintype() == "multipart": continue disposition = str(part.get("Content-Disposition") or "").lower() if "attachment" in disposition: continue ctype = part.get_content_type() payload = part.get_payload(decode=True) if not payload: continue charset = part.get_content_charset() or "utf-8" try: text = payload.decode(charset, errors="replace") except LookupError: text = payload.decode("utf-8", errors="replace") if ctype == "text/plain": plain_parts.append(text) elif ctype == "text/html": html_parts.append(text) if plain_parts: return "\n".join(p.strip() for p in plain_parts).strip() if html_parts: import re raw = "\n".join(html_parts) raw = re.sub(r"(?is)<(script|style).*?>.*?", " ", raw) raw = re.sub(r"(?s)<[^>]+>", " ", raw) raw = re.sub(r"[ \t]+", " ", raw) raw = re.sub(r"\s*\n\s*", "\n", raw) return raw.strip() return "" def fetch_customer_messages(customer_email, days_back=60): """Szuka maili od `customer_email` w INBOX z ostatnich `days_back` dni. Zwraca liste dict {date, subject, body}, od najnowszego do najstarszego.""" cfg = _imap_config() if not cfg["host"] or not cfg["user"] or not cfg["password"]: raise RuntimeError( "Brak konfiguracji konta w .env — wymagane: EMAIL_01_HOST, EMAIL_01_USERNAME, EMAIL_01_PASSWORD" ) imap = imaplib.IMAP4_SSL(cfg["host"], cfg["port"]) results = [] try: imap.login(cfg["user"], cfg["password"]) imap.select("INBOX") since = (datetime.now() - timedelta(days=days_back)).strftime("%d-%b-%Y") criteria = f'(FROM "{customer_email}" SINCE "{since}")' status, data = imap.search(None, criteria) if status != "OK" or not data or not data[0]: return results ids = data[0].split() for msg_id in reversed(ids): status, msg_data = imap.fetch(msg_id, "(RFC822)") if status != "OK" or not msg_data or not msg_data[0]: continue msg = email.message_from_bytes(msg_data[0][1]) subject = _decode_header_value(msg.get("Subject")) date_hdr = msg.get("Date") or "" body = _extract_text_body(msg) if not body: continue results.append({"date": date_hdr, "subject": subject, "body": body}) return results finally: try: imap.logout() except Exception: pass def main(): parser = argparse.ArgumentParser(description="Pobierz wiadomosci klienta z INBOX") parser.add_argument("--email", required=True, help="Adres email klienta (nadawca)") parser.add_argument("--days", type=int, default=60, help="Ile dni wstecz szukac (domyslnie 60)") parser.add_argument("--json", action="store_true", help="Wyjscie w formacie JSON") args = parser.parse_args() try: items = fetch_customer_messages(args.email, days_back=args.days) except Exception as exc: print(f"BLAD: {exc}") sys.exit(2) if not items: print("BRAK: nie znaleziono wiadomosci od tego klienta w INBOX") sys.exit(1) if args.json: print(json.dumps(items, ensure_ascii=False, indent=2)) else: for i, it in enumerate(items, 1): print(f"=== [{i}] {it['date']} | {it['subject']} ===") print(it["body"]) print() sys.exit(0) if __name__ == "__main__": main()