Добавлена опция сохранения копии отправленных писем в IMAP Sent
- Добавлены поля IMAP в модель SmtpSettings и миграция колонок - Реализовано сохранение копии MIME-сообщения через imaplib после SMTP/EWS/Outlook отправки - Обновлен routes.py: чтение и сохранение IMAP-настроек из формы - Обновлен шаблон admin/email_settings.html: блок настроек IMAP с подсказками
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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-кодом и билетом во вложении."""
|
||||
|
||||
@@ -76,6 +76,44 @@
|
||||
Outlook должен быть установлен и запущен на сервере, где работает приложение. Браузер не может запустить Outlook на твоём компьютере.
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
<h5 class="mb-3">Копия отправленных писем в IMAP</h5>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="imap_save_copy" name="imap_save_copy" value="1" {% if settings.imap_save_copy %}checked{% endif %}>
|
||||
<label class="form-check-label" for="imap_save_copy">Сохранять копию отправленного письма в папку IMAP</label>
|
||||
<div class="form-text text-muted">Работает после успешной отправки через SMTP/EWS/Outlook. Для отправки писем по-прежнему используется выбранный выше способ.</div>
|
||||
</div>
|
||||
|
||||
<div id="imap-fields">
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label for="imap_server" class="form-label">IMAP-сервер</label>
|
||||
<input type="text" class="form-control" id="imap_server" name="imap_server" value="{{ settings.imap_server or '' }}" placeholder="mail.integrita.ru">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="imap_port" class="form-label">Порт</label>
|
||||
<input type="number" class="form-control" id="imap_port" name="imap_port" value="{{ settings.imap_port or 993 }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="imap_use_ssl" name="imap_use_ssl" value="1" {% if settings.imap_use_ssl %}checked{% endif %}>
|
||||
<label class="form-check-label" for="imap_use_ssl">Использовать SSL/TLS</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="imap_username" class="form-label">IMAP логин (если пустой — используется логин отправки)</label>
|
||||
<input type="text" class="form-control" id="imap_username" name="imap_username" value="{{ settings.imap_username or '' }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="imap_password" class="form-label">IMAP пароль (если пустой — используется пароль отправки)</label>
|
||||
<input type="password" class="form-control" id="imap_password" name="imap_password" placeholder="Оставьте пустым, чтобы не менять">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="imap_folder" class="form-label">Папка для сохранения копий</label>
|
||||
<input type="text" class="form-control" id="imap_folder" name="imap_folder" value="{{ settings.imap_folder or 'Sent' }}" placeholder="Sent">
|
||||
<div class="form-text text-muted">Название папки на сервере: обычно <code>Sent</code>, <code>Sent Items</code> или <code>Отправленные</code>.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Сохранить настройки</button>
|
||||
<a href="{{ url_for('main.admin_email_send') }}" class="btn btn-outline-primary">Перейти к рассылке</a>
|
||||
|
||||
Reference in New Issue
Block a user