273 lines
9.1 KiB
Python
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()
|