1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【MailOrca 実装実況④】FastAPI + Jinja2 で「それっぽい」画面にしてみた【Web UI 編】

Posted at

はじめに

 前回は、メールの生データをパースしてメモリに蓄えるところまでを解説しました。

 最終回となる今回は、そのデータをブラウザで見やすく表示する Web UI 部分 についてお話しようと思います。

1. FastAPI + Jinja2 の王道構成

 MailOrca の画面は、FastAPI 標準の Jinja2Templates を使ってレンダリングしています。

@app.get("/", response_class=HTMLResponse)
async def index(request: Request) -> Response:
    """Render the main page listing received emails."""
    return templates.TemplateResponse(
        request=request,
        name="list.html",
        context={
            "mails": STORE.mails,
            "columns": CONFIG["ui"]["list_columns"],
        },
    )

 さりげないこだわりは、一覧画面の「カラム」を固定せず、config.json で制御可能な変数 CONFIG から動的に生成している点です。「Date」「Subject」「To」など、ユーザーが見たい項目を自由に入れ替えられるようにしています。

MailOrca_01.png

2. HTML メールの「CSS汚染」を防ぐ iframe

 もしかしたら、これが今回一番の工夫かもしれません。
 HTML メールを表示する際、そのままページ内に埋め込むと、メール内の CSS が管理画面全体のデザインを壊してしまう ことがあります(いわゆる CSS 汚染)。

 これを防ぐため、詳細画面では <iframe>srcdoc 属性を使っています。

<!-- detail.html -->
<iframe
  srcdoc="{{ mail.parsed.body_html }}"
  style="width: 100%; height: 500px; border: none;"
></iframe>

 こうすることで、メールのコンテンツを完全に隔離されたサンドボックス内で表示でき、管理画面の Bootstrap が崩れる心配もありません。

 ただし、一つ注意点があります。そのメールにリンクがあり、それをクリックした場合、相手先が iframe 内は不可と拒否される場合があります。そのリンクが target="_blank" となっていれば、別タブで開くので大丈夫でしょう。

3. 地味に便利な「URLリンク化」フィルタ

 プレーンテキストのメールを表示するとき、URL がただの文字列だと不便ですよね。そこで、Jinja2 のカスタムフィルタを作って、URL を自動的に <a> タグで囲うようにしました。

def urlize_text(text: str) -> str:
    # XSS対策で一旦エスケープ
    escaped = html.escape(text)
    # 正規表現でURLを置換
    return re.sub(r'(https?://[^\s]+)', r'<a href="\1" target="_blank">\1</a>', escaped)

templates.env.filters["urlize"] = urlize_text

 これをテンプレート側で {{ mail.parsed.body_text | urlize | safe }} と呼び出すだけで、使い勝手がグッと上がります。

4. API も生データダウンロードも

 Web UI だけでなく、自動化テストなどでも使いやすいように JSON API を生やしています。

 また、「やっぱりメーラーで確認したい」という時のために、受け取ったデータをそのまま .eml ファイルとしてダウンロードできる機能も実装しました。

@app.get("/mail/{mail_id}/download")
async def download_raw(mail_id: str):
    # 生の bytes をそのままレスポンスとして返す
    return Response(
        content=mail["raw"],
        media_type="message/rfc822",
        headers={"Content-Disposition": f"attachment; filename={mail_id}.eml"}
    )

おわりに

 年末年始に始めた MailOrca 開発は良い暇潰し……もとい、勉強になりました。実際に作ってみると、使い慣れた Python でも「非同期サーバーの共存」や「メールパースの泥臭さ」など、新しい発見がたくさんありました。

 「ここをもっとこうしたい!」というアイディアはまだまだ尽きませんが、ひとまず「自分が(Django などでの)開発で使いたいツール」としては満足のいく形になったと思います。

 もし興味を持っていただけたら、ぜひ GitHub を覗いてみてください。そして、皆さんも自分だけの「オレ得ツール」を作って、快適な開発ライフを!

 全4回、お付き合いいただきありがとうございました!

参考

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?