217 lines
7.6 KiB
Python
217 lines
7.6 KiB
Python
"""
|
||
=======================================================================
|
||
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()
|