在Go的net/smtp包中,可以通过自定义textproto.Conn来捕获SMTP服务器的响应。以下是实现方法:
package main
import (
"bufio"
"fmt"
"log"
"net"
"net/textproto"
"strings"
)
type loggingConn struct {
net.Conn
responses []string
}
func (lc *loggingConn) Read(b []byte) (n int, err error) {
n, err = lc.Conn.Read(b)
if n > 0 {
// 捕获响应并存储
response := string(b[:n])
lc.responses = append(lc.responses, response)
// 打印到日志
log.Printf("SMTP响应: %s", strings.TrimSpace(response))
}
return n, err
}
func sendEmailWithLogging(msg []byte) {
// 建立TCP连接
conn, err := net.Dial("tcp", "localhost:25")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 包装连接以捕获响应
lc := &loggingConn{Conn: conn}
text := textproto.NewConn(lc)
// 创建SMTP客户端
c, err := smtp.NewClient(text, "localhost")
if err != nil {
log.Fatal(err)
}
defer c.Close()
// 发送邮件命令
if err := c.Mail("sender@example.org"); err != nil {
log.Fatal(err)
}
if err := c.Rcpt("recipient@example.net"); err != nil {
log.Fatal(err)
}
wc, err := c.Data()
if err != nil {
log.Fatal(err)
}
_, err = wc.Write(msg)
if err != nil {
log.Fatal(err)
}
// 关闭Data写入器,此时会收到最终响应
err = wc.Close()
if err != nil {
log.Fatal(err)
}
// 打印所有捕获的响应
fmt.Println("所有SMTP响应:")
for i, resp := range lc.responses {
fmt.Printf("[%d] %s", i+1, resp)
}
err = c.Quit()
if err != nil {
log.Fatal(err)
}
}
// 或者使用更直接的方法,通过扩展smtp.Client
type loggingClient struct {
*smtp.Client
responses []string
}
func (lc *loggingClient) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
id, err := lc.Client.Text.Cmd(format, args...)
if err != nil {
return 0, "", err
}
lc.Client.Text.StartResponse(id)
defer lc.Client.Text.EndResponse(id)
code, msg, err := lc.Client.Text.ReadResponse(expectCode)
// 捕获响应
response := fmt.Sprintf("%d %s", code, msg)
lc.responses = append(lc.responses, response)
log.Printf("SMTP响应: %s", response)
return code, msg, err
}
func sendEmailWithClientExtension(msg []byte) {
c, err := smtp.Dial("localhost:25")
if err != nil {
log.Fatal(err)
}
defer c.Quit()
lc := &loggingClient{Client: c}
// 使用自定义的cmd方法发送命令
// 注意:这里需要重新实现各个方法,因为smtp.Client的方法不是导出的
// 实际使用中可能需要更复杂的包装
// 或者使用反射来包装Client
clientValue := reflect.ValueOf(c).Elem()
textField := clientValue.FieldByName("text")
if textField.IsValid() {
text := textField.Interface().(*textproto.Conn)
// 通过textproto.Conn直接发送命令并读取响应
id, err := text.Cmd("MAIL FROM:<sender@example.org>")
if err != nil {
log.Fatal(err)
}
text.StartResponse(id)
code, msg, err := text.ReadResponse(250)
text.EndResponse(id)
log.Printf("MAIL FROM响应: %d %s", code, msg)
}
}
对于更简单的场景,可以直接使用textproto.Conn来手动发送命令:
func sendEmailManual(msg []byte) {
conn, err := net.Dial("tcp", "localhost:25")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
text := textproto.NewConn(conn)
// 读取欢迎消息
_, welcome, err := text.ReadResponse(220)
log.Printf("服务器欢迎: %s", welcome)
// 发送EHLO
id, err := text.Cmd("EHLO localhost")
if err != nil {
log.Fatal(err)
}
text.StartResponse(id)
code, msgResp, err := text.ReadResponse(250)
text.EndResponse(id)
log.Printf("EHLO响应: %d %s", code, msgResp)
// 发送MAIL FROM
id, err = text.Cmd("MAIL FROM:<sender@example.org>")
if err != nil {
log.Fatal(err)
}
text.StartResponse(id)
code, msgResp, err = text.ReadResponse(250)
text.EndResponse(id)
log.Printf("MAIL FROM响应: %d %s", code, msgResp)
// 发送RCPT TO
id, err = text.Cmd("RCPT TO:<recipient@example.net>")
if err != nil {
log.Fatal(err)
}
text.StartResponse(id)
code, msgResp, err = text.ReadResponse(250)
text.EndResponse(id)
log.Printf("RCPT TO响应: %d %s", code, msgResp)
// 发送DATA
id, err = text.Cmd("DATA")
if err != nil {
log.Fatal(err)
}
text.StartResponse(id)
code, msgResp, err = text.ReadResponse(354)
text.EndResponse(id)
log.Printf("DATA响应: %d %s", code, msgResp)
// 发送邮件内容
dataWriter := text.DotWriter()
_, err = dataWriter.Write(msg)
if err != nil {
log.Fatal(err)
}
dataWriter.Close()
// 读取DATA命令的最终响应(包含队列ID)
code, finalMsg, err := text.ReadResponse(250)
log.Printf("邮件发送完成响应: %d %s", code, finalMsg)
// finalMsg包含"ok queued as ..."信息
// 发送QUIT
id, err = text.Cmd("QUIT")
if err != nil {
log.Fatal(err)
}
text.StartResponse(id)
code, msgResp, err = text.ReadResponse(221)
text.EndResponse(id)
log.Printf("QUIT响应: %d %s", code, msgResp)
}
第一种方法通过包装网络连接来捕获所有响应,第二种方法通过扩展smtp.Client,第三种方法直接使用textproto.Conn手动处理SMTP协议。第三种方法能最直接地获取到DATA命令结束后的最终响应,其中包含你需要的队列ID信息。