こんにちは。エンジニアの砂川です。
2023年10月頃にGoogle・Yahoo!から新しいメール送信者ガイドラインが出されました。 該当するメール送信者は、ガイドラインに沿っているか確認をし、沿っていない場合は対応する必要があります。
対応完了しきった皆様、お疲れ様でした。 絶賛対応中の方々、頑張っていきましょう。
ところで皆さんGmailのPostmaster Toolsの迷惑メール率の監視はいかがでしょうか?
クラウドワークスではGmailガイドラインで定めているPostmaster Toolsの迷惑メール率を監視し、閾値を超えるとアラートを通知するような仕組みを作り、運用をしています。
この記事では、その仕組みをご紹介します。
なぜ作ったのか
- 定期的にPostmaster Toolsにログインして監視するのが面倒
- (サブ)ドメイン毎の迷惑メール率を確認する時、切り替えが面倒
これらの面倒さを解決するために、各ドメインのメトリクスを一覧できるようDatadogのダッシュボードとSlack通知をするような仕組みを作りました。
全体像
全体像としては上記のように定期的にPostmaster Tools APIで迷惑メール率等を取得し、Datadogに送信、監視をしてSlack通知をしています。
定期的に実行する処理部分は、Go言語で実装しました。
Postmaster Tools APIを利用する準備
- Google Cloudでプロジェクト作成
- サービスアカウントを払い出し(メールアドレスが発行されます)
- Postmaster Toolsのユーザー管理にて、監視したいドメイン・サブドメイン毎に発行されたメールアドレスを登録
- 払い出したサービスアカウントのキーを発行(JSON)
払い出したキーはJSONファイルとなりますが、本番環境では環境変数で扱いたいので、ローカル開発環境では以下のように環境変数を設定して開発します。
export GOOGLE_APPLICATION_CREDENTIALS_JSON=$(cat ./credentials.json | jq -c '.')
Postmaster Tools APIを利用して迷惑メール率を取得
Postmaster Tools APIを利用して迷惑メール率を取得するコードは以下のようになります。
※一部実装を端折っています
package postmastertools import ( "context" "fmt" "golang.org/x/oauth2/google" "google.golang.org/api/gmailpostmastertools/v1" "google.golang.org/api/googleapi" "google.golang.org/api/option" "os" "time" ) // OkTrafficStat はPostmasterTools APIで取得に成功し、利用する値だけを抽出したドメインごとの値 type OkTrafficStat struct { // ドメイン // 例: domains/crowdworks.jp DomainName string // 迷惑メール率 // 例: 0.001 -> 0.1% UserReportedSpamRatio float64 } // NgTrafficStat はPostmasterTools APIで取得に失敗したドメインごとの理由 type NgTrafficStat struct { // ドメイン // 例: domains/crowdworks.jp DomainName string // 失敗した理由 // 例: "notFound", "badRequest" Reason string } // FetchResult はPostmaster Tools APIで取得したデータの集合 type FetchResult struct { OkTrafficStats []OkTrafficStat NgTrafficStats []NgTrafficStat } // FetchPostmasterTrafficStats はPostmaster ToolsのAPIを叩いた結果を返す // 1. 環境変数 GOOGLE_APPLICATION_CREDENTIALS_JSON にある client_email が登録されているドメインのリストを取得 // 2. ドメイン毎にAPIを叩いた結果をスライスに詰め込み、全体の結果を返す // TrafficStatの取得に失敗(2**系以外)でも、早期リターンせずに失敗としてスライスに詰め込んでいる理由 // - そのドメインで、対象日にメールが1通も送信されていなかったりしてデータが無いと404が返ってくる(これで早期リターンをしてしまうと、他ドメインのTrafficStatが取得できない) // - 全ドメイン分のTrafficStatを一旦取得するため func FetchPostmasterTrafficStats(targetDate time.Time) (*FetchResult, error) { ctx := context.Background() creds, err := google.CredentialsFromJSON(ctx, []byte(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")), "https://www.googleapis.com/auth/postmaster.readonly") if err != nil { return nil, fmt.Errorf("環境変数 GOOGLE_APPLICATION_CREDENTIALS_JSON からクレデンシャル作成に失敗: %v", err) } gmailpostmastertoolsService, err := gmailpostmastertools.NewService(ctx, option.WithCredentials(creds)) if err != nil { return nil, fmt.Errorf("NewService作成に失敗: %v", err) } domains, err := gmailpostmastertoolsService.Domains.List().Do() if err != nil { return nil, fmt.Errorf("DomainのList取得に失敗: %v", err) } targetDateStr := targetDate.Format("20060102") var okTrafficStats []OkTrafficStat var ngTrafficStats []NgTrafficStat for _, d := range domains.Domains { parent := d.Name + "/trafficStats/" + targetDateStr trafficStat, err := gmailpostmastertoolsService.Domains.TrafficStats.Get(parent).Do() if err != nil { ngTrafficStats = append(ngTrafficStats, NgTrafficStat{ DomainName: d.Name, Reason: err.(*googleapi.Error).Errors[0].Reason, }) } else { okTrafficStats = append(okTrafficStats, OkTrafficStat{ DomainName: d.Name, UserReportedSpamRatio: trafficStat.UserReportedSpamRatio, }) } } return &FetchResult{ OkTrafficStats: okTrafficStats, NgTrafficStats: ngTrafficStats, }, nil }
Postmaster Tools API の詳細は公式のドキュメントを御覧ください。
Datadogへカスタムメトリクスとして送信
Datadogへカスタムメトリクスを送信するコードは以下のようになっています。
※一部コメントや実装を端折っています
package datadog import ( "context" "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadog" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "time" ) // Metric はグラフ化しやすいよう加工されたDatadog用のメトリクス type Metric struct { // タイムスタンプ timestamp time.Time // ドメイン // 例: crowdworks.jp DomainName string // 迷惑メール率 // 例: 0.001 -> 0.1% UserReportedSpamRatio float64 } // ToDatadogSeries はDatadogに送信するためのデータを作成 func (m Metric) ToDatadogSeries(currentTime time.Time, datadogEnv string) []datadogV2.MetricSeries { tags := []string{ "env:" + datadogEnv, "domain:" + m.DomainName, } return []datadogV2.MetricSeries{ { Metric: "crowdworks.postmastertools.user_reported_spam_ratio", Type: datadogV2.METRICINTAKETYPE_GAUGE.Ptr(), Points: []datadogV2.MetricPoint{ { Timestamp: datadog.PtrInt64(currentTime.Unix()), Value: datadog.PtrFloat64(m.UserReportedSpamRatio), }, }, Tags: tags, }, } } // SubmitDatadog はMetricで与えられたドメインごとの値をDatadogに送信 func SubmitDatadog(metrics []Metric, currentTime time.Time, datadogEnv string) error { var series []datadogV2.MetricSeries for _, e := range metrics { series = append(series, e.ToDatadogSeries(currentTime, datadogEnv)...) } body := datadogV2.MetricPayload{Series: series} ctx := datadog.NewDefaultContext(context.Background()) configuration := datadog.NewConfiguration() apiClient := datadog.NewAPIClient(configuration) api := datadogV2.NewMetricsApi(apiClient) _, r, err := api.SubmitMetrics(ctx, body, *datadogV2.NewSubmitMetricsOptionalParameters()) if err != nil { return fmt.Errorf("MetricsApi.SubmitMetricsに失敗: %v", r) } return nil }
迷惑メール率をDatadogのカスタムメトリクスとして送信しています。
DatadogのAPIの詳細は以下をご覧ください。
Postmaster ToolsとDatadog API仕様による気をつけるべきポイント
Postmaster Toolsで確認できる最新の迷惑メール率は大体 3日前
の迷惑メール率です。
しかし、Datadogのカスタムメトリクス送信時に指定できるタイムスタンプには、未来は10分、過去は1時間という仕様があります。
よって、実際には 3日前
の迷惑メール率を、プログラム実行時の時刻(現在時刻)をタイムスタンプとして、Datadogに送信しています。
このタイムスタンプのズレは、アラート内容に 3日前
である旨を記述してカバーしています。
Datadogのカスタムメトリクスの詳細については、ドキュメントを御覧ください。
Datadogのダッシュボード
Datadogのダッシュボードを用意し、各ドメインの迷惑メール率の一覧を出して、見やすくなりました。
監視していて迷惑メール率が上がった場合は、なんらか対策を検討しましょう。
まとめ
迷惑メール率を1ドメインずつ目視チェック運用を継続するのは面倒です。 そのため、Postmaster Toolsの迷惑メール率をドメイン毎に一覧でき、悪くなった時もSlack通知ですぐに気付けるようにしました。
We're Hiring !
クラウドワークスでは一緒に課題を解決してくださるエンジニアを募集しています。