a2fe4a49e3
Сотрудник сам подключает ящик (autoconfig, вложения, пагинация списка, надёжный IMAP-поиск); в хедере работает /api/search по разделам и сущностям с ACL. Также устойчивость Mattermost-токена для чата. Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
3.5 KiB
Python
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
|