smtp协议_如何利用SMTP协议发送一封电子邮件?

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

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

e9102e91b1980ef0c49d325e0230143c.png什么是SMTP协议?

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

e9102e91b1980ef0c49d325e0230143c.pngSMTP协议是怎样交互的?

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

5a43a807b58f9942c7c7052d140d9994.png

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

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

416faa18920703955bec079e5db814ce.png

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

1da6b0e6b53646b9b7b541b7cb753af5.png

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

# 执行telnet命令> computer:home user$ telnet mail.xx.com 25Trying 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 .# 邮件内容> 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==>> .# 收到确认# 退出命令> QUIT# 确认收到< 221 ByeConnection closed by foreign host.

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

2612db739936a41a7c7de21fc221bb31.png

e9102e91b1980ef0c49d325e0230143c.png如何实现一个SMTP客户端?

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

package m_smtpimport (   "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:\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:\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 mainimport (   "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: 220Stage: 1, Code: 250Stage: 1, Code: 250Stage: 1, Code: 250Stage: 1, Code: 250Stage: 1, Code: 250Stage: 1, Code: 250Stage: 1, Code: 250Stage: 1, Code: 250Stage: 2, Code: 334Stage: 3, Code: 334Stage: 4, Code: 235Stage: 5, Code: 250Stage: 6, Code: 250Stage: 7, Code: 354Stage: 8, Code: 250Stage: 9, Code: 221finished
     ▼如果喜欢,欢迎点赞和关注▼▼往期回顾▼你知道Base64编码吗?跟我一起用Go语言实现它吧!你知道如何在Go语言中愉快的使用环境变量吗?(上)Go语言环境变量库env源码解析

188a5afcd310de57be0a5c16ba2e7c04.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值