177 lines
5.6 KiB
Python
177 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
================================================================================
|
|
Nazev: mailstore_folder_v1.0.py
|
|
Verze: 1.0
|
|
Datum: 2026-06-11
|
|
Autor: Vladimir Buzalka (asistovano Claude)
|
|
Popis: Vypise obsah jedne MailStore slozky jako seznam zprav
|
|
(datum | od | predmet) pres davkovy IMAP FETCH hlavicek.
|
|
Predstupen ingestu - overuje davkove cteni hlavicek.
|
|
|
|
Argument = plna cesta slozky (fullName z mapy), napr.:
|
|
"vladimir.buzalka@buzalka.cz/Exchange vladimir.buzalka/Sent Items"
|
|
|
|
Zdroj: MailStore IMAP server, port 143, STARTTLS, auth Prosty text (LOGIN).
|
|
IMAP FETCH BODY.PEEK[HEADER.FIELDS (...)] = hlavicky bez oznaceni
|
|
jako precteno. Davkove jednim prikazem, ne po jedne zprave.
|
|
|
|
Spusteni:
|
|
python mailstore_folder_v1.0.py "...slozka..." # poslednich 50
|
|
python mailstore_folder_v1.0.py "...slozka..." --limit 200
|
|
python mailstore_folder_v1.0.py "...slozka..." --all # vse (pozor velke slozky)
|
|
python mailstore_folder_v1.0.py "...slozka..." --oldest # od nejstarsich
|
|
================================================================================
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import email
|
|
import imaplib
|
|
import re
|
|
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!%"
|
|
|
|
DEFAULT_LIMIT = 50
|
|
|
|
|
|
# --- helpery ----------------------------------------------------------------
|
|
|
|
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
|
|
(MailStore neumi UTF8=ACCEPT). Vysledek je cisty ASCII."""
|
|
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:
|
|
"""Dekoduje MIME-encoded hlavicku (=?utf-8?...?=) na citelny text."""
|
|
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 fmt_date(raw: str | None) -> str:
|
|
if not raw:
|
|
return "?"
|
|
try:
|
|
dt = parsedate_to_datetime(raw)
|
|
return dt.strftime("%Y-%m-%d %H:%M")
|
|
except Exception:
|
|
return (raw or "")[:16]
|
|
|
|
|
|
def short(s: str, n: int) -> str:
|
|
s = s or ""
|
|
return s if len(s) <= n else s[: n - 1] + "…"
|
|
|
|
|
|
# IMAP FETCH header bloky prijdou jako tuple (b'N (BODY[...] {len}', b'<headers>')
|
|
_NUM_RX = re.compile(rb"^(\d+)\s")
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Vypis obsahu MailStore slozky")
|
|
ap.add_argument("folder", help="Plna cesta slozky (fullName z mapy)")
|
|
ap.add_argument("--limit", type=int, default=DEFAULT_LIMIT,
|
|
help=f"Pocet zprav (default {DEFAULT_LIMIT})")
|
|
ap.add_argument("--all", action="store_true", help="Vsechny zpravy (ignoruje --limit)")
|
|
ap.add_argument("--oldest", action="store_true",
|
|
help="Od nejstarsich (default: od nejnovejsich)")
|
|
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
|
|
total = int(data[0]) if data and data[0] else 0
|
|
print(f"Slozka: {args.folder}")
|
|
print(f"Zprav celkem: {total:,}")
|
|
if total == 0:
|
|
M.logout()
|
|
return 0
|
|
|
|
# urci rozsah porad. cisel (1 = nejstarsi, total = nejnovejsi)
|
|
if args.all:
|
|
lo, hi = 1, total
|
|
else:
|
|
n = min(args.limit, total)
|
|
lo, hi = (1, n) if args.oldest else (total - n + 1, total)
|
|
rng = f"{lo}:{hi}"
|
|
shown = hi - lo + 1
|
|
order = "nejstarsi" if args.oldest else "nejnovejsi"
|
|
print(f"Zobrazuji {shown} zprav ({order} prvni), rozsah #{rng}")
|
|
print("=" * 100)
|
|
|
|
# davkovy FETCH hlavicek
|
|
typ, msgs = M.fetch(rng, "(BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT)])")
|
|
rows = []
|
|
for item in msgs:
|
|
if not isinstance(item, tuple):
|
|
continue
|
|
meta, hdr_bytes = item[0], item[1]
|
|
m = _NUM_RX.match(meta or b"")
|
|
seqno = int(m.group(1)) if m else 0
|
|
hdr = email.message_from_bytes(hdr_bytes)
|
|
rows.append((seqno, fmt_date(hdr.get("Date")),
|
|
dec(hdr.get("From")), dec(hdr.get("Subject"))))
|
|
|
|
rows.sort(key=lambda r: r[0], reverse=not args.oldest)
|
|
|
|
print(f"{'#':>6} {'Datum':<16} {'Od':<32} Predmet")
|
|
print("-" * 100)
|
|
for seqno, d, frm, subj in rows:
|
|
print(f"{seqno:>6} {d:<16} {short(frm, 32):<32} {short(subj, 40)}")
|
|
|
|
M.logout()
|
|
print("=" * 100)
|
|
print(f"Vypsano {len(rows)} zprav.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except KeyboardInterrupt:
|
|
print("\nPreruseno", file=sys.stderr)
|
|
sys.exit(1)
|