Добавлена опция сохранения копии отправленных писем в IMAP Sent

- Добавлены поля IMAP в модель SmtpSettings и миграция колонок
- Реализовано сохранение копии MIME-сообщения через imaplib после SMTP/EWS/Outlook отправки
- Обновлен routes.py: чтение и сохранение IMAP-настроек из формы
- Обновлен шаблон admin/email_settings.html: блок настроек IMAP с подсказками
This commit is contained in:
InviteMe Developer
2026-08-31 14:51:14 +03:00
parent c5fd029818
commit a65cb35b30
5 changed files with 154 additions and 14 deletions
+74 -14
View File
@@ -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-кодом и билетом во вложении."""