全部产品
云市场

如何通过 SMTP 方式发送带附件的邮件?

更新时间:2018-12-04 12:21:48

通过 SMTP 的方式发送带附件的邮件的方法就是:构建一封 MIME 格式的邮件内容。

MIME 基础知识

  • MIME 表示多用途 Internet 邮件扩允协议。MIME 扩允了基本的面向文本的 Internet 邮件系统,以便可以在消息中包含二进制附件。

  • MIME 信息由正常的 Internet 文本邮件组成,文本邮件拥有符合 RFC 2822/5322 的信息头和格式化过的信息体。

  • MIME 协议的 RFC 地址:https://www.ietf.org/rfc/rfc2045.txt

MIME 信息剖析

一封普通的文本邮件的信息包含一个头部分(例如:From、To、Subject 等等)和一个体部分。体部分通常为单体类型(例如:text、image、audio、video、application 等等)或是复合类型(即:multipart)。头部分和体部分之间用一个空行进行分隔,并且体部分的类型由信头内容类型字段 Content-Type 描述。

  • 信头含义 (Headers)
域名 含义
Received 传输路径
Return-Path 回复地址
Delivered-To 发送地址
Reply-To 回复地址
From 发件人地址
To 收件人地址
Cc 抄送地址
Bcc 暗送地址
Date 日期和时间
Subject 主题
Message-ID 消息 ID
MIME-Version MIME 版本
Content-Type 内容的类型
Content-Transfer-Encoding 内容的传输编码方式
  • 内容类型(Content-Type),表现形式为:Content-Type: [type]/[subtype]。

    其中 type 的形式为:

    text:用于标准化地表示的文本信息,文本消息可以是多种字符集和或者多种格式的。
    Image:用于传输静态图片数据。
    Audio:用于传输音频或者音声数据。
    Video:用于传输动态影像数据,可以是与音频编辑在一起的视频数据格式。
    Application:用于传输应用程序数据或者二进制数据。
    Message:用于包装一个 E-mail 消息。
    Multipart:用于连接消息体的多个部分构成一个消息,这些部分可以是不同类型的数据。

    其中 subtype 用于指定 type 的详细形式,常用的 subtype 如下所示:

    text/plain(纯文本)
    text/html(HTML 文档)
    application/xhtml+xml(XHTML 文档)
    image/gif(GIF 图像)
    image/jpeg(JPEG 图像)
    image/png(PNG 图像)
    video/mpeg(MPEG 动画)
    application/octet-stream(任意的二进制数据)
    message/rfc822(RFC 822 形式)
    multipart/alternative(HTML 邮件的 HTML 形式和纯文本形式,相同内容使用不同形式表示。)

  • 内容传输编码(Content-Transfer-Encoding),指定内容区域使用的字符编码方式。通常为:7bit,8bit,binary,quoted-printable,base64。

MIME 的信体部分

  • 邮件中常见的简单类型有 text/plain(纯文本)和 text/html(超文本)。

  • 复杂的邮件内容格式采用 multipart 类型,可以包括纯文本/超文本、内嵌资源(图片)、附件类型等等。

    multipart 类型的邮件体被分为多个段,每个段又包含段头和段体两部分,这两部分之间也以空行分隔。

    段头含义:

域名 含义
Content-Type 段体的类型
Content-Transfer-Encoding 段体的传输编码方式
Content-Disposition 段体的安排方式
Content-ID 段体的 ID
Content-Location 段体的位置(路径)
Content-Base 段体的基位置

常见的 multipart 类型有三种:multipart/mixed, multipart/related 和 multipart/alternative。

复合类型层次关系示例图:

SMTP示例图

multipart 诸类型的共同特征是,在段头指定 boundary 参数字符串,段体内的每个子段以此字符串定界。所有的子段都以 —boundary 行开始,父段则以 —boundary— 行结束。段与段之间也以空行分隔。

注意:

附件邮件总大小不超过15M,一次最多不超过100个附件。

代码示例(python)

  1. # -*- coding:utf-8 -*-
  2. import urllib, urllib2
  3. import smtplib
  4. from email.mime.multipart import MIMEMultipart
  5. from email.mime.text import MIMEText
  6. from email.mime.application import MIMEApplication
  7. # 发件人地址,通过控制台创建的发件人地址
  8. username = 'xxx@xxx.com'
  9. # 发件人密码,通过控制台创建的发件人密码
  10. password = 'XXXXXXXX'
  11. # 收件人地址列表,支持多个收件人,最多30个
  12. rcptlist = ['to1@to.com', 'to2@to.com']
  13. receivers = ','.join(rcptlist)
  14. # 构建 multipart 的邮件消息
  15. msg = MIMEMultipart('mixed')
  16. msg['Subject'] = 'Test Email'
  17. msg['From'] = username
  18. msg['To'] = receivers
  19. # 构建 multipart/alternative 的 text/plain 部分
  20. alternative = MIMEMultipart('alternative')
  21. textplain = MIMEText('纯文本部分', _subtype='plain', _charset='UTF-8')
  22. alternative.attach(textplain)
  23. # 构建 multipart/alternative 的 text/html 部分
  24. texthtml = MIMEText('超文本部分', _subtype='html', _charset='UTF-8')
  25. alternative.attach(texthtml)
  26. # 将 alternative 加入 mixed 的内部
  27. msg.attach(alternative)
  28. # 附件类型
  29. # xlsx 类型的附件
  30. xlsxpart = MIMEApplication(open('测试文件1.xlsx', 'rb').read())
  31. xlsxpart.add_header('Content-Disposition', 'attachment', filename=Header("测试文件1.xlsx","utf-8").encode())
  32. msg.attach(xlsxpart)
  33. # jpg 类型的附件
  34. jpgpart = MIMEApplication(open('2.jpg', 'rb').read())
  35. jpgpart.add_header('Content-Disposition', 'attachment', filename=Header("2.jpg","utf-8").encode())
  36. msg.attach(jpgpart)
  37. # mp3 类型的附件
  38. mp3part = MIMEApplication(open('3.mp3', 'rb').read())
  39. mp3part.add_header('Content-Disposition', 'attachment', filename=Header("3.mp3","utf-8").encode())
  40. msg.attach(mp3part)
  41. # 发送邮件
  42. try:
  43. client = smtplib.SMTP()
  44. #python 2.7以上版本,若需要使用SSL,可以这样创建client
  45. #client = smtplib.SMTP_SSL()
  46. client.connect('smtpdm.aliyun.com')
  47. client.login(username, password)
  48. #发件人和认证地址必须一致
  49. client.sendmail(username, rcptlist, msg.as_string())
  50. client.quit()
  51. print '邮件发送成功!'
  52. except smtplib.SMTPRecipientsRefused:
  53. print '邮件发送失败,收件人被拒绝'
  54. except smtplib.SMTPAuthenticationError:
  55. print '邮件发送失败,认证错误'
  56. except smtplib.SMTPSenderRefused:
  57. print '邮件发送失败,发件人被拒绝'
  58. except smtplib.SMTPException,e:
  59. print '邮件发送失败, ', e.message

代码示例(GO)

  1. package main
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "fmt"
  6. "io/ioutil"
  7. "log"
  8. "mime"
  9. "net/smtp"
  10. "strings"
  11. "time"
  12. )
  13. // define email interface, and implemented auth and send method
  14. type Mail interface {
  15. Auth()
  16. Send(message Message) error
  17. }
  18. type SendMail struct {
  19. user string
  20. password string
  21. host string
  22. port string
  23. auth smtp.Auth
  24. }
  25. type Attachment struct {
  26. name string
  27. contentType string
  28. withFile bool
  29. }
  30. type Message struct {
  31. from string
  32. to []string
  33. cc []string
  34. bcc []string
  35. subject string
  36. body string
  37. contentType string
  38. attachment Attachment
  39. }
  40. func main() {
  41. user := "XXX@XXXXX.top"
  42. password := "TESXXXXXX"
  43. host := "smtpdm.aliyun.com"
  44. port := "25"
  45. var mail Mail
  46. mail = &SendMail{user: user, password: password, host: host, port: port}
  47. message := Message{from: user,
  48. to: []string{"XXXXX@qq.com", "XX@qq.com", "XXX@163.com"},
  49. cc: []string{},
  50. bcc: []string{},
  51. subject: "HELLO WORLD",
  52. body: "哈哈哈哈哈哈哈",
  53. contentType: "text/plain;charset=utf-8",
  54. // attachment: Attachment{
  55. // name: "test.jpg",
  56. // contentType: "image/jpg",
  57. // withFile: true,
  58. // },
  59. attachment: Attachment{
  60. name: "D:\\goProjects\\src\\测试pdf.pdf",
  61. contentType: "application/octet-stream",
  62. withFile: true,
  63. },
  64. }
  65. err := mail.Send(message)
  66. if err != nil {
  67. fmt.Println("Send mail error!")
  68. fmt.Println(err)
  69. } else {
  70. fmt.Println("Send mail success!")
  71. }
  72. }
  73. func (mail *SendMail) Auth() {
  74. // mail.auth = smtp.PlainAuth("", mail.user, mail.password, mail.host)
  75. mail.auth = LoginAuth(mail.user, mail.password)
  76. }
  77. func (mail SendMail) Send(message Message) error {
  78. mail.Auth()
  79. buffer := bytes.NewBuffer(nil)
  80. boundary := "GoBoundary"
  81. Header := make(map[string]string)
  82. Header["From"] = message.from
  83. Header["To"] = strings.Join(message.to, ";")
  84. Header["Cc"] = strings.Join(message.cc, ";")
  85. Header["Bcc"] = strings.Join(message.bcc, ";")
  86. Header["Subject"] = message.subject
  87. Header["Content-Type"] = "multipart/mixed;boundary=" + boundary
  88. Header["Mime-Version"] = "1.0"
  89. Header["Date"] = time.Now().String()
  90. mail.writeHeader(buffer, Header)
  91. body := "\r\n--" + boundary + "\r\n"
  92. body += "Content-Type:" + message.contentType + "\r\n"
  93. body += "\r\n" + message.body + "\r\n"
  94. buffer.WriteString(body)
  95. if message.attachment.withFile {
  96. attachment := "\r\n--" + boundary + "\r\n"
  97. attachment += "Content-Transfer-Encoding:base64\r\n"
  98. attachment += "Content-Disposition:attachment\r\n"
  99. attachment += "Content-Type:" + message.attachment.contentType + ";name=\"" + mime.BEncoding.Encode("UTF-8", message.attachment.name) + "\"\r\n"
  100. buffer.WriteString(attachment)
  101. defer func() {
  102. if err := recover(); err != nil {
  103. log.Fatalln(err)
  104. }
  105. }()
  106. mail.writeFile(buffer, message.attachment.name)
  107. }
  108. to_address := MergeSlice(message.to, message.cc)
  109. to_address = MergeSlice(to_address, message.bcc)
  110. buffer.WriteString("\r\n--" + boundary + "--")
  111. err := smtp.SendMail(mail.host+":"+mail.port, mail.auth, message.from, to_address, buffer.Bytes())
  112. return err
  113. }
  114. func MergeSlice(s1 []string, s2 []string) []string {
  115. slice := make([]string, len(s1)+len(s2))
  116. copy(slice, s1)
  117. copy(slice[len(s1):], s2)
  118. return slice
  119. }
  120. func (mail SendMail) writeHeader(buffer *bytes.Buffer, Header map[string]string) string {
  121. header := ""
  122. for key, value := range Header {
  123. header += key + ":" + value + "\r\n"
  124. }
  125. header += "\r\n"
  126. buffer.WriteString(header)
  127. return header
  128. }
  129. // read and write the file to buffer
  130. func (mail SendMail) writeFile(buffer *bytes.Buffer, fileName string) {
  131. file, err := ioutil.ReadFile(fileName)
  132. if err != nil {
  133. panic(err.Error())
  134. }
  135. payload := make([]byte, base64.StdEncoding.EncodedLen(len(file)))
  136. base64.StdEncoding.Encode(payload, file)
  137. buffer.WriteString("\r\n")
  138. for index, line := 0, len(payload); index < line; index++ {
  139. buffer.WriteByte(payload[index])
  140. if (index+1)%76 == 0 {
  141. buffer.WriteString("\r\n")
  142. }
  143. }
  144. }
  145. type loginAuth struct {
  146. username, password string
  147. }
  148. func LoginAuth(username, password string) smtp.Auth {
  149. return &loginAuth{username, password}
  150. }
  151. func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
  152. // return "LOGIN", []byte{}, nil
  153. return "LOGIN", []byte(a.username), nil
  154. }
  155. func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
  156. if more {
  157. switch string(fromServer) {
  158. case "Username:":
  159. return []byte(a.username), nil
  160. case "Password:":
  161. return []byte(a.password), nil
  162. }
  163. }
  164. return nil, nil
  165. }