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 на твоём компьютере. +
Sent, Sent Items или Отправленные.