マーケティングメールが迷惑メールになる？判定ツールを作成！

![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/d0ce8bec-728c-f33c-65ee-d7eeaf33200d.png)

メールマーケティングキャンペーンを実施する際、最大の課題の一つは、メッセージが迷惑メールフォルダーではなく受信トレイに届くようにすることです。

この記事では、メールが迷惑メールとしてマークされるかどうか、そしてその理由を検証できるツールを作成します。このツールはAPI形式でオンラインにデプロイされ、ワークフローに統合できるよう設計します。

## 迷惑メール検証の秘密

[Apache SpamAssassin](https://spamassassin.apache.org/) は、Apache Software Foundationによって維持されているオープンソースの迷惑メール検出プラットフォームです。多くのメールクライアントやメールフィルタリングツールが、このツールを使用してメッセージを迷惑メールとして分類しています。

SpamAssassinは、多数のルール、ベイジアンフィルタリング、ネットワークテストを使用して、特定のメールに迷惑メール「スコア」を割り当てます。一般的には、スコアが5以上のメールは迷惑メールとしてフラグ付けされるリスクが高いとされています。

SpamAssassinのスコアリングは透明性が高く、詳細にドキュメント化されているため、メールのどの部分が高い迷惑メールスコアを引き起こしているのかを特定し、文章を改善することが可能です。

## SpamAssassinを使ったメール検証方法

SpamAssassinはLinuxシステムで動作するよう設計されています。Linux OSを使用するか、Dockerコンテナを作成してインストールと実行を行う必要があります。

DebianまたはUbuntuシステムでは、以下のコマンドでSpamAssassinをインストールできます：

```bash
apt-get update && apt-get install -y spamassassin
sa-update
```

`sa-update`コマンドを使用すると、SpamAssassinのルールを最新の状態に保つことができます。

インストール後、メールメッセージをSpamAssassinのコマンドラインツールにパイプ入力できます。出力には迷惑メールスコアや、どのルールがトリガーされたのかが説明された注釈付きのメールが含まれます。

使用例：

```
spamassassin -t < input_email.txt > results.txt
```

この後、`results.txt`にはSpamAssassinのヘッダーとスコアが含まれる処理済みのメールが格納されます。例：

```
X-Spam-Checker-Version: SpamAssassin 4.0.0 (2022-12-13) on 254.254.254.254
X-Spam-Level: 
X-Spam-Status: No, score=0.2 required=5.0 tests=HTML_MESSAGE,
    MIME_HTML_ONLY,MISSING_MID,NO_RECEIVED,
    NO_RELAYS autolearn=no autolearn_force=no version=4.0.0

// ...

Content analysis details:   (0.2 points, 5.0 required)

 pts rule name              description
---- ---------------------- --------------------------------------------------
 0.1 MISSING_MID            Missing Message-Id: header
-0.0 NO_RECEIVED            Informational: message has no Received headers
-0.0 NO_RELAYS              Informational: message was not relayed via SMTP
 0.0 HTML_MESSAGE           BODY: HTML included in message
 0.1 MIME_HTML_ONLY         BODY: Message only has text/html MIME parts
```

## SpamAssassinをAPIとしてラップする

SpamAssassinはAPIとしてカプセル化することで最大の潜在能力を発揮します。この形式では柔軟性が向上し、さまざまなワークフローに統合可能です。

例えば、メールを送信する前に、コンテンツをSpamAssassin APIに送信して検証する仕組みを構築できます。迷惑メールの基準を満たしていないと判断された場合にのみ送信が許可されます。

以下のようなAPIを作成してみましょう。このAPIは次のメールフィールドを受け付けます：`subject`、`html_body`、`text_body`。これらをSpamAssassinに渡し、検証結果を返します。

### API例

```python
from fastapi import FastAPI
from datetime import datetime, timezone
from email.utils import format_datetime
from pydantic import BaseModel
import subprocess

def extract_analysis_details(text):
    lines = text.splitlines()

    start_index = None
    for i, line in enumerate(lines):
        if line.strip().startswith("pts rule"):
            start_index = i
            break

    if start_index is None:
        print("No content analysis details found.")
        return []

    data_lines = lines[start_index+2:]
    parsed_lines = []
    for line in data_lines:
        if line.strip() == "":
            break
        parsed_lines.append(line)

    results = []
    current_entry = None

    split_line = lines[start_index+1]
    pts_split, rule_split, *rest = split_line.strip().split(" ")

    pts_start = 0
    pts_end = pts_start + len(pts_split)

    rule_start = pts_end + 1
    rule_end = rule_start + len(rule_split)

    desc_start = rule_end + 1

    for line in parsed_lines:
        pts_str = line[pts_start:pts_end].strip()
        rule_name_str = line[rule_start:rule_end].strip()
        description_str = line[desc_start:].strip()

        if pts_str == "" and rule_name_str == "" and description_str:
            if current_entry:
                current_entry["description"] += " " + description_str
        else:
            current_entry = {
                "pts": pts_str,
                "rule_name": rule_name_str,
                "description": description_str
            }
            results.append(current_entry)

    return results

app = FastAPI()

class Email(BaseModel):
    subject: str
    html_body: str
    text_body: str

@app.post("/spam_check")
def spam_check(email: Email):
    # assemble the full email
    message = f"""From: example@example.com
To: recipient@example.com
Subject: {email.subject}
Date: {format_datetime(datetime.now(timezone.utc))}
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="__SPAM_ASSASSIN_BOUNDARY__"

--__SPAM_ASSASSIN_BOUNDARY__
Content-Type: text/plain; charset="utf-8"

{email.text_body}

--__SPAM_ASSASSIN_BOUNDARY__
Content-Type: text/html; charset="utf-8"

{email.html_body}

--__SPAM_ASSASSIN_BOUNDARY__--"""

    # Run SpamAssassin and capture the output directly
    output = subprocess.run(["spamassassin", "-t"],
                            input=message.encode('utf-8'),
                            capture_output=True)

    output_str = output.stdout.decode('utf-8', errors='replace')
    details = extract_analysis_details(output_str)
    return {"result": details}
```

上記のコードでは、`extract_analysis_details`というヘルパー関数を定義し、結果レポートからスコアリング理由のみを抽出します。この関数をさらに改良し、特定のルールを結果から除外するなどの機能を追加することもできます。

以下の入力例を見てみましょう：

**subject**

```
Claim Your Prize
```

**html_body**

```html
<h2>Claim Your Prize</h2>
<p>Dear Winner:</p>
<p>Click the link below to claim your prize.</p>
```

**text_body**

```
Claim Your Prize

Dear Winner:
Click the link below to claim your prize.
```

レスポンスの例：

```json
[
  {
    "pts": "0.1",
    "rule_name": "MISSING_MID",
    "description": "Missing Message-Id: header"
  },
  {
    "pts": "-0.0",
    "rule_name": "NO_RECEIVED",
    "description": "Informational: message has no Received headers"
  },
  {
    "pts": "3.1",
    "rule_name": "DEAR_WINNER",
    "description": "BODY: Spam with generic salutation of \"dear winner\""
  },
  {
    "pts": "-0.0",
    "rule_name": "NO_RELAYS",
    "description": "Informational: message was not relayed via SMTP"
  },
  {
    "pts": "0.0",
    "rule_name": "HTML_MESSAGE",
    "description": "BODY: HTML included in message"
  }
]
```

見てください！「Dear winner」は迷惑メールで一般的に使用される挨拶として検出されました。

## APIをオンラインでデプロイする

SpamAssassinを実行するには、ソフトウェアがインストールされたLinux環境が必要です。従来では、EC2インスタンスやDigitalOceanのドロップレットをデプロイする必要がありましたが、これにはコストがかかり、特に低トラフィックの場合は非効率的です。

サーバーレスプラットフォームでは、SpamAssassinのようなシステムパッケージをインストールすることはできません。

[Leapcell](https://leapcell.io/) はこの問題を完全に解決します。

[Leapcell](https://leapcell.io/) を使用すれば、SpamAssassinのようなシステムパッケージをデプロイしつつ、サーバーレスの利便性を保つことができます。つまり、使用回数に応じてのみ料金が発生し、通常はコストを削減できます。

LeapcellでAPIをデプロイするのは非常に簡単です。環境を設定する必要はありません。Pythonイメージをデプロイし、「ビルドコマンド」フィールドを適切に設定するだけです。

![デプロイ設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/9c245bd4-a3f9-bfdd-0a73-805255dee8fa.png)

デプロイが完了すると、迷惑メール検証のためのAPIが使用可能になります。このAPIが呼び出されるたびに、SpamAssassinが実行され、メールのスコアを算出して返します。

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/22f2a367-c4f9-dacf-75d7-4a7c9783c213.png)](https://leapcell.io/)