Добавлена опция сохранения копии отправленных писем в 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", "use_ews", "BOOLEAN DEFAULT 0")
|
||||||
_add_column_if_missing("smtp_settings", "ews_url", "VARCHAR(500)")
|
_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()
|
_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_local_outlook = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
use_ews = 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)
|
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)
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -233,6 +243,14 @@ class SmtpSettings(db.Model):
|
|||||||
app.config["MAIL_USE_LOCAL_OUTLOOK"] = settings.use_local_outlook
|
app.config["MAIL_USE_LOCAL_OUTLOOK"] = settings.use_local_outlook
|
||||||
app.config["MAIL_USE_EWS"] = settings.use_ews
|
app.config["MAIL_USE_EWS"] = settings.use_ews
|
||||||
app.config["EWS_URL"] = settings.ews_url or ""
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1100,6 +1100,21 @@ def admin_email_settings():
|
|||||||
if password:
|
if password:
|
||||||
settings.mail_password = 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()
|
db.session.commit()
|
||||||
SmtpSettings.apply_to_app(current_app)
|
SmtpSettings.apply_to_app(current_app)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import smtplib
|
import smtplib
|
||||||
|
import time
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.image import MIMEImage
|
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"
|
OUTLOOK_CONTENT_ID_PROP = "http://schemas.microsoft.com/mapi/proptag/0x3712001F"
|
||||||
|
|
||||||
|
|
||||||
def _send_smtp_email(to_address, subject, html_body, sender_name, attachments):
|
def _build_mime_message(to_address, subject, html_body, sender_name, attachments):
|
||||||
"""Отправляет письмо через SMTP с inline-вложениями."""
|
"""Собирает MIME-сообщение с inline-вложениями."""
|
||||||
config = current_app.config
|
config = current_app.config
|
||||||
|
sender = config.get("MAIL_DEFAULT_SENDER") or config.get("MAIL_USERNAME", "")
|
||||||
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 должны быть указаны в конфигурации")
|
|
||||||
|
|
||||||
msg = MIMEMultipart("related")
|
msg = MIMEMultipart("related")
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
msg["From"] = formataddr((sender_name, sender))
|
msg["From"] = formataddr((sender_name, sender))
|
||||||
msg["To"] = to_address
|
msg["To"] = to_address
|
||||||
|
msg["Date"] = email.utils.formatdate(localtime=True)
|
||||||
msg.attach(MIMEText(html_body, "html", "utf-8"))
|
msg.attach(MIMEText(html_body, "html", "utf-8"))
|
||||||
|
|
||||||
for att in attachments:
|
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)
|
img.add_header("Content-Disposition", "inline", filename=filename)
|
||||||
msg.attach(img)
|
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:
|
if use_ssl:
|
||||||
with smtplib.SMTP_SSL(server, port) as smtp:
|
with smtplib.SMTP_SSL(server, port) as smtp:
|
||||||
smtp.login(username, password)
|
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.login(username, password)
|
||||||
smtp.send_message(msg)
|
smtp.send_message(msg)
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
def _send_outlook_email(to_address, subject, html_body, sender_name, attachments):
|
def _send_outlook_email(to_address, subject, html_body, sender_name, attachments):
|
||||||
"""Отправляет письмо через локальный Outlook (MAPI/COM)."""
|
"""Отправляет письмо через локальный 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
|
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):
|
def _send_email(to_address, subject, html_body, sender_name="InviteMe", attachments=None):
|
||||||
"""Маршрутизирует отправку через SMTP, EWS или локальный Outlook."""
|
"""Маршрутизирует отправку через SMTP, EWS или локальный Outlook."""
|
||||||
attachments = attachments or []
|
attachments = attachments or []
|
||||||
@@ -142,6 +198,10 @@ def _send_email(to_address, subject, html_body, sender_name="InviteMe", attachme
|
|||||||
else:
|
else:
|
||||||
_send_smtp_email(to_address, subject, html_body, sender_name, attachments)
|
_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=""):
|
def send_invitation(guest, event_name=None, additional_text=""):
|
||||||
"""Отправляет приглашение гостю на email с QR-кодом и билетом во вложении."""
|
"""Отправляет приглашение гостю на email с QR-кодом и билетом во вложении."""
|
||||||
|
|||||||
@@ -76,6 +76,44 @@
|
|||||||
Outlook должен быть установлен и запущен на сервере, где работает приложение. Браузер не может запустить Outlook на твоём компьютере.
|
Outlook должен быть установлен и запущен на сервере, где работает приложение. Браузер не может запустить Outlook на твоём компьютере.
|
||||||
</div>
|
</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">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Сохранить настройки</button>
|
<button type="submit" class="btn btn-primary">Сохранить настройки</button>
|
||||||
<a href="{{ url_for('main.admin_email_send') }}" class="btn btn-outline-primary">Перейти к рассылке</a>
|
<a href="{{ url_for('main.admin_email_send') }}" class="btn btn-outline-primary">Перейти к рассылке</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user