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) }
まとめ
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
が含まれる。
参考
Emacs × Go でタグジャンプ
helm-gtagsを使ってて、RubyとかPHP書いてたときにはうまくタグを作れてたんだけどGolangでうまく作れてなかった。以下の記事を見つけて試しにやってみたらうまくいった。
/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をインストールする
markdown-pdfをインストールする
$ npm install markdown-pdf
適用したいCSSをgithubから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を書いていく
自分はまだ何も武器がないプログラマなので、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{}, } }
メモ。
使い方は以下の通り。
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) }