본문으로 건너뛰기
김신건의 로그

[Django] Email: send_mail, EmailMessage, 템플릿

· 수정 · 📖 약 1분 · 526자/단어 #django #email #smtp #mailgun #sendgrid
django email, send_mail, EmailMultiAlternatives, django mailer, SMTP, anymail

정의

Django의 django.core.mailSMTP, 콘솔, 파일, 메모리, dummy 등 다양한 백엔드를 통합한 이메일 전송 API. 외부 서비스(SendGrid, Mailgun, AWS SES, Postmark)는 django-anymail로 통일된 인터페이스 제공.

설정

# settings.py
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = "user@example.com"
EMAIL_HOST_PASSWORD = "..."         # 환경변수 권장
DEFAULT_FROM_EMAIL = "noreply@example.com"
SERVER_EMAIL = "alerts@example.com"  # 관리자 에러 알림용

백엔드 옵션

백엔드용도
smtp.EmailBackend실제 SMTP 전송
console.EmailBackend개발: 콘솔에 출력
filebased.EmailBackend파일에 저장
locmem.EmailBackend테스트: 메모리
dummy.EmailBackend무시 (안 보냄)
# 개발
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

# 테스트
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"

send_mail (단순)

from django.core.mail import send_mail

send_mail(
    subject="환영합니다",
    message="안녕하세요!",
    from_email="noreply@example.com",
    recipient_list=["alice@example.com"],
    fail_silently=False,
)

빠르고 간단. HTML, 첨부, CC/BCC 필요하면 EmailMessage.

EmailMessage

from django.core.mail import EmailMessage

email = EmailMessage(
    subject="...",
    body="...",
    from_email="noreply@example.com",
    to=["alice@example.com"],
    cc=["bob@example.com"],
    bcc=["audit@example.com"],
    reply_to=["support@example.com"],
    headers={"Message-ID": "..."},
)

email.attach("invoice.pdf", pdf_bytes, "application/pdf")
email.attach_file("/path/to/file.pdf")

email.send()

HTML + 텍스트 (EmailMultiAlternatives)

from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string

context = {"user": user, "url": activation_url}
text_body = render_to_string("emails/welcome.txt", context)
html_body = render_to_string("emails/welcome.html", context)

email = EmailMultiAlternatives(
    subject="환영합니다",
    body=text_body,                # 평문 (필수)
    from_email="noreply@example.com",
    to=[user.email],
)
email.attach_alternative(html_body, "text/html")
email.send()

이메일 클라이언트가 HTML 못 받으면 텍스트 fallback. 둘 다 항상 제공이 모범 사례.

템플릿

{# templates/emails/welcome.html #}
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
    <h1>{{ user.name }}님, 환영합니다!</h1>
    <p>아래 링크를 눌러 가입을 완료하세요:</p>
    <p><a href="{{ url }}">계정 활성화</a></p>
</body>
</html>
{# templates/emails/welcome.txt #}
{{ user.name }}님, 환영합니다!

가입 완료 링크: {{ url }}

다중 전송

from django.core.mail import send_mass_mail

send_mass_mail((
    ("Subject 1", "Body 1", "from@x.com", ["a@x.com"]),
    ("Subject 2", "Body 2", "from@x.com", ["b@x.com"]),
), fail_silently=False)

각 메시지에 같은 SMTP 연결 재사용 → 더 효율. 또는:

from django.core.mail import get_connection

with get_connection() as connection:
    for user in users:
        EmailMessage(..., connection=connection).send()

관리자 알림

서버 에러 자동 이메일 (DEBUG=False).

ADMINS = [("Alice", "alice@example.com")]
SERVER_EMAIL = "alerts@example.com"

AdminEmailHandler 로깅 기본 포함. 예외 발생 시 ADMINS에 이메일.

from django.core.mail import mail_admins, mail_managers

mail_admins("Site error", "Something went wrong")
mail_managers("Report", "...")    # MANAGERS = [...]

비동기 전송 (Celery)

이메일은 외부 API 호출이라 view에서 즉시 보내면 느림. Celery로 위임.

# tasks.py
from celery import shared_task
from django.core.mail import send_mail

@shared_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=5)
def send_email_async(subject, body, recipients):
    send_mail(subject, body, "noreply@example.com", recipients)

# view
send_email_async.delay("환영", "...", [user.email])

django-anymail도 자체 비동기 옵션.

django-anymail (외부 서비스)

pip install "django-anymail[sendgrid]"
# 또는 mailgun, ses, postmark, mailjet, sparkpost
EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend"
ANYMAIL = {
    "SENDGRID_API_KEY": env("SENDGRID_API_KEY"),
}

기존 send_mail/EmailMessage 코드 변경 없음. anymail 확장 기능(tags, metadata, bounces, opens)도 가능.

from anymail.message import AnymailMessage

email = AnymailMessage(
    subject="...",
    body="...",
    to=["alice@example.com"],
    metadata={"user_id": "42"},
    tags=["welcome"],
    track_opens=True,
    track_clicks=True,
)
email.send()

자주 보는 패턴

비밀번호 재설정

Django 내장 PasswordResetView가 이메일 자동 전송.

# urls.py
from django.contrib.auth import views as auth_views

urlpatterns = [
    path("password_reset/", auth_views.PasswordResetView.as_view(
        email_template_name="emails/password_reset.html",
        subject_template_name="emails/password_reset_subject.txt",
        success_url="/password_reset/done/",
    )),
    ...
]

가입 인증

from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes

def send_activation_email(user):
    token = default_token_generator.make_token(user)
    uid = urlsafe_base64_encode(force_bytes(user.pk))
    activation_url = f"https://example.com/activate/{uid}/{token}/"

    send_mail(
        "가입 인증",
        f"링크: {activation_url}",
        "noreply@example.com",
        [user.email],
    )

알림 모음 (daily digest)

@shared_task
def send_daily_digest():
    for user in User.objects.filter(daily_digest=True):
        if items := user.unread_notifications():
            send_mail(
                f"오늘의 알림 {len(items)}건",
                render_to_string("emails/digest.txt", {"items": items}),
                "noreply@example.com",
                [user.email],
            )

테스트

from django.core import mail
from django.test import TestCase

class EmailTest(TestCase):
    def test_welcome(self):
        send_mail("subject", "body", "from@x.com", ["to@x.com"])

        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, "subject")
        self.assertEqual(mail.outbox[0].to, ["to@x.com"])

locmem 백엔드는 mail.outbox에 누적. 테스트 후 자동 비움.

함정

1. SMTP 인증 실패

Gmail은 OAuth 또는 App Password 필요. 일반 비밀번호 X.

2. 동기 전송으로 view 지연

def signup(request):
    user.save()
    send_mail(...)    # 1-3초 대기
    return redirect(...)

→ Celery 또는 transaction.on_commit + threading.

3. From 도메인 / SPF / DKIM

from=noreply@example.com 보내려면 example.com 도메인에 SPF/DKIM 설정 필요. 안 그러면 스팸함 직행.

4. 첨부 파일 인코딩

email.attach("한글파일.pdf", pdf_bytes, "application/pdf")

UTF-8 파일명은 일부 클라이언트 깨짐. 영문 + 헤더 별도 설정 또는 Anymail 같은 라이브러리.

5. 메일 폭주

루프에서 send_mail 호출하면 SMTP rate limit 걸림. send_mass_mail 또는 큐.

모니터링

  • 전송 실패율
  • bounce / complaint 율
  • open / click (마케팅 이메일)
  • spam score (mail-tester.com)

SendGrid/Mailgun 대시보드 또는 anymail webhooks.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기