🔥

GoでGmail APIを使ってメールを送信する

2022/09/26に公開約8,400字

はじめに

Go で Gmail を送信したいと思って色々苦労したので自分用のメモ
今回は OAuth2 認証をして使用するやり方を書いていこうと思います

Project 作成

GCP コンソールに移動して以下のページでプロジェクトを作成
https://console.cloud.google.com/projectcreate
Image from Gyazo

API を有効化

有効な API とサービスをクリック
Image from Gyazo

API とサービスの有効化をクリック
Image from Gyazo

検索欄で Gmail と検索して API を有効にするボタンをクリック
Image from Gyazo

OAuth 同意

OAtuth 同意画面の設定する
Image from Gyazo

必須の情報を入力する特に注意する点はなし
Image from Gyazo

スコープはちゃんと設定しないと使用できないので注意
スコープを追加または削除をクリック後フィルタで gmail と検索https://mail.google.com/にチェックマークを入れ更新して保存して次へ
Image from Gyazo
テストユーザのところは自分の使っている google アカウントを入力
Image from Gyazo
これで OAtuh 同意画面は終了

OAuth クライアント ID を作成する

次に認証情報を作成をクリックして OAuth クライアント ID を作成する
https://i.gyazo.com/9171ccbd5e9b9ee1ff5047259d8b7668.png

画像のように承認済みの URI に
http://localhost
と入力して作成
Image from Gyazo

作成すると次の画面が出てくるので json をダウンロードします
Image from Gyazo

アクセストークンを取得

ディレクトリ作成

mkdir gmailSend

モジュールを初期化

go mod init gmailSend

先ほどダウンロードした json ファイル名を を credentials.json に変更して下のように配置

.
├── credentials.json
├── go.mod
├── go.sum
└── main.go
main.go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/gmail/v1"
	"google.golang.org/api/option"
)


func getClient(config *oauth2.Config) *http.Client {

	tokFile := "token.json"
	tok, err := tokenFromFile(tokFile)
	if err != nil {
		tok = getTokenFromWeb(config)
		saveToken(tokFile, tok)
	}
	return config.Client(context.Background(), tok)
}

func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
	authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
	fmt.Printf("Go to the following link in your browser then type the "+
		"authorization code: \n%v\n", authURL)

	var authCode string
	if _, err := fmt.Scan(&authCode); err != nil {
		log.Fatalf("Unable to read authorization code: %v", err)
	}

	tok, err := config.Exchange(context.TODO(), authCode)
	if err != nil {
		log.Fatalf("Unable to retrieve token from web: %v", err)
	}
	return tok
}

func tokenFromFile(file string) (*oauth2.Token, error) {
	f, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	tok := &oauth2.Token{}
	err = json.NewDecoder(f).Decode(tok)
	return tok, err
}

func saveToken(path string, token *oauth2.Token) {
	fmt.Printf("Saving credential file to: %s\n", path)
	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		log.Fatalf("Unable to cache oauth token: %v", err)
	}
	defer f.Close()
	json.NewEncoder(f).Encode(token)
}

func main() {
	ctx := context.Background()
	b, err := os.ReadFile("credentials.json")
	if err != nil {
		log.Fatalf("Unable to read client secret file: %v", err)
	}

	config, err := google.ConfigFromJSON(b, gmail.MailGoogleComScope)
	if err != nil {
		log.Fatalf("Unable to parse client secret file to config: %v", err)
	}
	client := getClient(config)

	srv, err := gmail.NewService(ctx, option.WithHTTPClient(client))
	if err != nil {
		log.Fatalf("Unable to retrieve Gmail client: %v", err)
	}
	fmt.Println("Created Gmail service", srv)
}

必要なモジュールをインストール

go mod tidy

実行

go run main.go

実行すると コンソールに URL が表示されるのでコピーしてブラウザで開く
テストユーザで入力した google アカウントを選択すると以下の画面になるので続行を選択
Image from Gyazo

以下のような画面が出ていれば OK そのまま続行
Image from Gyazo

URL の code=〇〇〇〇の〇〇部分をコピーして main.go を実行しているコンソールに貼り付ける
Image from Gyazo

Saving credential file to: token.json と表示されローカルに token.json が作成されれば OK

メールを送信

main.go
package main

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strings"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"golang.org/x/text/encoding/japanese"
	"golang.org/x/text/transform"
	"google.golang.org/api/gmail/v1"
	"google.golang.org/api/option"
)

func getClient(config *oauth2.Config) *http.Client {
	tokFile := "token.json"
	tok, err := tokenFromFile(tokFile)
	if err != nil {
		tok = getTokenFromWeb(config)
		saveToken(tokFile, tok)
	}
	return config.Client(context.Background(), tok)
}

func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
	authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
	fmt.Printf("Go to the following link in your browser then type the "+
		"authorization code: \n%v\n", authURL)

	var authCode string
	if _, err := fmt.Scan(&authCode); err != nil {
		log.Fatalf("Unable to read authorization code: %v", err)
	}

	tok, err := config.Exchange(context.TODO(), authCode)
	if err != nil {
		log.Fatalf("Unable to retrieve token from web: %v", err)
	}
	return tok
}

func tokenFromFile(file string) (*oauth2.Token, error) {
	f, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	tok := &oauth2.Token{}
	err = json.NewDecoder(f).Decode(tok)
	return tok, err
}


func saveToken(path string, token *oauth2.Token) {
	fmt.Printf("Saving credential file to: %s\n", path)
	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		log.Fatalf("Unable to cache oauth token: %v", err)
	}
	defer f.Close()
	json.NewEncoder(f).Encode(token)
}

func main() {
	ctx := context.Background()
	b, err := os.ReadFile("credentials.json")
	if err != nil {
		log.Fatalf("Unable to read client secret file: %v", err)
	}
	config, err := google.ConfigFromJSON(b, gmail.MailGoogleComScope)
	if err != nil {
		log.Fatalf("Unable to parse client secret file to config: %v", err)
	}
	client := getClient(config)

	srv, err := gmail.NewService(ctx, option.WithHTTPClient(client))
	if err != nil {
		log.Fatalf("Unable to retrieve Gmail client: %v", err)
	}
	fmt.Println("Created Gmail service", srv)
	//追記
	msgStr := "From: 'me'\r\n" +
		"reply-to: hoge@gmail.com\r\n" + //送信元
		"To: hogehoge@gmail.com\r\n" + //送信先
		"Subject:あいさつ\r\n" +
		"\r\n" + "hogeですhogehogeさんお元気ですか"
	reader := strings.NewReader(msgStr)
	transformer := japanese.ISO2022JP.NewEncoder()
	msgISO2022JP, err := ioutil.ReadAll(transform.NewReader(reader, transformer))
	if err != nil {
		log.Fatalf("Unable to convert to ISO2022JP: %v", err)
	}
	msg := []byte(msgISO2022JP)
	message := gmail.Message{}
	message.Raw = base64.StdEncoding.EncodeToString(msg)
	_, err = srv.Users.Messages.Send("me", &message).Do()
	if err != nil {
		fmt.Printf("%v", err)
	}
}


件名が文字化けするので変換処理をしている

	reader := strings.NewReader(msgStr)
	transformer := japanese.ISO2022JP.NewEncoder()
	msgISO2022JP, err := ioutil.ReadAll(transform.NewReader(reader, transformer))

https://qiita.com/yyoshiki41/items/79882e269ca6af4c2236

これで無事メールが届いていれば完了
お疲れ様でした

参考
https://developers.google.com/identity/protocols/oauth2
https://developers.google.com/gmail/api/quickstart/go
https://community.dal.co.jp/s/article/send-and-receive-gmail01
https://engineers-blog.agest.co.jp/entry/20220825/1661392800
https://www.y-hakopro.com/entry/google_oauth_api

GitHubで編集を提案

Discussion

ログインするとコメントできます