七叶笔记 » golang编程 » 「GoLang」用GoLang语言,给自己写读写邮件的工具

「GoLang」用GoLang语言,给自己写读写邮件的工具

大家知道,项目上线了,需要监控项目是否健康运行。正常运行时再检查项目运行结果是否正常。

原先用Python做了一个工具,在后台一直运行。

其工作原理如下:

每5分钟检查生产数据和备份数据是否正常同步,如果正常同步,则每半小时发送一个报告正常的邮件。如果没有正常同步,则每半小时发送一个报告异常的邮件。

刚好现在学GoLang,就去写个读写邮件的工具先练下手。

今天终于调通了读写邮件。

需要导入的包如下,(IDE会自动导入,一般不需要自己写)

   "github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
_ "github.com/emersion/go-message/charset"
"github.com/emersion/go-message/mail"
"gopkg.in/gomail.v2"  

发送邮件,可以各个参数分开写,当然也可以整体放到一个struct里面,如:

 type EmailInfo struct {
ServerHost string // 邮箱服务器地址,如腾讯邮箱为smtp.qq.com
ServerPort int    // 邮箱服务器端口,如腾讯邮箱为587
FromEmail  string // 发件人邮箱地址
FromPasswd string //发件人邮箱密码(注意,这里是明文形式)
Recipient []string //收件人邮箱
CC        []string //抄送
Subject []string //
Body string
Attachfilename string
AttackPath string
}  

发送邮件函数(

其中,最需要注意的就是编码问题,程序中发送邮件还是转成了base64编码。

from参数需要使用FormatAddress再次编码。

附件也需要注意编码问题

):

 func sendmail(emailInfo *EmailInfo)  {
// 发送邮件
var m *gomail.Message
m = gomail.NewMessage( gomail.SetEncoding(gomail.Base64), )  //将邮件内容更改为Base64编码
m.SetHeader("From", m.FormatAddress("www@qq.com","昵称"))//特殊说明,构造From(发件人信息)时需要使用m.FormatAddress方法,因为发件人指定中文名或特殊字符时,需要进行编码
m.SetHeader("To", emailInfo.Recipient...) // 切片可以传递给不定参数
if len(emailInfo.CC) != 0 {
m.SetHeader("Cc", emailInfo.CC...)
}
//m.SetAddressHeader("Cc", "dan@example.com", "昵称")
m.SetHeader("Subject", emailInfo.Subject...) //"测试邮件"
m.SetBody("text/html", emailInfo.Body)//"测试邮件"
if len(emailInfo.Attachfilename)>=1  { //name:="附件.png"  "C:\\Users\\www\\Downloads\\26.png"
m.Attach(emailInfo.AttackPath,
gomail.Rename(emailInfo.Attachfilename),
gomail.SetHeader(map[string][]string{
"Content-Disposition": []string{
fmt.Sprintf(`attachment; filename="%s"`, mime.QEncoding.Encode("UTF-8", emailInfo.Attachfilename)),
},
}),
)
}
d := gomail.NewDialer("smtp.qq.com", 587, "www@qq.com", "fabcdefghthikjh")
// d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
if err := d.DialAndSend(m); err != nil {
panic(err)
}
}  

m.SetHeader(“To”, emailInfo.Recipient…) // 切片可以传递给不定参数

这句代码学到了函数参数还可以这样传递。

main函数中,接收邮件的流程如下:

 //接收邮件log.Println("连接服务器")
c, err := client.DialTLS("imap.qq.com:993", nil)
check_error(err)

defer c.Logout()  // 结束后退出登录  
 if err = c.Login(emailInfo.FromEmail, emailInfo.FromPasswd); err != nil {
   log.Fatal(err)
}
log.Println("已登录")  

如果直接读收件箱,可以这样写(通过下标来指定读取哪几封邮件):

 // Select INBOXvar mbox *imap.MailboxStatusmbox, err = c.Select("INBOX", false)
check_error(err)
log.Println("Flags for INBOX:", mbox.Flags) // [\Answered \Flagged \Deleted \Draft \Seen]// Get the last 4 messagesifrom := uint32(1)
ito := mbox.Messages
if mbox.Messages > 3 {
   //  
seqset := new(imap.SeqSet)
seqset.AddRange(ifrom, ito)  

下面用一个for循环从指定位置读邮件,其中go func() {}部分都是启动一个goroutine子进程(工作线程)去读取内容。

代码中From和To可能还存在编码问题,需要解决–从最后的运行结果也可以看到。

 for i:=uint32(ifrom);i<ito; i++{
   var section imap.BodySectionName   
   items := []imap.FetchItem{section.FetchItem()}

   bodyMsg := make(chan *imap.Message, 1)
   go func() {
      if err := c.Fetch(seqset, items, bodyMsg); err != nil {
         log.Fatal(err)
      }
   }()
   msg := <-bodyMsg
   if msg == nil {
      log.Fatal("服务器没有返回信息")
   }

   r := msg.GetBody(§ion)
   if r == nil {
      log.Fatal("服务器没有返回 message body")
   }
   mr, err := mail.CreateReader(r)
   check_error(err)
   //打印信息   
   header := mr.Header
   if date, err := header.Date(); err == nil {
      log.Println("Date:", date)
   }
   if from, err := header.AddressList("From"); err == nil {
      log.Println("From:", from)
   }
   if to, err := header.AddressList("To"); err == nil {
      log.Println("To:", to)
   }
   if subject, err := header.Subject(); err == nil {
      log.Println("Subject:", subject)
   }
   // 
  for {
      p, err := mr.NextPart()
      if err == io.EOF {break} else {check_error(err)}

      switch h := p.Header.(type) {
      case *mail.InlineHeader:      // This is the message's text (can be plain-text or HTML)         b, _ := ioutil.ReadAll(p.Body)
         log.Printf("Got text: %s\n", string(b))
      case *mail.AttachmentHeader:   // This is an attachment         filename, _ := h.Filename()
         log.Printf("Got attachment: %s\n", filename)
      }
   }
   log.Printf("读取第 %d 封信, 结束!\n", i)
}  

上面是读取信内容。

如果需要读取邮件列表,则可以这样写:

  // 列出邮件
mailboxes := make(chan *imap.MailboxInfo, 15)
done := make(chan error, 1)
go func () {       //创建一个子goroutine(工作进程)去读邮件列表,子进程读完即返回。
done <- c.List("", "*", mailboxes)
}()

log.Println("邮箱:")
for m := range mailboxes {    //邮件列表是一个channel
mbox, err := c.Select(m.Name, false)
check_error(err)
to := mbox.Messages
log.Printf("%s : %d", m.Name, to)//各邮箱的名字,如:收件箱,发件箱,草稿箱 和 邮箱内有多少邮件
}

if err = <-done; err != nil {
log.Fatal(err)
}  

读写邮件,其实还是需要去了解邮件协议。

一封传统的电子邮件:

From: “Tim” <tim@example.com>To: “joe Zhang” <zhang@example.com>Subject: TestDate: Wed, 17 May 2020 12:02:29 -0400Message-ID: <NDBBIAKOPKHFGPLCODIGIEKBCHAA.tim@example.com>

Hello World.

在结构上,这封信分为三个部分:首先是信件头,然后是一个空行,最后是信件内容。

符合规范RFC 822。但是协议规定导致

1)非英语字符都不能在电子邮件中使用;

2)电子邮件中不能插入二进制文件(如图片);

3)电子邮件不能有附件

这导致补充协议 MIME系列诞生。

MIME对传统电子邮件的扩展,表现在它在信件头部分添加了几条语句,主要有三条

第一条是:指明这封信使用了MIME规范

第二条语句是:它表明传递的信息类型和采用的编码

Content-Type: text/plain; charset=”ISO-8859-1″

Content-Type表明信息类型,缺省值为” text/plain”。它包含了主要类型(primary type)和次要类型(subtype)两个部分,两者之间用”/”分割。主要类型有9种,分别是application、audio、example、image、message、model、multipart、text、video。

经常使用的有:

text/plain:纯文本,文件扩展名.txt
text/html:HTML文本,文件扩展名.htm和.html
image/jpeg:jpeg格式的图片,文件扩展名.jpg
image/gif:GIF格式的图片,文件扩展名.gif
audio/x-wave:WAVE格式的音频,文件扩展名.wav
audio/mpeg:MP3格式的音频,文件扩展名.mp3
video/mpeg:MPEG格式的视频,文件扩展名.mpg
application/zip:PK-ZIP格式的压缩文件,文件扩展名.zip

如果信息的主要类型是”text”,那还须指明编码类型”charset”,缺省值是ASCII,也可能”ISO-8859-1″、”UTF-8″、”GB2312″等

MIME规定了第三条语句:

Content-transfer-encoding: base64

举个例子:

邮件的源码:

Date: Wed, 18 Jun 2008 18:07:51 +0800 (CST)

From: xxx <xxx@163.com>

To: yifeng.ruan@gmail.com

Message-ID: <14410503.1073611213783671983.JavaMail.coremail@bj163app54.163.com>

Subject: =?gbk?B?xOO6ww==?=

MIME-Version: 1.0

Content-Type: multipart/alternative;

boundary=”—-=_Part_287491_22998031.1213783671982″

——=_Part_287491_22998031.1213783671982

Content-Type: text/plain; charset=gbk

Content-Transfer-Encoding: base64

IAq4+b7dsr+209PQudi55raoo6yyu7XD1Nq12Le9yM66zs341b7Jz7nSz+DTprXEtqvO96Osx+vE49TaxOO1xLKpv83W0AogIArW0Ln6yr2x6tPvIC0gyO7Su7fltcTN+MLnyNXWvgoKtcS12jEy1cXNvMasyb6z/aOst/HU8s7Sw8fXt76/xOO1xM/gudjU8MjOoaPQu9C7us/X96OhtMvNvMas1Nq4vbz+wO/D5g==

——=_Part_287491_22998031.1213783671982

Content-Type: text/html; charset=gbk

Content-Transfer-Encoding: quoted-printable

<DIV> </DIV><DIV>=B8=F9=BE=DD=B2=BF=B6=D3=D3=D0=B9=D8=B9=E6=B6=A8=A3=AC=B2=BB=B5=C3=D4==DA=B5=D8=B7=BD=C8=CE=BA=CE=CD=F8=D5=BE=C9=CF=B9=D2=CF=E0=D3=A6=B5=C4=B6=AB==CE=F7=A3=AC=C7=EB=C4=E3=D4=DA=C4=E3=B5=C4=B2=A9=BF=CD=D6=D0</DIV><DIV> ……

可以看到这封信的MIME语句是:

MIME-Version: 1.0Content-Type: multipart/alternative;boundary=”—-=_Part_287491_22998031.1213783671982″

“Content-Type: multipart/alternative;”表明这封信的内容,是纯文本和HTML文本的混合。另两个可能的值是multipart/mixed和multipart/related,分别表示”信件内容中有二进制内容”和”信件带有附件”。

“boundary=”—-=_Part_287491_22998031.1213783671982″”表明不同信件内容的分割线是”—-=_Part_287491_22998031.1213783671982″,它通常是一个很长的随机字符串。

信件内容部分又有两个子信件头:

Content-Type: text/plain; charset=gbkContent-Transfer-Encoding: base64

Content-Type: text/html; charset=gbkContent-Transfer-Encoding: quoted-printable

它们表明,第一个部分是gbk编码的纯文本,编码转换格式是base64。第二个部分是gbk编码的HTML文本,编码转化格式是quoted-printable。

相关文章