LoginSignup
2
1

More than 1 year has passed since last update.

goでutf8メールを送信する

Last updated at Posted at 2022-02-23

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

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

メールを送信するには、お決まりのインターネットメッセージフォーマットを守る必要があります。そこで決まっているのが、 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://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
}

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1