go-imap

go-imap库是为了实现连接邮箱服务器获取邮件内容而诞生的工具包。

地址

https://github.com/emersion/go-imap

示例

在使用该包的时候还需要一定的imap协议知识,当然都是边看边学就好,英文不好真的是吃亏。
连接服务器

c, err := client.DialTLS(conf.Conf.MTRIMAPAddr, nil)
defer c.Logout()

登录

//login
    if err := c.Login(conf.Conf.MTRIMAPUsername, conf.Conf.MTRIMAPPassword); err != nil {
        return "", errors.Errorf(err, "[getVoucher]: login fail!")

    }

选择邮箱,这里有分收件箱,发件箱等,这里选择的是发件箱

mbox, err := c.Select("INBOX", false)
    if err != nil {
        return "", errors.Errorf(err, "[getVoucher]: Select INBOX fail!")
    }

其中邮箱的状态

// A mailbox status.
type MailboxStatus struct {
    // The mailbox name.
    Name string
    // True if the mailbox is open in read-only mode.
    ReadOnly bool
    // The mailbox items that are currently filled in. This map's values
    // should not be used directly, they must only be used by libraries
    // implementing extensions of the IMAP protocol.
    Items map[StatusItem]interface{}

    // The Items map may be accessed in different goroutines. Protect
    // concurrent writes.
    ItemsLocker sync.Mutex

    // The mailbox flags.
    Flags []string
    // The mailbox permanent flags.
    PermanentFlags []string
    // The sequence number of the first unseen message in the mailbox.
    UnseenSeqNum uint32

    // The number of messages in this mailbox.
    Messages uint32
    // The number of messages not seen since the last time the mailbox was opened.
    Recent uint32
    // The number of unread messages.
    Unseen uint32
    // The next UID.
    UidNext uint32
    // Together with a UID, it is a unique identifier for a message.
    // Must be greater than or equal to 1.
    UidValidity uint32
}

然后我们选择所需要邮件的数量,总数就是mbox.Messages。最大的数量就是最新的一份邮件。seqset设置了start和stop标志了读取的邮件编号从start到stop。若start设置为mbox.Messages.那么就只读最新的一封邮件,以此类推。

    //seqset 开始结束
    from := getfromIndex(mbox.Messages)
    to := mbox.Messages
    seqset := new(imap.SeqSet)
    seqset.AddRange(from, to)
    messages := make(chan *imap.Message, 10)
    section := &imap.BodySectionName{}

type SeqSet struct {
    Set []Seq
}
// Seq represents a single seq-number or seq-range value (RFC 3501 ABNF). Values
// may be static (e.g. "1", "2:4") or dynamic (e.g. "*", "1:*"). A seq-number is
// represented by setting Start = Stop. Zero is used to represent "*", which is
// safe because seq-number uses nz-number rule. The order of values is always
// Start <= Stop, except when representing "n:*", where Start = n and Stop = 0.
type Seq struct {
    Start, Stop uint32
}

那么接下来需要设置的是需要获取到邮件的那些内容。通过item来表示。通过以下方式来设置,FetchFlags表示要获取对应的邮件的flag信息。

目前定义的系统标记有:
/Seen
邮件已读
/Answered
邮件已回复
/Flagged
邮件标记为紧急或者特别注意。
/Deleted
邮件为删除状态。
/Draft
邮件未写完(标记为草稿状态)。
/Recent

//items 是需要筛选的 刷选模式为flag和body
    items := []imap.FetchItem{imap.FetchFlags}
    uidItem := imap.FetchUid
    items = append(items, section.FetchItem())
    items = append(items, imap.FetchItem(uidItem))

使用channel来获取数据

done := make(chan error, 1)
    go func() {
        done <- c.Fetch(seqset, items, messages)
    }()

使用for msg := range messages 方式来获取每一个channel返回的数据。

for msg := range messages {
        //只是未读邮件
        if len(msg.Flags) == 0 {
            section, err := imap.ParseBodySectionName("BODY[]")
            if err != nil {
                return "", errors.Errorf(err, "ParseBodySectionName err!")
            }
            r := msg.GetBody(section)
            if r == nil { //循环读取 不退出
                continue
            }

            //设置已读
            mr, err := mail.CreateReader(r)
            if err != nil {
                return "", errors.Errorf(err, "CreateReader fail")
            }
            header := mr.Header
            //判断header.subject 是否包含MTR Confirmation Note
            subject, err := header.Subject()

            if strings.Contains(subject, target) {
                //若包含则进入解析body 对body查找对应的orderID
                for {
                    p, err := mr.NextPart()
                    if err == io.EOF {
                        break
                    } else if err != nil {
                        return "", errors.Errorf(err, " message Reader NextPart err")
                    }

                    switch h := p.Header.(type) {
                    case mail.TextHeader: //一封邮件会有两份是TextHeader 类型 一个不是html
                        content, err := ioutil.ReadAll(p.Body)
                        if err != nil {
                            return "", errors.Errorf(err, "Read message fail")
                        }
                        if !strings.Contains(string(content), refNo) { //应该是break 不需要再循环

                            break
                        }

                        logger.Info("find target refNo:", refNo, "msg.SeqNum", msg.SeqNum, "len of message", len(content))
                        findTargetFlag = true

                        content, err = convHtml(ctx, downloadPath, content)
                        if err != nil {
                            return "", errors.Errorf(err, "convHtml fail")
                        }
                        if content == nil || len(content) == 0 {
                            return "", fmt.Errorf("convHtml content is nil")
                        }
                        targetContent = string(content)
                        msgSeq = int(msg.SeqNum)

                    case mail.AttachmentHeader:
                        filename, _ := h.Filename()
                    }

                }
            }

        } //只对未读邮件做处理
    }

踩坑

在设置邮件状态的时候,一开始把以下函数放在了range中。导致了死锁。因为在range中相当于把box资源加锁。而在setflag中也需要去获得锁。相当于setflag去请求一个不未释放的锁。以后这种需要多加注意了。

//设置邮件已读状态
func setSeenFlag(c *client.Client, msgSeq int) (err error) {
    if c == nil {
        return fmt.Errorf("[setSeenFlag] para c is nil")
    }
    if msgSeq < 0 {
        return fmt.Errorf("[setSeenFlag] para msgSeq is invalid")
    }

    seen_Set, err := imap.ParseSeqSet(strconv.Itoa(msgSeq))
    if err != nil{
        return errors.Errorf(err, "ParseSeqSet fail!")
    }
    done := make(chan error, 1)

    go func() {
        done <- c.Store(seen_Set, imap.AddFlags, []interface{}{imap.SeenFlag}, nil)
    }()

    if err != nil {
        return errors.Errorf(err, "[setSeenFlag] resv down err")
    }
    //there can be bug set time out 2s
    select {
    case err := <-done:
        if err != nil {
            return errors.Errorf(err, "[setSeenFlag] resv down err")
        }

    case <-time.After(2 * time.Second):
        return fmt.Errorf("set flag time out(2s)")
    }
    return nil
}

以上踩坑还向作者反馈,感谢emersion指出问题所在!

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐