Add Outlook/Soubory/Clario/Feasibility scripts and reports; ignore Incoming, Outlook downloads & profile
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
=======================================================================
|
||||
Název: download_all_inbox_eml_v1.0.py
|
||||
Verze: 1.0
|
||||
Datum: 2026-06-03
|
||||
Popis: Stáhne zprávy z Outlook Inboxu jako .eml. Virtualizovaný seznam
|
||||
řeší navigací klávesnicí (ArrowDown) — Outlook sám scrolluje
|
||||
a dorenderovává. Postup:
|
||||
1. vybrat první zprávu
|
||||
2. stáhnout vybranou (pravý klik → Download → Download as EML)
|
||||
3. ArrowDown na další
|
||||
4. opakovat, dokud se výběr (aria-selected) přestane hýbat
|
||||
|
||||
Používá persistent profil z outlook_login_v1.0.py.
|
||||
|
||||
Nastavení:
|
||||
LIMIT – max počet zpráv (None = celý Inbox)
|
||||
SKIP_EXISTING – přeskočit zprávy, jejichž EML už v downloads/ existuje
|
||||
=======================================================================
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
PROFILE_DIR = BASE_DIR / "outlook_profile"
|
||||
OUT_DIR = BASE_DIR / "downloads"
|
||||
START_URL = "https://outlook.cloud.microsoft/mail/"
|
||||
|
||||
LIMIT = 30 # max počet zpráv; None = celý Inbox
|
||||
SKIP_EXISTING = False # False = existující stejný soubor přepsat (smazat + uložit nový)
|
||||
|
||||
|
||||
def safe_name(name: str) -> str:
|
||||
"""Očistí název pro filesystem (Windows)."""
|
||||
name = re.sub(r'[<>:"/\\|?*\r\n\t]', "_", name).strip().strip(".")
|
||||
return name[:150] or "message"
|
||||
|
||||
|
||||
def download_selected(page, out_dir: Path) -> Path | None:
|
||||
"""Pravý klik na vybranou zprávu → Download as EML. Vrátí cestu nebo None."""
|
||||
selected = page.locator('[role="option"][aria-selected="true"]').first
|
||||
if selected.count() == 0:
|
||||
return None
|
||||
|
||||
selected.click(button="right")
|
||||
page.wait_for_timeout(600)
|
||||
|
||||
# Download (rodič submenu)
|
||||
download_parent = None
|
||||
for name in ("Download", "Stáhnout"):
|
||||
loc = page.get_by_role("menuitem", name=name).first
|
||||
if loc.count() and loc.is_visible():
|
||||
download_parent = loc
|
||||
break
|
||||
if download_parent is None:
|
||||
page.keyboard.press("Escape")
|
||||
return None
|
||||
|
||||
download_parent.hover()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# Download as EML (submenu); fallback = klik přímo na Download
|
||||
eml_item = None
|
||||
for name in ("Download as EML", "Stáhnout jako EML", "Stáhnout jako .eml"):
|
||||
loc = page.get_by_role("menuitem", name=name).first
|
||||
if loc.count() and loc.is_visible():
|
||||
eml_item = loc
|
||||
break
|
||||
|
||||
try:
|
||||
if eml_item is not None:
|
||||
with page.expect_download(timeout=15_000) as dl:
|
||||
eml_item.click()
|
||||
else:
|
||||
with page.expect_download(timeout=15_000) as dl:
|
||||
download_parent.click()
|
||||
download = dl.value
|
||||
except Exception:
|
||||
page.keyboard.press("Escape")
|
||||
return None
|
||||
|
||||
fname = safe_name(download.suggested_filename or "message.eml")
|
||||
if not fname.lower().endswith(".eml"):
|
||||
fname += ".eml"
|
||||
target = out_dir / fname
|
||||
|
||||
if target.exists():
|
||||
if SKIP_EXISTING:
|
||||
return target # už máme — neukládat znovu
|
||||
target.unlink() # přepsat: smazat starou verzi a uložit novou
|
||||
|
||||
download.save_as(str(target))
|
||||
return target
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not PROFILE_DIR.exists():
|
||||
print(f" Profil nenalezen: {PROFILE_DIR}")
|
||||
print(" Nejprve spusť outlook_login_v1.0.py.")
|
||||
return
|
||||
OUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
with sync_playwright() as p:
|
||||
context = p.chromium.launch_persistent_context(
|
||||
user_data_dir=str(PROFILE_DIR),
|
||||
headless=False,
|
||||
no_viewport=True,
|
||||
accept_downloads=True,
|
||||
args=[
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"--start-maximized",
|
||||
],
|
||||
)
|
||||
page = context.pages[0] if context.pages else context.new_page()
|
||||
|
||||
# 1) Otevřít Outlook
|
||||
print(" 1/4 Otevírám Outlook...")
|
||||
page.goto(START_URL)
|
||||
page.wait_for_load_state("domcontentloaded")
|
||||
search_selector = (
|
||||
'[placeholder*="Search"], [aria-label*="Search"], '
|
||||
'[placeholder*="Hledat"], [aria-label*="Hledat"]'
|
||||
)
|
||||
page.wait_for_selector(search_selector, timeout=30_000)
|
||||
|
||||
# 2) Inbox / Doručená pošta
|
||||
print(" 2/4 Otevírám Inbox...")
|
||||
inbox_candidates = [
|
||||
'div[role="treeitem"]:has-text("Inbox")',
|
||||
'div[role="treeitem"]:has-text("Doručená pošta")',
|
||||
'text=Inbox',
|
||||
'text=Doručená pošta',
|
||||
]
|
||||
for sel in inbox_candidates:
|
||||
loc = page.locator(sel).first
|
||||
if loc.count() and loc.is_visible():
|
||||
loc.click()
|
||||
break
|
||||
page.wait_for_selector('div[role="option"]', timeout=15_000)
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
# 3) Vybrat první zprávu
|
||||
print(" 3/4 Vybírám první zprávu...")
|
||||
page.locator('div[role="option"]').first.click()
|
||||
page.wait_for_timeout(800)
|
||||
|
||||
# 4) Smyčka: stáhni vybranou → ArrowDown → dokud se výběr hýbe
|
||||
# Pozn.: oddělovače sekcí (Today/Yesterday/...) jsou role="button"
|
||||
# aria-expanded — kurzor na nich ZASTAVÍ a žádná zpráva nemá
|
||||
# aria-selected (selected.count()==0). Takový krok jen přeskočíme
|
||||
# (ArrowDown dál), NEpočítáme ho a NEukončujeme smyčku.
|
||||
print(" 4/4 Stahuji zprávy...\n")
|
||||
saved = 0
|
||||
dividers = 0
|
||||
failed = 0
|
||||
prev_label = None
|
||||
no_progress = 0 # kolikrát po sobě se výběr neposunul
|
||||
NO_PROGRESS_MAX = 4 # tolik = konec seznamu / zaseknutí
|
||||
|
||||
while LIMIT is None or saved < LIMIT:
|
||||
selected = page.locator('[role="option"][aria-selected="true"]').first
|
||||
|
||||
# (a) stojíme na oddělovači sekce → krok přes něj
|
||||
if selected.count() == 0:
|
||||
dividers += 1
|
||||
no_progress += 1
|
||||
if no_progress >= NO_PROGRESS_MAX:
|
||||
print(" Konec seznamu / zaseknutí — končím.")
|
||||
break
|
||||
page.keyboard.press("ArrowDown")
|
||||
page.wait_for_timeout(250)
|
||||
continue
|
||||
|
||||
label = selected.get_attribute("aria-label") or ""
|
||||
|
||||
# (b) výběr se neposunul (konec seznamu)
|
||||
if label == prev_label:
|
||||
no_progress += 1
|
||||
if no_progress >= NO_PROGRESS_MAX:
|
||||
print(" Konec seznamu (výběr se nehýbe).")
|
||||
break
|
||||
page.keyboard.press("ArrowDown")
|
||||
page.wait_for_timeout(250)
|
||||
continue
|
||||
|
||||
# (c) nová zpráva → stáhni
|
||||
no_progress = 0
|
||||
prev_label = label
|
||||
target = download_selected(page, OUT_DIR)
|
||||
|
||||
if target is None:
|
||||
failed += 1
|
||||
print(f" [!] selhalo: {label[:70]}")
|
||||
else:
|
||||
saved += 1
|
||||
print(f" [{saved:>4}] {target.name}")
|
||||
|
||||
# refokus seznamu (klik na zprávu, ne na oddělovač) + posun dál
|
||||
try:
|
||||
selected.click()
|
||||
except Exception:
|
||||
pass
|
||||
page.wait_for_timeout(200)
|
||||
page.keyboard.press("ArrowDown")
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
print(f"\n Hotovo. Uloženo {saved}, oddělovačů přeskočeno {dividers}, "
|
||||
f"selhalo {failed} → {OUT_DIR}")
|
||||
input(" Stiskni Enter pro zavření okna... ")
|
||||
context.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user