Webhook APIを使って添付ファイル付きメールを処理する(Google Cloud Cloud Functions + Go言語)

Customers Mail CloudのWebhookは2種類あります。

  1. メール受信時
  2. メール送信時

メール受信時のWebhookはその名の通り、メールを受け取った際に任意のURLをコールするものです。この記事では添付ファイル付きメールを受け取った際のWebhook処理について解説します。

<!—more—>

フォーマットはマルチパートフォームデータ

Webhookの形式として、JSONとマルチパートフォームデータ(multipart/form-data)が選択できます。この二つの違いは、添付ファイルがあるかどうかです。JSONの場合、添付ファイルは送られてきません。今回のようにメールに添付ファイルがついてくる場合は、後者を選択してください。

Webhook設定ダイアログ

GCPの準備

まずGCPにてプロジェクトを作成します。このプロジェクトは課金設定されている必要があります。

以下5つのAPIを有効にします。

  • Cloud Functions
  • Cloud Build
  • Artifact Registry
  • Cloud Run
  • Cloud Logging API

gcloud CLIをインストールします。各OS別にダウンロードするバイナリが異なるので注意してください。

ファイルを伸張したら、installコマンドを実行します。

$ /path/to/google-cloud-sdk/install.sh 

インストールすると gcloud コマンドが使えるようになります。続けてコンポーネントをアップデートします。

$ gcloud components update

関数を作成する

まずCloud Functionsにアップロードする関数を作ります。適当なディレクトリを作成し、その中に移動します。

$ mkdir cmc
$ cd cmc

そしてGoファイルを作成します。今回は webhook.go としています。

package helloworld

import (
    "encoding/json"
    "fmt"
    "strconv"
    "html"
    "net/http"
    "github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

func init() {
  functions.HTTP("HelloHTTP", HelloHTTP)
}

func HelloHTTP(w http.ResponseWriter, r *http.Request) {
    // この中にコードを書きます

    // 最後にdoneと出力しておく
    fmt.Fprint(w, "done")
}

依存関係を解決するため、 go.mod を作成します。

$ go mod init example.com/hello
$ go mod tidy

関数をローカルでテスト実行する

開発中はローカルで実行してテストするので、そのための準備をします。

$ mkdir cmd
$ cd cmd

cmdディレクトリの中に main.go を作成します。

package main

import (
  "log"
  "os"

  // Blank-import the function package so the init() runs
  _ "example.com/hello"
  "github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
)

func main() {
  // Use PORT environment variable, or default to 8080.
  port := "8080"
  if envPort := os.Getenv("PORT"); envPort != "" {
    port = envPort
  }
  if err := funcframework.Start(port); err != nil {
    log.Fatalf("funcframework.Start: %v\n", err)
  }
}

先ほどと同じく cmd の中でも依存関係を解決します。

$ go mod tidy

後は main.go を実行してHTTPサーバーを立ち上げます。

$ export FUNCTION_TARGET=HelloHTTP
$ go run /path/to/cmd/main.go

これで http://localhost:8080 で関数を呼び出せます。

送信されてくるデータについて

メールを受信すると、以下のようなWebhookが送られてきます(データは一部マスキングしています)。JSONにしていますが、実際にはmultipart/form-dataです。

{
    "filter": "info@smtps.jp",
    "headers": [
      {name: 'Return-Path', value: '<user@example.com>'},
        :
      {name: 'Date', value: 'Thu, 27 Apr 2023 15:56:26 +0900'}
    ],
    "subject": "Webhookのテスト",
    "envelope-to": "user@smtps.jp",
    "server_composition": "sandbox",
    "html": "<div dir=\\\\\\\\\\\\\\\\"ltr\\\\\\\\\\\\\\\\">Webhookのテスト用メールです。<div>...</div></div>",
    "text": "Webhookのテスト用メールです。\\\\\\\\\\\\\\\\r\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\r\\\\\\\\\\\\\\\\n--\\\\\\\\\\\\\\\\r\\\\\\\\\\\\\\\\n...",
    "envelope-from": "info@smtps.jp",
    "attachments": 1,
    "attachment1": "...."
}

Goのコード

以下のコードは webhook.go の中身です。

構造体の作成

multipart/form-data の場合、まず r.ParseMultipartForm を実行します。その際に、最大メモリサイズを指定します。パースが終われば、 r.MultipartForm.Value にて送信されてきたパラメーターのキーと値が取得できます。

const maxMemory = 2 * 1024 * 1024 // 2MB
if err := r.ParseMultipartForm(maxMemory); err != nil {
    http.Error(w, "Unable to parse form", http.StatusBadRequest)
    fmt.Printf("Error parsing form: %v", err)
    return
}

for key, value := range r.MultipartForm.Value {
    fmt.Printf("%v = %v \n", key, value[0])
}

添付ファイルを削除する処理を追加する

添付ファイルが送られている場合に備えて、ファイルを削除する処理を追加します。

defer func() {
    if err := r.MultipartForm.RemoveAll(); err != nil {
        http.Error(w, "Error cleaning up form files", http.StatusInternalServerError)
        fmt.Printf("Error cleaning up form files: %v", err)
    }
}()

添付ファイルの存在確認

添付ファイルがあるかどうかは attachments キーの値が1以上かどうかで判定できます。

attachments, err := strconv.Atoi(r.MultipartForm.Value["attachments"][0])
if err != nil {
    fmt.Println(err)
}
if attachments > 0 {
  // 添付ファイルあり
}

添付ファイルをローカルに保存する

添付ファイルデータを受け取ってファイルとして保存する際には、以下のように処理します。送信されてくる際のキー名は関係ないようです。

if attachments > 0 {
    for _, headers := range r.MultipartForm.File {
        for _, h := range headers {
            fmt.Println("File: %s (%v bytes)", h.Filename, h.Size)
            // h.Open()でファイルの内容が取れます
        }
    }
}

Webhookの結果は管理画面で確認

Webhookでデータが送信されたログは管理画面で確認できます。送信時のAPIキー設定など、HTTPヘッダーを編集するといった機能も用意されていますので、運用に応じて細かなカスタマイズが可能です。

Webhookログ

まとめ

メールと連携したシステムはよくあります。通常、メールサーバを立てて、その中で処理することが多いのですが、メールサーバが落ちてしまうとシステムが稼働しなくなったり、メール文面の解析が煩雑でした。Customers Mail Cloudを使えばそうした手間なくJSONで処理できて便利です。

添付ファイルまで処理対象にしたい時には、この方法を利用してください。

受信サーバ | Customers Mail Cloud