2017年2月25日 星期六

GO 語言如何寄 E-Mail

最近突然對 E-Mail 有興趣,所以研究一下
首先,根據 RFC 會知道 Email 的內文格式如下:
Subject: This is a test mail!
From: test@example.com
Content-Type: multipart/mixed; boundary="qwertyuio"

--qwertyuio
This is the body of email.

--qwertyuio--

(換行要使用 "\r\n" ,不能是 "\n"

若要追加附件,則增加 mime body即可:
Subject: This is a test mail!
From: test@example.com
Content-Type: multipart/mixed; boundary="qwertyuio"

--qwertyuio
This is the body of email.

--qwertyuio
Content-Type: text/html; name="example.html"
Content-Disposition: attachment; filename="example.html"

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"/><title>Example</title></head>
<body>This is an attachment!</body>
</html>
--qwertyuio
Content-Type: text/plain; name="example.txt"
Content-Disposition: attachment; filename="example.txt"

An example text file

--qwertyuio--


上面是寄送兩個附件檔(example.html 和 example.txt)的電子郵件。
如果要追加圖片之類的檔案,一樣做法,不過最好將圖片以 base64 加密。我試過不加密的話,寄出去會是混亂的圖。


知道如何寄後,接下來就能寫 GoLang code 了
寄信需要的是 smtp 這個 package,以 Gmail 為例,寄信的 code 如下:
package main

import (
	"net/smtp"
)

// 以下 variable 可參考 Gmail 的 smtp 設定說明
var (
	host     = "smtp.gmail.com:587"
	username = "example@gmail.com"
	password = "Pass123"
)

func main() {
	auth := smtp.PlainAuth(host, username, password, "smtp.gmail.com")

	to := []string{"recipient@example.net"}
	msg := []byte(
		"Subject: This is a test mail!\r\n" +
			"From: test@example.com\r\n" +
			`Content-Type: multipart/mixed; boundary="qwertyuio"` + "\r\n" +
			"\r\n" +
			"--qwertyuio\r\n" +
			"This is the body of email.\r\n" +
			"\r\n" +
			"--qwertyuio--\r\n",
	)
	smtp.SendMail(
		host,
		auth,
		username,
		to,
		msg,
	)

}

若要加入圖片,由於需要 base64 encode,需要再 import "encoding/base64" package,如下:
package main

import (
	"encoding/base64"
	"io/ioutil"
	"log"
	"net/smtp"
	"strings"
)

var (
	host     = "smtp.gmail.com:587"
	username = "example@gmail.com"
	password = "Pass123"
)

func main() {
	auth := smtp.PlainAuth(host, username, password, "smtp.gmail.com")

	to := []string{"recipient@example.net"}

	imageByte, err := ioutil.ReadFile("/tmp/sample.png")

	if err != nil {
		log.Fatal(err)
	}
	// 為方便閱讀,用 string array 存 mail body
	// 之後再用 strings.join 合併
	messages := []string{
		"Subject: This is a test mail!",
		"From: test@example.com",
		"Content-Type: multipart/mixed; boundary=\"qwertyuio\"",
		"",
		"--qwertyuio",
		"This is the body of email.",
		"",
		"--qwertyuio",
		"Content-Type: image/png;",
		"Content-Transfer-Encoding: base64",
		"",
		[]byte(
			base64.StdEncoding.EncodeToString(
				string(imageByte),
			),
		),
		"--qwertyuio--",
	}

	err = smtp.SendMail(
		host,
		auth,
		username,
		to,
		[]byte(strings.Join(messages, "\r\n")),
	)

	if err != nil {
		log.Fatal(err)
	}
}
但這樣子 mime body 多了的話就變得麻煩了,Go Lang 有提供 "mime/multipart" package 方便處理,概念上其實是一樣的:
package main

import (
	"bytes"
	"encoding/base64"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/smtp"
)

var (
	host     = "smtp.gmail.com:587"
	username = "example@gmail.com"
	password = "Pass123"
)

func main() {
	auth := smtp.PlainAuth(host, username, password, "smtp.gmail.com")
	to := []string{"recipient@example.net"}

	boundary := "qwertyuiuhgfgh"

	mailbody := &bytes.Buffer{}
	mailbody.Write([]byte("Subject: This is a test mail!\r\n"))
	mailbody.Write([]byte("From: test@example.com" + "\r\n"))
	mailbody.Write([]byte(`Content-Type: multipart/mixed; boundary="` + boundary + `"` + "\r\n"))

	w := multipart.NewWriter(mailbody)
	w.SetBoundary(boundary)

	// mail body
	bw, err := w.CreatePart(map[string][]string{})
	if err != nil {
		log.Fatal(err)
	}

	bw.Write([]byte("This is the body of email."))

	imageByte, err := ioutil.ReadFile("/tmp/sample.png")

	if err != nil {
		log.Fatal(err)
	} else {
		bw, _ = w.CreatePart(
			map[string][]string{
				"Content-Type": []string{`image/png`},
				"Content-Disposition": []string{
					`attachment; filename="sample.png"`,
				},
				"Content-Transfer-Encoding": []string{"base64"},
			},
		)

		// 和先前的 base64.StdEncoding.EncodeToString 一樣
		// 都是將讀取圖片得到的 byte array 以 base 64 加密
		//
		// 但 base64.StdEncoding.EncodeToString 回傳的是加密後的 String value
		// 這次已經使用 io.Writer 了,就沒必要特地變成 String 再轉成 []byte
		// 直接將加密後得到的 byte 輸入至 bw 中
		encoder := base64.NewEncoder(base64.StdEncoding, bw)
		encoder.Write(imageByte)
	}

	w.Close()

	err = smtp.SendMail(
		host,
		auth,
		username,
		to,
		mailbody.Bytes(),
	)

	if err != nil {
		log.Fatal(err)
	}
}





 參考 RFC:
  • Email 內文格式相關
    • RFC 2822
  • Boundary 的使用
    • RFC 2046

沒有留言:

張貼留言