凡庸な「Like演算子」を捨てろ:正規表現とルールエンジンによる、極限のメール自動仕分け設計論
業務自動化の現場で、多くのエンジニアが陥る罠がある。それは、「件名に『請求書』が含まれていたらフォルダを移動する」といった、`Like`演算子や`InStr`関数による単純な文字列比較で、複雑な業務ロジックを構築しようとすることだ。
もし君が、`If mail.Subject Like “*[重要]*” Or mail.Subject Like “*注文*” Then…` といった、条件分岐の迷宮(Spaghetti Code)を書き始めているなら、今すぐその手を止めてほしい。それは「自動化」ではなく、単なる「コードによる負債の蓄積」だ。
実務におけるメールのパターンは、もっと残酷で、もっと複雑だ。
「注文番号 `ORD-12345` が件名に含まれている」
「本文中に `[承認依頼]` というタグがあり、かつその後に日付形式の文字列が続く」
「特定の正規化されたメールアドレス形式を持つ送信者からのメール」
これらを、いかにして「バグを許さず」「保守コストを最小限に抑え」「高パフォーマンスに」実装するか。本稿では、`VBScript.RegExp`を核とした、プロフェッショナルな「ルールエンジン型」の設計思想を伝授する。
1. なぜ「If文の羅列」は破綻するのか
設計者として、まず以下の3つのリスクを認識せよ。
1. 密結合(Tight Coupling)の恐怖: 振り分け条件(ビジネスロジック)と、メール監視の仕組み(インフラ層)が混ざると、条件を一つ変えるたびに監視ロジック全体を書き換え、再テストする必要が生じる。
2. パターンの脆弱性: `Like`演算子では、空白の有無、全角半角の揺らぎ、複雑な数字のパターンに対応できない。
3. パフォーマンスの劣化: 受信メールが急増した際、肥大化した`If-Else`ツリーを上から順に評価していく設計は、CPUサイクルを無駄に消費し、Outlookの応答性を著しく低下させる。
我々が目指すべきは、「条件(Pattern)」と「実行(Action)」を分離した、データ駆動型の設計である。
2. アーキテクチャ設計:ルールエンジンの概念
堅牢なシステムを作るには、以下の3層構造を意識せよ。
- Rule Layer (データ層): 「どのパターン」を「どこへ」送るかという定義。
- Engine Layer (ロジック層): 正規表現を用いてパターンを照合し、ルールを判定する心臓部。
- Event Layer (インターフェース層): Outlookのイベントを検知し、Engineへ渡すトリガー。
3. 実装:プロダクション品質のコード例
ここでは、保守性を極限まで高めた実装例を示す。単なるコピペではなく、各オブジェクトの役割を理解してほしい。
A. クラスモジュール: `clsMailRule`
まず、ルールを「オブジェクト」として定義する。これにより、条件の追加・変更が容易になる。
' クラスモジュール名: clsMailRule
Option Explicit
Public Pattern As String ' 正規表現パターン
Public TargetType As String ' "Subject" または "Body"
Public ActionType As String ' "Move", "Flag", "Read"
Public TargetValue As String ' 移動先フォルダ名やフラグ設定値
Public IsActive As Boolean ' ルールの有効/無効
' コンストラクタ的な役割の初期化メソッド
Public Sub Init(pPattern As String, pTarget As String, pAction As String, pValue As String)
Me.Pattern = pPattern
Me.TargetType = pTarget
Me.ActionType = pAction
Me.TargetValue = pValue
Me.IsActive = True
End Sub
B. 標準モジュール: `modRegexEngine`
ここがシステムの心臓部だ。`VBScript.RegExp`を使い、ルールを評価する。
' 標準モジュール名: modRegexEngine
Option Explicit
' ルールセットを保持するコレクション
Private RuleCollection As Collection
' システムの初期化:ここでルールを定義する(本来はExcelやDBから読み込むのが理想)
Public Sub InitializeEngine()
Set RuleCollection = New Collection
Dim rule As clsMailRule
' ルール1: 件名に「ORD-数字5桁」が含まれる場合、特定のフォルダへ移動
Set rule = New clsMailRule
rule.Init "^ORD-\d{5}", "Subject", "Move", "注文管理フォルダ"
RuleCollection.Add rule
' ルール2: 本文に「【重要】」が含まれ、かつ「期限:YYYY/MM/DD」の形式がある場合、フラグ付与
Set rule = New clsMailRule
rule.Init "【重要\].*期限:\d{4}/\d{2}/\d{2}", "Body", "Flag", "True"
RuleCollection.Add rule
' ルール3: 特定のドメインからのメールを既読にする
Set rule = New clsMailRule
rule.Init "@critical-system\.com$", "Subject", "Read", ""
RuleCollection.Add rule
End Sub
' メインロジック:メールを解析し、ルールに合致すればアクションを実行する
Public Sub ProcessMail(ByVal mail As Outlook.MailItem)
Dim rule As clsMailRule
Dim regEx As Object
Set regEx = CreateObject("VBScript.RegExp")
' 正規表現の基本設定
With regEx
.Global = True
.IgnoreCase = True
.MultiLine = True
End With
On Error GoTo ErrorHandler
For Each rule In RuleCollection
If Not rule.IsActive Then GoTo NextRule
' 判定対象のテキストを決定
Dim targetText As String
If rule.TargetType = "Subject" Then
targetText = mail.Subject
Else
targetText = mail.Body ' 本文が巨大な場合は、必要に応じてHTMLBodyとの使い分けを検討せよ
End If
' 正規表現マッチング実行
regEx.Pattern = rule.Pattern
If regEx.Test(targetText) Then
' マッチした場合、アクションを実行
ExecuteAction mail, rule
Exit Sub ' 一つのメールに対して一つのアクションで終了する場合
End If
NextRule:
Next rule
Exit Sub
ErrorHandler:
Debug.Print "Error processing mail: " & Err.Description
' 実務ではここでログファイルへの書き出しを行うこと
End Sub
' アクション実行部:ここで具体的な処理を分離する
Private Sub ExecuteAction(ByVal mail As Outlook.MailItem, ByVal rule As clsMailRule)
Select Case rule.ActionType
Case "Move"
' フォルダ移動ロジック(フォルダの存在チェックを忘れるな)
Dim targetFolder As Outlook.MAPIFolder
Set targetFolder = mail.Parent.Session.GetDefaultFolder(olFolderInbox).Folders(rule.TargetValue)
mail.Move targetFolder
Case "Flag"
mail.FlagRequest.MarkAsTask
mail.Importance = olImportanceHigh
Case "Read"
mail.UnRead = False
End Select
End Sub
C. ThisOutlookSession
イベントハンドラは、極限までシンプルに保つ。
' ThisOutlookSession
Option Explicit
Private WithEvents objItems As Outlook.Items
Private Sub Application_Startup()
' 受信トレイの監視を開始
Dim inbox As Outlook.Folder
Set inbox = Application.GetNamespace("MAPI").GetDefaultFolder(olFolderInbox)
Set objItems = inbox.Items
' エンジンの初期化
Call InitializeEngine
Debug.Print "Engine Started."
End Sub
' NewMailExは、大量のメールが同時に届いた際にも、
' 各メールのEntryIDを確実に捕捉できるため、ItemAddより推奨される
Private Sub Application_NewMailEx(ByVal EntryID As String)
Dim ns As Outlook.NameSpace
Dim item As Object
Set ns = Application.GetNamespace("MAPI")
Set item = ns.GetItemFromID(EntryID)
' MailItemであることを確認してからエンジンへ渡す
If TypeOf item Is Outlook.MailItem Then
Call ProcessMail(item)
End If
End Sub
4. プロフェッショナルが守るべき「極限の注意点」
① `mail.Body` vs `mail.HTMLBody` の戦い
`mail.Body` はプレーンテキストを返すが、`mail.HTMLBody` はHTMLソースを返す。
HTMLBodyは解析の負荷が非常に高く、正規表現のパターンもタグ(`
② オブジェクトのライフサイクル
`VBScript.RegExp` オブジェクトを、ループの中で毎回 `CreateObject` してはならない。本稿の例のように、メソッド内で一度生成して使い回すか、あるいはクラスのメンバ変数として保持し、再利用することで、メモリ確保のオーバーヘッドを最小化できる。
③ 堅牢なエラーハンドリング
自動化ツールがエラーで止まると、ユーザーの業務が止まる。
`On Error Resume Next` は「思考停止」の証だ。必ず `ErrorHandler` を用意し、「どのメールが、どのルールで、なぜ失敗したのか」をログに残す設計にしろ。
④ 外部連携(Excel/DB)への拡張
実務では、ルールをVBAコード内にハードコードするのは素人の仕事だ。
`InitializeEngine` 内で、Excelシートや外部のJSONファイル、あるいはSQL Serverからルールをロードするように変更せよ。そうすれば、「VBAコードを触らずに、業務ルールだけを更新する」という、真の運用保守が可能になる。
結論
正規表現を用いたメール振り分けは、単なる「テクニック」ではない。それは、「変化し続けるビジネスルールを、いかにして静的なシステムに組み込むか」という設計思想の戦いである。
`Like`演算子による場当たり的な実装を卒業し、オブジェクト指向に基づいた「ルールエンジン」を構築せよ。その先にのみ、真に堅牢で、メンテナンスフリーな自動化の世界が待っている。
