Goでgmail APIを叩いて下書きを作成する

Go Quickstart  |  Gmail API  |  Google Developers

上記を参考にした。サンプルコードは最後に記載する。実装してる途中でメールの題名に日本語を設定できず、RFC2822について調べる必要があったのでメモしておく。

RFC2822

https://www.ietf.org/rfc/rfc2822.txt

電子メールのフォーマットを定めている。Goのgmailパッケージを使用する際にRFC2822のフォーマットでメッセージを記述する必要があった。基本的なフォーマットは以下の通り。

Header1 CRLF
Header2 CRLF
CRLF
Body

ヘッダーでは:で区切られたヘッダーフィールドとヘッダーボディが記載される。一つのヘッダーの終わりにはCRLFがくる。ヘッダーとボディの間にはCRLFだけの行が入る。この行以降は全てボディである。文字数制限などもあるがここでは省略。具体例は以下の通り。

From: xxxatxxx\r\n
To: yyyatyyy\r\n
Subject: test\r\n
\r\n
test

gmailではボディをHTMLで表現できる。この時、ヘッダにはContent-Type: text/html; charset=UTF-8とコンテンツタイプを設定するといい。

また、RFC2822ではUS-ASCIIを前提としている。ボディ部分に関してはMIMEの導入によりContent-Typeを指定することでUS-ASCII以外を指定できるようになったが、通常だとヘッダ部分は依然としてUS-ASCII以外使用できない。ここに関してだが、RFC2047でヘッダのエンコード仕様が定められている。具体的には、

=?charset?encoding?encoded-text?=

の形式でヘッダをエンコードすることができる(ヘッダでUTF-8の文字が使える!)。概要はこちらを参照。

例えば、題名を「テスト」としたい場合、

=?UTF-8?B?44OG44K544OI?=

となる。文字コード(charset)はUTF-8エンコードの形式(encoding)はB(base64)、「テスト」をbase64エンコードした値が44OG44K544OIとなる

これでメールの題名に日本語を設定できるようになる。

サンプルコード

package main

import (
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "os"
    "os/user"
    "path/filepath"
    "time"

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

// getClient uses a Context and Config to retrieve a Token
// then generate a Client. It returns the generated Client.
func getClient(ctx context.Context, config *oauth2.Config) *http.Client {
    cacheFile, err := tokenCacheFile()
    if err != nil {
        log.Fatalf("Unable to get path to cached credential file. %v", err)
    }
    tok, err := tokenFromFile(cacheFile)
    if err != nil {
        tok = getTokenFromWeb(config)
        saveToken(cacheFile, tok)
    }
    return config.Client(ctx, tok)
}

// getTokenFromWeb uses Config to request a Token.
// It returns the retrieved Token.
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 code string
    if _, err := fmt.Scan(&code); err != nil {
        log.Fatalf("Unable to read authorization code %v", err)
    }

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

// tokenCacheFile generates credential file path/filename.
// It returns the generated credential path/filename.
func tokenCacheFile() (string, error) {
    usr, err := user.Current()
    if err != nil {
        return "", err
    }
    tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials")
    os.MkdirAll(tokenCacheDir, 0700)
    return filepath.Join(tokenCacheDir,
        url.QueryEscape("gmail-go-quickstart.json")), err
}

// tokenFromFile retrieves a Token from a given file path.
// It returns the retrieved Token and any read error encountered.
func tokenFromFile(file string) (*oauth2.Token, error) {
    f, err := os.Open(file)
    if err != nil {
        return nil, err
    }
    t := &oauth2.Token{}
    err = json.NewDecoder(f).Decode(t)
    defer f.Close()
    return t, err
}

// saveToken uses a file path to create a file and store the
// token in it.
func saveToken(file string, token *oauth2.Token) {
    fmt.Printf("Saving credential file to: %s\n", file)
    f, err := os.OpenFile(file, 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() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    ctx := context.Background()

    b, err := ioutil.ReadFile("client_secret.json")
    if err != nil {
        log.Fatalf("Unable to read client secret file: %v", err)
    }

    // If modifying these scopes, delete your previously saved credentials
    // at ~/.credentials/gmail-go-quickstart.json
    config, err := google.ConfigFromJSON(b, gmail.GmailComposeScope, gmail.GmailModifyScope, gmail.MailGoogleComScope)
    if err != nil {
        log.Fatalf("Unable to parse client secret file to config: %v", err)
    }
    client := getClient(ctx, config)

    srv, err := gmail.New(client)
    if err != nil {
        log.Fatalf("Unable to retrieve gmail Client %v", err)
    }

    now := time.Now()

    const rfc2822 = "Mon Jan 02 15:04:05 -0700 2006"
    emailDate := now.Format(rfc2822)

    subjectStr := fmt.Sprintln("テスト")
    subject := base64.StdEncoding.EncodeToString([]byte(subjectStr))

    message := gmail.Message{
        Raw: base64.URLEncoding.EncodeToString([]byte("Date: " + emailDate + "\r\n" +
            "From: xxxatxxx.xxx\r\n" +
            "To: xxxatxxx.xxx\r\n" +
            "Subject: =?UTF-8?B?" + subject + "?=\r\n" +
            "Content-Type: text/html; charset=UTF-8\r\n" +
            "\r\n" +
            "<html><body>test</body></html>")),
    }

    udSrv := gmail.NewUsersDraftsService(srv)
    if _, err = udSrv.Create("me", &gmail.Draft{Message: &message}).Do(); err != nil {
        log.Fatalln(err)
    }
}

まとめ

RFC2822に関しては実装に必要なとこだけ拾って読んだ程度なのでちゃんと読みたい。

Goでbase64エンコードを実装する

base64を自分の理解のために実装してみる。

手順はこちらの記事がわかりやすいです。RFCで読むならこちら

実装したコードは以下の通りです。ほとんど標準パッケージ

package main

import (
    "fmt"
    "os"
)

const encodeStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
const padding = '='

func main() {
    if len(os.Args) != 2 {
        panic("Invalid arguments")
    }
    input := os.Args[1]

    result := decode(input)
    fmt.Println("Result: ", result)
}

func decode(input string) string {
    src := []byte(input)
    dst := make([]byte, (len(src)+2)/3*4)

    si, di := 0, 0
    n := (len(src) / 3) * 3
    for si < n {
        v := uint(src[si+0])<<16 | uint(src[si+1])<<8 | uint(src[si+2])

        dst[di+0] = encodeStr[v>>18&0x3F]
        dst[di+1] = encodeStr[v>>12&0x3F]
        dst[di+2] = encodeStr[v>>6&0x3F]
        dst[di+3] = encodeStr[v&0x3F]

        si += 3
        di += 4
    }

    remain := len(src) - si
    if remain == 0 {
        return string(dst)
    }

    v := uint(src[si+0]) << 16
    if remain == 2 {
        v |= uint(src[si+1]) << 8
    }

    dst[di+0] = encodeStr[v>>18&0x3F]
    dst[di+1] = encodeStr[v>>12&0x3F]

    switch remain {
    case 1:
        dst[di+2] = byte(padding)
        dst[di+3] = byte(padding)
    case 2:
        dst[di+2] = encodeStr[v>>6&0x3F]
        dst[di+3] = byte(padding)
    }

    return string(dst)
}

まとめ

  • コードを書いてみると、base64エンコードされたときに末尾に追加される=は最大で2つ、ということもわかる。
  • ビット演算とか普段使わないから勉強になる。

nginxでレスポンスをgzipで圧縮する

nginxでレスポンスをgzipで圧縮するための設定を調べた。

nginxでgzipモジュールを提供しているのでこれを利用する。設定例は以下の通り。

http {
    gzip  on;
    gzip_comp_level   2;
    gzip_types text/plain text/css text/xml application/xml text/javascript application/javascript application/json;
    gzip_buffers      4 8k;
}

クライアント側はリクエストヘッダーにAccept-Encodingを含める必要がある。 なお、curl--compressedオプションを指定すると、-Hで以下のヘッダーを付与してくれる。

Accept-Encoding: deflate, gzip

レスポンスヘッダーにはContent-Encoding: gzipが含まれる。

参考

www.oreilly.co.jp

www.oreilly.co.jp

Emacs × Go でタグジャンプ

helm-gtagsを使ってて、RubyとかPHP書いてたときにはうまくタグを作れてたんだけどGolangでうまく作れてなかった。以下の記事を見つけて試しにやってみたらうまくいった。

Using GNU GLOBAL · GitHub

/usr/local/etc/gtags.conf上記の通り修正して以下のコマンドを実行する。

$ gtags -v --gtagslabel=pygments --debug --explain

logrotateのタイミングでsupervisordのプロセスが落ちる

結論から言うと、ドキュメントちゃんと読んでなかったおれが悪い。

例えば、nginxのログに対してlogrotateの設定ファイルを書くと、

/var/log/nginx/*log {
    create 0644 nginx nginx
    daily
    rotate 10
    missingok
    notifempty
    compress
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
    endscript
}

みたいな感じになる。nginxのプロセスに対してUSR1シグナルを送ることでログファイルを開き直していることがわかる。Rails書いていた時に使用していたUnicornでもたしか同じような設定だったと思う。

なので、supervisordのログに対して同じようなlogrotateの設定ファイルを書いていたら、ログローテーションは正しく行われずにsupervisordのプロセスだけ落ちて、supervisordが管理していた子プロセスはそのまま生きている(親はinit)という状態になってしまい、再起動するのにいちいちkillコマンドを実行しなくてはならなくなった。。。

原因だが、supervisordでログを開き直すのにはUSR2シグナルを送る必要があるのに、特に確認もせずにUSR1シグナルを送る記述をしていたためだった。ドキュメントにもちゃんと書いてある!

http://supervisord.org/running.html#signals

おそらくsupervisordでUSR1シグナルを受け取った場合の処理は何もしておらずデフォルトの動作となるためsueprvisordのプロセスだけ落ちてしまうという結果になったと思う。

まとめ

ちゃんとドキュメント読もう

Convert markdown to pdf

自分用メモ。 markdown-pdfを使用してmarkdownをpdfに変換する


node.jsの公式サイトからnpmをインストールする

Node.js

markdown-pdfをインストールする

$ npm install markdown-pdf

適用したいCSSgithubからcloneする

自分はmixu/markdown-stylesを使用した

$ git clone https://github.com/mixu/markdown-styles.git

markdown-pdfを実行する

$ markdown-pdf -o xxx.pdf -s markdown-styles/layouts/github/assets/css/github-markdown.css xxx.md

golangでpanicをrecoverしたときにスタックトレースを表示する

golangではそもそもpanicしないようにコードを書くべきだしpanicが発生するならここで発生するのだと把握した上でコードを書くべきだ、という認識はある。とはいえ、golang書き始めてまもないので「念のために」recoverをコードに書いておきたかった。

具体的なコードは以下の通り。

https://play.golang.org/p/CxiR4j5Q6F_a

package main

import (
    "fmt"
    "log"
    "runtime"
)

func main() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    defer func() {
        if err := recover(); err != nil {
            log.Printf("[ERROR] %s\n", err)
            for depth := 0; ; depth++ {
                _, file, line, ok := runtime.Caller(depth)
                if !ok {
                    break
                }
                log.Printf("======> %d: %v:%d", depth, file, line)
            }
        }
    }()

    a := []int{1, 2}
    fmt.Println(a[2])
}

結果は以下の通り。

2009/11/10 23:00:00 main.go:13: [ERROR] runtime error: index out of range
2009/11/10 23:00:00 main.go:19: ======> 0: /tmp/sandbox464635953/main.go:15
2009/11/10 23:00:00 main.go:19: ======> 1: /usr/local/go/src/runtime/asm_amd64p32.s:472
2009/11/10 23:00:00 main.go:19: ======> 2: /usr/local/go/src/runtime/panic.go:491
2009/11/10 23:00:00 main.go:19: ======> 3: /usr/local/go/src/runtime/panic.go:28
2009/11/10 23:00:00 main.go:19: ======> 4: /tmp/sandbox464635953/main.go:25
2009/11/10 23:00:00 main.go:19: ======> 5: /usr/local/go/src/runtime/proc.go:195
2009/11/10 23:00:00 main.go:19: ======> 6: /usr/local/go/src/runtime/asm_amd64p32.s:1040

2017年を振り返る

2017年はWebエンジニア1年目の年だった。つらつらとやったことを振り返る。

最初の半年はRailsを書いていた。初めてのWeb開発だったので最初はさすがに覚えることが多く大変だったが、ビジネスロジックが中心だったので1ヶ月もするとだいたい慣れてきた。ビジネスロジックを単に実装しているだけだとフレームワークの昨日の暗記ゲームみたいな気がしてきた。半年くらいたって、プログラマーとしてもっと本質的なことを勉強しないといけないと思うようになった。ただこの時点では、本質的なことを学ぶといっても何が本質的なことかわからなかった。

7月くらいから数ヶ月、PHPを書いていた。既存サービスの改修。PHP書いたことは正直あまり印象に残っていないが、アプリケーション以外の部分も担当するようになった。開発環境が用意されてないからVagrantスクリプト書いたり、Apache JMeterで簡単な負荷検証やったりした。この辺りから本やブログを片っ端から読むようになって、プログラマーとして本質的なことがぼんやり見えてきた気がした。とりあえずコンピュータサイエンスかなと。

9月。仕事ではまだPHPを書いていた。コンピュータサイエンスわからんことばっかりだったからとりあえずOSの教科書読もうと思って、Operating System Conceptsを読もうとした。英語で1000ページくらい、気合いで読んだ。OSがやってることがなんとなくわかるようになった。プロセスやらスケジューリングやら。HTTP以下のレイヤーもよくわからんと思って、ハイパフォーマンスブラウザネットワーキングを読んだ。こういった本を読むと、不思議なことに今まで書いたコードとの繋がりが少し見えてきて楽しくなってきた。一方で、本を読んでばかりだったのでなかなか上手いコードを書けるようにならないのが気がかりだった。

10月。GoでAPIを書くことになった。フレームワークを使用しないことになってログまわりやらミドルウェアやらを自分で書くことになった。これが楽しくて、今までフレームワークに任せていた部分を理解できている気がした。CIまわりを整備したりもした。開発のチームリーダー的なポジションとなったが、他に議論できる知り合いもいなかったので悩んだ時はOSSのコードをよく参考にするようになった。ちょっとコード書けるようになってきたかもと浮かれた。コンピュータサイエンスの勉強では、引き続きOSまわりを勉強していたがやはりコードに落とし込めないことが気がかりだった。アルゴリズムの勉強も始めた。

2018年は以下のことを軸に自分のリソースを割り当てたい。

Goを書いていく

自分はまだ何も武器がないプログラマなので、Goの人、と言われるくらいになる。

コンピュータサイエンスを学ぶ

ここを勉強しておかないとプログラマとして死ぬ気がするのでやる。2018年前半はアルゴリズムを継続的にやる。後半はまた考える。

negroniの実装をちゃんと理解できていなかったので自分で書き直してみる

以前、ミドルウェアをうまく扱うパッケージとしてnegroniを紹介したが、改めてコードを読むとちゃんと理解できていなかったので自分で書き直してみる。車輪の再発明GitHubに上げるまでもないのでここに残す。

GitHub - urfave/negroni: Idiomatic HTTP Middleware for Golang

package chain

import (
    "net/http"
)

// Handler is an interface.
type Handler interface {
    ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
}

// HandlerFunc is an adapter to allow the use of ordinary functions as HTTP handlers.
type HandlerFunc func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)

// ServeHTTP simply calls f(w, r, next).
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    f(w, r, next)
}

// Middleware is a stack of middlwares.
type Middleware struct {
    handler Handler
    next    *Middleware
}

// ServeHTTP calls ServeHTTP of next middleware.
func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    m.handler.ServeHTTP(w, r, m.next.ServeHTTP)
}

// Wrap converts http.Handler into chain.Handler
func Wrap(h http.Handler) Handler {
    return HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
        h.ServeHTTP(w, r)
        next(w, r)
    })
}

// New is the constructor of Middlewre
func New(handlers ...Handler) Middleware {
    return *build(handlers)
}

func build(handlers []Handler) *Middleware {
    var next *Middleware

    if len(handlers) == 0 {
        return voidMiddleware()
    } else if len(handlers) > 1 {
        next = build(handlers[1:])
    } else {
        next = voidMiddleware()
    }

    return &Middleware{handlers[0], next}
}

func voidMiddleware() *Middleware {
    return &Middleware{
        handler: HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {}),
        next:    &Middleware{},
    }
}

メモ。

  • ミドルウェアスタックをlinked listで実装している。
  • 配列からlinked listを構成するときに再帰している。
  • インターフェースの使い方うまいなー(なんかパターンがありそうな)。

使い方は以下の通り。

package main

import (
    "./chain"
    "net/http"
)

func hello1(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    w.Write([]byte("Hello1\n"))
    next(w, r)
}

func hello2(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    w.Write([]byte("Hello2\n"))
    next(w, r)
}

func hello3(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    w.Write([]byte("Hello3\n"))
    next(w, r)
}

// MyHandler is a sample handler.
type MyHandler struct{}

// ServeHTTP greet to world.
func (mh MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, world!\n"))
}

func main() {
    n := chain.New(
        chain.HandlerFunc(hello1),
        chain.HandlerFunc(hello2),
        chain.HandlerFunc(hello3),
        chain.Wrap(MyHandler{}),
    )
    http.ListenAndServe(":8888", n)
}

参考

takayukinakata.hatenablog.com