Files
janssen/EmailsImport/jnj_unsent_probe_v1.0.py
2026-06-16 18:03:41 +02:00

273 lines
9.1 KiB
Python

"""
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()