Golang Cafe #37、#37.1 まとめ Gmail API を試す。

2014/07/06に開催された「Golang Cafe #37」についてのまとめです。


今回は先日(密かに)Gmail APIをGo言語で試す回となりました。
Go言語でGoogle APIを使用するにはGoogle APIs Client Library for Goがあるのですが、先人の結果を見ると思わしい結果が得られなかったとのことなので直接APIを叩いてみることにしました。

事前準備

まずは、Google Developer ConsoleからGmail APIをONにします。
詳細な手順については、Golang Cafe #18 まとめ goauth2を試すにまとめてあるので参考にしてください。


次にgoauth2パッケージを導入します。
Gmail APIを使用するにはOAuth2による認証が必要になります。
Go言語用にはgoauth2が提供されているので、こちらを使用します。

$ go get code.google.com/p/goauth2/oauth

でインストールします。
こちらも詳細な使用方法はGolang Cafe #18 まとめ goauth2を試すにまとめてあるので参考にしてください。


※重要
APIの認可を取得する際、何種類かのレベルがあります。
今回は一番安全なhttps://www.googleapis.com/auth/gmail.readonlyで認可を取得しています。
フルアクセスの認可を取得した場合、誤ってメールを削除する可能性もあるので十分気をつけてください。

APIを実行する

ここではgoauth2による認証は完了しているという前提でまとめておきます。
今回は一覧の取得と、そこから得られたIDを元に1件の本文を取得する部分だけをまとめておきます。
というのもAPIの多さと、JSONに対応する構造体の定義が大変なため必要最低限の検証にしました。


今回検証したAPIは、


APIを実行した結果はJSONで得られるので、これに対応する構造体を定義しておきます。

type Response struct {
    Messages           []Message `json:"messages"`
    NextPageToken      string    `json:"nextPageToken"`
    ResultSizeEstimate uint      `json:"resultSizeEstimate"`
}

type Message struct {
    Id           string   `json:"id"`
    ThreadId     string   `json:"threadId"`
    LabelIds     []string `json:"labelIds"`
    Snippet      string   `json:"snippet"`
    HistoryId    uint64   `json:"hostoryId"`
    SizeEstimate int      `json:"sizeEstimate"`
    Raw          string   `json:"raw"`
    Payload      Payload  `json:"payload"`
}

type Payload struct {
    Body Attachments `json:"body"`
}

type Attachments struct {
    AttachmentId string `json:attachmentId`
    Data         string `json:"data"`
    Size         int    `json:"size"`
}

面倒ですが地道に定義するしかなさそうです。
注意する点はリファレンスのページに書いてある型をそのまま鵜呑みにするとエラーになります。
特に本文が入っているであろうMessage.RawやAttachments.Dataに関してはリファレンスのページでは[]byteと書いてありますが、stringで受けていないとjson.Unmarshalした際にエラーになります。


一覧を取得するサンプルコードです。

func list(client *http.Client, userAddress string) {
    url := fmt.Sprintf(
        "https://www.googleapis.com/gmail/v1/users/%s/messages?maxResults=10",
        userAddress)

    res, err := client.Get(url)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Get Error: %v\n", err)
        os.Exit(1)
    }

    data, err := ioutil.ReadAll(res.Body)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ReadAll Error: %v\n", err)
        os.Exit(1)
    }

    gres := Response{}
    err = json.Unmarshal(data, &gres)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unmarshal Error: %v\n", err)
        os.Exit(1)
    }

    for _, msg := range gres.Messages {
        fmt.Printf("%s,%d\n", msg.Id, len(msg.Raw))

        get(client, userAddress, msg.Id)

        time.Sleep(1 * time.Second)
    }

    fmt.Printf("%d件\n", len(gres.Messages))
}

関数の引数で渡されるclientですが、transport.Client()メソッドで取得できるhttpのClientになります。
こちらはgoauth2の認証が完了していれば、その認証済の情報がセットされた状態になっているので、GETなりPOSTなり通常通り使用するだけで大丈夫うです。
もう1つ注意点は、Users.messages: listで使用するuserIdですが、こちらは必ずhoge@gmail.comのように@gmail.comを必ず付けるようにしてください。


次に1件のメール本文を取得します。先の一覧で取得したMessage.Idを使用します。

func get(client *http.Client, userAddres, id string) {
    url := fmt.Sprintf(
        "https://www.googleapis.com/gmail/v1/users/%s/messages/%s?format=full",
        userAddres, id)
    // url := fmt.Sprintf(
    //     "https://www.googleapis.com/gmail/v1/users/%s/messages/%s?format=raw",
    //     userAddres, id)

    res, err := client.Get(url)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Get Error: %v\n", err)
        os.Exit(1)
    }

    data, err := ioutil.ReadAll(res.Body)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ReadAll Error: %v\n", err)
        os.Exit(1)
    }

    msg := Message{}
    err = json.Unmarshal(data, &msg)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unmarshal Error: %v\n", err)
        os.Exit(1)
    }

    if msg.Payload.Body.Data == "" {
        fmt.Println("empty")
    } else {
        b64, err := base64.URLEncoding.DecodeString(msg.Payload.Body.Data)
        if err != nil {
            fmt.Printf("Error0: %v\n", err)
        }

        fmt.Println(string(b64))
    }
    // if msg.Raw == "" {
    //     fmt.Println("empty")
    // } else {
    //     b64, err := base64.URLEncoding.DecodeString(msg.Raw)
    //     if err != nil {
    //         fmt.Printf("Error0: %v\n", err)
    //     }

    //     fmt.Println(string(b64))
    // }
}

urlでformat=fullにした場合は、Message.Payload.Body.Dataにbase64エンコードされたボディ部分が入ります。format=rawにした場合は、Message.Rawにbase64エンコードされたメール全体(ヘッダ+ボディ)が入ります。
それらをデコードすることで内容を得ることができます。


デコードの方法については、Go言語ではencoding/base64パッケージでbase64エンコードとデコードがサポートされていますが、StdEncodingではなくURLEncodingの方を使用してください。(http://golang.org/pkg/encoding/base64/#pkg-variables
両者の違いは、

StdEncoding A-Z、a-z、0-9および+/
URLEncoding A-Z、a-z、0-9および-_

のようです。
ちなみにGo言語ではbase64エンコード、デコード使用する文字を独自に定義することもできるようです。


またformat=rawにした場合は、mail.Messageパッケージを使用して解析すれば良いと思います。(すいません、まだ未検証です)
詳し手順はGolang Cafe #16 まとめ その1 mailパッケージを参考にしてください。

まとめ

躓くポイントは何箇所かりますが、今までGolang Cafeでやってきた内容で十分対応できそうです。
今回は基本的なところだけを試したましたが、同様にほかのAPIも実行できるのではないかと思います。
Google APIs Client Library for Goソースコードもみてみましたが、やはりJSONに対応した構造体の定義に苦戦しているようなコメントアウトした部分が多々ありました。


次回のGolang Cafeはなんらかのフレームワークを試してみるそうです。


※掲載しているソースコードは十分なエラー処理はなされていませんので、ご自身の判断で参考にしてください。いかなる問題が起きても当方は責任はおいません。