""" jnj_unsent_probe v1.1 Nazev: jnj_unsent_probe_v1.0.py (verze 1.1.0 — bohatsi vypis) Verze: 1.1.0 Datum: 2026-06-16 Autor: vladimir.buzalka Bezi: JNJ stroj (Outlook MAPI), Python z Thonny. JEN CTE, nic nezapisuje/nenahrava. UCEL (diagnostika): Cte e-maily PRIMO z ziveho Outlooku (MAPI) a vypisuje "identifikatory neodeslani", ktere se pri exportu do .msg ztraci nebo nejsou spolehlive. Slouzi k OVERENI, ktery zivy priznak spolehlive oznaci NEODESLANY e-mail (napr. hustakova nabidka, kterou Exchange odmitl SendAsDenied). Pro kazdou nalezenou polozku vypise vedle sebe: - folder, subject, prijemce - item.Sent (object model bool — odeslano?) - PR_MESSAGE_FLAGS + dekodovane bity UNSENT / SUBMIT / READ - ma Internet Message-ID? (PR_0x1035) - ma PR_CLIENT_SUBMIT_TIME? (0x0039) - PR_LAST_VERB_EXECUTED (0x1081) - body_has_error (zive item.Body obsahuje SendAsDenied / could not be sent?) - pokud ano -> vypise i snippet chyby DULEZITE: tohle je SONDA. Z jejiho vystupu se rozhodne, ktery priznak je spolehlivy detektor, a teprve pak se z toho udela produkcni flagovani. Filtry (argumenty): --to SUBSTR jen polozky, jejichz prijemce obsahuje SUBSTR (napr. hustak) --subject SUBSTR jen polozky s SUBSTR v predmetu (napr. icotrokinra) --days N okno poslednich N dni dle ReceivedTime (default 90; 0 = vse) --all vypsat VSE (jinak jen "podezrele" = bez Internet Message-ID) --limit N max N vypsanych polozek (default 60) --folders LIST carkou oddelene: inbox,sent,drafts,deleted,outbox,archive (default vse uvedene) Priklady: python jnj_unsent_probe_v1.0.py --to hustak --all python jnj_unsent_probe_v1.0.py --subject icotrokinra --days 60 """ import argparse import sys from datetime import datetime, timedelta import win32com.client if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8", errors="replace") # MAPI proptagy PR_MESSAGE_FLAGS = "http://schemas.microsoft.com/mapi/proptag/0x0E070003" PR_INTERNET_MSG_ID = "http://schemas.microsoft.com/mapi/proptag/0x1035001E" PR_CLIENT_SUBMIT_TIME = "http://schemas.microsoft.com/mapi/proptag/0x00390040" PR_LAST_VERB = "http://schemas.microsoft.com/mapi/proptag/0x10810003" # MSGFLAG bity MSGFLAG_READ = 0x1 MSGFLAG_UNSENT = 0x8 MSGFLAG_SUBMIT = 0x4 # Default folder ID (OlDefaultFolders) DEFAULT_FOLDERS = { "inbox": 6, "sent": 5, "drafts": 16, "deleted": 3, "outbox": 4, } ERR_MARKERS = ("SendAsDenied", "could not be sent", "TransportSend", "MapiExceptionSendAs", "nemáte oprávnění", "on behalf of") def prop(item, tag, default=None): try: v = item.PropertyAccessor.GetProperty(tag) return v if v is not None else default except Exception: return default def get_to(item): try: return item.To or "" except Exception: return "" def body_error_snippet(item): """Zive telo (item.Body) — obsahuje stopu chyby odeslani?""" try: b = item.Body or "" except Exception: return None for m in ERR_MARKERS: i = b.find(m) if i >= 0: return b[max(0, i - 10):i + 90].replace("\r", " ").replace("\n", " ") return None def describe(item): subj = str(getattr(item, "Subject", "") or "")[:42] to = get_to(item)[:32] try: sent = bool(item.Sent) except Exception: sent = None flags = prop(item, PR_MESSAGE_FLAGS, 0) or 0 unsent = bool(flags & MSGFLAG_UNSENT) submit = bool(flags & MSGFLAG_SUBMIT) read = bool(flags & MSGFLAG_READ) mid = prop(item, PR_INTERNET_MSG_ID) if not mid: mid = prop(item, "http://schemas.microsoft.com/mapi/proptag/0x1035001F") # unicode varianta has_mid = bool(mid) submit_time = prop(item, PR_CLIENT_SUBMIT_TIME) last_verb = prop(item, PR_LAST_VERB) err = body_error_snippet(item) try: rdate = item.ReceivedTime.strftime("%Y-%m-%d %H:%M") if item.ReceivedTime else "?" except Exception: rdate = "?" try: eid = str(item.EntryID)[-20:] except Exception: eid = "?" return { "subject": subj, "to": to, "sent": sent, "flags": flags, "unsent": unsent, "submit": submit, "read": read, "has_mid": has_mid, "mid_val": (str(mid)[:60] if mid else "-"), "submit_time": bool(submit_time), "last_verb": last_verb, "err": err, "rdate": rdate, "eid": eid, } def matches(item, args): if args.to: if args.to.lower() not in get_to(item).lower(): try: # zkus i recipients rec = "; ".join(str(r.Address or r.Name or "") for r in item.Recipients) except Exception: rec = "" if args.to.lower() not in rec.lower(): return False if args.subject: if args.subject.lower() not in str(getattr(item, "Subject", "") or "").lower(): return False return True def walk(folder, path, args, cutoff, out, counters): cur = f"{path}/{folder.Name}" try: items = folder.Items try: items.Sort("[ReceivedTime]", True) except Exception: pass except Exception: return for item in items: if len(out) >= args.limit: return try: if not str(getattr(item, "MessageClass", "")).upper().startswith("IPM.NOTE"): continue except Exception: continue if cutoff is not None: try: rt = item.ReceivedTime if rt is not None and rt.replace(tzinfo=None) < cutoff: continue except Exception: pass if not matches(item, args): continue counters["seen"] += 1 d = describe(item) if (not args.all) and d["has_mid"]: continue # ma Message-ID -> neni podezrely (pokud neni --all) d["folder"] = cur out.append(d) try: subs = list(folder.Folders) except Exception: subs = [] for sub in subs: if len(out) >= args.limit: return walk(sub, cur, args, cutoff, out, counters) def find_archive(ns): try: root = ns.GetDefaultFolder(6).Parent for f in root.Folders: try: if str(f.Name).strip().lower() == "archive": return f, root.Name except Exception: continue except Exception: pass return None, None def main(): ap = argparse.ArgumentParser(description="jnj_unsent_probe v1.0 (diagnostika)") ap.add_argument("--to", default="") ap.add_argument("--subject", default="") ap.add_argument("--days", type=int, default=90) ap.add_argument("--all", action="store_true") ap.add_argument("--limit", type=int, default=60) ap.add_argument("--folders", default="inbox,sent,drafts,deleted,outbox,archive") args = ap.parse_args() cutoff = None if args.days == 0 else (datetime.now() - timedelta(days=args.days)) want = [x.strip().lower() for x in args.folders.split(",") if x.strip()] print(f"=== jnj_unsent_probe v1.0 ===") print(f"Filtr: to~'{args.to}' subject~'{args.subject}' okno={'vse' if cutoff is None else str(args.days)+'d'} " f"| {'VSE' if args.all else 'jen bez Message-ID'} | slozky={want}") outlook = win32com.client.Dispatch("Outlook.Application") ns = outlook.GetNamespace("MAPI") out = [] counters = {"seen": 0} for name in want: if len(out) >= args.limit: break if name == "archive": arch, mbox = find_archive(ns) if arch is not None: walk(arch, f"/{mbox}", args, cutoff, out, counters) else: print(" (Archive nenalezena)") continue fid = DEFAULT_FOLDERS.get(name) if not fid: continue try: root = ns.GetDefaultFolder(fid) except Exception as e: print(f" ({name} nedostupna: {e})") continue walk(root, f"/{root.Parent.Name}", args, cutoff, out, counters) print(f"\nProsmatrovano polozek: {counters['seen']} vypsano: {len(out)}\n") n_unsent = n_noid = n_err = 0 for i, d in enumerate(out, 1): if d["unsent"]: n_unsent += 1 if not d["has_mid"]: n_noid += 1 if d["err"]: n_err += 1 print(f"[{i}] {d['folder']} ({d['rdate']})") print(f" subject : {d['subject']}") print(f" to : {d['to']}") print(f" Sent={d['sent']} UNSENT={d['unsent']} SUBMIT={d['submit']} " f"has_MsgID={d['has_mid']} submit_time={d['submit_time']} ERR={'YES' if d['err'] else '-'}") print(f" MsgID : {d['mid_val']}") print(f" EntryID[-20:] (=jmeno .msg): {d['eid']}") if d["err"]: print(f" ERR : ...{d['err']}...") print() print(f"SOUHRN: vypsano={len(out)} UNSENT-flag={n_unsent} bez-MsgID={n_noid} s-chybou-v-tele={n_err}") if __name__ == "__main__": main()