2
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?

Blastengine × Python で「ラーメン屋の順番が近づいたらメール通知する仕組み」を作ってみた

この記事は 「エンジニアが知っておくべき メール送信・運用ノウハウ、メールの認証技術やセキュリティについて投稿しよう! by blastengine Advent Calendar 2025」 の 7 日目の記事です。

ラーメン屋さんでよくある「順番待ちシステム」、呼び出し時に メール通知 も飛んでくれたら便利ですよね。

本稿では、メール配信サービス blastengine のトランザクション配信 API を使って、

「自分の順番が近づいてきたお客さんに、Python からメール通知を送る」

弊社では店舗内に券売機などのあらゆる情報をダッシュボードにまとめるサーバーがありますので、そこで全て実行するため、すでに入っているPythonにしました。
本来であれば、Dockerなどでまとめた方が運用は楽かと思います。

というミニサービスを実装してみます。

今回やること

  • ラーメン店の想定シナリオ
  • blastengine API のざっくり概要
  • Python でトランザクションメールを送るクライアント実装
  • 「順番が近づいたら通知する」キュー判定ロジック
  • メール運用・認証・セキュリティのポイント(SPF/DKIM・List-Unsubscribe・レート制限など)

想定シナリオ

ラーメン屋さんでこんな運用を想定します。

  • 入店待ちのお客さんは、受付時に メールアドレス+人数 を登録
  • システム側では「受付番号(整理券番号)」を発行
  • 店内の呼び出し番号(現在提供中の整理券番号)が進んだら、
    • 「あと 3 組以内になったタイミング」でメール通知
    • 「○番のお客様、そろそろご来店ください」的な文面

実際にはタブレット受付や LINE 連携など色々ありますが、ここでは 「順番管理テーブル+定期的なバッチ処理」 という単純な構成で考えます。

blastengine API のざっくり概要

blastengine では HTTPS の REST API でメール配信を行えます。

認証(BearerToken)

  • 管理画面で apiKey を発行
  • 「ログインID + apiKey」を SHA256 → 小文字 → base64 でエンコードしたものを BearerToken として使用
  • リクエストヘッダ Authorization: Bearer <生成したトークン> を付けると API を叩けます

トランザクション配信 API

順番通知は「個別のイベント発生時に送るメール」なので、トランザクション配信 を使うのが自然です。

  • エンドポイント: POST https://app.engn.jp/api/v1/deliveries/transaction
  • リクエスト JSON(抜粋)
    • from … 送信元メールアドレス/名前
    • to … 宛先(単一アドレス)
    • subject … 件名
    • text_part … プレーンテキスト本文
    • html_part … HTML本文(任意)
    • list_unsubscribe … List-Unsubscribe ヘッダ設定(任意)

レスポンスは 201 Created のときに { "delivery_id": 1 } のような配信 ID が返ってきます。

レート制限 & 種別

  • レート制限: デフォルトで 500 リクエスト/分
  • 配信種別:
    • TRANSACTION … 即時配信(今回はこちら)
    • BULK … 一斉配信
    • SMTP … SMTP 経由

ラーメン屋の順番通知くらいなら、レート制限を気にする場面はほぼ無いですが、設計としては覚えておくと安心です。

全体アーキテクチャ(シンプル版)

今回はサンプルなので、インフラはざっくりと以下のイメージにします。

  • queue テーブル(or メモリ上のリスト)
    • ticket_no … 受付番号
    • email … お客さんのメールアドレス
    • notified … すでに「順番が近い」通知を送ったかどうか
  • 店員が POS などで「呼び出し番号」を更新
    • current_ticket_no を別テーブル/設定で管理
  • 1 分に一度くらいのバッチ(cron / systemd timer / Cloud Functions など)
    • current_ticket_no を読み込む
    • 「あと N 組以内」に入ったけど、まだ notified = False のレコードを抽出
    • blastengine のトランザクション API でメール送信
    • 送信できたら notified = True に更新

記事では DB の代わりに Python のリスト+簡易ロジック で表現します。

Python 環境準備

想定環境

  • Python 3.10+
  • requests ライブラリを利用
pip install requests python-dotenv

環境変数

セキュリティのため、BearerToken 等は コードにベタ書きしない 前提で書きます。

.env(ローカル開発用)の例:

BLASTENGINE_BEARER_TOKEN=your_generated_bearer_token_here
BLASTENGINE_FROM_EMAIL=ramen@example.com
BLASTENGINE_FROM_NAME=ラーメン屋さん
BLASTENGINE_LIST_UNSUBSCRIBE_URL=https://example.com/unsubscribe

blastengine クライアントを実装する

まずはトランザクション配信をラップするクライアントを作ります。

blastengine_client.py

import os
import logging
from typing import Optional, Dict, Any

import requests
from dotenv import load_dotenv

load_dotenv()

BLASTENGINE_API_ENDPOINT = "https://app.engn.jp/api/v1/deliveries/transaction"
BLASTENGINE_BEARER_TOKEN = os.environ.get("BLASTENGINE_BEARER_TOKEN")
FROM_EMAIL = os.environ.get("BLASTENGINE_FROM_EMAIL", "no-reply@example.com")
FROM_NAME = os.environ.get("BLASTENGINE_FROM_NAME", "順番待ちラーメン")


logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)


class BlastengineError(Exception):
    """blastengine 連携時のエラー"""
    pass


def send_transactional_mail(
    to_email: str,
    subject: str,
    text_part: str,
    html_part: Optional[str] = None,
    list_unsubscribe: Optional[Dict[str, str]] = None,
) -> int:
    """
    blastengine のトランザクション配信 API を叩いてメールを送る。

    :param to_email: 宛先メールアドレス
    :param subject: 件名
    :param text_part: テキスト本文
    :param html_part: HTML 本文(任意)
    :param list_unsubscribe: {"mailto": "...", "url": "..."} など(任意)
    :return: delivery_id
    """
    if not BLASTENGINE_BEARER_TOKEN:
        raise BlastengineError("BLASTENGINE_BEARER_TOKEN が設定されていません")

    headers = {
        "Authorization": f"Bearer {BLASTENGINE_BEARER_TOKEN}",
        "Content-Type": "application/json",
        "Accept-Language": "ja-JP",
    }

    payload: Dict[str, Any] = {
        "from": {
            "email": FROM_EMAIL,
            "name": FROM_NAME,
        },
        "to": to_email,
        "subject": subject,
        "text_part": text_part,
    }

    if html_part:
        payload["html_part"] = html_part

    if list_unsubscribe:
        payload["list_unsubscribe"] = list_unsubscribe

    logger.debug("Blastengine payload: %s", payload)

    resp = requests.post(BLASTENGINE_API_ENDPOINT, headers=headers, json=payload, timeout=5)

    if resp.status_code != 201:
        logger.error("Blastengine Error: status=%s body=%s", resp.status_code, resp.text)
        raise BlastengineError(
            f"blastengine API でエラーが発生しました: {resp.status_code} {resp.text}"
        )

    data = resp.json()
    delivery_id = data.get("delivery_id")
    logger.info("メール送信成功: delivery_id=%s → %s", delivery_id, to_email)
    return int(delivery_id)

ポイント:

  • BearerToken は Authorization: Bearer ... にセット(公式ドキュメントと同じ形)
  • レスポンスコードが 201 以外ならエラー扱い
  • list_unsubscribe を渡せるようにしておく(後述)

「順番が近づいたら通知する」ロジック

次に、順番待ちを表すデータ構造と、「通知すべき人」を判定する関数を書きます。

簡易モデル

本当は DB で持つべきですが、ここでは Python の dataclass で表現します。

queue_logic.py

from __future__ import annotations

from dataclasses import dataclass
from typing import List


@dataclass
class QueueEntry:
    ticket_no: int          # 整理券番号
    email: str              # メールアドレス
    notified: bool = False  # すでに「順番が近い」通知を送ったかどうか


def find_entries_to_notify(
    entries: List[QueueEntry],
    current_ticket_no: int,
    threshold: int = 3,
) -> List[QueueEntry]:
    """
    現在呼び出している番号からみて「あと N 組以内」かつ
    まだ通知していないエントリを返す。

    :param entries: 受付済みのエントリ一覧
    :param current_ticket_no: 現在呼び出している番号
    :param threshold: 何組前になったら通知するか
    """
    targets: List[QueueEntry] = []

    for entry in entries:
        # すでに呼び出し済み or まだかなり先の人はスキップ
        if entry.ticket_no <= current_ticket_no:
            continue

        distance = entry.ticket_no - current_ticket_no

        if 0 < distance <= threshold and not entry.notified:
            targets.append(entry)

    return targets

順番待ち通知サービスのメインスクリプト

ここまでを組み合わせて、「現在の呼び出し番号」をもとに blastengine で通知を送るスクリプトを書きます。

notify_ramen_queue.py

from typing import List

from blastengine_client import send_transactional_mail
from queue_logic import QueueEntry, find_entries_to_notify


def build_mail_text(entry: QueueEntry, current_ticket_no: int) -> str:
    """
    順番通知メールの本文(テキスト)を生成する。
    """
    distance = entry.ticket_no - current_ticket_no

    return f"""\
{entry.ticket_no}番でお待ちのお客様

ラーメン屋「順番待ちラーメン」です。
ただいま {current_ticket_no} 番までご案内が進んでおり、
お客様の順番まで「あと {distance} 組」となりました。

お席の準備ができ次第、順次ご案内いたしますので、
店舗付近でお待ちいただけますと幸いです。

※本メールに心当たりがない場合は破棄してください。
"""


def build_mail_html(entry: QueueEntry, current_ticket_no: int) -> str:
    """
    簡単な HTML 版本文も用意しておく(任意)
    """
    distance = entry.ticket_no - current_ticket_no

    return f"""\
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>順番が近づいてきました</title>
  </head>
  <body>
    <p>{entry.ticket_no}番でお待ちのお客様</p>
    <p>ラーメン屋「順番待ちラーメン」です。</p>
    <p>ただいま <strong>{current_ticket_no} 番</strong> までご案内が進んでおり、<br />
    お客様の順番まで <strong>あと {distance} 組</strong> となりました。</p>
    <p>お席の準備ができ次第、順次ご案内いたしますので、<br />
    店舗付近でお待ちいただけますと幸いです。</p>
    <hr />
    <p>※本メールに心当たりがない場合は破棄してください。</p>
  </body>
</html>
"""


def send_notifications_for_queue(
    entries: List[QueueEntry],
    current_ticket_no: int,
    threshold: int = 3,
    dry_run: bool = True,
) -> None:
    """
    現在の呼び出し番号をもとに、通知すべきエントリにメールを送信する。

    :param entries: 受付済みエントリ一覧(DB の代わりにリストを利用)
    :param current_ticket_no: 現在の呼び出し番号
    :param threshold: 何組前で通知するか
    :param dry_run: True のときは blastengine には投げずログだけ出す
    """
    targets = find_entries_to_notify(entries, current_ticket_no, threshold)

    for entry in targets:
        subject = f"【順番待ち】まもなく {entry.ticket_no} 番のご案内です"
        text_body = build_mail_text(entry, current_ticket_no)
        html_body = build_mail_html(entry, current_ticket_no)

        # トランザクションメールだが、将来的に配信停止を受け付けたい場合は
        # list_unsubscribe を設定できる
        list_unsubscribe = {
            "url": "https://example.com/unsubscribe",  # 実際の退会・配信停止 URL を設定
        }

        if dry_run:
            print("---- DRY RUN ----")
            print("To:", entry.email)
            print("Subject:", subject)
            print(text_body)
            print("-----------------")
        else:
            # 実際に送信
            send_transactional_mail(
                to_email=entry.email,
                subject=subject,
                text_part=text_body,
                html_part=html_body,
                list_unsubscribe=list_unsubscribe,
            )
            # 送信できたらフラグ更新(本来は DB に書き戻す)
            entry.notified = True


if __name__ == "__main__":
    # サンプルデータ
    queue_entries = [
        QueueEntry(ticket_no=1, email="user1@example.com", notified=True),
        QueueEntry(ticket_no=2, email="user2@example.com", notified=True),
        QueueEntry(ticket_no=3, email="user3@example.com", notified=False),
        QueueEntry(ticket_no=4, email="user4@example.com", notified=False),
        QueueEntry(ticket_no=5, email="user5@example.com", notified=False),
        QueueEntry(ticket_no=8, email="user8@example.com", notified=False),
    ]

    current_ticket = 2   # ただいま 2 番をご案内中
    threshold = 3        # 「あと 3 組以内」で通知

    # テスト時は dry_run=True にしておく
    send_notifications_for_queue(
        entries=queue_entries,
        current_ticket_no=current_ticket,
        threshold=threshold,
        dry_run=True,
    )

このスクリプトを実行すると、current_ticket_no = 2 のときに、
ticket_no 3, 4, 5 のお客さんに対する通知メール本文が「DRY RUN」として出力されます。

実運用では dry_run=False にして、DB から取得したキューを使うイメージです。

メール送信・運用ノウハウ&セキュリティの話

Advent Calendar のテーマにもあるので、blastengine のドキュメントを踏まえつつ、実運用で意識すべきポイントをいくつかまとめます。

1. SPF / DKIM / DMARC の設定

  • SPF: 送信元 IP からのメールを許可する DNS 設定
  • DKIM: 電子署名で改ざん検知・なりすまし防止(blastengine では署名を管理できる API 群 signatures が用意されています)
  • DMARC: SPF/DKIM 結果を踏まえて「なりすまし時にどうするか」を宣言するポリシー

特に Gmail / Office365 / キャリアメール向けには、SPF+DKIM+DMARC の 3 点セット を整えておくのがほぼ必須です。

blastengine 側で DKIM 署名を設定し、送信ドメインの DNS に公開鍵を登録しておく構成にしておくと、安全かつ到達率も上がります。

2. トランザクションメールでも「うざくない運用」を意識

順番通知はトランザクションメール(サービス運用上必要な通知)に分類されますが、それでも以下は意識した方が良いです。

  • 1 件の来店につき、通知は 1〜2 通までに抑える
  • 文面に「本メールに心当たりがない場合は〜」などの一文を入れる
  • 将来的に「順番通知のメールは不要」というユーザー向けに、退会 or 通知 OFF の導線を用意しておく

blastengine の API では list_unsubscribe というヘッダを設定できます。これは Gmail などで「配信停止」ボタンとして UI に出ることもあるヘッダで、URL か mailto を指定できます。

"list_unsubscribe": {
  "url": "https://example.com/unsubscribe/XXXXXXXX"
}

公式ドキュメントでは、「mailto/url を使う場合は DKIM の作成者署名を設定し、データサイズを一定バイト数以下に抑える」といった注意点も記載されています。

3. レート制限と再送制御

blastengine API には 1 分あたり 500 リクエストの呼び出し制限があります。

ラーメン店規模では余裕がありますが、今後他店舗展開して順番待ち通知が大量に飛ぶようなケースでは、

  • 複数店舗のジョブを分散させる
  • 429 (Too Many Requests) を受け取ったら Retry-After を見てリトライ
  • 「1 件のエラーでキューが止まらない」ように設計

といった運用を考えておくと安心です。

4. 配信ログ・エラーログを定期チェック

blastengine には配信ログ・エラーリストを取得する API もあります。

  • バウンス(HARDERROR)になっているアドレスは除外する
  • エラー停止アドレス(DROP)に対しては再送しない
  • キャリアメールなどでドメイン制限にひっかかっていないかを見る

などを定期的に回していくと、配信品質の維持 に繋がります。

5. 機密情報の扱いと権限管理

  • BearerToken をコードに直書きしない(環境変数・Secret Manager 等に)
  • blastengine の管理画面アカウントの権限も細かく分ける
  • 開発環境・ステージング環境用の別アカウント or 別 API キーを用意する

順番待ちのような一見カジュアルな通知であっても、メールアドレス自体は立派な個人情報なので、鍵の管理と権限設計 はしっかりやっておきたいところです。
いい気づきですね。その視点、とてもQiita向きです。
記事の締めや「振り返り」セクションとして使えるように、そのまま貼れる文章としてまとめました。

実装してみての所感(没になったけど、blastengine は強い)

今回、実際にこの仕組みをローカルで動かしてみて、

「そもそも“メールだけで通知”という体験が、すでに時代遅れなのではないか?」

という根本的な課題に気づき、このラーメン屋の順番通知サービス自体は最終的に没案となりました。

現在では、

  • LINE通知
  • アプリのプッシュ通知
  • ブラウザ通知(Web Push)
  • SMS

など、より即時性が高く、開封率の高い通知手段が当たり前になっており、
「順番が近づいたら“メールだけ”で知らせる」というUXは、どうしても弱く感じてしまいます。

それでも blastengine が「強い」と感じた理由

一方で、実装を通して強く感じたのは、

「blastengine は“確実にメールを届けたい”という要件に対して、かなり信頼できるサービスである」

という点です。

特に以下の点は、業務システムや重要通知用途では非常に大きな価値だと感じました。

  • ✅ トランザクションメールを API 一発で安定して送れる
  • ✅ SPF / DKIM / DMARC などの 到達率・なりすまし対策の前提が整っている
  • list-unsubscribe など、迷惑メール対策の実装が容易
  • ✅ レート制限やエラー管理など、運用を前提とした設計

前職でblastengineを知っていたらすぐに移行したと思います。

今回の「順番待ち通知」は没になりましたが、

  • 会員登録の本登録メール
  • パスワードリセット
  • 決済完了通知
  • 契約書送付
  • 障害・インシデント通知

といった “確実に届かないと困るメール”の基盤としては、blastengine は非常に有力な選択肢だと感じました。

「時代遅れなプロダクト」でも、実装から得られる学びは大きい

今回の経験で改めて実感したのは、

  • プロダクトのアイデア自体が没になることはよくある
  • しかし、その過程で触った 技術・サービス・運用知識は必ず次に生きる

ということです。

今回であれば、

  • トランザクションメールの設計
  • APIベースのメール配信
  • メール認証(SPF / DKIM / DMARC)
  • メール運用の実務的な注意点

といった知識は、別のプロダクトや業務システムでそのまま再利用可能です。

まとめ(個人的な結論)

  • 「メールだけで順番通知」は UX 的に時代遅れ → 企画としては没

  • しかし、

    • blastengine 自体の信頼性・実用性は非常に高い
    • “確実に届ける必要があるメール”用途では今後も積極的に使いたい
  • 没プロダクトでも、
    技術検証としては十分すぎるほどの価値があった

というのが今回の正直な結論です。

2
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
2
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?