goでutf8メールを送信する

Goでメールを送信することは簡単です。`net/smtp`の`SendMail()`で送信できます。しかし、これは平文の送信に限った話です。

マルチバイト文字である日本語を平文のまま送信してしまうと、メーラーによっては文字化けが発生します。よって、utf8エンコーディングしてから送信する必要が出てきます。

メールを送信するには、お決まりのインターネットメッセージフォーマットを守る必要があります。そこで決まっているのが、 [RFC2822](https://datatracker.ietf.org/doc/html/rfc2822) という文書です。ここで特に気にしないといけないのが、一行あたりの文字数です。翻訳してみると、

>There are two limits that this standard places on the number of characters in a line. Each line of characters MUST be no more than 998 characters, and SHOULD be no more than 78 characters, excluding the CRLF.
>>この標準では、1行の文字数に2つの制限があります。 文字の各行は998文字以下である必要があり、CRLFを除いて78文字以下である必要があります。

Google翻訳が意味不明ですが、
>CRLF を除いて、各行は 998 文字を超えてはならならず(MUST)、78 文字を超えるべきではない(SHOULD)。
という意味になります。要するに「一行の文字数はは78を超えるな」ということです。

上記規約を守るように、ヘッダと本文を適度に改行したソースを下記に示しています。
[go で utf8メールを送信](https://qiita.com/yamasaki-masahide/items/a9f8b43eeeaddbfb6b44)をめちゃくちゃ参考にしています。
上記記事は最終更新からだいぶ時間が経っていたので、検証を含め見やすくリファクタリングしています。
そして、タイトルと本文が文字化けせず送信できることも確認しています。

```go
// メール一行で超えるべきではない文字数
// https://datatracker.ietf.org/doc/html/rfc2822#section-2.1.1
const (
	charactorLimitForOneLineOfEmail = 78
	smtpServer = "smtp.example.com:25"
)

func main() {
	to := "to@example.com"
	from := "from@example.com"
	subject := "タイトル"
	body := "本文"
	sendEmail(to, from, subject, body)
}

// 適切な長さにカットしCRLFを挿入
func cutAndAddCrlf(msg string) string {
	buffer := bytes.Buffer{}
	for k, c := range strings.Split(msg, "") {
		buffer.WriteString(c)
		if (k+1)%charactorLimitForOneLineOfEmail == 0 {
			buffer.WriteString("\r\n")
		}
	}
	return buffer.String()
}

// UTF8文字列を指定文字数で分割
func utf8Split(utf8string string, length int) []string {
	result := []string{}
	buffer := bytes.Buffer{}
	for k, c := range strings.Split(utf8string, "") {
		buffer.WriteString(c)
		if (k+1)%length == 0 {
			result = append(result, buffer.String())
			buffer.Reset()
		}
	}
	if buffer.Len() > 0 {
		result = append(result, buffer.String())
	}
	return result
}

// タイトルをMIMEエンコード
func encodeSubject(subject string) string {
	buffer := bytes.Buffer{}
	buffer.WriteString("Subject:")
	limit := charactorLimitForOneLineOfEmail / 6 // Unicodeでは一文字が最大6バイトになるため
	for _, line := range utf8Split(subject, limit) {
		buffer.WriteString(" =?utf-8?B?")
		buffer.WriteString(base64.StdEncoding.EncodeToString([]byte(line)))
		buffer.WriteString("?=\r\n")
	}
	return buffer.String()
}

// ヘッダを作る
func makeHeader(from, to, subject string) bytes.Buffer {
	header := bytes.Buffer{}
	header.WriteString(fmt.Sprintf("From: %s\r\n", from))
	header.WriteString(fmt.Sprintf("To: %s\r\n", to))
	header.WriteString(encodeSubject(subject))
	header.WriteString("MIME-Version: 1.0\r\n")
	header.WriteString("Content-Type: text/plain; charset=\"utf-8\"\r\n")
	header.WriteString("Content-Transfer-Encoding: base64\r\n")

	return header
}

// 設定したメールアドレスは To にセットされる
func SendEmail(to string, from string, subject string, body string) error {
	header := makeHeader(from, to, subject)
	encodedBody := base64.StdEncoding.EncodeToString([]byte(body))

	message := header
	message.WriteString(cutAndAddCrlf(encodedBody))
	auth := getAuth(config.GetConf().Email)
	err := smtp.SendMail(smtpServer, auth, from, []string{to}, message.Bytes())
	if err != nil {
		logger.Errorln(err)
		return err
	}
	return nil
}

```