现代生活中,除了短信、微信、QQ等通讯工具,我们还普遍使用着电子邮件。电子邮件的产生远比前几样工具早,目前使用频率虽然没有其他几样高,但却在工作、举证、身份识别等正式场合扮演着不可或缺的角色。

为了实现邮件的发送,我们不得不提SMTP协议。

什么是SMTP协议?

SMTP的全称是Simple Mail Transfer Protocol,是基于tcp的用于发送传输邮件的纯文本协议。早期的邮件传输都是点对点的,这就需要邮件的收发双方都同时在线才能完成邮件的发送,后来为了解决发送端在接收端离线时也能发送邮件,引入了邮件服务器的技术。也就是说,我们发送一封邮件是先发送到我们自己所属的邮件投递服务器,然后由我们的邮件投递服务器投递给对方的邮件服务器,对方上线后,再由对方的邮件服务器投递给对方或者由对方从邮件服务器获取。在这些投递过程中,SMTP协议发挥着重要的作用。

SMTP协议是怎样交互的?

我们以发件人往SMTP服务器发送邮件为例,将这个行为类比成发件人去揽收站投递邮件,如下图:

一般电子邮件怎么发(你知道一封电子邮件是怎么发送出去的吗)(1)

可以很明显的看到,在这个投递过程中,发件人和服务器之间是一种命令响应式的结构。

在SMTP协议中,给客户端定义了许多可用的命令,下表是常用命令:

一般电子邮件怎么发(你知道一封电子邮件是怎么发送出去的吗)(2)

也给服务端定义了很多响应,1开头为接收到信息但还未处理,2开头为确认应答类,3开头为需要进一步确认,4开头为临时错误消息,5开头为永久错误消息,下表是常用响应:

一般电子邮件怎么发(你知道一封电子邮件是怎么发送出去的吗)(3)

基于以上我们可以在telnet命令中尝试:

# 执行telnet命令 > computer:home user$ telnet mail.xx.com 25 Trying 127.0.0.1... Connected to mail.xx.com. Escape character is '^]'. # 服务器响应 < 220 mail.xx.com Anti-spam GT for Coremail System (icmhosting[20181212]) # 发送开始通信命令 > EHLO mail.xx.com < 250-mail < 250-PIPELINING < 250-AUTH LOGIN PLAIN < 250-AUTH=LOGIN PLAIN < 250-coremail abcdefg < 250-STARTTLS < 250-SMTPUTF8 < 250 8BITMIME # 请求认证 > AUTH LOGIN # 要求输入用户名,dXNlcm5hbWU6是username:的base64编码 < 334 dXNlcm5hbWU6 # 输入base64编码后的用户名 > MTIzNDU2QHh4LmNvbQ== # 要求输入密码,UGFzc3dvcmQ6是Password:的base64编码 < 334 UGFzc3dvcmQ6 # 输入base64编码后的密码 > YWJjZGVmZw== # 返回认证成功信息 < 235 Authentication successful # 指定发件人 > MAIL FROM:<123456@xx.com> # 响应收到 < 250 Mail OK # 指定收件人 > RCPT TO:<654321@yy.org> # 响应收到 < 250 Mail OK # 准备发送邮件正文 > DATA # 等待输入 < 354 End data with <CR><LF>.<CR><LF> # 邮件内容 > To:654321@yy.org > From:123456@xx.com > Subject:Test mail from telnet > Mime-Version:1.0 > Content-Type:Multipart/Mixed;boundary=My-Boundary > Content-Transfer-Encoding:base64 > > --My-Boundary > Content-Type:Text/Plain;charset=utf-8 > Content-Transfer-Encoding:base64 > # 下面这串字符串是base64编码后的邮件正文,编码前是:This is a test mail from telnet command. > VGhpcyBpcyBhIHRlc3QgbWFpbCBmcm9tIHRlbG5ldCBjb21tYW5kLg== > > . # 收到确认 < 250 Mail OK queued as AQAAfwAnOED_4v5ev6lwAQ--.56424S2 # 退出命令 > QUIT # 确认收到 < 221 Bye Connection closed by foreign host.

然后我们就可以看到邮件发送出去了。

一般电子邮件怎么发(你知道一封电子邮件是怎么发送出去的吗)(4)

如何实现一个SMTP客户端?

下面,我们以Go语言为例,实现一个简单的SMTP发送程序,中间会使用到Base64编解码,关于Base64可以参考这篇:你知道Base64编码吗?跟我一起用Go语言实现它吧

package m_smtp import ( "bufio" "errors" "fmt" "xx.com/user/test/base64" "io" "net" ) // 定义结构体 type MSMTP struct { Host string // 主机名 Port int // 端口号 Account string // 认证的用户名 Password string // 密码 From string // 发件人 To string // 收件人 Subject string // 主题 Body string // 内容 conn *net.TCPConn // 连接 } var ( stageInit = 0 // 初始阶段 stageHelo = 1 // 开始通信阶段 stageAuthLogin = 2 // 请求认证阶段 stageAuthLoginUsername = 3 // 输入用户名阶段 stageAuthLoginPassword = 4 // 输入密码阶段 stageSetFrom = 5 // 设置发件人阶段 stageSetTo = 6 // 设置收件人阶段 stageStartSendData = 7 // 开始发送数据阶段 stageSendData = 8 // 发送数据阶段 stageQuit = 9 // 退出阶段 stageFinish = 10 // 结束 ) func SendMail(msmtp MSMTP) error { var err error // 获取tcp连接 msmtp.conn, err = getConn(msmtp.Host, msmtp.Port) if err != nil { return errors.New(fmt.Sprintf("can not create connection: %v\n", err)) } defer msmtp.conn.Close() // 执行发送 return doSend(msmtp) } // 获取tcp连接 func getConn(host string, port int) (conn *net.TCPConn, err error) { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", host, port)) conn, err = net.DialTCP("tcp", nil, tcpAddr) return } // 执行发送 func doSend(msmtp MSMTP) error { var err error var servRespMsg string stage := stageInit reader := bufio.NewReader(msmtp.conn) F: for { // 以换行符为分隔读取响应消息 servRespMsg, err = reader.ReadString('\n') if err != nil || err == io.EOF { fmt.Printf("received error: %v", err) break F } err = nil // 从头3位获取响应码 code := servRespMsg[0:3] fmt.Printf("Stage: %d, Code: %s\n", stage, code) switch stage { case stageInit: if code != "220" { continue } err = sendHelo(msmtp) stage = stageHelo case stageHelo: if servRespMsg[0:4] != "250 " { continue } err = sendAuthLogin(msmtp) stage = stageAuthLogin case stageAuthLogin: if code != "334" { continue } err = sendAuthLoginUsername(msmtp) stage = stageAuthLoginUsername case stageAuthLoginUsername: if code != "334" { continue } err = sendAuthLoginPassword(msmtp) stage = stageAuthLoginPassword case stageAuthLoginPassword: if code != "235" { continue } err = sendSetFrom(msmtp) stage = stageSetFrom case stageSetFrom: if code != "250" { continue } err = sendSetTo(msmtp) stage = stageSetTo case stageSetTo: if code != "250" { continue } err = sendStartSendData(msmtp) stage = stageStartSendData case stageStartSendData: if code != "354" { continue } err = sendData(msmtp) stage = stageSendData case stageSendData: if code != "250" { continue } err = sendQuit(msmtp) stage = stageQuit case stageQuit: if code != "221" { continue } stage = stageFinish fmt.Println("finished") break F } if err != nil { break F } } return err } // 发送开始通信命令 func sendHelo(msmtp MSMTP) error { msg := fmt.Sprintf("EHLO %s\n", msmtp.Host) err := sendMsg(msmtp.conn, msg) if err != nil { return errors.New(fmt.Sprintf("send ehlo error: %v\n", err)) } return nil } // 发送请求认证命令 func sendAuthLogin(msmtp MSMTP) (err error) { msg := "AUTH LOGIN\n" err = sendMsg(msmtp.conn, msg) if err != nil { return errors.New(fmt.Sprintf("try auth failed: %v", err)) } return } // 发送用户名 func sendAuthLoginUsername(msmtp MSMTP) (err error) { msg := base64.Encode([]byte(msmtp.Account)) "\n" err = sendMsg(msmtp.conn, msg) if err != nil { return errors.New(fmt.Sprintf("send auth login username failed: %v", err)) } return } // 发送密码 func sendAuthLoginPassword(msmtp MSMTP) (err error) { msg := base64.Encode([]byte(msmtp.Password)) "\n" err = sendMsg(msmtp.conn, msg) if err != nil { return errors.New(fmt.Sprintf("send auth login password failed: %v", err)) } return } // 设置发件人 func sendSetFrom(msmtp MSMTP) (err error) { msg := fmt.Sprintf("MAIL FROM:<%s>\n", msmtp.From) err = sendMsg(msmtp.conn, msg) if err != nil { return errors.New(fmt.Sprintf("set mail from address failed: %v", err)) } return } // 设置收件人 func sendSetTo(msmtp MSMTP) (err error) { msg := fmt.Sprintf("RCPT TO:<%s>\n", msmtp.To) err = sendMsg(msmtp.conn, msg) if err != nil { return errors.New(fmt.Sprintf("set mail to address failed: %v", err)) } return } // 开始发送数据 func sendStartSendData(msmtp MSMTP) (err error) { msg := "DATA\n" err = sendMsg(msmtp.conn, msg) if err != nil { return errors.New(fmt.Sprintf("start send data failed: %v", err)) } return } // 发送数据 func sendData(msmtp MSMTP) (err error) { msg := fmt.Sprintf("To:%s\n" "Subject:%s\n" "Mime-version:1.0\n" "Content-Type:Multipart/Mixed;boundary=m-boundary\n" "Content-Transfer-Encoding:base64\n" "From:%s\n" "\n" "--m-boundary\n" "Content-Type:Text/Plain;charset=utf-8\n" "Content-Transfer-Encoding:base64\n" "\n" "%s\n" "\n" ".\n", msmtp.To, msmtp.Subject, msmtp.From, base64.Encode([]byte(msmtp.Body))) err = sendMsg(msmtp.conn, msg) if err != nil { return errors.New(fmt.Sprintf("send data failed: %v", err)) } return } // 发送结束命令 func sendQuit(msmtp MSMTP) (err error) { msg := "QUIT\n" err = sendMsg(msmtp.conn, msg) if err != nil { return errors.New(fmt.Sprintf("send quit failed: %v", err)) } return } // 发送消息 func sendMsg(conn *net.TCPConn, msg string) error { _, err := conn.Write([]byte(msg)) if err != nil { return errors.New(fmt.Sprintf("err write on: %s, err: %v", msg, err)) } return nil }

调用:

package main import ( "fmt" "xx.com/user/test/m_smtp" ) func main() { var msmtp = m_smtp.MSMTP{ Host: "mail.xx.com", Port: 25, Account: "123456@xx.com", Password: "abcdefg", From: "123456@xx.com", To: "654321@yy.org", Subject: "test mail in client", Body: "this is body of test mail.", } err := m_smtp.SendMail(msmtp) if err != nil { fmt.Printf("Error: %v", err) } }

执行后输出:

Stage: 0, Code: 220 Stage: 1, Code: 250 Stage: 1, Code: 250 Stage: 1, Code: 250 Stage: 1, Code: 250 Stage: 1, Code: 250 Stage: 1, Code: 250 Stage: 1, Code: 250 Stage: 1, Code: 250 Stage: 2, Code: 334 Stage: 3, Code: 334 Stage: 4, Code: 235 Stage: 5, Code: 250 Stage: 6, Code: 250 Stage: 7, Code: 354 Stage: 8, Code: 250 Stage: 9, Code: 221 finished

,