メールの挨拶と署名をルールベースで消そうとして、初見メール5件中3件で失敗した話

ルール作成時の12件では全勝だった解析器が、初見サンプルでは箇条書きを署名と誤認して本文を消してしまった実験記録。「迷ったら残す」安全策と初見テスト運用を決めるまで。

#テキスト解析#テスト#TypeScript

ビジネスメールのやり取りを LINE や Slack のようなチャットの吹き出しとして表示するスマホアプリ「MelTalk」を作っています。コンセプトは「メールを溶かして、会話にする。」です。

相手のメールをそのまま吹き出しに入れると、毎回「いつもお世話になっております。株式会社〇〇の佐藤です。」から始まり、本題の後ろに10行近い署名と、これまでの往復の引用履歴がぶら下がります。たった2行の返事を読むために毎回スクロールするのは、チャットとは呼べません。吹き出しの中には、その1通で新しく書かれた本題だけを出したいのです。

ところが、いざ「挨拶と署名と引用を自動で取り除く仕組み」を作ってみると、ルールを書いていたときには気づかなかった罠をいくつも踏みました。ルールを作りながら用意した12通のメールでは12通とも完璧に動いたのに、ルールを書き終えてから新しく作った「初見のメール」5通に試したら、3通で失敗したのです。

なぜ LLM ではなく、端末の中のルールでやるのか

いまなら「メールの本文を大規模言語モデル(LLM)に渡して、本題だけ抜き出してもらえばいいのでは」と考えるのが自然だと思います。文脈を読んで挨拶と本題の境目を見分けるのは、LLM がいちばん得意な仕事です。

それでも MelTalk で受信メールの解析を端末内のルールベースにしたのには、はっきりした理由があります。

  1. 他人のメール本文を外部のサーバーに送りたくない。 就活の選考結果や研究室のやり取り、取引先との連絡など、メールには外に出せない個人情報が詰まっています。サーバーを持たず、端末の中で解析を完結させれば、「受信メールの本文を外部に保存・送信しない」と胸を張って言えます
  2. Gmail の審査の壁がある。 Gmail のメールを読み取るスコープは Google の「制限付きスコープ」にあたります。データを外部サーバーに送信・保存する構成にすると、一般公開の審査が格段に重くなり、年1回のセキュリティ評価まで求められる可能性があります。端末内処理で完結させることは、個人の開発として現実的にリリースするための条件でもありました
  3. 毎回通信を待たずに、一覧を一瞬で出したい。 受信トレイを開いたときに何十通ものメールを即座に吹き出しにするには、端末の中で数ミリ秒で終わる処理が必要です

そこでまず、「正規表現と行単位のヒューリスティックだけで、どこまで挨拶・署名・引用を剥がせるか」を試作して確かめることにしました。

12通で12勝0敗、でもそれは「ルールが答えを知っていた」だけ

試作では、就活・研究室・インターン・メルマガ・自動通知などを想定した架空のメール(.eml ファイル)を用意し、次の4つの処理を順番にかけるパイプラインを組みました。

処理最初に書いたルール
分類(対話 / 通知)List-Unsubscribe や Auto-Submitted などのヘッダーがあれば通知。返信(In-Reply-To あり)なら対話。送信元が noreply@ や info@ なら通知
引用履歴の除去Gmail の「2026年10月14日(水)...」、Outlook の「差出人: / 送信日時:」、「-----Original Message-----」以降を除去。> で始まる行は、後ろに本文が続く場合だけ「行間返信」とみなして残す
署名の除去①区切り線(-- や罫線)以降が12行以内なら署名 ②結びの挨拶の後ろに句点のない短い行だけが続けば署名 ③末尾に連絡先らしい行(TEL・〒・株式会社 など)のまとまりがあれば署名
挨拶・宛名の除去先頭4行以内の敬称(様・先生・さん)で終わる行までを宛名として除去。冒頭の定型挨拶と名乗り(「鈴木です。」)を文単位で除去

最初に用意した12通のサンプルに対してこのルールを走らせると、12通中12通すべてで、人間が期待したとおりの本題だけがきれいに抜き出せました。

一瞬「ルールベースで十分いけるじゃないか」と喜びかけましたが、すぐに思い直しました。この12通は、ルールを書きながら何度も見比べて、通るようにルールを足していったサンプルです。言ってみれば、試験問題を横に見ながら作ったカンニングペーパーで満点を取ったようなもので、未知のメールに対する実力を測ったことにはなりません。

そこで、ルールに一切手を触れずに、新しく5通の「初見サンプル(ホールドアウト)」を作ってそのまま流し込んでみました。結果は散々でした。

サンプル種別結果何が起きたか
01〜12ルール作成時12 / 12 成功ルールが答えを知っている参考値
13 HTML のみのメール初見成功タグを剥がしたあと通常のルールで処理できた
14「平素より大変お世話になっております」初見失敗(残りすぎ)辞書に「平素より」「〜でございます」「お願い申し上げます」がなく、挨拶が残った
15 箇条書きの直後に署名初見失敗(本文の消しすぎ)句点のない箇条書き(・学生証 など)を署名の一部と誤認し、本文を消した
16 引用ヘッダーの折り返し初見成功(偶然)引用検出は失敗したが、別の署名ルール(@ を含む行)がたまたま消した
17 info@ からの最初の連絡初見失敗(誤分類)返信ではないためアドレスだけで判定し、人間からの大事な連絡を「通知」に埋もれさせた

5通のうち、まともに成功したのは1通だけ(もう1通は別のルールが偶然カバーしただけ)で、実質5件中3件で失敗しました。

失敗にも「笑って許せる失敗」と「絶対にしてはいけない失敗」がある

この3件の失敗を並べてみて、初めて気づいたことがあります。それは、失敗の種類によってユーザーへのダメージがまったく非対称だということです。

サンプル14のように、「平素より大変お世話になっております」が辞書から漏れて吹き出しに残ってしまう失敗は、見た目が少し野暮ったくなるだけです。読み手は1行読み飛ばせば済みますし、大事な情報は1文字も失われていません。

しかし、サンプル15は致命的でした。面接の持ち物を知らせるメールの末尾に、句点のない箇条書きが並んでいました。

当日は以下のものをご持参ください。
・学生証
・筆記用具
・履歴書(写真貼付)

よろしくお願いいたします。
株式会社サンプル 人事部 佐藤

最初に書いた署名ルール②は、「結びの挨拶や末尾付近で、句点(。)のない短い行が続いていたら、部署名や名前の並び(署名)とみなして削る」というものでした。このルールが ・学生証 などの箇条書きまで「句点のない短い行の塊」に巻き込んでしまい、肝心の持ち物リストを丸ごと消し去ってしまったのです。もし最後の1文まで消えて本文が空になっていれば安全策(空になったら元の全文に戻す)が働いたのですが、「当日は以下のものをご持参ください。」という1文だけが残ったため、空判定にも引っかかりませんでした。吹き出しだけを見たユーザーは、持ち物が書いてあることにすら気づけません。

同じくらい重かったのがサンプル17です。小さな会社や研究室、イベント運営などでは、共用アドレスの info@ から人間が手書きで最初の連絡を送ってくることがよくあります。それをアドレスの文字列だけで「自動通知・メルマガ」のタブに放り込んでしまうと、返信すべき大事な連絡に気づけなくなります。

「迷ったら残す」を設計の真ん中に据える

この失敗から、MelTalk のルール設計の原則を「いかにきれいに削るか」から「絶対に本文を消しすぎない・連絡を埋めない(迷ったら残す)」へ切り替えました。要件定義にも非機能要件(NF-03)として明文化し、次の対策を実装しました。

  1. 署名の開始位置を「連絡先らしい行」以降に限定する。 「句点のない短い行」という曖昧な条件で削るのをやめ、TEL・〒・メールアドレス・URL・株式会社・大学 などの明確な連絡先パターンが現れた行から下だけを署名候補にしました。その上にある箇条書き(・ や -、数字で始まる行)は、絶対に署名に含めません
  2. 削った挨拶や署名は「消す」のではなく「折りたたむ」。 吹き出しの中で削った部分があるときは、ワンタップで「挨拶・署名を表示」と展開できるようにし、さらに元のメール全文を見る画面もすぐ開けるようにしました。「何かが削られていること」自体を隠さない設計です
  3. info@ だけで通知と決めつけない。 自動通知と即断するのは noreply@ などの返信不可アドレスや List-Unsubscribe ヘッダーがあるときだけにしました。さらに、万が一分類が違ったときのために、ユーザーが手動で「対話 / 通知」を切り替えたらその送信元アドレスを記憶する機能(F-07)を入れました
  4. 相手の署名から「所属」と「敬称」を逆利用する。 受信時に剥がした署名は、捨てるのではなく返信の下書き作り(スマート送信)に活かします。署名に「〇〇大学 △△研究室」とあれば宛名の所属に使い、肩書きに「教授」「准教授」とあれば、宛名の敬称を「様」ではなく「先生」に自動で切り替えるようにしました

また、cleanBody.ts のコードを見直す中で、「罫線で囲まれたお知らせ本文」「結びの挨拶の後ろに書かれた追伸(P.S.)」「冒頭が『〜さん』で終わる1行本文」など、本文を消しすぎる恐れがある境界ケースをさらに3つ見つけ、先に再現テスト(overtrim-test.ts)を書いてからすべて塞ぎました。

初見サンプル第2弾(7件)で「消しすぎ0件」を確かめる

対策を入れたあと、さきほどの初見サンプル(13〜17)をもう一度走らせると5件とも通りました。でも、一度失敗を見て対策を練った時点で、その5件はもう「初見」ではありません。

そこで、同じ過ちを繰り返さないよう、「精度を測るときは必ず新しい初見サンプルを書き、ルールを一切調整せずに1回だけ測って記録する」という運用を開発ガイドのルール(D-023)にしました。さっそく第2弾として、ルールを見ずに7件の新しいメール(holdout2-test.ts)を作って測った結果がこちらです。

サンプル結果内容
H1 英文のメール失敗(残りすぎ)Hi Taro, や Best regards, と英語の署名が残った
H2 本文が URL で終わる成功末尾の URL を署名と誤認せず本文に残せた
H3 挨拶と名乗りが読点でつながる失敗(残りすぎ)「お世話になっております、サンプル商事の佐藤です。」が1文になっていて残った
H4 結びに修飾語が2つ重なる失敗(残りすぎ)「引き続きどうぞよろしくお願いいたします。」が辞書から漏れて残った
H5「iPhoneから送信」失敗(残りすぎ)端末の自動署名1行が残った
H6 1段落で名前だけの結び失敗(残りすぎ)末尾の「花子」だけが残った
H7「ご確認のほど、」の後に署名成功結びと署名を正しく剥がせた

成功は7件中2件、失敗は5件でした。数字だけ見るとまだ発展途上ですが、私にとって一番大事だったのは、失敗した5件がすべて「挨拶や署名が残りすぎた」側であり、「本文を誤って消してしまった」件数が 0 件だったことです。記録を残したあとで、この5件のパターンも辞書とルールに追加し、回帰テストに組み込みました。

ルールベースのテキスト処理は、作っている最中のサンプルだけを見ていると「100%動いている」という錯覚に陥りやすい分野です。「答えを知らないテストデータを別に持つこと」と、「間違えたときにどちら側へ倒れるかを最初から決めておくこと」。この2つが、AI を使わない泥臭いルール作りでいちばん頼りになる命綱だと実感しました。

このノートに出てくるプロダクト

ほかのノート

ノートの一覧に戻る