213 lines
7.3 KiB
Python
213 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
================================================================================
|
|
Nazev: mailstore_read_v1.0.py
|
|
Verze: 1.0
|
|
Datum: 2026-06-11
|
|
Autor: Vladimir Buzalka (asistovano Claude)
|
|
Popis: Precte JEDNU konkretni zpravu z MailStore slozky a vypise jeji plny
|
|
obsah - hlavicky, telo (text), seznam priloh. Volitelne ulozi
|
|
prilohy na disk. Posledni dilek rucniho prohlizece archivu.
|
|
|
|
Argumenty: <slozka> <cislo>
|
|
slozka = plna cesta (fullName z mapy / vystupu mailstore_folder)
|
|
cislo = poradove cislo zpravy (# z mailstore_folder), nebo UID s --uid
|
|
|
|
Zdroj: MailStore IMAP, port 143, STARTTLS, auth Prosty text (LOGIN).
|
|
FETCH <n> (RFC822) = cely syrovy EML, naparsovan emailem.
|
|
|
|
Spusteni:
|
|
python mailstore_read_v1.0.py "...slozka..." 63627
|
|
python mailstore_read_v1.0.py "...slozka..." 12345 --uid # cislo je UID
|
|
python mailstore_read_v1.0.py "...slozka..." 63627 --save .\att # ulozi prilohy
|
|
python mailstore_read_v1.0.py "...slozka..." 63627 --raw # vypise cely EML
|
|
================================================================================
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import email
|
|
import imaplib
|
|
import os
|
|
import ssl
|
|
import sys
|
|
from email.header import decode_header
|
|
from email.utils import parsedate_to_datetime
|
|
|
|
# --- konfigurace ------------------------------------------------------------
|
|
HOST = "192.168.1.53"
|
|
PORT = 143
|
|
USER = "admin"
|
|
PASS = "*$N(B)vMUym!%"
|
|
|
|
BODY_PREVIEW_CHARS = 4000 # kolik znaku tela vypsat na obrazovku
|
|
|
|
|
|
def connect() -> imaplib.IMAP4:
|
|
ctx = ssl.create_default_context()
|
|
ctx.check_hostname = False
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
M = imaplib.IMAP4(HOST, PORT)
|
|
M.starttls(ssl_context=ctx)
|
|
M.login(USER, PASS)
|
|
return M
|
|
|
|
|
|
def encode_mutf7(s: str) -> str:
|
|
"""Nazev IMAP slozky -> modified UTF-7 (RFC 3501) kvuli diakritice."""
|
|
import base64 as _b64
|
|
res = []
|
|
i, n = 0, len(s)
|
|
while i < n:
|
|
ch = s[i]; o = ord(ch)
|
|
if 0x20 <= o <= 0x7e:
|
|
res.append("&-" if ch == "&" else ch); i += 1
|
|
else:
|
|
j = i
|
|
while j < n and not (0x20 <= ord(s[j]) <= 0x7e):
|
|
j += 1
|
|
enc = _b64.b64encode(s[i:j].encode("utf-16-be")).decode("ascii").rstrip("=").replace("/", ",")
|
|
res.append("&" + enc + "-"); i = j
|
|
return "".join(res)
|
|
|
|
|
|
def dec(s: str | None) -> str:
|
|
if not s:
|
|
return ""
|
|
out = []
|
|
for txt, enc in decode_header(s):
|
|
if isinstance(txt, bytes):
|
|
out.append(txt.decode(enc or "utf-8", errors="replace"))
|
|
else:
|
|
out.append(txt)
|
|
return "".join(out).replace("\r", " ").replace("\n", " ").strip()
|
|
|
|
|
|
def html_to_text(html: str) -> str:
|
|
"""HTML -> text. Zkusi bs4 (je v projektu), jinak hrubsi fallback."""
|
|
try:
|
|
from bs4 import BeautifulSoup
|
|
try:
|
|
soup = BeautifulSoup(html, "lxml")
|
|
except Exception:
|
|
soup = BeautifulSoup(html, "html.parser")
|
|
for t in soup(["script", "style", "head"]):
|
|
t.decompose()
|
|
text = soup.get_text(separator="\n")
|
|
except Exception:
|
|
import re
|
|
text = re.sub(r"<[^>]+>", "", html)
|
|
lines = [ln.strip() for ln in text.splitlines()]
|
|
return "\n".join(ln for ln in lines if ln)
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Precist jednu zpravu z MailStore")
|
|
ap.add_argument("folder", help="Plna cesta slozky")
|
|
ap.add_argument("number", help="Poradove cislo zpravy (nebo UID s --uid)")
|
|
ap.add_argument("--uid", action="store_true", help="Cislo je IMAP UID, ne poradi")
|
|
ap.add_argument("--save", metavar="DIR", help="Ulozit prilohy do adresare")
|
|
ap.add_argument("--raw", action="store_true", help="Vypsat cely syrovy EML a skoncit")
|
|
args = ap.parse_args()
|
|
|
|
M = connect()
|
|
typ, data = M.select(f'"{encode_mutf7(args.folder)}"', readonly=True)
|
|
if typ != "OK":
|
|
print(f"Slozku nelze otevrit: {data}", file=sys.stderr)
|
|
return 1
|
|
|
|
# FETCH cele zpravy (RFC822). UID FETCH kdyz --uid.
|
|
if args.uid:
|
|
typ, msg_data = M.uid("FETCH", args.number, "(RFC822)")
|
|
else:
|
|
typ, msg_data = M.fetch(args.number, "(RFC822)")
|
|
if typ != "OK" or not msg_data or not isinstance(msg_data[0], tuple):
|
|
print(f"Zpravu #{args.number} nelze nacist (typ={typ})", file=sys.stderr)
|
|
M.logout()
|
|
return 1
|
|
|
|
raw = msg_data[0][1]
|
|
M.logout()
|
|
|
|
if args.raw:
|
|
sys.stdout.buffer.write(raw)
|
|
return 0
|
|
|
|
msg = email.message_from_bytes(raw)
|
|
|
|
# --- hlavicky ---
|
|
print("=" * 80)
|
|
print(f"Slozka : {args.folder}")
|
|
print(f"{'UID' if args.uid else 'Cislo'} : {args.number}")
|
|
print("-" * 80)
|
|
print(f"Datum : {msg.get('Date')}")
|
|
print(f"Od : {dec(msg.get('From'))}")
|
|
print(f"Komu : {dec(msg.get('To'))}")
|
|
if msg.get("Cc"):
|
|
print(f"Kopie : {dec(msg.get('Cc'))}")
|
|
print(f"Predmet : {dec(msg.get('Subject'))}")
|
|
print(f"Msg-ID : {msg.get('Message-ID')}")
|
|
print(f"EML velikost: {len(raw):,} bytu")
|
|
|
|
# --- telo + prilohy ---
|
|
body_text = body_html = ""
|
|
attachments = [] # (filename, size, payload)
|
|
for part in msg.walk():
|
|
if part.is_multipart():
|
|
continue
|
|
ct = part.get_content_type()
|
|
disp = str(part.get("Content-Disposition") or "")
|
|
payload = part.get_payload(decode=True)
|
|
if "attachment" in disp or (part.get_filename() and ct not in ("text/plain", "text/html")):
|
|
attachments.append((dec(part.get_filename()) or "(bez nazvu)",
|
|
len(payload or b""), payload or b""))
|
|
elif ct == "text/plain" and not body_text:
|
|
body_text = (payload or b"").decode(part.get_content_charset() or "utf-8", errors="replace")
|
|
elif ct == "text/html" and not body_html:
|
|
body_html = (payload or b"").decode(part.get_content_charset() or "utf-8", errors="replace")
|
|
|
|
print("-" * 80)
|
|
if attachments:
|
|
print(f"Prilohy ({len(attachments)}):")
|
|
for name, size, _ in attachments:
|
|
print(f" - {name} ({size:,} B)")
|
|
else:
|
|
print("Prilohy: zadne")
|
|
|
|
# telo: preferuj plain, jinak html->text
|
|
text = body_text or (html_to_text(body_html) if body_html else "")
|
|
src = "text/plain" if body_text else ("text/html->text" if body_html else "(zadne)")
|
|
print("-" * 80)
|
|
print(f"TELO ({src}, {len(text):,} znaku):")
|
|
print("-" * 80)
|
|
if text:
|
|
print(text[:BODY_PREVIEW_CHARS])
|
|
if len(text) > BODY_PREVIEW_CHARS:
|
|
print(f"\n... [zkraceno, celkem {len(text):,} znaku] ...")
|
|
else:
|
|
print("(prazdne telo)")
|
|
|
|
# --- ulozeni priloh ---
|
|
if args.save and attachments:
|
|
os.makedirs(args.save, exist_ok=True)
|
|
print("-" * 80)
|
|
for name, size, payload in attachments:
|
|
safe = name.replace("/", "_").replace("\\", "_") or "att.bin"
|
|
path = os.path.join(args.save, safe)
|
|
with open(path, "wb") as f:
|
|
f.write(payload)
|
|
print(f"Ulozeno: {path} ({size:,} B)")
|
|
|
|
print("=" * 80)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except KeyboardInterrupt:
|
|
print("\nPreruseno", file=sys.stderr)
|
|
sys.exit(1)
|