go言語で迷惑メールを迷惑メール相談センターに転送する

FreeBSDの自宅サーバで、個人のメールサーバを運用しています。
どうしてもSPAMメールが多いので、spamassassinを使っているのですが、それでもgmailなどに比べるとフィルタできないメールが多いです。
そこで、「迷惑メール相談センターに転送してあげると、そこからSPAM来なくなるよ」みたいなのをどこかで見た気がするので、手動で転送していました。(実際には、ここに転送したから止まるものではないと思いますが)
[情報提供方法 | 情報提供のお願い | 迷惑メール相談センター](https://www.dekyo.or.jp/soudan/contents/ihan/howto.html)

今までの手順は、spamassassinでSPAM判定されなかったSPAMメールに対して、以下をやっていました。
1. 手動で迷惑メール相談センターに転送
2. IMAPのlearnフォルダに移動
3. cronでlearnフォルダの中身をsa-learnに食わせて、削除

しかし、iPhoneで転送するとメールのヘッダがちゃんと転送されなさそうだし、手動でやるのもかなりだるいので、ここを自動化したいと思っていました。

そこで、今回は何度も [A Tour of Go](https://tour.golang.org/basics/1) をやっても覚えられなかった go言語を使って、メール転送プログラムを書いてみることにしました。
OS は FreeBSD 12.0、go は 1.11.4 です。

# メール送信のライブラリ

go 標準のライブラリでも、net/smtp パッケージなどがあり、頑張ればメールが送れそうです。
しかし、迷惑メール相談センターは元の迷惑メールを添付で送る方法を要求しており、自前でやるのはちょっと面倒そうです。(SMTPはそんなに難しいプロトコルではないので、自前でやってもそれほど大変ではないのですが)
そこで、今回は [gomail](https://github.com/go-gomail/gomail) と言うのを使ってみることにしました。

# メールを添付してメールを送る

[gomail のドキュメント](https://godoc.org/gopkg.in/gomail.v2)を読んでいると、一番簡単なメール送信のサンプルとして以下のようなものが出てきます。

```go
m := gomail.NewMessage()
m.SetHeader("From", "alex@example.com")
m.SetHeader("To", "bob@example.com", "cora@example.com")
m.SetAddressHeader("Cc", "dan@example.com", "Dan")
m.SetHeader("Subject", "Hello!")
m.SetBody("text/html", "Hello <b>Bob</b> and <i>Cora</i>!")
m.Attach("/home/Alex/lolcat.jpg")

d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")

// Send the email to Bob, Cora and Dan.
if err := d.DialAndSend(m); err != nil {
    panic(err)
}
```

最初のサンプルでファイルの添付までやってくれているので、ほぼそのまま真似してメールを送ってみました。

すると、受信したメールの該当部分のマルチパートのヘッダは以下のようになりました。添付ファイルの中身は、Base64エンコードされています。

```text
Content-Disposition: attachment; filename="spamsample"
Content-Transfer-Encoding: base64
Content-Type: application/octet-stream; name="spamsample"
```

ちなみに、Thunrderbirdでメールをforwardすると以下のようなヘッダになります。

```text
Content-Type: message/rfc822;
 name="=?UTF-8?B?44Ko44Ot44Ko44Ot5L2T6aiTLmVtbA==?="
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment;
 filename*0*=utf-8''%E3%82%A8%E3%83%AD%E3%82%A8%E3%83%AD%E4%BD%93%E9%A8%93;
 filename*1*=%2E%65%6D%6C
```

迷惑メール自体は添付できているので、これでも良いような気はしますが、相手がどのように受け取ったメールを処理しているかわからないので、もう少し Thunderbird に寄せたいと思います。

まずは、Message.Attachのドキュメントを読むと、第2引数でFileSettingと言うのを取ることがわかります。
これで、添付ファイルの名前を実際のファイル名と変えたり、任意のヘッダを付けたりすることができることがわかります。
そこで、Content-Type ヘッダで message/rfc822にしてみたり、 Content-Transfer-Encoding で 8bit にしてみたりすると、ヘッダ自身は変わるのですが、添付本体はBase64のままになりました。
goの場合、$GOPATH/src にパッケージのソースが入るので、gomailもソースを読むことができます。
添付ファイルをBase64エンコードしているところを探してみたところ、writeto.go の addFiles が以下のようになっていました。

```go:writeto.go(addFilesからさらに抜粋)
func (w *messageWriter) addFiles(files []*file, isAttachment bool) {
	for _, f := range files {
		・・・

		if _, ok := f.Header["Content-Transfer-Encoding"]; !ok {
			f.setHeader("Content-Transfer-Encoding", string(Base64))
		}

		・・・

		w.writeHeaders(f.Header)
		w.writeBody(f.CopyFunc, Base64)
	}
}
```
ヘッダについては、Content-Transfer-Encodingが引数で渡されていればそれを使いますが、writeBodyの第2引数は常にBase64でした。
そこで、他に使えるものがないかソースを見ていたところ、Message.newPart() あたりが使えるのではないかと思いましたが、goでは小文字で始まるメンバは外部に公開していないので使うことができません。
newPart()を呼んでいるものと言うことで、SetBodyとAddAlternative, AddAlternativeWriterあたりに辿り着きました。
SetBodyは本文用で追加のpart用ではないので、AddAlternativeWriterを使って、以下のようにしてみました。

```go:meiwaku.go
                f := func(w io.Writer) error {
                        h, err := os.Open(spamfilename)
                        if err != nil {
                                return err
                        }
                        if _, err := io.Copy(w, h); err != nil {
                                h.Close()
                                return err
                        }
                        return h.Close()
                }
                m.AddAlternativeWriter("message/rfc822", f, gomail.SetPartEncoding(gomail.Unencoded))
```

すると、無事に以下のようなヘッダになりました。

```text
Content-Transfer-Encoding: 8bit
Content-Type: message/rfc822; charset=UTF-8
```

Thunderbirdのものと比べると、Content-Typeヘッダにcharsetがあったり、Content-Dispositionヘッダがなかったりするあたりが違いますが、一応このメールをThunderbirdで見るとメールが転送されたファイルらしく見えます。

ここまでで実現の目処が立ったので、後はコマンドライン引数からファイルを受け取れるようにするのと、各種設定をソース直書きから設定ファイルに追い出すことにします。

# コマンドライン引数

「go コマンドライン引数」とかでググると、flagsとか言うパッケージがひっかかります。
ただ、ちょっと触って見てわかったのは、flagsはコマンドライン引数でオプション(スイッチ)を指定するときに便利なもので、ファイル名を引数で渡したいだけであれば、os.Args を使えば事足りることがわかりました。
os.Args は、[]stringを返し、[0]がコマンドそのもの、[1:]が引数です。
(最初、[0]の存在を忘れていて、コマンドをメールに添付してしまったのは秘密です)

# 設定ファイル

Qtとか.Netとかだと、設定を扱う基本的な仕組みがあり、どこに設定ファイルを持つかを悩む必要はありません。
goはどうかなと思って調べると、言語自体にはそう言う機能はなさそうです。
[toml](https://github.com/BurntSushi/toml)と言うのが良く検索でひっかかるので、使ってみることにしました。
これは、設定ファイルの仕様を決めているだけで、どこに置くかは決めていないようだったので、面倒なのでperlのpitに習って .config/meiwaku と言うファイルに設定を置くことにしました。
(ちなみに、naoyaさんが[go-pit](https://github.com/naoya/go-pit)を作っているらしいです)
後は、設定を持つ構造体を作って、toml.DecodeFile()を呼ぶだけで設定の外部化ができました。

```text:~/.config/meiwaku
From = "me@example.com" ←メールの転送元
To = "me@example.com" ←メールの転送先(テストが終わったらmeiwaku@dekyo.or.jpに)
MailServer = "localhost" ←メールサーバホスト名
Port = 25 ←ポート
Username = "" ←SMTP認証のユーザ名
Password = "" ←SMTP認証のパスワード
```

gomailは、通常のSMTP認証くらいまではサポートしているように見えますが、gmailとかでどうなるかは試していません。(自宅サーバはlocalhostからだったら認証なしでメールが送れます)

# githubに

できあがったものはgithubにpushしてあります。ソースはmeiwaku.go 1本で、短いです。

https://github.com/false-git/meiwaku

今まで、go get とかあまり理解していなかったのですが、githubにpushしてあると、go get で依存関係も含めてソースを持ってきて、build installまでしてくれます。

```shell
go get github.com/false-git/meiwaku
```

こうすると、$GOPATH/bin/meiwaku ができて、PATHを通せば実行できます。