z230
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user