В Django почта встроена — модуль django.core.mail, — поэтому отправка письма сводится к тому, чтобы указать SMTP-сервер в settings.py и вызвать send_mail(). Ниже — настройка fmailer и всё, что реально нужно в продакшене: HTML-письма, шаблоны, вложения, массовая рассылка и фоновая отправка через Celery.
| Параметр | Значение |
|---|---|
| SMTP-хост | smtp.fmailer.ru |
| Порт | 587 |
| Шифрование | STARTTLS (соединение переводится в TLS до авторизации) |
| Логин | Логин SMTP-токена вашего домена |
| Пароль | Пароль этого токена — показывается один раз, при создании |
Добавьте параметры в settings.py. Доступы берём из окружения, чтобы они не попадали в репозиторий:
# settings.py
import os
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.fmailer.ru"
EMAIL_PORT = 587
EMAIL_USE_TLS = True # STARTTLS на порту 587
EMAIL_USE_SSL = False # нельзя включать вместе с EMAIL_USE_TLS
EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"]
EMAIL_TIMEOUT = 10
DEFAULT_FROM_EMAIL = "Магазин <noreply@mail.example.ru>"
SERVER_EMAIL = DEFAULT_FROM_EMAIL # адрес для служебных писем Django об ошибкахА сам .env или переменные процесса:
EMAIL_HOST_USER=<логин токена>
EMAIL_HOST_PASSWORD=<пароль токена> Для шифрования с первого байта на порту 465 поменяйте три параметра. EMAIL_USE_TLS и EMAIL_USE_SSL взаимоисключающие — если включить оба, Django упадёт при запуске:
# Порт 465 — шифрование сразу, без STARTTLS
EMAIL_PORT = 465
EMAIL_USE_TLS = False
EMAIL_USE_SSL = TrueFrom: должен быть на подтверждённом домене, иначе к нему не применяются подпись DKIM и запись SPF. Письмо от @yandex.ru или @mail.ru через сторонний релей не проходит DMARC и уходит в спам. send_mail() — самый короткий путь: тема, текст, отправитель, получатели:
from django.core.mail import send_mail
send_mail(
subject="Подтверждение заказа №4417",
message="Заказ принят. Доставка — завтра до 18:00.",
from_email=None, # возьмёт DEFAULT_FROM_EMAIL
recipient_list=["client@example.ru"],
fail_silently=False,
) Оставляйте fail_silently=False. С True Django проглатывает все ошибки SMTP, и сломанная конфигурация выглядит точно так же, как рабочая. Проверить настройки, не написав ни одного view:
python manage.py shell -c "from django.core.mail import send_mail; \
send_mail('Тест', 'Работает.', None, ['you@example.ru'], fail_silently=False)"send_mail() отправляет только текст. Для HTML нужен EmailMultiAlternatives — он собирает multipart/alternative из обеих частей. Текстовую часть добавляйте всегда: письмо без неё почтовые фильтры считают признаком спама:
from django.core.mail import EmailMultiAlternatives
text = "Заказ принят. Доставка — завтра до 18:00."
html = "<h1>Заказ принят</h1><p>Доставка — завтра до 18:00.</p>"
msg = EmailMultiAlternatives(
subject="Подтверждение заказа №4417",
body=text, # текстовая часть — она нужна всегда
to=["client@example.ru"],
)
msg.attach_alternative(html, "text/html")
msg.send()HTML прямо во view быстро становится нечитаемым. Отрисуйте обычный шаблон и получите из него текстовую версию:
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
html = render_to_string("email/order_confirmed.html", {"order": order})
msg = EmailMultiAlternatives(
subject=f"Подтверждение заказа №{order.id}",
body=strip_tags(html),
to=[order.customer.email],
)
msg.attach_alternative(html, "text/html")
msg.send() Если передать несколько адресов в recipient_list, они попадут в один заголовок To: и получатели увидят друг друга. Для писем клиентам отправляйте по одному письму на адрес, переиспользуя соединение:
from django.core.mail import EmailMultiAlternatives, get_connection
# Одно SMTP-соединение, отдельное письмо каждому получателю.
connection = get_connection()
connection.open()
for user in users:
EmailMultiAlternatives(
subject="Еженедельный отчёт",
body=render_report(user),
to=[user.email], # ровно один получатель на письмо
connection=connection,
).send()
connection.close()from django.core.mail import EmailMessage
msg = EmailMessage(
subject="Счёт на оплату",
body="Счёт за март во вложении.",
to=["client@example.ru"],
)
msg.attach_file("/srv/invoices/2026-03.pdf") # файл с диска
msg.attach("invoice.pdf", pdf_bytes, "application/pdf") # или байты из памяти
msg.send()SMTP-бэкенд Django блокирующий: запрос ждёт соединение, TLS-рукопожатие и ответ сервера. Вынесите отправку в задачу Celery — пользователь получит ответ сразу, а временный сбой SMTP станет повтором вместо ошибки 500:
# tasks.py
from celery import shared_task
from django.core.mail import EmailMultiAlternatives
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_order_confirmation(self, order_id: int) -> None:
order = Order.objects.get(pk=order_id)
msg = EmailMultiAlternatives(
subject=f"Подтверждение заказа №{order.id}",
body=render_confirmation(order),
to=[order.customer.email],
)
try:
msg.send()
except Exception as exc: # временный сбой SMTP — повторим с задержкой
raise self.retry(exc=exc)# views.py — ответ уходит сразу, письмо отправит воркер
send_order_confirmation.delay(order.id)# settings/dev.py — печатать письма в консоль вместо отправки
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" В тестах менять ничего не нужно — Django подставляет бэкенд в памяти и складывает письма в django.core.mail.outbox:
from django.core import mail
from django.test import TestCase
class OrderEmailTests(TestCase):
def test_confirmation_is_sent(self):
place_order(self.customer)
self.assertEqual(len(mail.outbox), 1)
self.assertIn("Подтверждение", mail.outbox[0].subject)| Ошибка | Причина и решение |
|---|---|
SMTPAuthenticationError | Неверный логин или пароль токена. Пароль показывается один раз — при создании. |
SMTPServerDisconnected, таймаут соединения | Хостинг блокирует почтовые порты. Поставьте EMAIL_PORT = 8587 или 8465. |
SMTPNotSupportedError: STARTTLS extension not supported | EMAIL_USE_TLS = True на порту 465. Там нужен EMAIL_USE_SSL. |
SMTPSenderRefused (550) | Домен отправителя не подтверждён или не покрывается токеном. |
| Письмо принято, но не дошло | Посмотрите журнал отправки в панели — там будет причина отказа. |
SMTPRecipientsRefused | Адрес получателя некорректен или попал в список подавления после жёсткого отказа. |
587 вместе с EMAIL_USE_TLS = True — это STARTTLS, самый распространённый вариант для SMTP-бэкенда Django. Порт 465 тоже работает: там нужно EMAIL_USE_SSL = True и EMAIL_USE_TLS = False. Включать оба флага одновременно нельзя — Django выбросит ошибку при старте.
Неверный логин или пароль. В EMAIL_HOST_USER нужен полный логин SMTP-токена вашего домена, а в EMAIL_HOST_PASSWORD — пароль, который показывается один раз при создании токена. Если пароль утерян, создайте новый токен: восстановить старый нельзя.
Большинство хостингов и облачных провайдеров блокируют исходящие подключения на порты 25, 465 и 587. Поменяйте EMAIL_PORT на высокий порт — 8587 для STARTTLS или 8465 для SSL/TLS — и соединение установится.
Не стоит. send_mail блокирует запрос на всё время SMTP-обмена, поэтому медленное соединение превращается в медленную страницу. Отдайте отправку в Celery, Django-Q или django-mailer и сразу возвращайте ответ — заодно получите автоматические повторы при временных сбоях.
Отправляйте с подтверждённого домена, чтобы работали DKIM и SPF, а DMARC сходился. В DEFAULT_FROM_EMAIL должен стоять адрес на этом домене, а не на Яндексе или Mail.ru. И всегда добавляйте текстовую часть рядом с HTML.
В разработке поставьте EMAIL_BACKEND в django.core.mail.backends.console.EmailBackend — письма будут печататься в консоль. В тестах Django сам подменяет бэкенд на locmem и складывает все сообщения в django.core.mail.outbox.