Files
InviteMe/app/services/email_service.py
T

206 lines
7.6 KiB
Python

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.utils import formataddr
from pathlib import Path
from flask import current_app, render_template
from app.services.ticket_service import generate_ticket_image
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-вложениями."""
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 должны быть указаны в конфигурации")
msg = MIMEMultipart("related")
msg["Subject"] = subject
msg["From"] = formataddr((sender_name, sender))
msg["To"] = to_address
msg.attach(MIMEText(html_body, "html", "utf-8"))
for att in attachments:
path = att["path"]
cid = att.get("cid")
filename = att.get("filename", path.name)
with open(path, "rb") as img_file:
img = MIMEImage(img_file.read())
if cid:
img.add_header("Content-ID", f"<{cid}>")
img.add_header("Content-Disposition", "inline", filename=filename)
msg.attach(img)
if use_ssl:
with smtplib.SMTP_SSL(server, port) as smtp:
smtp.login(username, password)
smtp.send_message(msg)
else:
with smtplib.SMTP(server, port) as smtp:
smtp.starttls()
smtp.login(username, password)
smtp.send_message(msg)
def _send_outlook_email(to_address, subject, html_body, sender_name, attachments):
"""Отправляет письмо через локальный Outlook (MAPI/COM)."""
try:
import win32com.client
except ImportError as e:
raise RuntimeError("Для отправки через Outlook необходима библиотека pywin32") from e
try:
outlook = win32com.client.Dispatch("Outlook.Application")
except Exception as e:
raise RuntimeError("Не удалось подключиться к Outlook. Убедитесь, что Outlook установлен и запущен.") from e
mail = outlook.CreateItem(0) # olMailItem
mail.To = to_address
mail.Subject = subject
mail.HTMLBody = html_body
for att in attachments:
path = att["path"]
cid = att.get("cid")
filename = att.get("filename", path.name)
# 1 = olByValue, 0 = позиция (inline)
attachment = mail.Attachments.Add(str(path), 1, 0, filename)
if cid:
attachment.PropertyAccessor.SetProperty(OUTLOOK_CONTENT_ID_PROP, cid)
mail.Send()
def _send_ews_email(to_address, subject, html_body, sender_name, attachments):
"""Отправляет письмо через Exchange Web Services (EWS)."""
try:
from exchangelib import Credentials, Configuration, Account, Message, Mailbox, HTMLBody, FileAttachment
except ImportError as e:
raise RuntimeError("Для отправки через EWS необходима библиотека exchangelib") from e
config = current_app.config
ews_url = config.get("EWS_URL", "")
username = config.get("MAIL_USERNAME", "")
password = config.get("MAIL_PASSWORD", "")
if not ews_url or not username or not password:
raise ValueError("Для EWS должны быть указаны EWS URL, логин и пароль")
try:
credentials = Credentials(username=username, password=password)
ews_config = Configuration(service_endpoint=ews_url, credentials=credentials)
account = Account(
primary_smtp_address=username,
config=ews_config,
autodiscover=False,
access_type="delegate",
)
msg = Message(
account=account,
subject=subject,
body=HTMLBody(html_body),
to_recipients=[Mailbox(email_address=to_address)],
)
for att in attachments:
path = att["path"]
filename = att.get("filename", path.name)
cid = att.get("cid")
with open(path, "rb") as f:
content = f.read()
file_att = FileAttachment(name=filename, content=content, content_id=cid)
msg.attach(file_att)
msg.send()
except Exception as e:
raise RuntimeError(f"Ошибка отправки через EWS: {e}") from e
def _send_email(to_address, subject, html_body, sender_name="InviteMe", attachments=None):
"""Маршрутизирует отправку через SMTP, EWS или локальный Outlook."""
attachments = attachments or []
if current_app.config.get("MAIL_USE_LOCAL_OUTLOOK"):
_send_outlook_email(to_address, subject, html_body, sender_name, attachments)
elif current_app.config.get("MAIL_USE_EWS"):
_send_ews_email(to_address, subject, html_body, sender_name, attachments)
else:
_send_smtp_email(to_address, subject, html_body, sender_name, attachments)
def send_invitation(guest, event_name=None, additional_text=""):
"""Отправляет приглашение гостю на email с QR-кодом и билетом во вложении."""
config = current_app.config
if event_name is None:
event_name = guest.event.name if guest.event else "Мероприятие"
# Генерируем билет
try:
ticket_path = generate_ticket_image(guest, guest.event)
except Exception as e:
ticket_path = None
current_app.logger.warning(f"Не удалось сгенерировать билет для {guest.email}: {e}")
html_body = render_template(
"email/invitation.html",
guest=guest,
event_name=event_name,
additional_text=additional_text,
qr_cid="qr_code_image",
ticket_cid="ticket_image" if ticket_path else None,
)
attachments = []
# QR-код
qr_path = event_qr_folder(guest.event_id) / guest.qr_filename
if not qr_path.exists():
qr_path = Path(config["QR_FOLDER"]) / guest.qr_filename
if qr_path.exists():
attachments.append({"path": qr_path, "cid": "qr_code_image", "filename": guest.qr_filename})
# Билет
if ticket_path and ticket_path.exists():
attachments.append({
"path": ticket_path,
"cid": "ticket_image",
"filename": f"ticket_{guest.token}.png",
})
_send_email(
to_address=guest.email,
subject=f"Приглашение на мероприятие: {event_name}",
html_body=html_body,
attachments=attachments,
)
guest.invitation_sent = True
return True
def send_test_email(to_address):
"""Отправляет тестовое письмо для проверки настроек."""
html_body = render_template("email/test_email.html")
_send_email(
to_address=to_address,
subject="Тестовое письмо InviteMe",
html_body=html_body,
)
return True