Files
janssen/mailstore/mailstore_read_v1.0.py
2026-06-11 21:49:04 +02:00

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)