Files
janssen/EmailsImport/mailbox_mirror_v1.0.py
2026-06-08 07:20:37 +02:00

200 lines
7.0 KiB
Python

"""
mailbox_mirror v1.0 | 2026-06-08 | vladimir.buzalka
Zrcadlí primární JNJ schránku (BEZ Online Archive) za posledních 30 dní do
osobní schránky vladimir.buzalka@buzalka.cz.
Princip — bezestavový diff přes Internet Message-ID:
1. Projdi Inbox(+podsložky), Sent, Deleted; vyber emaily z posledních 30 dní.
Sestav manifest = [{message_id, folder, is_read}] (jen metadata, žádná těla).
2. POST /mirror-plan → server porovná manifest se stavem schránky:
- smaže ze schránky zprávy které v manifestu nejsou (smazané v JNJ)
- vrátí to_add = message_id které ve schránce chybí
3. Pro každé to_add: ulož .msg, zašifruj (Fernet → .emsg), POST /upload.
Žádná SQLite, žádný graph_id bookkeeping — zdrojem pravdy jsou obě schránky.
Mazání běží jen v rámci 30denního okna, starší archiv zůstává nedotčen.
Omezení JNJ:
- Zscaler DLP → soubory se posílají šifrované (.emsg)
- Online Archive vynechán (GetDefaultFolder vrací jen primární schránku)
Spouštění: opakovaně (Task Scheduler). Bezpečně opakovatelné a idempotentní.
Závislosti: pywin32, requests, cryptography. Outlook musí běžet.
"""
import sys
import base64
import hashlib
import tempfile
from pathlib import Path
from datetime import datetime, timedelta, timezone
import win32com.client
import requests
import urllib3
from cryptography.fernet import Fernet
sys.stdout.reconfigure(encoding="utf-8")
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
TOKEN = "13e1bb01-9fd5-44a8-8ce9-4ee27133d340"
BASE_URL = "https://msgs.buzalka.cz"
PLAN_URL = f"{BASE_URL}/mirror-plan"
UPLOAD_URL = f"{BASE_URL}/upload"
WINDOW_DAYS = 30
PR_INTERNET_MESSAGE_ID = "http://schemas.microsoft.com/mapi/proptag/0x1035001E"
# olFolderInbox=6, olFolderSentMail=5, olFolderDeletedItems=3
FOLDERS_TO_MIRROR = [6, 5, 3]
# Šifrovací klíč odvozený z TOKENu (stejný algoritmus jako server)
_FERNET = Fernet(base64.urlsafe_b64encode(hashlib.sha256(TOKEN.encode()).digest()))
def get_mid(item) -> str:
try:
mid = item.PropertyAccessor.GetProperty(PR_INTERNET_MESSAGE_ID)
except Exception:
mid = None
return mid or f"entryid:{item.EntryID}"
def collect_manifest(ns, cutoff_local):
"""Projdi cílové složky + podsložky, vrať (manifest, index).
manifest = [{message_id, folder, is_read}]
index = {message_id: (entry_id, folder_path)} — pro fázi uploadu
"""
restrict = (
"@SQL=\"urn:schemas:httpmail:datereceived\" >= '%s'"
% cutoff_local.strftime("%Y/%m/%d %H:%M:%S")
)
manifest = []
index = {}
def walk(folder, folder_path):
current = f"{folder_path}/{folder.Name}"
try:
items = folder.Items.Restrict(restrict)
items.Sort("[ReceivedTime]", False)
n = 0
for item in items:
try:
if not item.MessageClass.upper().startswith("IPM.NOTE"):
continue
mid = get_mid(item)
manifest.append({
"message_id": mid,
"folder": current,
"is_read": (not item.UnRead),
})
index[mid] = (item.EntryID, current)
n += 1
except Exception as e:
print(f" chyba položky v {current}: {e}")
print(f" {current}: {n}")
except Exception as e:
print(f" CHYBA složka {current}: {e}")
return # nedostupná složka → nelez do podsložek
try:
subfolders = list(folder.Folders)
except Exception:
subfolders = []
for sub in subfolders:
walk(sub, current)
seen_roots = set()
for fid in FOLDERS_TO_MIRROR:
root = ns.GetDefaultFolder(fid)
mailbox = root.Parent.Name
key = (mailbox, root.Name)
if key in seen_roots:
continue
seen_roots.add(key)
walk(root, f"/{mailbox}")
return manifest, index
def upload_one(ns, entry_id, folder):
"""Ulož email jako .msg, zašifruj a nahraj na /upload (server naimportuje)."""
item = ns.GetItemFromID(entry_id)
with tempfile.TemporaryDirectory() as tmp:
safe_name = f"{entry_id[-20:]}.msg"
tmp_path = Path(tmp) / safe_name
item.SaveAs(str(tmp_path), 3) # 3 = olMSG
with open(tmp_path, "rb") as f:
encrypted = _FERNET.encrypt(f.read())
enc_name = safe_name[:-4] + ".emsg"
resp = requests.post(
UPLOAD_URL,
headers={"Authorization": f"Bearer {TOKEN}"},
files={"file": (enc_name, encrypted, "application/octet-stream")},
data={"folder": folder},
timeout=60,
)
resp.raise_for_status()
return resp.json()
def main():
print(f"=== mailbox_mirror v1.0 ===")
print(f"Start: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
cutoff_utc = datetime.now(timezone.utc) - timedelta(days=WINDOW_DAYS)
cutoff_graph = cutoff_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
cutoff_local = cutoff_utc.astimezone()
print(f"Okno: posledních {WINDOW_DAYS} dní (cutoff {cutoff_graph})\n")
outlook = win32com.client.Dispatch("Outlook.Application")
ns = outlook.GetNamespace("MAPI")
print("1) Sestavuji manifest z JNJ schránky...")
manifest, index = collect_manifest(ns, cutoff_local)
print(f"{len(manifest)} emailů v okně\n")
print("2) Posílám plán na server (diff + mazání přebytků)...")
resp = requests.post(
PLAN_URL,
headers={"Authorization": f"Bearer {TOKEN}"},
json={"manifest": manifest, "cutoff": cutoff_graph},
timeout=300,
)
resp.raise_for_status()
plan = resp.json()
to_add = plan.get("to_add", [])
print(f" schránka={plan.get('mailbox_count')} | manifest={plan.get('manifest_count')}")
print(f" smazáno ze schránky: {plan.get('deleted')}")
print(f" k nahrání: {len(to_add)}\n")
if not to_add:
print("Schránka je v synchronu, nic nenahrávám.")
else:
print("3) Nahrávám chybějící emaily...")
uploaded = 0
errors = 0
for i, mid in enumerate(to_add, 1):
entry_id, folder = index.get(mid, (None, None))
if not entry_id:
print(f" [{i}/{len(to_add)}] chybí index pro {mid[:40]} — přeskočeno")
errors += 1
continue
try:
upload_one(ns, entry_id, folder)
uploaded += 1
if uploaded % 50 == 0:
print(f" [{datetime.now().strftime('%H:%M:%S')}] "
f"nahráno {uploaded}/{len(to_add)}")
except Exception as e:
print(f" CHYBA upload {mid[:40]}: {e}")
errors += 1
print(f"\n nahráno {uploaded} | chyby {errors}")
print(f"\n=== Hotovo === {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if __name__ == "__main__":
main()