From a65cb35b305c3e472a8211b0f67047647f16d14d Mon Sep 17 00:00:00 2001 From: InviteMe Developer Date: Mon, 31 Aug 2026 14:51:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BE=D0=BF=D1=86=D0=B8=D1=8F=20=D1=81=D0=BE?= =?UTF-8?q?=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BF=D0=B8=D0=B8=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D0=B8=D1=81=D0=B5=D0=BC?= =?UTF-8?q?=20=D0=B2=20IMAP=20Sent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены поля IMAP в модель SmtpSettings и миграция колонок - Реализовано сохранение копии MIME-сообщения через imaplib после SMTP/EWS/Outlook отправки - Обновлен routes.py: чтение и сохранение IMAP-настроек из формы - Обновлен шаблон admin/email_settings.html: блок настроек IMAP с подсказками --- app/__init__.py | 9 +++ app/models.py | 18 +++++ app/routes.py | 15 +++++ app/services/email_service.py | 88 +++++++++++++++++++++---- app/templates/admin/email_settings.html | 38 +++++++++++ 5 files changed, 154 insertions(+), 14 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 969413a..a5b0afb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -152,6 +152,15 @@ def _migrate_existing_data(app): _add_column_if_missing("smtp_settings", "use_ews", "BOOLEAN DEFAULT 0") _add_column_if_missing("smtp_settings", "ews_url", "VARCHAR(500)") + # Настройки IMAP для сохранения копий отправленных писем + _add_column_if_missing("smtp_settings", "imap_server", "VARCHAR(255)") + _add_column_if_missing("smtp_settings", "imap_port", "INTEGER DEFAULT 993") + _add_column_if_missing("smtp_settings", "imap_use_ssl", "BOOLEAN DEFAULT 1") + _add_column_if_missing("smtp_settings", "imap_username", "VARCHAR(255)") + _add_column_if_missing("smtp_settings", "imap_password", "VARCHAR(255)") + _add_column_if_missing("smtp_settings", "imap_folder", "VARCHAR(255)") + _add_column_if_missing("smtp_settings", "imap_save_copy", "BOOLEAN DEFAULT 0") + # Переносим файлы в папки по мероприятиям _migrate_files_to_event_folders() diff --git a/app/models.py b/app/models.py index afee8a1..83df0eb 100644 --- a/app/models.py +++ b/app/models.py @@ -198,6 +198,16 @@ class SmtpSettings(db.Model): use_local_outlook = db.Column(db.Boolean, nullable=False, default=False) use_ews = db.Column(db.Boolean, nullable=False, default=False) ews_url = db.Column(db.String(500), nullable=True) + + # IMAP copy of sent messages + imap_server = db.Column(db.String(255), nullable=True) + imap_port = db.Column(db.Integer, nullable=False, default=993) + imap_use_ssl = db.Column(db.Boolean, nullable=False, default=True) + imap_username = db.Column(db.String(255), nullable=True) + imap_password = db.Column(db.String(255), nullable=True) + imap_folder = db.Column(db.String(255), nullable=True) + imap_save_copy = db.Column(db.Boolean, nullable=False, default=False) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) @property @@ -233,6 +243,14 @@ class SmtpSettings(db.Model): app.config["MAIL_USE_LOCAL_OUTLOOK"] = settings.use_local_outlook app.config["MAIL_USE_EWS"] = settings.use_ews app.config["EWS_URL"] = settings.ews_url or "" + + app.config["IMAP_SERVER"] = settings.imap_server or "" + app.config["IMAP_PORT"] = settings.imap_port or 993 + app.config["IMAP_USE_SSL"] = settings.imap_use_ssl + app.config["IMAP_USERNAME"] = settings.imap_username or settings.mail_username or "" + app.config["IMAP_PASSWORD"] = settings.imap_password or settings.mail_password or "" + app.config["IMAP_FOLDER"] = settings.imap_folder or "Sent" + app.config["IMAP_SAVE_COPY"] = settings.imap_save_copy except Exception: pass diff --git a/app/routes.py b/app/routes.py index dbff1ec..90855b5 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1100,6 +1100,21 @@ def admin_email_settings(): if password: settings.mail_password = password + # IMAP: копия отправленных писем (общая для всех режимов отправки) + settings.imap_save_copy = bool(request.form.get("imap_save_copy")) + settings.imap_server = request.form.get("imap_server", "").strip() + try: + settings.imap_port = int(request.form.get("imap_port", "993").strip()) + except ValueError: + settings.imap_port = 993 + settings.imap_use_ssl = bool(request.form.get("imap_use_ssl")) + settings.imap_username = request.form.get("imap_username", "").strip() + settings.imap_folder = request.form.get("imap_folder", "Sent").strip() or "Sent" + + imap_password = request.form.get("imap_password", "").strip() + if imap_password: + settings.imap_password = imap_password + db.session.commit() SmtpSettings.apply_to_app(current_app) diff --git a/app/services/email_service.py b/app/services/email_service.py index 2502c2d..ca58950 100644 --- a/app/services/email_service.py +++ b/app/services/email_service.py @@ -1,4 +1,5 @@ import smtplib +import time from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage @@ -13,26 +14,16 @@ from app.utils.paths import event_qr_folder OUTLOOK_CONTENT_ID_PROP = "http://schemas.microsoft.com/mapi/proptag/0x3712001F" -def _send_smtp_email(to_address, subject, html_body, sender_name, attachments): - """Отправляет письмо через SMTP с inline-вложениями.""" +def _build_mime_message(to_address, subject, html_body, sender_name, attachments): + """Собирает MIME-сообщение с inline-вложениями.""" config = current_app.config - - server = config.get("MAIL_SERVER", "") - port = config.get("MAIL_PORT", 465) - username = config.get("MAIL_USERNAME", "") - password = config.get("MAIL_PASSWORD", "") - use_ssl = config.get("MAIL_USE_SSL", True) - sender = config.get("MAIL_DEFAULT_SENDER") or username - - if not username or not password: - raise ValueError("MAIL_USERNAME и MAIL_PASSWORD должны быть указаны в конфигурации") - if not server or not port: - raise ValueError("MAIL_SERVER и MAIL_PORT должны быть указаны в конфигурации") + sender = config.get("MAIL_DEFAULT_SENDER") or config.get("MAIL_USERNAME", "") msg = MIMEMultipart("related") msg["Subject"] = subject msg["From"] = formataddr((sender_name, sender)) msg["To"] = to_address + msg["Date"] = email.utils.formatdate(localtime=True) msg.attach(MIMEText(html_body, "html", "utf-8")) for att in attachments: @@ -46,6 +37,26 @@ def _send_smtp_email(to_address, subject, html_body, sender_name, attachments): img.add_header("Content-Disposition", "inline", filename=filename) msg.attach(img) + return msg + + +def _send_smtp_email(to_address, subject, html_body, sender_name, attachments): + """Отправляет письмо через SMTP с inline-вложениями.""" + config = current_app.config + + server = config.get("MAIL_SERVER", "") + port = config.get("MAIL_PORT", 465) + username = config.get("MAIL_USERNAME", "") + password = config.get("MAIL_PASSWORD", "") + use_ssl = config.get("MAIL_USE_SSL", True) + + if not username or not password: + raise ValueError("MAIL_USERNAME и MAIL_PASSWORD должны быть указаны в конфигурации") + if not server or not port: + raise ValueError("MAIL_SERVER и MAIL_PORT должны быть указаны в конфигурации") + + msg = _build_mime_message(to_address, subject, html_body, sender_name, attachments) + if use_ssl: with smtplib.SMTP_SSL(server, port) as smtp: smtp.login(username, password) @@ -56,6 +67,8 @@ def _send_smtp_email(to_address, subject, html_body, sender_name, attachments): smtp.login(username, password) smtp.send_message(msg) + return msg + def _send_outlook_email(to_address, subject, html_body, sender_name, attachments): """Отправляет письмо через локальный Outlook (MAPI/COM).""" @@ -132,6 +145,49 @@ def _send_ews_email(to_address, subject, html_body, sender_name, attachments): raise RuntimeError(f"Ошибка отправки через EWS: {e}") from e +def _save_imap_copy(to_address, subject, html_body, sender_name, attachments): + """Сохраняет копию отправленного письма в IMAP-папку (обычно Sent).""" + import imaplib + + config = current_app.config + if not config.get("IMAP_SAVE_COPY"): + return + + server = config.get("IMAP_SERVER", "") + port = config.get("IMAP_PORT", 993) + username = config.get("IMAP_USERNAME", "") + password = config.get("IMAP_PASSWORD", "") + folder = config.get("IMAP_FOLDER", "Sent") + use_ssl = config.get("IMAP_USE_SSL", True) + + if not server or not username or not password: + raise ValueError("Для сохранения копии в IMAP должны быть указаны сервер, логин и пароль") + + msg = _build_mime_message(to_address, subject, html_body, sender_name, attachments) + msg_bytes = msg.as_bytes() + date_str = imaplib.Time2Internaldate(time.time()) + + try: + if use_ssl: + mail = imaplib.IMAP4_SSL(server, port) + else: + mail = imaplib.IMAP4(server, port) + try: + mail.starttls() + except Exception: + pass + + mail.login(username, password) + mail.select(folder) + result, data = mail.append(folder, "", date_str, msg_bytes) + if result != "OK": + raise RuntimeError(f"IMAP append failed: {data}") + mail.close() + mail.logout() + except Exception as e: + raise RuntimeError(f"Ошибка сохранения копии в IMAP: {e}") from e + + def _send_email(to_address, subject, html_body, sender_name="InviteMe", attachments=None): """Маршрутизирует отправку через SMTP, EWS или локальный Outlook.""" attachments = attachments or [] @@ -142,6 +198,10 @@ def _send_email(to_address, subject, html_body, sender_name="InviteMe", attachme else: _send_smtp_email(to_address, subject, html_body, sender_name, attachments) + # Сохраняем копию в IMAP, если включено + if current_app.config.get("IMAP_SAVE_COPY"): + _save_imap_copy(to_address, subject, html_body, sender_name, attachments) + def send_invitation(guest, event_name=None, additional_text=""): """Отправляет приглашение гостю на email с QR-кодом и билетом во вложении.""" diff --git a/app/templates/admin/email_settings.html b/app/templates/admin/email_settings.html index f66fe1b..1f3325a 100644 --- a/app/templates/admin/email_settings.html +++ b/app/templates/admin/email_settings.html @@ -76,6 +76,44 @@ Outlook должен быть установлен и запущен на сервере, где работает приложение. Браузер не может запустить Outlook на твоём компьютере. +
+
Копия отправленных писем в IMAP
+
+ + +
Работает после успешной отправки через SMTP/EWS/Outlook. Для отправки писем по-прежнему используется выбранный выше способ.
+
+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Название папки на сервере: обычно Sent, Sent Items или Отправленные.
+
+
+
Перейти к рассылке