eof怎么结束输入_你知道一封电子邮件是怎么发送出去的吗?

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

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

什么是SMTP协议?

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

SMTP协议是怎样交互的?

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

ddf0b1db0ae7a144ad515c7f8098c46c.png

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

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

785f5f145e89ce205cee0f298142f551.png

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

8c531d299322ef23c59a2e8a8340a0bc.png

基于以上我们可以在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.

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

b0805ae095f9bc265ecd84d13d46091b.png

如何实现一个SMTP客户端?

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

文科僧程序猿:你知道Base64编码吗?跟我一起用Go语言实现它吧!​zhuanlan.zhihu.com
27459bcb67dff0a0c5b3b4498efe5e6c.png
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: %vn", 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: %sn", 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 %sn", msmtp.Host)
   err := sendMsg(msmtp.conn, msg)
   if err != nil {
      return errors.New(fmt.Sprintf("send ehlo error: %vn", err))
   }
   return nil
}

// 发送请求认证命令
func sendAuthLogin(msmtp MSMTP) (err error) {
   msg := "AUTH LOGINn"
   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 := "DATAn"
   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:%sn"+
      "Subject:%sn"+
      "Mime-version:1.0n"+
      "Content-Type:Multipart/Mixed;boundary=m-boundaryn"+
      "Content-Transfer-Encoding:base64n"+
      "From:%sn"+
      "n"+
      "--m-boundaryn"+
      "Content-Type:Text/Plain;charset=utf-8n"+
      "Content-Transfer-Encoding:base64n"+
      "n"+
      "%sn"+
      "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 := "QUITn"
   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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值