LayerX エンジニアブログ

LayerX の エンジニアブログです。

LayerX インボイスのメール受信の仕組み

こんにちは!LayerXの mosa(榎本)です。

今日は、LayerXインボイスの機能であるメール受信について解説していきます。

LayerXインボイスでは、様々な方法で請求書ファイルを取り込むことができます。直接経理の方がアップロードできるのはもちろん、特定のメールアドレスに添付ファイル付きで送ることで、ファイルを取り込む仕組みがあります。流れとしては、以下の図のようになります。

  1. システムがメールアドレスを発行する
  2. ユーザー様が取引先にメールアドレスをお伝えする
  3. 取引先が月末に、当該アドレスに請求書を送付する

これにより、ユーザー様を介さずに、直接請求書をシステムに取り込むことができます。もともと取引先がメールで請求書を送っている場合、送り先のアドレスを変えるだけで済むため、業務フローを変更することなく運用にのせることができます。

このアーキテクチャをどのように組んだかを解説します。 (なんだかんだでメールって難しいので、開発の一助になれば幸いです。)

SendGrid

当時重視したのは、最速で開発できることです。

メールのプロトコルに不慣れなので、「HTTPで受けられること」「パースがある程度終わっているもの」がないか探した結果、SendGridの Inbound Email Parse Webhook機能に行き着きました。どちらの特徴も備えた上で、spamやなりすましチェックもやってくれます。(SendGridはメール送信用のSaaSだと思っていたので、受信もできることをその時知りました。)

使い方は単純で、ドメインを認証して向き先を設定し、受けるAPIを用意するのみ。

実際には、以下のペイロードがmutlipartで渡ってきます。大体パース済なのが嬉しいです。

  • メールヘッダー
  • To
  • From
  • Subject
  • Envelope
  • 本文(HTML)
  • 本文(TEXT)
  • charsets
  • attachments(添付ファイル数)
  • Sender IP
  • Spam Score, Spam Report
  • DKIM, SPFの結果

ヘッダーや添付ファイルなどをさらにパースするための各言語のヘルパーがあり、LayerX インボイスでは Goの sendgrid/sendgrid-go を利用しました。

取りこぼさない設計

このメール受取機能は、なるべくメールを取りこぼさないことが要件に入ります。通常のサービス側はアップデートの際、メンテナンスを告知した上で停止する選択肢を取ることができます。しかしメール受取機能は、お客様のさらに取引先(サービスを利用していません)から直に送られてくるため、メンテナンスであることを伝える術がなく、基本的に無停止であることが求められます。そのために、以下のことに気をつけました。

  1. 通常のサービスAPIから切り離すこと
  2. 必ず最初に生データをS3に上げ、後続処理に流すこと

サービスAPIとの切り離し

通常のサービスAPIから切り離すことで、薄く安定したコンポーネントとし、独立したメンテナンスを可能にしました。

はじめはAPI Gateway ⇒ Lambda のマネージドな構成にしましたが、 API Gatewayの制約上10MB以上のデータを受けられない関係から、現在は独立したECSをたててwebhookを受けています。(受けられないことそのものよりも、音もなく受信に失敗してしまい、送信側が気付けないことを問題視しました。)

最初に生データをS3に上げる

f:id:mosa_siru:20210510151434p:plain

受けたwebhookはまず生データをS3にあげて、そのS3 object keyを後続のジョブワーカーのためにenqueueします。実際の処理はワーカー側が行います。これにより、何かあったときでもリトライを可能にしています。また、生データが残っていることが調査を可能にしたケースも何件もありました。

以下は薄いメール受信APIの例です。

func inboundParseWebhookHandler(c echo.Context) error {
    bucket := "some-bucket"
    s3Key := fmt.Sprintf(fileKeyFormat, time.Now().Format("2006/01/02/15"), id)

    body, err := ioutil.ReadAll(c.Request().Body)
    if err != nil {
        return err
    }
    contentType := c.Request().Header.Get("Content-Type")
    err = putS3Object(bucket, s3Key, bytes.NewReader(body), contentType)
    if err != nil {
        return err
    }
    err = enqueueSendGridInboundParseWebhook(s3Key)
    if err != nil {
        return err
    }
    return c.String(http.StatusOK, s3Key)
}

メンテナンス

どうしても停止したい場合はどうしたら良いでしょうか。

SendGridのInbound Email Parse Webhook機能は、ステータスコード2xxを返さない限り、3日以内なら何度でもリトライしてくれる機能があります。そのため、メンテ中には503を返すことで、メンテ明けに問題なく受け取ることができるようにしています。便利ですね!

最後に

というわけで、メール受信のアーキテクチャを紹介しました。

実際にはなんだかんだでパースに苦労したり、泥臭い文字化けとの戦いがあったりしましたが、それは折をみて紹介しようと思います。メールって難しい……

今回はもろもろの前提をふまえSendGridを選びましたが、AWSならSESの受信機能をつかうのも全然ありだと思います。(東京リージョン来ないかなー)

最後に、LayerXでは現実的な設計をもとに高速に開発する仲間を募集しております!

We are hiring!

herp.careers