想要解析邮件?IMAP协议轻松助你,不再烦恼!

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 电子邮件仍是关键的通讯工具,利用编程语言自动化处理能显著提高效率。本文介绍使用Go语言从IMAP服务器读取、解析邮件及处理相关信息。首先概述POP3/IMAP/SMTP协议的作用,强调IMAP协议的优势及其在邮件客户端与服务器间双向同步的特点。接着,指导如何获取授权码以连接第三方服务。通过实战演示,展示使用`go-imap`库连接服务器、读取邮件详情(如主题、收件人等)、标记邮件为已读的过程。最后,对比`Store`与`UidStore`方法的区别,指出使用`UidStore`更安全可靠。本文提供了一段详细的Go语言示例代码,帮助读者快速上手。

电子邮件在现代通信中依然扮演着重要的角色。为了提升邮件处理的效率,使用编程语言进行自动化处理变得尤为重要。

本文将详细介绍如何使用 Go 语言从 IMAP 服务器读取邮件,解析邮件内容,并存储或处理所需信息。

说到邮件服务,我们就得先了解几个和邮件相关的协议。

什么是 POP3/IMAP/SMTP 服务

  • POP3 (Post Office Protocol - Version 3)协议用于支持使用电子邮件客户端获取并删除在服务器上的电子邮件。
  • IMAP (Internet Message Access Protocol)协议用于支持使用电子邮件客户端交互式存取服务器上的邮件。
  • SMTP (Simple Mail Transfer Protocol)协议用于支持使用电子邮件客户端发送电子邮件。

IMAP 和 POP 有什么区别

SMTP 协议就不用多说了,专门用于发送邮件,这个协议也是我们在编程开发中用的最多的协议之一。

POP 允许电子邮件客户端下载服务器上的邮件,但是你在电子邮件客户端上的操作(如:移动邮件、标记已读等)不会反馈到服务器上的,比如:你通过电子邮件客户端收取了 QQ 邮箱中的 3 封邮件并移动到了其他文件夹,这些移动动作是不会反馈到服务器上的,也就是说,QQ 邮箱服务器上的这些邮件是没有同时被移动的。

需要特别注意的是,第三方客户端通过 POP 收取邮件时,也是有可能同步删除服务端邮件。在第三方客户端设置 POP 时,请留意是否有 保留邮件副本/备份 相关选项。如有该选项,且要保留服务器上的邮件,请勾选该选项。

在 IMAP 协议上,电子邮件客户端的操作都会反馈到服务器上,你对邮件进行的操作(如:移动邮件、标记已读、删除邮件等)服务器上的邮件也会做相应的动作。也就是说,IMAP 是“双向”的。同时,IMAP 可以只下载邮件的主题,只有当你真正需要的时候,才会下载邮件的所有内容。在 POP3 和 IMAP 协议上,QQ邮箱推荐你使用IMAP协议来存取服务器上的邮件。

授权码

在我们开发之前,我们需要先准备好对应邮箱的授权码,这个授权码是邮箱用于登录第三方客户端/服务的专用密码,适用于登录以下服务:POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV 服务。

不同的邮箱会有不同的获取方式,但是一般获取方式都非常简单,可以自行通过搜索引擎检索一下即可。

比如:QQ 邮箱的授权码的获取方式是:

在邮箱帐号与安全点击 设备管理 > 授权码管理,对授权码进行管理,即可获得。

实战

今天我们就通过 Go 语言来演示一下如何解析邮件。

首先我们先下载第三方 imap 协议的插件包:

go get -v github.com/emersion/go-imap@v1.2.1

接下来的就是示例代码,很多重要信息,在代码里都有注释信息,因此请多留意注释:

下面的代码逻辑大致是:读取指定邮箱中的收件箱邮件,每次读取 2 封邮件,并解析出邮件的主题、收件人、发件人、收件时间、邮件正文,读取完毕之后,给每封邮件标记已读。

package mail_parse

import (
    "io"
    "io/ioutil"
    "log"
    "strings"
    "time"

    "github.com/emersion/go-imap"
    "github.com/emersion/go-imap/client"
    "github.com/emersion/go-message/mail"
)

const (
    Addr          string = "imap.qq.com:993"
    UserName      string = "123456789@qq.com" // 邮箱地址
    Password      string = ""                 // 这里的密码是使用开启 imap 协议后对应的服务商给到的密码,不是邮箱账号密码
    Folder        string = "INBOX"            // 邮箱文件夹,比如: INBOX 收件箱、Sent Messages 发件箱、Drafts 草稿箱、Trash、Junk 垃圾箱
    ReadBatchSize int    = 2                  // 每次读取的邮件数量
)

// IMAP(Internet Message Access Protocol)是一种用于在互联网上访问电子邮件的协议。
// 它允许用户通过 Internet 访问他们在邮件服务器上存储的电子邮件。
// Go 语言的 go-imap 库是一个用于从 IMAP 服务器获取电子邮件的库,它可以帮助你在 Go 代码中访问 IMAP 协议

func ReadEmail() {
   
    log.Println("开始连接服务器")

    // 建立与 IMAP 服务器的连接
    c, err := client.DialTLS(Addr, nil)
    if err != nil {
   
        log.Fatalf("连接 IMAP 服务器失败: %+v \n", err)
    }
    log.Println("连接成功!")
    // 最后一定不要忘记退出登录
    defer c.Logout()

    // 登录
    if err := c.Login(UserName, Password); err != nil {
   
        log.Fatalf("邮箱[%s] 登录失败: %v \n", Addr, err)
    }
    log.Printf("邮箱[%s] 登录成功!\n", UserName)

    // 列出当前邮箱中的文件夹
    mailboxes := make(chan *imap.MailboxInfo, 10)
    done := make(chan error, 1) // 记录错误的 chan
    go func() {
   
        done <- c.List("", "*", mailboxes)
    }()
    log.Println("-->当前邮箱的文件夹 Mailboxes:")
    var folderExists bool
    for m := range mailboxes {
   
        log.Println("* ", m.Name)
        if m.Name == Folder {
   
            folderExists = true
        }
    }
    if err := <-done; err != nil {
   
        log.Fatalf("列出邮箱列表时,出现错误:%v \n", err)
    }
    log.Println("-->列出邮箱列表完毕!")
    if !folderExists {
   
        log.Fatalf("文件夹[%s] 不存在 \n", Folder)
    }

    // 选择指定的文件夹
    mbox, err := c.Select(Folder, false)
    if err != nil {
   
        log.Fatalf("选择邮件箱失败: %v \n", err)
    }
    log.Printf("mbox %+v \n", mbox)
    log.Printf("当前文件夹[%s]中,总共有 %d 封邮件 \n", Folder, mbox.Messages)
    if mbox.Messages == 0 {
   
        log.Fatalf("当前文件夹[%s]中没有邮件", Folder)
    }

    // 创建一个序列集,用于批量读取邮件
    seqset := new(imap.SeqSet)

    // 假设需要获取最后4封邮件时
    // from := uint32(1)
    // to := mbox.Messages // 此文件下的邮件总数
    // if mbox.Messages > 3 {
   
    //     from = mbox.Messages - 3
    // }
    // seqset.AddRange(from, to) // 添加指定范围内的邮件编号

    // 搜索指定状态的邮件
    criteria := imap.NewSearchCriteria()
    criteria.WithoutFlags = []string{
   imap.SeenFlag} // 未读邮件标记
    // criteria.WithFlags = []string{imap.SeenFlag} // 已读邮件标记
    uids, err := c.Search(criteria)
    // 在这里也可以使用 UidSearch 方法,但是用了 UidSearch 方法后,下面的很多方法都需要使用 Uid 开头的方法
    // 也就是说 Fetch -> UidFetch,Store -> UidStore,Copy -> UidCopy,Move -> UidMove,Search -> UidSearch
    // uids, err := c.UidSearch(criteria)
    // 关于 Store 方法和 UidStore 方法
    // Store 和 UidStore 方法都是用于在 IMAP 中更新邮件标志的,但它们有一些区别:
    //
    // Store:使用的是消息序列号(message sequence number)来标识邮件。序列号是动态的,每次邮件删除或添加时,序列号可能会改变。序列号从1开始,按邮件在邮箱中的位置进行排序。
    // UidStore:使用的是消息的唯一标识符(UID)来标识邮件。UID 是固定的,不会因为邮件的添加或删除而改变,适合于需要确保唯一标识邮件的操作。
    // 在标记为已读时,使用 UidStore 方法更为安全和可靠,因为它使用邮件的唯一标识符,可以避免由于序列号变化导致的潜在问题。
    if err != nil {
   
        log.Fatalf("搜索邮件时出现错误:%v \n", err)
    }
    log.Printf("搜索到的邮件 uids: %+v \n", uids)
    if len(uids) == 0 {
   
        log.Println("没有搜索到邮件")
        return
    }
    log.Printf("搜索到的邮件总共有 %v 封 %+v \n", len(uids), uids)

    // 获取整个消息正文
    // imap.FetchEnvelope:请求获取邮件的信封数据(例如发件人、收件人、主题等元数据)。
    // imap.FetchRFC822:请求获取完整的邮件内容,包括所有头部和正文。
    items := []imap.FetchItem{
   imap.FetchFlags, imap.FetchEnvelope, imap.FetchRFC822}

    for i, uidsCount := 0, len(uids); i < uidsCount; i += ReadBatchSize {
   
        // 清空序列集中的所有邮件编号,以便添加新的邮件编号。每次循环开始时调用此方法,确保序列集中只有当前批次的邮件编号
        seqset.Clear()

        // 添加一批邮件到序列集中
        if i+ReadBatchSize < uidsCount {
   
            seqset.AddNum(uids[i : i+ReadBatchSize]...) // 添加指定范围内的邮件编号
        } else {
   
            seqset.AddNum(uids[i:]...) // 添加剩余的邮件编号
        }

        // 获取邮件内容 Start
        messages := make(chan *imap.Message, ReadBatchSize) // 创建一个通道,用于接收邮件消息
        fetchDone := make(chan error, 1)                    // 创建一个通道,用于接收错误消息
        go func() {
   
            // Fetch方法用于从服务器获取邮件数据,这里请求了邮件的信封和完整内容
            fetchDone <- c.Fetch(seqset, items, messages)
        }()
        log.Println("开始读取邮件内容")
        for msg := range messages {
   
            readEveryMsg(msg)
        }
        if err := <-fetchDone; err != nil {
   
            log.Fatalf("获取邮件信息出现错误:%v \n", err)
        }
        // 获取邮件内容 End

        // 给邮件打标记 Start
        item := imap.FormatFlagsOp(imap.AddFlags, true) // 标记为已读
        // item := imap.FormatFlagsOp(imap.RemoveFlags, true) // 标记为未读
        flags := []interface{
   }{
   imap.SeenFlag}
        log.Printf("即将给这些邮件 [%s] 打标记 \n", seqset)
        if err := c.Store(seqset, item, flags, nil); err != nil {
   
            log.Fatalf("给邮件打标记失败:%v \n", err)
        }
        // 给邮件打标记 End

        time.Sleep(time.Second * 10) // 休眠10秒
    }

    log.Println("读取了所有邮件,完毕!")

}

// document link: https://github.com/emersion/go-imap/wiki/Fetching-messages
func readEveryMsg(msg *imap.Message) {
   
    log.Printf("每一封邮件的消息序列号 %+v \n", msg.SeqNum)
    log.Println("-------------------------")
    // 获取邮件正文
    r := msg.GetBody(&imap.BodySectionName{
   })
    if r == nil {
   
        log.Fatal("服务器没有返回消息内容")
    }

    mr, err := mail.CreateReader(r)
    if err != nil {
   
        log.Fatalf("邮件读取时出现错误: %v \n", err)
    }
    if date, err := mr.Header.Date(); err == nil {
   
        log.Println("收件时间 Date:", date)
    }
    if from, err := mr.Header.AddressList("From"); err == nil {
   
        log.Println("发件人 From:", from)
    }
    if to, err := mr.Header.AddressList("To"); err == nil {
   
        log.Println("收件人 To:", to)
    }
    if subject, err := mr.Header.Subject(); err == nil {
   
        log.Println("邮件主题 Subject:", subject)
    }
    log.Printf("抄送 Cc: %+v \n", msg.Envelope.Cc)

    for {
   
        p, err := mr.NextPart()
        if err == io.EOF {
   
            break
        } else if err != nil {
   
            log.Fatalf("读取邮件内容时出现错误:%v \n", err)
        }

        switch h := p.Header.(type) {
   
        case *mail.InlineHeader:
            // 这是消息的文本(可以是纯文本或 HTML)
            contentType := h.Get("Content-Type")
            b, _ := ioutil.ReadAll(p.Body)
            if strings.HasPrefix(contentType, "text/plain") {
   
                log.Printf("得到正文 -> TEXT: %v \n", string(b))
            } else if strings.HasPrefix(contentType, "text/html") {
   
                log.Printf("得到正文 -> HTML: %v \n", len(b))
            }
            break
        case *mail.AttachmentHeader:
            // 这是一个附件
            filename, _ := h.Filename()
            log.Printf("得到附件: %v \n", filename)
            break
        }
    }

    log.Println("一封邮件读取完毕")
    log.Printf("------------------------- \n\n")
}

值得一提

需要说明一下的是,上面代码中,我们给邮件标记已读时,采用的是 Store 方法,其实也可以使用 UidStore 方法,那么这两个方法有什么区别呢?

关于 Store 方法和 UidStore 方法

StoreUidStore 方法都是用于在 IMAP 中更新邮件标志的,但它们有一些区别:

Store:使用的是消息序列号(message sequence number)来标识邮件。序列号是动态的,每次邮件删除或添加时,序列号可能会改变。序列号从1开始,按邮件在邮箱中的位置进行排序。

UidStore:使用的是消息的唯一标识符(UID)来标识邮件。UID 是固定的,不会因为邮件的添加或删除而改变,适合于需要确保唯一标识邮件的操作。

在标记为已读时,使用 UidStore 方法更为安全和可靠,因为它使用邮件的唯一标识符,可以避免由于序列号变化导致的潜在问题。但是经过我的测试,发现使用 Store 方法也没啥太大的问题,但是使用的时候一定要配套使用,也就是说,要是你使用了 Uid 开头的方法时,很多方法你都需要改成 Uid 开头的方法才能有效使用,比如: Fetch -> UidFetch,Store -> UidStore,Copy -> UidCopy,Move -> UidMove,Search -> UidSearch。否则,可能会有一些意料之外的事情发生。这是我看文档以及自己摸索得出来的结论,如果你觉得我理解的不对,也可以予以纠正。

好了,聊到这里基本上就结束了。本文主要还是以代码为主,毕竟没有什么比几行代码来得干脆了。不过,可不要将上面的代码直接放到项目中跑呀,放到自己的项目中还是需要稍作调整的,上面代码只是为了方便我在本地调试,因此才有大批量的 log 输出。

如果刚好你也有类似的需求,希望这篇文章可以帮得到你。

相关文章
|
4月前
|
XML 监控 网络协议
云深处绝影四足机器人协议学习解析
本文详细介绍并解析了云深处绝影X20四足机器人的通信协议,包括TCP服务端端口号、基于Service的请求/响应通信机制、通信帧结构、消息类型、常见的通信示例如获取状态和导航请求,以及运动控制的参数和命令。文中还提出了对协议中某些未明确说明或可能存在的问题的疑惑。
53 0
云深处绝影四足机器人协议学习解析
|
2月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
152 3
|
3月前
|
域名解析 存储 网络协议
深入解析网络通信关键要素:IP 协议、DNS 及相关技术
本文详细介绍了IP协议报头结构及其各字段的功能,包括版本、首部长度、服务类型、总长度、标识、片偏移、标志、生存时间(TTL)、协议、首部检验和等内容。此外,还探讨了IP地址的网段划分、特殊IP地址的应用场景,以及路由选择的大致流程。最后,文章简要介绍了DNS协议的作用及其发展历史,解释了域名解析系统的工作原理。
138 5
深入解析网络通信关键要素:IP 协议、DNS 及相关技术
|
4月前
|
缓存 网络协议 安全
【网络攻防战】DNS协议的致命弱点:如何利用它们发动悄无声息的网络攻击?
【8月更文挑战第26天】DNS(域名系统)是互联网的关键组件,用于将域名转换为IP地址。然而,DNS协议存在安全漏洞,包括缺乏身份验证机制、缓存中毒风险及放大攻击的可能性。通过具体案例,如DNS缓存中毒和DNS放大攻击,攻击者能够误导用户访问恶意站点或对目标服务器实施DDoS攻击。为了防范这些威胁,可以采用DNSSEC实现数字签名验证、利用加密的DNS服务(如DoH或DoT)、限制DNS服务器响应以及及时更新DNS软件等措施。理解并应对DNS的安全挑战对于确保网络环境的安全至关重要。
123 2
|
21天前
|
监控 网络协议 网络性能优化
网络通信的核心选择:TCP与UDP协议深度解析
在网络通信领域,TCP(传输控制协议)和UDP(用户数据报协议)是两种基础且截然不同的传输层协议。它们各自的特点和适用场景对于网络工程师和开发者来说至关重要。本文将深入探讨TCP和UDP的核心区别,并分析它们在实际应用中的选择依据。
49 3
|
2天前
|
传感器
Modbus协议深入解析
Modbus协议是由Modicon公司(现施耐德电气)于1979年发明的串行通信协议,主要用于工业自动化系统中的PLC通信。本文深入解析了Modbus协议的主从模式、数据类型(线圈、离散输入、保持寄存器、输入寄存器)、帧结构和通信过程,并介绍了其应用场景和重要性。
9 0
|
1月前
|
网络协议 网络安全 网络虚拟化
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算。通过这些术语的详细解释,帮助读者更好地理解和应用网络技术,应对数字化时代的挑战和机遇。
77 3
|
26天前
|
负载均衡 网络协议 算法
OSPF与其他IGP协议的比较:全面解析与应用场景
OSPF与其他IGP协议的比较:全面解析与应用场景
41 0
|
3月前
|
前端开发 JavaScript 安全
深入解析 http 协议
HTTP(超文本传输协议)不仅用于传输文本,还支持图片、音频和视频等多种类型的数据。当前广泛使用的版本为 HTTP/1.1。HTTPS 可视为 HTTP 的安全增强版,主要区别在于添加了加密层。HTTP 请求和响应均遵循固定格式,包括请求行/状态行、请求/响应头、空行及消息主体。URL(统一资源定位符)用于标识网络上的资源,其格式包含协议、域名、路径等信息。此外,HTTP 报头提供了附加信息,帮助客户端和服务端更好地处理请求与响应。状态码则用于指示请求结果,如 200 表示成功,404 表示未找到,500 表示服务器内部错误等。
83 0
深入解析 http 协议
|
3月前
|
数据采集 存储 JSON
从零到一构建网络爬虫帝国:HTTP协议+Python requests库深度解析
在网络数据的海洋中,网络爬虫遵循HTTP协议,穿梭于互联网各处,收集宝贵信息。本文将从零开始,使用Python的requests库,深入解析HTTP协议,助你构建自己的网络爬虫帝国。首先介绍HTTP协议基础,包括请求与响应结构;然后详细介绍requests库的安装与使用,演示如何发送GET和POST请求并处理响应;最后概述爬虫构建流程及挑战,帮助你逐步掌握核心技术,畅游数据海洋。
76 3

推荐镜像

更多