Golang Cafe #16 まとめ その1 mailパッケージ
2014/02/09に開催された「Golang Cafe #16」についてのまとめその1です。
今回はmailパッケージとsmtpパッケージと+αをみていきました。長くなりそうなのでまずはmailパッケージをまとめておきます。
+TakashiYokoyama氏が準備してくれたサンプルはこちらにあります。私の書いたサンプルはGitHubに置いてあります。
mailパッケージ
mailパッケージはメールの内容を解析するパッケージです。
POP3やIMAPのプロトコルでサーバーからメールを取得する機能はありません。あくまでPOP3やIMAPのプロトコルでサーバーから取得したメールの内容を解析するパッケージです。
できることは以下の3つです。
- メールアドレスの解析
- メールヘッダの解析
- メール本文の解析
メールアドレスの解析
func main() { parse("Barry Gibbs <bg@example.com>") parse("\"Example.co.jp\" <ship-confirm@example.co.jp>") parse("hoge..hoge@example.ne.jp") parse("hoge..@example.ne.jp") parse("..hoge@example.ne.jp") parse("") parse("account.example.com") parse("=?utf-8?q?B=C3=B6b?= <bob@example.com>") } func parse(address string) { if addr, err := mail.ParseAddress(address); err != nil { fmt.Printf("Error: %v\n", err) } else { fmt.Printf("%v -> Name: %v, Address: %v\n", address, addr.Name, addr.Address) } }
$ go run sample01.go Barry Gibbs-> Name: Barry Gibbs, Address: bg@example.com "Example.co.jp" -> Name: Example.co.jp, Address: ship-confirm@example.co.jp hoge..hoge@example.ne.jp -> Name: , Address: hoge..hoge@example.ne.jp hoge..@example.ne.jp -> Name: , Address: hoge..@example.ne.jp Error: mail: missing word in phrase Error: mail: no address Error: mail: missing phrase =?utf-8?q?B=C3=B6b?= -> Name: B〓b, Address: bob@example.co
mail.ParseAddressメソッドに対象の文字列を指定します。
表示名とメールアドレスの分割と、メールアドレスとしての正当性も判断してくれるようです。(ピリオドの連続も大丈夫そうです)
メールヘッダの解析とメール本文の解析
近年ではutf8のメールも多くなってきましたが、まだまだiso-2022-jpのメールも多くあります。
Go言語ではデフォルトではiso-2022-jpのエンコードとデコードはサポートしていないようなのでgo.textパッケージを追加で入れる必要があります。以下のサンプルを実行する前にチェックアウトしておいてください。
$ hg clone https://code.google.com/p/go.text/
詳細はこちらで確認して下さい。
package main import ( // hg clone https://code.google.com/p/go.text/ "code.google.com/p/go.text/encoding/japanese" "code.google.com/p/go.text/transform" "fmt" "io/ioutil" "net/mail" "os" ) func main() { // メールの内容をそのまま保存したmail.emlを同じディレクトリに用意してください。 var f *os.File var err error if f, err = os.Open("./mail.eml"); err != nil { fatal("Error: %v\n", err) } defer f.Close() var message *mail.Message if message, err = mail.ReadMessage(f); err != nil { fatal("Error: %v\n", err) } for k, v := range message.Header { fmt.Printf("%v\n%v\n\n", k, v) } fmt.Printf("%v -> %v\n", "content-type", message.Header.Get("content-type")) // addrs, err := message.Header.AddressList("from") // fmt.Printf("1, %v\n", err) // for _, addr := range addrs { // fmt.Printf("Name: %v, Address: %v\n", addr.Name, addr.Address) // } var body []byte // if body, err = ioutil.ReadAll(message.Body); err != nil { // fatal("Error: %v\n", err) // } // utf8エンコーディングの場合は変換処理は不要です。 // dst := make([]byte, len(body)*2) // var dlen int // transformer := japanese.ISO2022JP.NewDecoder() // if dlen, _, err = transformer.Transform(dst, body, true); err != nil { // fmt.Fprintf(os.Stderr, "Error: ", err) // os.Exit(1) // } //fmt.Printf("%v\n", string(dst[:dlen])) // transform.NewReaderを使うほうがスマート! if body, err = ioutil.ReadAll( transform.NewReader(message.Body, japanese.ISO2022JP.NewDecoder())); err != nil { fatal("Error: %v\n", err) } fmt.Printf("%v\n", string(body)) } func fatal(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, format, args) os.Exit(1) }
実行結果は省略しますので確認したい方は各自でお願いします。
メールのヘッダと本文を解析するには、
- mail.ReadMessageメソッドにio.Readerを渡します。戻り値としてMessageを取得できます。
- Message.Headerに解析されたヘッダの内容がmapでキー名:値(文字列)の配列の形式で格納されています。rangeで順番に取得することもできるし、Getメソッドで特定のヘッダを取得することもできます。
- Message.Bodyが本文を取得するためのio.Readerを返すので、適当な方法で読み込みます。
今回はメール自体がiso-2022-jpのエンコードのためデコード処理を行っています。
因みにメールアドレスの表示名がiso-2022-jpでエンコーディングされている場合、Header.AddressListのメソッドを呼んだ時点でエラーになるようです。Header.Get("from")で取得して自力でデコードする必要がありそうです。
multipartの場合
Goのパッケージをみていたところ使えそうなものがありました。mimeパッケージとmultipartパッケージです。
mime.ParseMediaType関数とmultipart.Readerおよびmultipart.Partを利用すればできそうです。
func main() { // メールの内容をそのまま保存したmail2.emlを同じディレクトリに用意してください。 var f *os.File var err error if f, err = os.Open("./mail2.eml"); err != nil { fmt.Fprintf(os.Stderr, "Error: ", err) os.Exit(1) } defer f.Close() var message *mail.Message if message, err = mail.ReadMessage(f); err != nil { fmt.Fprintf(os.Stderr, "Error: ", err) os.Exit(1) } contentType := message.Header.Get("content-type") parseBody(message.Body, contentType) } func parseBody(body io.Reader, contentType string) { var mediatype string var params map[string]string var err error if mediatype, params, err = mime.ParseMediaType(contentType); err != nil { fmt.Fprintf(os.Stderr, "Error: ", err) os.Exit(1) } switch strings.Split(mediatype, "/")[0] { case "text", "html": fmt.Printf("%v charset=%v\n\n", "text or html.", params["charset"]) case "message": fmt.Printf("message: do something \n\n") case "multipart": //fmt.Printf("%v\n\n", "multipart") boundary := params["boundary"] r := multipart.NewReader(body, boundary) var part *multipart.Part for { if part, _ = r.NextPart(); part == nil { // err では判定できません。 break } contentType := part.Header.Get("content-type") parseBody(part, contentType) part.Close() } default: // 画像などの添付ファイルなど。ファイルに書き出します。 fmt.Printf("%v, filename=%v\n\n", mediatype, params["name"]) } }
参考程度に実装内容(一例です)を載せておきます。気になるところを一つだけ。
Reader.NextPartメソッドの戻り値のerrがio.EOFではなく、独自に定義したerrorでもないので、エラーの型で判定して処理ができません。