【テクニカル・上級編】【中級者向け】受信メールの添付ファイルが画像の場合のみ、OCR処理を外部API経由で呼び出しテキスト化する連携 – Outlook VBA解析バイブル

スポンサーリンク

Outlook VBAを掌握する極限の知見:受信メール画像添付ファイルの外部OCR非同期連携アーキテクチャ

業務自動化の現場において、Outlook VBAは依然として強力なランタイムである。しかし、多くの開発者は「イベントをフックして添付ファイルを保存し、APIを叩く」という表層的な実装に終始し、オブジェクトのライフサイクル管理やメモリリーク、さらにはVBAが抱えるシングルスレッドの限界という現実の壁に直面して挫折する。

今回は、中級者から一歩進み、エンタープライズ環境に耐えうる堅牢なシステムを構築するための極限の知見を公開する。テーマは「受信メールの画像添付ファイルを判定し、外部OCR APIへHTTP経由で連携してテキスト化する仕組みの構築」だ。

単に動くだけのコードではない。メモリの枯渇を防ぎ、Outlook自体のフリーズを回避するためのアーキテクチャを解説する。

—

1. アーキテクチャの全体像と設計思想

VBAから外部REST API(今回はJSONを返し、multipart/form-dataで画像を受け付ける一般的なOCRエンドポイントを想定)を安全に叩くためには、以下の3つの壁を突破する必要がある。

1. イベントドリブンにおけるオブジェクトのスコープ管理
`ItemAdd`イベントの引数として渡されるアイテムを適切に解放しないと、Outlookのプロセス内にメモリリークが蓄積し、やがて強制終了を引き起こす。
2. WinINet / MSXML2.ServerXMLHTTP.6.0 による堅牢なHTTP通信
VBA標準のHTTPクライアントはバイナリデータの扱いに癖がある。特にマルチパートフォームデータの組み立てには厳密なバイト配列の操作が求められる。
3. ファイルI/Oの最適化
不要な一時ファイルをディスクに残さない、あるいは適切にクリーンアップするライフサイクル管理。

—

2. 実装コード:堅牢性を極めたVBAモジュール

以下のコードは、`Application_NewMailEx`または`Items_ItemAdd`から呼び出される、実戦投入可能なモジュール群の核心部分である。

Option Explicit

‘ ==============================================================================
定数定義
‘ ==============================================================================
Private Const API_ENDPOINT As String = “https://api.example-ocr.com/v1/extract”
Private Const API_KEY As String = “YOUR_SECURE_API_KEY_HERE”
Private Const TEMP_DIR As String = “C:\Temp_Outlook_OCR\”

‘ ==============================================================================
‘ 概要: 新着メールの監視イベント(Session_NewMailExを使用することを推奨)
‘ ==============================================================================
Public Sub ProcessIncomingMail(ByVal ItemID As String)
Dim ns As Outlook.NameSpace
Dim mailItem As Outlook.MailItem
Dim attachments As Outlook.Attachments
Dim att As Outlook.Attachment
Dim i As Long

On Error GoTo ErrorHandler

‘ セッションの取得
Set ns = Application.GetNamespace(“MAPI”)
Set mailItem = ns.GetItemFromID(ItemID)

‘ メールアイテムかどうかの型安全な確認
If mailItem.Class <> olMail Then GoTo Cleanup

Set attachments = mailItem.Attachments
If attachments.Count = 0 Then GoTo Cleanup

‘ 一時ディレクトリの確保
EnsureDirectoryExists TEMP_DIR

For i = 1 To attachments.Count
Set att = attachments.Item(i)

‘ 画像ファイル(JPEG / PNG)の判定(拡張子およびMIMEタイプ)
If IsImageAttachment(att) Then
Dim localPath As String
localPath = TEMP_DIR & att.FileName

‘ 添付ファイルのディスクへの書き出し
att.SaveAsFile localPath

‘ 外部OCR APIの呼び出し(バイナリ送信)
Dim ocrResult As String
ocrResult = CallOcrApi(localPath)

‘ 結果をメールの本文に追記、またはカスタムプロパティ/ログへ保存
If Len(ocrResult) > 0 Then
AppendResultToMail mailItem, att.FileName, ocrResult
End If

‘ 一時ファイルの即時削除(I/O最適化)
If Dir(localPath) <> “” Then Kill localPath
End If

‘ ループ内でのオブジェクト解放
Set att = Nothing
Next i

‘ 変更を保存して既読フラグ等の制御
mailItem.Save

Cleanup:
‘ ==============================================================================
‘ メモリ最適化: オブジェクトの明示的解放
‘ Outlook VBAでは、参照カウントを確実にデクリメントさせることが極限の安定性を生む
‘ ==============================================================================
On Error Resume Next
Set att = Nothing
Set attachments = Nothing
Set mailItem = Nothing
Set ns = Nothing
Exit Sub

ErrorHandler:
Debug.Print “Error ” & Err.Number & “: ” & Err.Description
Resume Cleanup
End Sub

‘ ==============================================================================
‘ 補助関数: 画像添付ファイルの判定
‘ ==============================================================================
Private Function IsImageAttachment(ByVal att As Outlook.Attachment) As Boolean
Dim ext As String
Dim pos As Long

pos = InStrRev(att.FileName, “.”)
If pos = 0 Then
IsImageAttachment = False
Exit Function
End If

ext = LCase(Mid(att.FileName, pos + 1))

Select Case ext
Case “jpg”, “jpeg”, “png”, “tiff”, “bmp”
IsImageAttachment = True
Case Else
IsImageAttachment = False
End Select
End Function

‘ ==============================================================================
‘ 外部API連携: ServerXMLHTTP.6.0 を用いた堅牢なHTTP POST処理
‘ ==============================================================================
Private Function CallOcrApi(ByVal filePath As String) As String
Dim http As Object
Dim fileData() As Byte
Dim boundary As String
Dim body() As Byte
Dim headBytes() As Byte
Dim footBytes() As Byte

On Error GoTo ApiError

‘ バイナリとしてファイル読み込み
fileData = ReadFileToByteArray(filePath)
If UBound(fileData) < 0 Then Exit Function boundary = "----OutlookVBAFormDataBoundary" & Format(Now, "yyyymmddhhmmss") Set http = CreateObject("MSXML2.ServerXMLHTTP.6.0") http.Open "POST", API_ENDPOINT, False http.setRequestHeader "Authorization", "Bearer " & API_KEY http.setRequestHeader "Content-Type", "multipart/form-data; boundary=" & boundary ' マルチパートボディの構築(バイト配列の結合) Dim headString As String headString = "--" & boundary & vbCrLf & _ "Content-Disposition: form-data; name=""file""; filename=" & Chr(34) & ExtractFileName(filePath) & Chr(34) & vbCrLf & _ "Content-Type: application/octet-stream" & vbCrLf & vbCrLf headBytes = StrConv(headString, vbFromUnicode) footBytes = StrConv(vbCrLf & "--" & boundary & "--" & vbCrLf, vbFromUnicode) body = ConcatByteArrays(headBytes, fileData, footBytes) ' 同期リクエスト送信(タイムアウト設定: 接続10秒、送信10秒、受信30秒) http.setTimeouts 10000, 10000, 30000, 30000 http.send body If http.Status = 200 Then CallOcrApi = http.responseText Else CallOcrApi = "[API Error: " & http.Status & " - " & http.statusText & "]" End If ApiCleanUp: Set http = Nothing Exit Function ApiError: CallOcrApi = "[Error: " & Err.Description & "]" Resume ApiCleanUp End Function ' ============================================================================== ' 高度なバイナリ処理: ファイルをバイト配列として安全にロード ' ============================================================================== Private Function ReadFileToByteArray(ByVal path As String) As Byte() Dim fn As Integer Dim data() As Byte fn = FreeFile Open path For Binary Access Read As #fn If LOF(fn) > 0 Then
ReDim data(LOF(fn) – 1)
Get #fn, , data
End If
Close #fn

ReadFileToByteArray = data
End Function

‘ ==============================================================================
‘ ユーティリティ: バイト配列の結合
‘ ==============================================================================
Private Function ConcatByteArrays(arr1() As Byte, arr2() As Byte, arr3() As Byte) As Byte()
Dim result() As Byte
Dim l1 As Long, l2 As Long, l3 As Long

l1 = UBound(arr1) – LBound(arr1) + 1
l2 = UBound(arr2) – LBound(arr2) + 1
l3 = UBound(arr3) – LBound(arr3) + 1

ReDim result(l1 + l2 + l3 – 1)

Call CopyMemory(result(0), arr1(0), l1)
Call CopyMemory(result(l1), arr2(0), l2)
Call CopyMemory(result(l1 + l2), arr3(0), l3)

ConcatByteArrays = result
End Function

Private Sub EnsureDirectoryExists(ByVal path As String)
If Len(Dir(path, vbDirectory)) = 0 Then
MkDir path
End If
End Sub

Private Function ExtractFileName(ByVal path As String) As String
Dim p As Long
p = InStrRev(path, “\”)
If p > 0 Then
ExtractFileName = Mid(path, p + 1)
Else
ExtractFileName = path
End If
End Sub

Private Sub AppendResultToMail(ByVal mail As Outlook.MailItem, ByVal fileName As String, ByVal ocrText As String)
Dim originalBody As String
originalBody = mail.Body
mail.Body = originalBody & vbCrLf & “— [OCR Result for ” & fileName & “] —” & vbCrLf & ocrText & vbCrLf
End Sub

(注: 実際のバイト配列コピーには、標準APIである `RtlMoveMemory` (別名 `CopyMemory`) の宣言をモジュール上部に記述する必要がある)

If VBA7 Then
Private Declare PtrSafe Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” (Destination As Any, Source As Any, ByVal Length As LongPtr)
Else
Private Declare Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” (Destination As Any, Source As Any, ByVal Length As Long)
End If

—

3. チーフアーキテクトが指摘する「陥りがちな罠」と極限の対策

1. メモリリークとCOMオブジェクトの残存

VBAで `For Each` を使って `Attachments` を回す際、ループ変数やアイテムオブジェクトを明示的に `Set xxx = Nothing` しないコードが散見される。これにより、Outlookプロセスが終了してもメモリ上にCOM参照が残り続け、数日稼働させるとOutlookが突然クラッシュする(いわゆる「ゾンビプロセス化」)現象が起きる。
対策: 上記コードの通り、スコープを抜ける前、あるいはループの各イテレーションの終わりで必ず `Set` 解放を行うこと。

2. シングルスレッドランタイムにおける同期的APIブロック

VBAは本質的にシングルスレッドである。`ServerXMLHTTP` の同期モード (`False`ではなく `True`、すなわち非同期という意味ではなく `False`で同期実行) でタイムアウトを長めに設定すると、APIからのレスポンスが返ってくるまでOutlookのUIスレッド全体が完全にフリーズする。
大容量の画像をOCRに投げる場合、ユーザーは「Outlookが固まった」と勘違いして強制終了させかねない。
極限の対策: 本番環境で数メガバイト超の画像を扱う場合は、VBA単体で完結させるのではなく、VBAからWindows Script Host (WSH) や PowerShell スクリプトを `WshShell.Run` でバックグラウンド非同期実行(`vbHide`)し、実際のAPI通信とテキスト化を別プロセスにオフロードする設計が真のエンタープライズアーキテクチャと言える。

3. セキュリティと認証情報のハードコーディング回避

コード内にAPIキーを直接記述することは、リバースエンジニアリング(VBAのパスワード保護は容易に解除される)の観点から絶対に避けるべきである。
実運用では、Windowsの資格情報マネージャー(Credential Manager)からAPIキーを動的に取得するか、環境変数から読み込むラッパーを必ず挟むべきである。

—

結び

Outlook VBAは、正しく飼い慣らせば、モダンなクラウド型API群とレガシーシステムをシームレスに繋ぐ極めて強力なグルー言語(糊付け言語)となる。
表面上のコードを模倣するのではなく、メモリのライフサイクル、OSのリソース管理、そして非同期と同期のトレードオフを熟知した上で、堅牢な自動化基盤を構築してほしい。

タイトルとURLをコピーしました