邮件服务器
最近由于涉及到邮件服务器相关内容的工作,并且需要进行部分的协议转发的工作故了解一下邮件服务器的协议。当前选择应用较为广泛的SMTP的邮件协议。
SMTP协议概述
SMTP全称是Simple Mail Transfer Protocol,最近的协议定稿是rfc5321,根据文档的描述,基础的设计方案如下;
通过SMTP客户端发送邮件信息到一个或者多个SMTP服务端,并记录发送的状态结果。SMTP客户端一旦确定目标的方式域,确定要向其复制副本的SMTP服务器的身份一条消息将被传输,然后执行该传输。 将邮件传输到SMTP服务器,SMTP客户端建立双向传输通道该SMTP服务器。 SMTP客户端确定通过解析目标来运行SMTP服务器的适当主机中间Mail eXchanger主机或最终主机的域名目标主机。
SMTP服务器可以是最终目的地,也可以是中间“中继”(即,它可以担当SMTP的角色客户端收到消息后)或“网关”(即,它可能使用SMTP以外的其他协议进一步传输邮件)。SMTP命令由SMTP客户端生成并发送到SMTP
服务器。 SMTP答复从SMTP服务器发送到SMTP客户端响应命令。
换句话说,消息传输可以在单个连接中发生介于原始SMTP发送者和最终SMTP接收者之间,或者可以通过中介系统发生在一系列传输中。在任一情况下,服务器在结束时发出了成功响应邮件数据,则发生了邮件责任的正式移交:
该协议要求服务器必须承担以下责任:传递消息或正确报告失败情况。 一旦建立了传输通道并进行了初始握手完成后,SMTP客户端通常会启动邮件事务。此类交易由一系列命令组成,这些命令用于指定邮件的发件人和目的地以及邮件内容(包括标题部分中的任何行或其他结构)本身。当同一封邮件发送到多个接收者,此协议鼓励仅传输一个位于同一目的地的所有收件人的数据副本(或中间中继)主机。
服务器对每个命令进行响应。 回复可能表示该命令已被接受,其他命令是预期,或者存在临时或永久错误条件。指定发件人或收件人的命令可以包括服务器允许的SMTP服务扩展请求。 该对话框是一次锁定步骤,一次尽管可以通过共同商定的扩展名对此进行修改请求,例如命令流水线。
SMTP程序实现的概述
SMTP程序的实现过程主要包括如下几个点:会话启动,邮件发送,转发邮件,验证邮箱名称和扩展邮件列表,以及打开和关闭连接。
- 会话启动:主要是客户端打开一个连接并且服务端响应返回并开始后续传输工作。
- 客户端初始化:当会话启动之后,服务端会发送给客户端一个greeting指令,并且客户端接收了该指令并发送EHLO指令到服务端,当打开会话之后并使用了EHLO指令之后就意味着客户端可以通过协议来进行通信并进行有机的发送工作。
- 邮件发送:主要有三个步骤来进行邮件的发送工作,第一是通过MAIL FORM指令发送当前的发送邮件者的信息,第二是通过RCPT指令来获取需要发送到哪些用户的信息,第三是通过DATA指令来接受客户端发送过来的邮件的内容信息,并通过end of mail的符号来标识该邮件内容结束。
- 转发邮件:通过实现的251或551来判断该邮件是否应该转发到对应配置的地址上去。
主要的流程如上所示,通过大概四个步骤就可以将整个邮件发送出去。
当前简单的指令的交互过程如下:
S: 220 smtp.example.com ESMTP Postfix
C: HELO relay.example.com
S: 250 smtp.example.com, I am glad to meet you
C: MAIL FROM:<[email protected]>
S: 250 Ok
C: RCPT TO:<[email protected]>
S: 250 Ok
C: RCPT TO:<[email protected]>
S: 250 Ok
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: From: "Bob Example" <[email protected]>
C: To: Alice Example <[email protected]>
C: Cc: [email protected]
C: Date: Tue, 15 Jan 2008 16:02:43 -0500
C: Subject: Test message
C:
C: Hello Alice.
C: This is a test message with 5 header fields and 4 lines in the message body.
C: Your friend,
C: Bob
C: .
S: 250 Ok: queued as 12345
C: QUIT
S: 221 Bye
{The server closes the connection}
具体的指令的格式和含义,大家可以详细查阅文档。
golang实现的go-smtp库
实例代码如下:
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"time"
"github.com/emersion/go-smtp"
)
// The Backend implements SMTP server methods.
type Backend struct{}
// Login handles a login command with username and password.
func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
if username != "username" || password != "password" {
return nil, errors.New("Invalid username or password")
}
return &Session{}, nil
}
// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
return nil, smtp.ErrAuthRequired
}
// A Session is returned after successful login.
type Session struct{}
func (s *Session) Mail(from string, opts smtp.MailOptions) error {
log.Println("Mail from:", from)
return nil
}
func (s *Session) Rcpt(to string) error {
log.Println("Rcpt to:", to)
return nil
}
func (s *Session) Data(r io.Reader) error {
if b, err := ioutil.ReadAll(r); err != nil {
return err
} else {
log.Println("Data:", string(b))
}
return nil
}
func (s *Session) Reset() {
fmt.Println(" reset ")
}
func (s *Session) Logout() error {
fmt.Println(" logout ")
return nil
}
func main() {
be := &Backend{}
s := smtp.NewServer(be)
s.Addr = ":1025"
s.Domain = "localhost"
s.ReadTimeout = 10 * time.Second
s.WriteTimeout = 10 * time.Second
s.MaxMessageBytes = 1024 * 1024
s.MaxRecipients = 50
s.AllowInsecureAuth = true
log.Println("Starting server at", s.Addr)
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
编译运行该脚本,并且在编写一个客户端脚本
package main
import (
"log"
"strings"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
)
func main() {
// Set up authentication information.
auth := sasl.NewPlainClient("", "username", "password")
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
to := []string{"[email protected]"}
msg := strings.NewReader("To: [email protected]\r\n" +
"Subject: discount Gophers!\r\n" +
"\r\n" +
"This is the email body.\r\n")
err := smtp.SendMail("localhost:1025", auth, "[email protected]", to, msg)
if err != nil {
log.Fatal(err)
}
err1 := smtp.SendMail("localhost:1025", auth, "[email protected]", to, msg)
if err1 != nil {
log.Fatal(err1)
}
}
当运行客户端脚本是就可以看见服务端打印的数据信息。
go-smtp实现流程
在服务端脚本中,最终调用的是ListenAndServe,该方法是SMTP中Server的实例;
// Serve accepts incoming connections on the Listener l.
func (s *Server) Serve(l net.Listener) error {
s.locker.Lock()
s.listeners = append(s.listeners, l) // 添加到监听这列表中
s.locker.Unlock()
for {
c, err := l.Accept() // 检查是否有请求进来
if err != nil {
select {
case <-s.done:
// we called Close()
return nil
default:
return err
}
}
go s.handleConn(newConn(c, s)) // 有请求进来就调用handleConn处理
}
}
func (s *Server) handleConn(c *Conn) error {
s.locker.Lock()
s.conns[c] = struct{}{} // 初始化一个对应的连接信息
s.locker.Unlock()
defer func() {
c.Close() // 处理完成关闭连接
s.locker.Lock()
delete(s.conns, c) // 删除对应的数据
s.locker.Unlock()
}()
c.greet() // 发送greet信息
for {
line, err := c.ReadLine() // 读客户端发送回来的数据
if err == nil {
cmd, arg, err := parseCmd(line) // 解析每一行的数据指令
if err != nil {
c.protocolError(501, EnhancedCode{5, 5, 2}, "Bad command") // 如果出错则返回错误指令
continue
}
c.handle(cmd, arg) // 如果过没有出错则依次根据客户端传过来的指令进行处理
} else {
if err == io.EOF { // 如果连接关闭则返回
return nil
}
if err == ErrTooLongLine { // 是否太长了
c.WriteResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection")
return nil
}
if neterr, ok := err.(net.Error); ok && neterr.Timeout() { // 是否空闲超时
c.WriteResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye")
return nil
}
c.WriteResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry") //连接出错
return err
}
}
}
// ListenAndServe listens on the network address s.Addr and then calls Serve
// to handle requests on incoming connections.
//
// If s.Addr is blank and LMTP is disabled, ":smtp" is used.
func (s *Server) ListenAndServe() error {
network := "tcp"
if s.LMTP {
network = "unix"
}
addr := s.Addr
if !s.LMTP && addr == "" {
addr = ":smtp"
}
l, err := net.Listen(network, addr) // 监听端口
if err != nil {
return err
}
return s.Serve(l) // 调用服务处理每一个进来的请求
}
从服务端对每一个新建立的请求处理可知,每一个新进来的请求都会发送greet信息,然后再根据客户端传入的指令来依次处理。
func parseCmd(line string) (cmd string, arg string, err error) {
line = strings.TrimRight(line, "\r\n") // 通过\r\n来解析每一条指令
l := len(line)
switch {
case strings.HasPrefix(strings.ToUpper(line), "STARTTLS"):
return "STARTTLS", "", nil
case l == 0:
return "", "", nil
case l < 4:
return "", "", fmt.Errorf("Command too short: %q", line)
case l == 4:
return strings.ToUpper(line), "", nil
case l == 5:
// Too long to be only command, too short to have args
return "", "", fmt.Errorf("Mangled command: %q", line)
}
// If we made it here, command is long enough to have args
if line[4] != ' ' {
// There wasn't a space after the command?
return "", "", fmt.Errorf("Mangled command: %q", line)
}
// I'm not sure if we should trim the args or not, but we will for now
//return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " "), nil
return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " \n\r"), nil
}
...
// Commands are dispatched to the appropriate handler functions.
func (c *Conn) handle(cmd string, arg string) {
// If panic happens during command handling - send 421 response
// and close connection.
defer func() {
if err := recover(); err != nil {
c.WriteResponse(421, EnhancedCode{4, 0, 0}, "Internal server error")
c.Close()
stack := debug.Stack()
c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.State().RemoteAddr, err, stack)
}
}()
if cmd == "" {
c.protocolError(500, EnhancedCode{5, 5, 2}, "Speak up")
return
}
cmd = strings.ToUpper(cmd) // 根据解析的命令来处理
switch cmd {
case "SEND", "SOML", "SAML", "EXPN", "HELP", "TURN":
// These commands are not implemented in any state
c.WriteResponse(502, EnhancedCode{5, 5, 1}, fmt.Sprintf("%v command not implemented", cmd))
case "HELO", "EHLO", "LHLO":
lmtp := cmd == "LHLO"
enhanced := lmtp || cmd == "EHLO"
if c.server.LMTP && !lmtp {
c.WriteResponse(500, EnhancedCode{5, 5, 1}, "This is a LMTP server, use LHLO")
return
}
if !c.server.LMTP && lmtp {
c.WriteResponse(500, EnhancedCode{5, 5, 1}, "This is not a LMTP server")
return
}
c.handleGreet(enhanced, arg)
case "MAIL":
c.handleMail(arg)
case "RCPT":
c.handleRcpt(arg)
case "VRFY":
c.WriteResponse(252, EnhancedCode{2, 5, 0}, "Cannot VRFY user, but will accept message")
case "NOOP":
c.WriteResponse(250, EnhancedCode{2, 0, 0}, "I have sucessfully done nothing")
case "RSET": // Reset session
c.reset()
c.WriteResponse(250, EnhancedCode{2, 0, 0}, "Session reset")
case "BDAT":
c.handleBdat(arg)
case "DATA":
c.handleData(arg)
case "QUIT":
c.WriteResponse(221, EnhancedCode{2, 0, 0}, "Goodnight and good luck")
c.Close()
case "AUTH":
if c.server.AuthDisabled {
c.protocolError(500, EnhancedCode{5, 5, 2}, "Syntax error, AUTH command unrecognized")
} else {
c.handleAuth(arg)
}
case "STARTTLS":
c.handleStartTLS()
default:
msg := fmt.Sprintf("Syntax errors, %v command unrecognized", cmd)
c.protocolError(500, EnhancedCode{5, 5, 2}, msg)
}
}
通过命令的解析和执行大概可以了解了主要的处理过程,处理的对应的指令信息与文档中描述的方案基本一致。
总结
本文只是简单的了解了SMTP的协议的部分内容,并通过go-smtp来查看现有的开源SMTP服务的实现情况。由于本人才疏学浅,如有错误请批评指正。