3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Django Rest FrameworkでDockerとMailCatcherを使ってメール送信を行おう!

Last updated at Posted at 2022-11-22

前提

  • docker-compose.ymlを使用
  • ある程度Dockerの知識を持っていること
  • メール送信する際にSwaggerを使用します

mailcatcherとは

MailCatcher runs a super simple SMTP server which catches any message sent to it to display in a web interface.

公式ドキュメントに記載されている通り簡易的なSMTPサーバでMailCatcherに送信されたメールをWebインターフェイス(Webブラウザ)に表示させることができます
本番環境ではAWSのSESを使うのでローカルでメール送信テストを行いたいときはMailCatcherを使ってみましょう

SMTP(メール)サーバ用のコンテナを作ろう!

Run mailcatcher, set your favourite app to deliver to smtp://127.0.0.1:1025 instead of your default SMTP server, then check out http://127.0.0.1:1080 to see the mail that's arrived so far.

公式ドキュメントに記載されている通り
MailCatcherのイメージを指定して

  • SMTP用の1025番ポート
  • Webブラウザで閲覧する用の1080番ポート

の2種類のポートを解放します

docker-compose.yml
  mail:
    container_name: mail
    image: schickling/mailcatcher
    ports:
      - "1080:1080"
      - "1025:1025"

settings.pyに以下の設定を記載します
検証用のため、EMAIL_HOST_USERとEMAIL_HOST_PASSWORDは空白にします

settings.py
# Djangoのメールの設定
EMAIL_HOST = 'mail'
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
# SMTPの1025番ポートを指定
EMAIL_PORT = 1025
# 送信中の文章の暗号化をFalseにします
EMAIL_USE_TLS = False

MailCatcherにアクセスしてみよう!

コンテナを起動し、http://127.0.0.1:1080にアクセスし、以下のMailCatcherの画面が表示されたら成功です

スクリーンショット 2022-11-06 17.27.14.png

メールを送る設定をしてみよう!

今回は新規ユーザに招待メールを送るという簡単な機能を実装します
以下のファイルにコードを記載していきます

  • models.py
  • serializers.py
  • views.py
  • emails.py
  • templateファイル群

に以下のコードを記載していきます

models.py

システムユーザのModelを作成します
システムユーザの作成方法について知りたい方は以下の記事を参考にしてください

models.py
import uuid

from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.core.validators import RegexValidator
from django.db import models


class User(AbstractUser):
    """システムユーザ"""

    username_validator = UnicodeUsernameValidator()

    class Role(models.IntegerChoices):
        """システムユーザのロール

        Args:
            MANAGEMENT(0): 管理者
            GENERAL(1):    一般
        """

        MANAGEMENT = 0, "管理者"
        GENERAL = 1, "一般"

    # 不要なフィールドはNoneにすることができる
    first_name = None
    last_name = None
    date_joined = None
    groups = None
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
        db_comment="システムユーザID",
    )
    employee_number = models.CharField(
        unique=True,
        validators=[RegexValidator(r"^[0-9]{8}$")],
        max_length=8,
        db_comment="社員番号",
    )
    username = models.CharField(
        max_length=150,
        unique=True,
        validators=[username_validator],
        db_comment="ユーザ名",
    )
    email = models.EmailField(
        max_length=254,
        unique=True,
        db_comment="メールアドレス",
    )
    role = models.PositiveIntegerField(
        choices=Role.choices,
        default=Role.MANAGEMENT,
        db_comment="システムユーザのロール",
    )
    created_at = models.DateTimeField(
        auto_now_add=True,
        db_comment="作成日",
    )
    updated_at = models.DateTimeField(
        auto_now=True,
        db_comment="更新日",
    )
    is_verified = models.BooleanField(
        default=False,
        db_comment="有効化有無",
    )
    created_by = models.ForeignKey(
        "self",
        null=True,
        on_delete=models.SET_NULL,
        related_name="%(class)s_created_by",
        db_comment="作成者",
    )
    updated_by = models.ForeignKey(
        "self",
        null=True,
        on_delete=models.SET_NULL,
        related_name="%(class)s_updated_by",
        db_comment="更新者",
    )

    USERNAME_FIELD = "employee_number"
    REQUIRED_FIELDS = ["email", "username"]

    class Meta:
        ordering = ["employee_number"]
        db_table = "User"
        db_table_comment = "システムユーザ"

    def save(self, *args, **kwargs):
        # 既に登録されているシステム利用者情報の保存処理
        if self.id:
            if not "updated_by" in kwargs:
                self.updated_by = self
            else:
                self.updated_by = kwargs.get("updated_by")
                kwargs.pop("updated_by")
        super(User, self).save(*args, **kwargs)

    def __str__(self):
        return self.username


class UserInvitation(models.Model):
    """ユーザ招待用テーブルに対応するモデルクラス"""

    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
        db_comment="システムユーザID",
    )
    token = models.CharField(
        max_length=255,
        db_comment="ユーザ招待用メールURL用トークン",
    )
    expiry = models.DateTimeField(
        null=True,
        default=None,
        db_comment="有効期限",
    )
    created_at = models.DateTimeField(
        auto_now_add=True,
        db_comment="作成日時",
    )
    user = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        related_name="user_invitation",
        db_comment="社員テーブル外部キー",
    )
    is_used = models.BooleanField(
        default=False,
        db_comment="使用有無",
    )

    class Meta:
        db_table = "Invitation"
        db_table_comment = "ユーザ招待用"

serializers.py

EmailのSerilaizerを作成します

serializers.py
from rest_framework import serializers


class LoginSerializer(serializers.ModelSerializer):
    """ログイン用シリアライザ"""

    employee_number = serializers.CharField(max_length=255)

    class Meta:
        model = User
        fields = ["employee_number", "password"]

        
class InviteUserSerializer(serializers.ModelSerializer):
    """ユーザ招待用シリアライザ"""

    def create(self, validated_data, created_by, updated_by):
        return User.objects.create_user(
            created_by=created_by, updated_by=updated_by, **validated_data
        )

    class Meta:
        model = User
        fields = [
            "employee_number",
            "username",
            "role",
            "email",
        ]

views.py

actionデコレータを使ってメール送信機能を実装します

views.py
from django.http import JsonResponse
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet
from rest_framework.viewsets import ViewSet

from .models import User
from .serializers import LoginSerializer, UserSerilaizer, InviteUserSerializer
from .emails import send_welcome_email


class UserViewSet(ModelViewSet):
    queryset = User.objects.all()

    def get_serializer_class(self):
        if self.action == "invite_user":
            return EmailSerializer
        else:
            return UserSerilaizer

    @action(detail=False, methods=["POST"])
    def invite_user(self, request):
        """指定したメールアドレス宛へユーザの招待メールを送る

        Args:
            request: リクエスト

        Returns:
            HttpResponse
        """
        # Userの新規登録と招待用トークンを作成する
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.create(
            serializer.validated_data,
            created_by=request.user,
            updated_by=request.user,
        )
        token = secrets.token_urlsafe(64)
        expiry = timezone.now() + timedelta(days=1)
        UserInvitation.objects.create(token=token, user=user, expiry=expiry)
        # 今回はフロントエンドのローカル環境のオリジンを指定
        base_url = "localhost:3000"
        # 初回登録用のURLへ遷移
        url = base_url + "/verify-user/" + token
        send_invitation_email(
            email=user.email,
            url=url,
        )
        return JsonResponse(
            data={"msg": "招待メールを送信しました"},
            status=status.HTTP_200_OK,
        )

emails.py

メール送信用のメソッドを作成します
今回はDjangoのEmailMultiAlternativesを使ってメール送信機能を実装します
EmailMultiAlternativesを使うとHTMLとテキスト両方のバージョンでメールを送信することができます

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


def send_invitation_email(email, url):
    plaintext = render_to_string(
        "invite_user_email.txt",
        {
            "email": email,
            "url": url,
        },
    )
    html_text = render_to_string(
        "invite_user_email.html",
        {
            "email": email,
            "url": url,
        },
    )
    
    msg = EmailMultiAlternatives(
        subject="アカウント登録のお知らせ",
        body=plaintext,
        from_email=None,
        to=[email],
        alternatives=[(html_text, "text/html")],
    )

    # 送信
    msg.send()

templateファイル群

今回は

  • invite_user_email.html
  • invite_user_email.txt

の2種類を作成します

welcome_email.html
<pre>

下記URLへ24時間以内にアクセスしてアカウントの本登録をお願いします。
▼アカウント認証URL
<a href={{url}}>{{ url }}</a>
    
</pre>
welcome_email.txt
下記URLへ24時間以内にアクセスしてアカウントの本登録をお願いします。
▼アカウント認証URL
{{ url }}

メールを送信してみよう!

emailを入力してPOSTします
スクリーンショット 2024-01-09 9.11.37.png

スクリーンショット 2024-01-09 9.11.56.png

http://127.0.0.1:1080にアクセスし、メールが受信できたら成功です

スクリーンショット 2024-01-09 9.10.53.png

メールが送信できたかどうかテストコードを書いてみたい

Pytestでメール送信のテストコードを書きたい方はこちらの記事を書きました

記事の紹介

以下の記事も書きましたのでよかったら読んでみてください

参考

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?