Files
ACORE/apps/mail_connector/smtp_client.py
T
lokses a2fe4a49e3 mail + portal: самоподключение почты, веб-клиент и глобальный поиск
Сотрудник сам подключает ящик (autoconfig, вложения, пагинация списка, надёжный IMAP-поиск); в хедере работает /api/search по разделам и сущностям с ACL. Также устойчивость Mattermost-токена для чата.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 12:22:53 +03:00

108 lines
3.5 KiB
Python

"""Отправка писем через SMTP."""
from __future__ import annotations
import mimetypes
import smtplib
import ssl
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from apps.mail_connector.mail_settings import MailSettings
class SmtpSendError(Exception):
pass
# filename, content bytes, content_type
Attachment = tuple[str, bytes, str]
def send_message(
settings: MailSettings,
*,
username: str,
password: str,
sender: str,
to_addrs: list[str],
subject: str,
body: str,
cc_addrs: list[str] | None = None,
bcc_addrs: list[str] | None = None,
attachments: list[Attachment] | None = None,
) -> None:
cc_addrs = cc_addrs or []
bcc_addrs = bcc_addrs or []
attachments = attachments or []
recipients = [a for a in to_addrs + cc_addrs + bcc_addrs if a]
if attachments:
msg = MIMEMultipart('mixed')
body_part = MIMEMultipart('alternative')
body_part.attach(MIMEText(body, 'plain', 'utf-8'))
msg.attach(body_part)
for filename, content, content_type in attachments:
msg.attach(_make_attachment_part(filename, content, content_type))
else:
msg = MIMEMultipart('alternative')
msg.attach(MIMEText(body, 'plain', 'utf-8'))
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = ', '.join(to_addrs)
if cc_addrs:
msg['Cc'] = ', '.join(cc_addrs)
try:
if settings.use_ssl and settings.smtp_port == 465:
ctx = ssl.create_default_context()
if not settings.verify_ssl:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
with smtplib.SMTP_SSL(settings.smtp_host, settings.smtp_port, context=ctx) as smtp:
smtp.login(username, password)
smtp.sendmail(sender, recipients, msg.as_string())
else:
with smtplib.SMTP(settings.smtp_host, settings.smtp_port) as smtp:
if settings.use_ssl:
ctx = ssl.create_default_context()
if not settings.verify_ssl:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
smtp.starttls(context=ctx)
smtp.login(username, password)
smtp.sendmail(sender, recipients, msg.as_string())
except smtplib.SMTPException as e:
raise SmtpSendError(str(e)) from e
except OSError as e:
raise SmtpSendError(str(e)) from e
def _make_attachment_part(filename: str, content: bytes, content_type: str) -> MIMEBase:
ctype = (content_type or '').split(';')[0].strip()
if not ctype or ctype == 'application/octet-stream':
guessed, _ = mimetypes.guess_type(filename)
ctype = guessed or 'application/octet-stream'
maintype, _, subtype = ctype.partition('/')
if not subtype:
maintype, subtype = 'application', 'octet-stream'
part = MIMEBase(maintype, subtype)
part.set_payload(content)
encoders.encode_base64(part)
# ASCII filename preferred; otherwise RFC2047-encoded fallback
try:
filename.encode('ascii')
part.add_header('Content-Disposition', 'attachment', filename=filename)
except UnicodeEncodeError:
part.add_header(
'Content-Disposition',
'attachment',
filename=str(Header(filename, 'utf-8')),
)
return part