notebook
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user