diff --git a/src/model.py b/src/model.py index abc9687..519e92b 100644 --- a/src/model.py +++ b/src/model.py @@ -3,6 +3,8 @@ import email import os import re import json +import hashlib +from datetime import datetime CONFIG_FILE = "config.json" @@ -12,7 +14,7 @@ class MailArchiverModel: self.imap_port = 993 def load_config(self) -> dict: - """Загружает сохраненные настройки из JSON-файла.""" + """Загружает сохраненные настройки из JSON-файла при старте программы.""" if os.path.exists(CONFIG_FILE): try: with open(CONFIG_FILE, "r", encoding="utf-8") as f: @@ -22,7 +24,7 @@ class MailArchiverModel: return {"email": "", "password": "", "path": ""} def save_config(self, email_user: str, password_app: str, output_dir: str): - """Сохраняет настройки формы в JSON-файл.""" + """Сохраняет настройки формы в JSON-файл при нажатии кнопки 'ЗАПУСТИТЬ РЕЗЕРВНОЕ КОПИРОВАНИЕ'.""" data = { "email": email_user, "password": password_app, @@ -35,8 +37,9 @@ class MailArchiverModel: print(f"Ошибка сохранения конфига: {e}") def clean_filename(self, title: str) -> str: - """Очищает строку от запрещенных в Windows символов для имени файла.""" - return re.sub(r'[\\/*?:"<>|]', "", title)[:60] + """Очищает строку от запрещенных в Windows символов.""" + clean = re.sub(r'[\\/*?:"<>|]', "", title) + return clean.strip()[:50] def connect_and_fetch_ids(self, email_user: str, password_app: str, folder: str = "INBOX"): """Подключается к серверу и возвращает список ID писем.""" @@ -52,7 +55,10 @@ class MailArchiverModel: return mail, mail_ids def download_single_email(self, mail_instance, mail_id: bytes, output_dir: str) -> str: - """Скачивает одно письмо по его ID и сохраняет в формате .eml.""" + """ + Скачивает одно письмо по его ID и сохраняет в формате .eml. + Имя файла гарантирует 100% уникальность на основе даты и Message-ID. + """ status, msg_data = mail_instance.fetch(mail_id, "(RFC822)") if status != "OK": raise Exception(f"Ошибка при скачивании письма ID {mail_id.decode()}") @@ -62,9 +68,9 @@ class MailArchiverModel: raw_email = response_part[1] msg = email.message_from_bytes(raw_email) - raw_subject = msg.get("Subject", "NoSubject") + # 1. Извлекаем тему письма + raw_subject = msg.get("Subject", "Без темы") decoded_parts = email.header.decode_header(raw_subject) - subject_text = "" for part, encoding in decoded_parts: if isinstance(part, bytes): @@ -73,16 +79,37 @@ class MailArchiverModel: subject_text += str(part) clean_subject = self.clean_filename(subject_text) - mail_id_str = mail_id.decode() + if not clean_subject: + clean_subject = "Без_темы" + + # 2. Получаем дату отправки письма + date_tuple = email.utils.parsedate_tz(msg.get("Date")) + if date_tuple: + local_dt = datetime.fromtimestamp(email.utils.mktime_tz(date_tuple)) + date_prefix = local_dt.strftime("%Y-%m-%d_%H-%M") + else: + date_prefix = "0000-00-00_00-00" + + # 3. Извлекаем глобальный глобально-уникальный Message-ID письма + msg_id_header = msg.get("Message-ID", "").strip() + if msg_id_header: + # Создаем короткий уникальный хэш от Message-ID + unique_hash = hashlib.md5(msg_id_header.encode('utf-8')).hexdigest()[:8] + else: + # Фоллбэк: если Message-ID отсутствует, используем хэш содержимого + unique_hash = hashlib.md5(raw_email).hexdigest()[:8] + + # 4. Формируем гарантированно уникальное имя файла + # Пример: "2026-09-10_18-30_a1b2c3d4_Заказ_оформлен.eml" + filename = f"{date_prefix}_{unique_hash}_{clean_subject}.eml" + filepath = os.path.join(output_dir, filename) if not os.path.exists(output_dir): os.makedirs(output_dir, exist_ok=True) - - filename = f"mail_{mail_id_str}_{clean_subject}.eml" - filepath = os.path.join(output_dir, filename) with open(filepath, "wb") as f: f.write(raw_email) return subject_text if subject_text else "Без темы" - raise Exception("Данные письма повреждены или пусты.") \ No newline at end of file + + raise Exception("Данные письма повреждены или пусты.") diff --git a/version.txt b/version.txt index 8135da2..41d40ce 100644 --- a/version.txt +++ b/version.txt @@ -1,8 +1,8 @@ # UTF-8 VSVersionInfo( ffi=FixedFileInfo( - filevers=(1, 0, 0, 0), - prodvers=(1, 0, 0, 0), + filevers=(1, 0, 0, 1), + prodvers=(1, 0, 0, 1), mask=0x3f, flags=0x0, OS=0x40004, @@ -18,12 +18,12 @@ VSVersionInfo( [ StringStruct(u'CompanyName', u'ООО "Альянс - Технологии Сервиса"'), StringStruct(u'FileDescription', u'Yandex Mail Archiver - Утилита резервного копирования почты Яндекс'), - StringStruct(u'FileVersion', u'1.0.0.0'), + StringStruct(u'FileVersion', u'1.0.0.1'), StringStruct(u'InternalName', u'YandexMailArchiver'), StringStruct(u'LegalCopyright', u'Copyright © 2026. Все права защищены.'), StringStruct(u'OriginalFilename', u'YandexMailArchiver.exe'), StringStruct(u'ProductName', u'Yandex Mail Archiver'), - StringStruct(u'ProductVersion', u'1.0.0.0') + StringStruct(u'ProductVersion', u'1.0.0.1') ] ), StringTable( @@ -36,7 +36,7 @@ VSVersionInfo( StringStruct(u'LegalCopyright', u'Copyright © 2026. Все права защищены.'), StringStruct(u'OriginalFilename', u'YandexMailArchiver.exe'), StringStruct(u'ProductName', u'Yandex Mail Archiver'), - StringStruct(u'ProductVersion', u'1.0.0.0') + StringStruct(u'ProductVersion', u'1.0.0.1') ] ) ]