[FastAPI]SESを使用し、再招待メールを送信しよう！


# はじめに
本記事では、**FastAPI**、**AWS SES**、**SQLAlchemy**、**PostgreSQL**を使用して、システムにおける**再招待メール**の機能を実装する方法について解説します。再招待メールが必要な背景やシナリオを含め、実装手順を技術的に詳細に説明し、初心者でも理解できる内容にしています。

# 再招待メールが必要な背景

再招待メールは、システムでユーザーを招待する際に、さまざまな理由で最初の招待メールが無効になった場合や、ユーザーが登録手続きを完了しなかった場合に必要になります。以下は、再招待メールが必要となる代表的なシナリオです。

### 再招待メールが必要なシナリオ

1. **トークンの有効期限切れ**  
   招待メールに含まれるトークンには有効期限が設定されている場合があり、ユーザーがその期間内にメールを確認せず、登録を完了しなかった場合は、再招待が必要です。

2. **ユーザーがメールを見落とした、または削除した**  
   ユーザーが誤ってメールを削除したり、スパムフォルダに入ってしまったりして、招待メールを見逃してしまう場合があります。この場合、ユーザーが再度招待メールをリクエストすることがあります。

3. **メール送信エラー**  
   招待メールの送信時に、メールアドレスが無効だったり、メールサーバーの問題でメールが送信されなかった場合、メールアドレスの確認後に再度招待メールを送信する必要があります。

4. **ユーザーが登録を途中で放棄した**  
   ユーザーが招待メールを受け取ったものの、何らかの理由で登録を完了しなかった場合、再招待を送ることで、ユーザーに登録を促します。

# データベース設計（PostgreSQL）

再招待メールの実装には、**ユーザー情報**と**招待トークン**を管理するために、PostgreSQLに2つのテーブルを作成します。

### ユーザーテーブル（users）

| カラム名      | データ型      | 説明                              |
|---------------|---------------|-----------------------------------|
| **id**        | INTEGER       | ユーザーの一意のID（主キー）。    |
| **email**     | VARCHAR(255)  | ユーザーのメールアドレス（ユニーク）。|
| **name**      | VARCHAR(255)  | ユーザー名。                      |
| **is_active** | BOOLEAN       | アクティブかどうかを示すフラグ（招待を受けた後に有効化）。 |

```sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(255),
    is_active BOOLEAN DEFAULT FALSE
);
```

### 招待トークンテーブル（invitation_tokens）

| カラム名      | データ型      | 説明                             |
|---------------|---------------|-----------------------------------|
| **id**        | INTEGER       | トークンの一意のID（主キー）。   |
| **email**     | VARCHAR(255)  | 招待されたメールアドレス。        |
| **token**     | VARCHAR(255)  | 招待トークン。                    |
| **is_used**   | BOOLEAN       | トークンが使用済みかどうかを示すフラグ。 |
| **expires_at**| TIMESTAMP     | トークンの有効期限。              |

```sql
CREATE TABLE invitation_tokens (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    token VARCHAR(255) NOT NULL,
    is_used BOOLEAN DEFAULT FALSE,
    expires_at TIMESTAMP NOT NULL
);
```

# FastAPIのルート設計

再招待メールを送信するAPIは、既存の招待メール送信APIに追加する形で実装します。具体的には、次の2つのエンドポイントが必要です。

1. **招待メール送信API**: 新規のユーザー招待時にメールを送信します。
2. **再招待メール送信API**: 招待メールを受け取っていない、またはトークンが有効期限切れのユーザーに再度メールを送信します。


# 再招待メール送信のシナリオとフロー

再招待メールの送信フローは、次の手順で実施されます。

1. **再招待リクエストの受け取り**  
   ユーザーからの再招待リクエストがサーバーに送られます。リクエストには、ユーザーのメールアドレスが含まれます。

2. **既存のトークンの確認**  
   データベースで該当するユーザーのメールアドレスに対して、既に発行されているトークンが有効かどうか確認します。トークンが未使用かつ有効期限内であれば、再招待は必要ありません。

3. **トークンの再生成**  
   トークンが無効、もしくは期限切れの場合、新しいトークンを生成し、再度データベースに保存します。

4. **AWS SESによる再招待メールの送信**  
   新しいトークンを使用して、AWS SESで再招待メールをユーザーに送信します。

# 5. AWS SESを用いたメール送信

AWS SESを使って招待メールを送信します。メールの本文には、生成したトークンを含めます。

```python
import boto3
from botocore.exceptions import NoCredentialsError
from datetime import datetime, timedelta

# SESクライアントの初期化
ses_client = boto3.client('ses', region_name='us-east-1')

def send_invitation_email(email: str, token: str):
    try:
        response = ses_client.send_email(
            Source='noreply@example.com',
            Destination={
                'ToAddresses': [email]
            },
            Message={
                'Subject': {'Data': 'You are invited to join the system'},
                'Body': {
                    'Text': {'Data': f'Please use this token to register: {token}'}
                }
            }
        )
        return response
    except NoCredentialsError:
        print("AWS credentials not found")
```

# 再招待メールのAPI実装

再招待メール送信のAPIを実装します。このAPIは、既存のトークンが無効または期限切れの場合に新しいトークンを生成し、再度送信します。

### データベース接続設定

まず、SQLAlchemyを使ってデータベース接続を設定します。

```python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

DATABASE_URL = "postgresql://user:password@localhost/user_registration"

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
```

### ユーザーモデルとトークンモデル

SQLAlchemyでテーブルモデルを定義します。

```python
from sqlalchemy import Column, Integer, String, Boolean, DateTime

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    name = Column(String)
    is_active = Column(Boolean, default=False)

class InvitationToken(Base):
    __tablename__ = 'invitation_tokens'
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String)
    token = Column(String)
    is_used = Column(Boolean, default=False)
    expires_at = Column(DateTime)
```

### 再招待メール送信API

```python
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
import uuid
from datetime import datetime, timedelta

app = FastAPI()

# データベースセッションの依存関係
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.post("/resend-invite/")
def resend_invite(email: str, db: Session = Depends(get_db)):
    # 既存の招待トークンを確認
    invitation = db.query(InvitationToken).filter_by(email=email, is_used=False).first()
    
    # トークンの期限が切れているか、無効な場合
    if invitation is None or invitation.expires_at < datetime.utcnow():
        # 新しいトークンを生成
        new_token = str(uuid.uuid4())
        expires_at = datetime.utcnow() + timedelta(days=2)  # 新しい有効期限
        new_invitation = InvitationToken(email=email, token=new_token, expires_at=expires_at)

        # データベースに保存
        db.add(new_invitation)
        db.commit()

        # SESを使用して再招待メールを送信
        send_invitation_email(email, new_token)
        return {"message": "New invitation sent"}
    
    else:
        # トークンがまだ有効な場合は、既存のトークンで再送信
        send_invitation_email(email, invitation.token)
        return {"message": "Existing invitation resent"}
```

# まとめ

再招待メールは、ユーザーが招待メールを見逃したり、トークンが無効になった場合に非常に重要な機能です。本記事では、FastAPI、PostgreSQL、AWS SESを用いて再招待メールの実装を行いました。再招待が必要となるシナリオを踏まえ、トークンの管理と再招待のフローを効率的に設計しました。

**今後の改善点**として、招待トークンの有効期限を動的に設定したり、再招待の頻度を制御する仕組みを導入することで、より柔軟なシステムを構築することが可能です。