Golang Cafe #16 まとめ その1 mailパッケージ

2014/02/09に開催された「Golang Cafe #16」についてのまとめその1です。
今回はmailパッケージsmtpパッケージと+αをみていきました。長くなりそうなのでまずはmailパッケージをまとめておきます。


+TakashiYokoyama氏が準備してくれたサンプルはこちらにあります。私の書いたサンプルはGitHubに置いてあります。

mailパッケージ

mailパッケージはメールの内容を解析するパッケージです。
POP3IMAPプロトコルでサーバーからメールを取得する機能はありません。あくまでPOP3IMAPプロトコルでサーバーから取得したメールの内容を解析するパッケージです。
できることは以下の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)
}

実行結果は省略しますので確認したい方は各自でお願いします。
メールのヘッダと本文を解析するには、

  1. mail.ReadMessageメソッドにio.Readerを渡します。戻り値としてMessageを取得できます。
  2. Message.Headerに解析されたヘッダの内容がmapでキー名:値(文字列)の配列の形式で格納されています。rangeで順番に取得することもできるし、Getメソッドで特定のヘッダを取得することもできます。
  3. 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でもないので、エラーの型で判定して処理ができません。


Golang Cafe #16 まとめ その2 smtpパッケージ」へ続く。