Guest User

Untitled

a guest
Nov 14th, 2025
47
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 9.72 KB | Source Code | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. mail_url_worker.py
  4. Запускается постоянно (или как systemd service). Каждые 5 секунд проверяет IMAP-ящик
  5. на письма с темой "url", парсит URL из текста, скачивает указанной утилитой,
  6. упаковывает в ZIP и отправляет обратно отправителю.
  7. """
  8.  
  9. import imaplib
  10. import email
  11. from email.header import decode_header, make_header
  12. from email.message import EmailMessage
  13. import smtplib
  14. import subprocess
  15. import tempfile
  16. import zipfile
  17. import os
  18. import time
  19. import re
  20. import logging
  21. from typing import List
  22. from pathlib import Path
  23.  
  24. # --- Константы для настройки ---
  25. IMAP_HOST = "imap.example.com"
  26. IMAP_PORT = 993
  27. IMAP_USER = "[email protected]"
  28. IMAP_PASS = "your_imap_password"
  29.  
  30. SMTP_HOST = "smtp.example.com"
  31. SMTP_PORT = 587
  32. SMTP_USER = "[email protected]"
  33. SMTP_PASS = "your_smtp_password"
  34.  
  35. MAILBOX = "INBOX"
  36. CHECK_INTERVAL = 5  # секунд
  37.  
  38. # Список доступных внешних утилит и их команд (шаблоны).
  39. # Шаблон должен содержать {url} и {outdir}
  40. DOWNLOADERS = {
  41.     # "key": ["command", "arg1", ...] - будет выполнено через subprocess, cwd=outdir
  42.     "single-page-with-images": ["wget", "--page-requisites", "--convert-links", "--no-parent", "--adjust-extension", "-E", "{url}"],
  43.     "recursive-depth-1": ["wget", "-r", "-l", "1", "--no-parent", "--adjust-extension", "-E", "{url}"],
  44.     # пример: можно добавить "httrack": ["httrack", "{url}", "-O", "{outdir}"]
  45. }
  46.  
  47. # По умолчанию использовать один из ключей DOWNLOADERS, если не указан в тексте письма
  48. DEFAULT_DOWNLOADER = "single-page-with-images"
  49.  
  50. # Регулярка для парсинга URL из текста (простая, достаточно для обычных случаев)
  51. URL_RE = re.compile(r"https?://[^\s<>\"']+")
  52.  
  53. # Логирование
  54. logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
  55.  
  56.  
  57. # --- Помощные функции ---
  58. def imap_connect():
  59.     logging.info("Connecting to IMAP %s:%s", IMAP_HOST, IMAP_PORT)
  60.     M = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT)
  61.     M.login(IMAP_USER, IMAP_PASS)
  62.     return M
  63.  
  64.  
  65. def smtp_send_with_attachment(to_addr: str, subject: str, body: str, attachment_path: str):
  66.     msg = EmailMessage()
  67.     msg["From"] = SMTP_USER
  68.     msg["To"] = to_addr
  69.     msg["Subject"] = subject
  70.     msg.set_content(body)
  71.  
  72.     with open(attachment_path, "rb") as f:
  73.         data = f.read()
  74.         maintype = "application"
  75.         subtype = "zip"
  76.         msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=os.path.basename(attachment_path))
  77.  
  78.     logging.info("Connecting to SMTP %s:%s", SMTP_HOST, SMTP_PORT)
  79.     with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as s:
  80.         s.starttls()
  81.         s.login(SMTP_USER, SMTP_PASS)
  82.         s.send_message(msg)
  83.     logging.info("Sent email with attachment to %s", to_addr)
  84.  
  85.  
  86. def parse_subject(msg) -> str:
  87.     raw = msg.get("Subject", "")
  88.     try:
  89.         return str(make_header(decode_header(raw)))
  90.     except Exception:
  91.         return raw
  92.  
  93.  
  94. def extract_text_from_email(msg) -> str:
  95.     # Возвращает объединённый текст всех text/plain и text/html (html очищается)
  96.     parts = []
  97.     if msg.is_multipart():
  98.         for part in msg.walk():
  99.             ctype = part.get_content_type()
  100.             if ctype == "text/plain":
  101.                 try:
  102.                     parts.append(part.get_payload(decode=True).decode(part.get_content_charset() or "utf-8", errors="ignore"))
  103.                 except Exception:
  104.                     pass
  105.             elif ctype == "text/html" and not parts:
  106.                 # если нет plain — извлечь из html (простая очистка)
  107.                 try:
  108.                     html = part.get_payload(decode=True).decode(part.get_content_charset() or "utf-8", errors="ignore")
  109.                     text = re.sub(r"<[^>]+>", " ", html)
  110.                     parts.append(text)
  111.                 except Exception:
  112.                     pass
  113.     else:
  114.         ctype = msg.get_content_type()
  115.         payload = msg.get_payload(decode=True)
  116.         if payload:
  117.             try:
  118.                 text = payload.decode(msg.get_content_charset() or "utf-8", errors="ignore")
  119.             except Exception:
  120.                 text = str(payload)
  121.             if ctype == "text/html":
  122.                 text = re.sub(r"<[^>]+>", " ", text)
  123.             parts.append(text)
  124.     return "\n".join(parts)
  125.  
  126.  
  127. def find_urls(text: str) -> List[str]:
  128.     return list(dict.fromkeys(URL_RE.findall(text)))  # сохранить порядок и удалить дубликаты
  129.  
  130.  
  131. def choose_downloader_from_text(text: str) -> str:
  132.     # Ищем строку вида: downloader: ключ
  133.     m = re.search(r"downloader\s*[:=]\s*([A-Za-z0-9_\-]+)", text, flags=re.I)
  134.     if m:
  135.         key = m.group(1)
  136.         if key in DOWNLOADERS:
  137.             return key
  138.         else:
  139.             logging.warning("Requested downloader %s not in DOWNLOADERS, using default", key)
  140.     return DEFAULT_DOWNLOADER
  141.  
  142.  
  143. def run_downloader_for_url(downloader_key: str, url: str, outdir: str) -> int:
  144.     template = DOWNLOADERS[downloader_key]
  145.     cmd = [part.format(url=url, outdir=outdir) for part in template]
  146.     logging.info("Running downloader: %s", " ".join(cmd))
  147.     # Запустить процесс. stdout/stderr будут в лог, вернуть код возврата.
  148.     try:
  149.         res = subprocess.run(cmd, cwd=outdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=300)
  150.         logging.debug("stdout: %s", res.stdout.decode(errors="ignore"))
  151.         logging.debug("stderr: %s", res.stderr.decode(errors="ignore"))
  152.         return res.returncode
  153.     except Exception as e:
  154.         logging.exception("Downloader failed: %s", e)
  155.         return -1
  156.  
  157.  
  158. def zip_directory(src_dir: str, zip_path: str):
  159.     with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
  160.         for root, dirs, files in os.walk(src_dir):
  161.             for f in files:
  162.                 full = os.path.join(root, f)
  163.                 arcname = os.path.relpath(full, src_dir)
  164.                 zf.write(full, arcname)
  165.     logging.info("Created zip %s", zip_path)
  166.  
  167.  
  168. # --- Основной цикл обработки писем ---
  169. def process_email_item(M, num):
  170.     # Получить письмо
  171.     res, data = M.fetch(num, "(RFC822)")
  172.     if res != "OK":
  173.         logging.error("Failed fetch %s: %s", num, res)
  174.         return
  175.     raw = data[0][1]
  176.     msg = email.message_from_bytes(raw)
  177.     subject = parse_subject(msg)
  178.     frm = email.utils.parseaddr(msg.get("From"))[1] or IMAP_USER
  179.     logging.info("Processing mail %s from %s subject=%s", num, frm, subject)
  180.  
  181.     body_text = extract_text_from_email(msg)
  182.     downloader_key = choose_downloader_from_text(body_text)
  183.     urls = find_urls(body_text)
  184.     if not urls:
  185.         logging.info("No URLs found in message %s — marking seen", num)
  186.         M.store(num, "+FLAGS", "\\Seen")
  187.         return
  188.  
  189.     # Создать временную директорию для скачивания
  190.     with tempfile.TemporaryDirectory(prefix="maildl_") as tmpdir:
  191.         # Для каждого URL — создать subdir и запустить скачивание
  192.         for i, url in enumerate(urls, start=1):
  193.             sub = os.path.join(tmpdir, f"{i}")
  194.             os.makedirs(sub, exist_ok=True)
  195.             rc = run_downloader_for_url(downloader_key, url, sub)
  196.             logging.info("Downloader returned %s for %s", rc, url)
  197.  
  198.         # Запаковать содержимое tmpdir в zip
  199.         zip_path = os.path.join(tempfile.gettempdir(), f"download_{int(time.time())}.zip")
  200.         zip_directory(tmpdir, zip_path)
  201.  
  202.         # Отправить ZIP обратно отправителю
  203.         subject_reply = f"Re: {subject} (downloaded {len(urls)} URLs)"
  204.         body_reply = f"Downloaded {len(urls)} URLs using downloader '{downloader_key}'.\nOriginal subject: {subject}\n"
  205.         try:
  206.             smtp_send_with_attachment(frm, subject_reply, body_reply, zip_path)
  207.         except Exception:
  208.             logging.exception("Failed to send reply")
  209.  
  210.         # удалить zip
  211.         try:
  212.             os.remove(zip_path)
  213.         except Exception:
  214.             pass
  215.  
  216.     # пометить письмо прочитанным и переместить в папку (опционально)
  217.     M.store(num, "+FLAGS", "\\Seen")
  218.     # при желании: M.copy(num, "Processed"); M.store(num, "+FLAGS", "\\Deleted")
  219.     logging.info("Finished processing mail %s", num)
  220.  
  221.  
  222. def main_loop():
  223.     while True:
  224.         try:
  225.             M = imap_connect()
  226.             M.select(MAILBOX)
  227.             # Поиск непрочитанных писем с темой "url" (регистронезависимо)
  228.             typ, data = M.search(None, '(UNSEEN SUBJECT "url")')
  229.             if typ != "OK":
  230.                 logging.error("Search failed: %s", typ)
  231.             else:
  232.                 nums = data[0].split()
  233.                 logging.info("Found %d new 'url' messages", len(nums))
  234.                 for num in nums:
  235.                     try:
  236.                         process_email_item(M, num)
  237.                     except Exception:
  238.                         logging.exception("Error processing mail %s", num)
  239.             M.logout()
  240.         except Exception:
  241.             logging.exception("Main loop exception")
  242.         time.sleep(CHECK_INTERVAL)
  243.  
  244.  
  245. if __name__ == "__main__":
  246.     main_loop()
  247.  
Advertisement
Add Comment
Please, Sign In to add comment