やりたいこと
Google Workspace を使うことで独自ドメインで Gmail を使えるようになったので、
Gmail API を使ってサーバーから独自ドメインでメールを送る。
言語は Go 言語で書く。
最初に
色々試して、そしてものすごくハマったので、
やりたいことに対して必要じゃない作業がある可能性がある。
丸2日「googleapi: Error 400: Precondition check failed., failedPrecondition」というエラーから永遠に抜け出せなかった。
使用するサービスと予めやっておくこと
本記事ではドメインを psychedelicnekopunch.com で進める
- AWS Route 53
- 本記事には関係ないと思うけどやっておく
- DKIM を使用してメールを認証する
- ドメインの DKIM 鍵を生成する
- TXT レコードの設定
- 名前: 「google._domainkey」
- google._domainkey.psychedelicnekopunch.com
- タイプ: 「TXT - テキスト」
- 値: 「"v=DKIM1; k=rsa; p=xxxxx" "xxxxx" "xxxxx" "xxxxx" "xxxxx"」
- xxxxx の値はもっと長い文字列になっている。 100文字毎に " " で囲んで半角スペースを入れて100文字毎に " " で囲むを繰り返す。
- 名前: 「google._domainkey」
- TXT レコードの設定
- ドメインの DKIM 鍵を生成する
- MX レコードの設定
- DKIM を使用してメールを認証する
- 本記事には関係ないと思うけどやっておく
- Google Admin (Google Workspace)
- ユーザーの作成
- system@psychedelicnekopunch.com
- customer@psychedelicnekopunch.com
- noreply@psychedelicnekopunch.com
- ユーザーの作成
- Google Cloud Platform
- system@psychedelicnekopunch.com で進める
Google Cloud Platform
プロジェクトを作成する
- 右上「」クリック → 左メニュー「 IAM と管理」 → 「リソースの管理」
- 「プロジェクトを作成」
- 「プロジェクト名」は適当につける
- 作成したプロジェクトに移動
IAM でユーザー権限を設定する
- 「 追加」
- Google Admin で作成したユーザーを追加
- system@xxx
- 「ロールを選択」 → 「Project」 → 「オーナー」
- customer@xxx 、 noreply@xxx
- 「ロールを選択」 → 「Project」 → 「閲覧者」
- system@xxx
Gmail を有効にする
- 右上「」クリック → 左メニュー「 API とサービス」 → 「ライブラリ」
- 「 API とサービス検索」 → 「gmail」と入力
- 「Gmail API」が出てくるのでクリック
- 「有効にする」をクリック
- 「Gmail API」が出てくるのでクリック
- 「 API とサービス検索」 → 「gmail」と入力
OAuth 同意画面の設定
- 右上「」クリック → 左メニュー「 API とサービス」 → 「 OAuth 同意画面」
- 「内部」
- アプリ名: 適当に
- ユーザーサポートメール: customer@psychedelicnekopunch.com
- 承認済みドメイン: ← 本記事ではやらなくていいと思う
- デベロッパーの連絡先情報: system@psychedelicnekopunch.com
- 「保存して次へ」
- スコープ
- 「スコープの追加または削除」 → 「スコープの手動追加」
- 「https://mail.google.com/」 → 「更新」
- 「保存して次へ」
- 「スコープの追加または削除」 → 「スコープの手動追加」
- 「内部」
サービスアカウントを作成する
- 右上「」クリック → 左メニュー「 IAM と管理」 → 「サービスアカウント」
- 「 サービスアカウントを追加」
- 1 サービス アカウントの詳細
- サービスアカウント名: 適当につける
- 「作成」
- 以下省略してもいいかも
- 2 このサービス アカウントにプロジェクトへのアクセスを許可する (省略可)
- 「ロールを選択」 → 「Project」 → 「閲覧者」
- 「続行」
- 3 ユーザーにこのサービス アカウントへのアクセスを許可 (省略可)
- サービスアカウントユーザーロール
- noreply@psychedelicnekopunch.com
- サービスアカウント管理者ロール
- system@psychedelicnekopunch.com
- 「完了」
- サービスアカウントユーザーロール
- 2 このサービス アカウントにプロジェクトへのアクセスを許可する (省略可)
サービスアカウントの詳細を編集する
- 上部「 編集」クリック
- 一意の ID (クライアント ID) をコピーしておく
- Google Admin で使う
- サービス アカウントのステータス
- 「ドメイン全体の委任を表示」
- 「G Suite ドメイン全体の委任を有効にする」にチェック
- 「ドメイン全体の委任を表示」
- キー
- 「鍵を追加」 → 「新しい鍵を作成」 → キーのタイプ「JSON」 → 「作成」
- json ファイルがダウンロードされるので、この json ファイルをサーバーにアップロードしておく。
- 「鍵を追加」 → 「新しい鍵を作成」 → キーのタイプ「JSON」 → 「作成」
- 最後に「保存」を忘れずに
Google Admin (Google Workspace)
サービスアカウントのクライアント ID の登録
- 右上「」クリック → 左メニュー「セキュリティ」 → 「API の制御」
- 「ドメイン全体の委任」 → 「ドメイン全体の委任を管理」
- API クライアント → 「新しく追加」
- クライアント ID : Google Cloud Platform でコピーしたサービスアカウントの「一意の ID (クライアント ID)」 をペースト
- OAuth スコープ: 「https://mail.google.com/」
- 「承認」
- API クライアント → 「新しく追加」
- 「ドメイン全体の委任」 → 「ドメイン全体の委任を管理」
Golang で書く Gmail API を使用したサンプルコード
- メールヘッダーや本文などのコードは下記参照
アップロードした JSON ファイルを使って認証された gmail.Service 型を取得して、
各 API を呼び出すんだけど、 google.golang.org/api/gmail/v1 に書いてあるサンプルコードでの認証方法がどれも「googleapi: Error 400: Precondition check failed., failedPrecondition」エラーが出て動かなかった。
Go + GmailAPIでサービスアカウントからメールを送信する の認証方法でうまくいった。
ファイル構成
// /var/www/html/psychedelicnekopunch/go-sample
/infrastructure
├ Config.go
└ Gmail.go
main.go
psychedelicnekopunch-xxxxxxxxxx.json // サービスアカウントで生成した JSON ファイル
infrastructure/Config.go
package infrastructure
type Config struct {
AbsolutePath string
Environment string
}
func NewConfig() *Config {
c := new(Config)
c.AbsolutePath = "/var/www/html/psychedelicnekopunch/go-sample"
// production, staging, development
c.Environment = "production"
return c
}
infrastructure/Gmail.go
package infrastructure
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"net/mail"
"net/smtp"
"strings"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
"google.golang.org/api/gmail/v1"
)
type GMail struct {
Environment string
From mail.Address
Service *gmail.Service
}
func NewGMail() *GMail {
c := NewConfig()
return newGMail(c.Environment, c)
}
func newGMail(environment string, c *Config) *GMail {
from := mail.Address{ Name: "サイケデリックねこパンチ", Address: "noreply@psychedelicnekopunch.com" }
// アップロードした JSON ファイル
json, err := ioutil.ReadFile(c.AbsolutePath + "/psychedelicnekopunch-xxxxxxxxxx.json")
if err != nil {
panic(err)
}
config, err := google.JWTConfigFromJSON(json, "https://mail.google.com/")
if err != nil {
panic(err)
}
config.Subject = from.Address
ctx := context.Background()
tokenSource := config.TokenSource(ctx)
service, err := gmail.NewService(ctx, option.WithTokenSource(tokenSource))
if err != nil {
panic(err)
}
return &GMail{
Environment: environment,
From: from,
Service: service,
}
}
func (m *GMail) writeString(b *bytes.Buffer, s string) *bytes.Buffer {
_, err := b.WriteString(s)
if err != nil {
fmt.Print(err.Error())
}
return b
}
/**
* go で utf8 メールを送信
* https://qiita.com/yamasaki-masahide/items/a9f8b43eeeaddbfb6b44
*
* サンプルコードほぼ丸パクリ
*/
// サブジェクトを MIME エンコードする
func (m *GMail) encodeSubject(subject string) string {
// UTF8 文字列を指定文字数で分割する
b := bytes.NewBuffer([]byte(""))
strs := []string{}
length := 13
for k, c := range strings.Split(subject, "") {
b.WriteString(c)
if k%length == length-1 {
strs = append(strs, b.String())
b.Reset()
}
}
if b.Len() > 0 {
strs = append(strs, b.String())
}
// MIME エンコードする
b2 := bytes.NewBuffer([]byte(""))
b2.WriteString("Subject:")
for _, line := range strs {
b2.WriteString(" =?utf-8?B?")
b2.WriteString(base64.StdEncoding.EncodeToString([]byte(line)))
b2.WriteString("?=\r\n")
}
return b2.String()
}
// 本文を 76 バイト毎に CRLF を挿入して返す
func (m *GMail) encodeBody(body string) string {
b := bytes.NewBufferString(body)
s := base64.StdEncoding.EncodeToString(b.Bytes())
b2 := bytes.NewBuffer([]byte(""))
for k, c := range strings.Split(s, "") {
b2.WriteString(c)
if k % 76 == 75 {
b2.WriteString("\r\n")
}
}
return b2.String()
}
/**
* メールを送る
*/
func (m *GMail) Send(to string, subject string, body string) (err error) {
msg := bytes.NewBuffer([]byte(""))
msg = m.writeString(msg, "From: " + m.From.String() + "\r\n")
msg = m.writeString(msg, "To: " + to + "\r\n")
// msg = m.writeString(msg, "Bcc: " + m.From.String() + "\r\n")
msg = m.writeString(msg, m.encodeSubject(subject))
msg = m.writeString(msg, "MIME-Version: 1.0\r\n")
msg = m.writeString(msg, "Content-Type: text/plain; charset=\"utf-8\"\r\n")
msg = m.writeString(msg, "Content-Transfer-Encoding: base64\r\n")
msg = m.writeString(msg, "\r\n")
msg = m.writeString(msg, m.encodeBody(body))
message := new(gmail.Message)
message.Raw = base64.StdEncoding.EncodeToString(msg.Bytes())
_, err = m.Service.Users.Messages.Send("me", message).Do()
return err
}
main.go
package main
import (
"fmt"
"github.com/psychedelicnekopunch/go-sample/infrastructure"
)
func main() {
mail := infrastructure.NewGMail()
msg := `0123456789
メール
送信
TEST
寿限無(じゅげむ) 寿限無(じゅげむ) 五劫(ごこう)のすりきれ 海砂利(かいじゃり)水魚(すいぎょ)の水行末(すいぎょうまつ) 雲来末(うんらいまつ) 風来末(ふうらいまつ) 食(く)う寝(ね)るところに 住(す)むところ やぶらこうじの ぶらこうじ パイポ パイポ パイポの シューリンガン シューリンガンの グーリンダイ グーリンダイの ポンポコピーのポンポコナの 長久命(ちょうきゅうめい)の長助(ちょうすけ) パブロ・ディエゴ・ホセ・フランシスコ・デ・パウラ・ホアン・ネポムセーノ・マリア・デ・ロス・レメディオス・クリスピーン・クリスピアーノ・デ・ラ・サンティシマ・トリニダード・ルイス・イ・ピカソ`
// mail.Send(送信先メールアドレス, 件名, 本文)
err := mail.Send("info@sample.com", "メール送信 TEST 1234", msg)
if err != nil {
fmt.Print(err.Error())
return
}
fmt.Print("success")
}