imap.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. package mail
  2. import (
  3. "errors"
  4. "eta/eta_crawler/utils"
  5. "fmt"
  6. "io"
  7. "log"
  8. "os"
  9. "path"
  10. "regexp"
  11. "strings"
  12. "time"
  13. "github.com/emersion/go-imap"
  14. "github.com/emersion/go-imap/client"
  15. "github.com/emersion/go-message"
  16. "github.com/emersion/go-message/mail"
  17. "github.com/h2non/filetype"
  18. )
  19. type MailMessage struct {
  20. Date time.Time `description:"收件时间"`
  21. Uid uint32 `description:"该邮件在邮箱中的唯一id"`
  22. FromAddress string `description:"发件人邮箱"`
  23. From string `description:"发件人名称"`
  24. Title string `description:"邮件标题"`
  25. Content string `description:"邮件主体正文"`
  26. Resources map[string]string `description:"正文内嵌资源"`
  27. Attachment map[string][]byte `description:"附件资源"`
  28. }
  29. func ListenMail(mailAddress, folder, userName, password string, readBatchSize, fromEmailIndex int, mailMessageChan chan MailMessage, mailMessageDoneChan chan bool) (err error) { // 收件箱
  30. defer func() {
  31. // 处理结束
  32. mailMessageDoneChan <- true
  33. if err != nil {
  34. fmt.Println("err:", err.Error())
  35. }
  36. }()
  37. // 建立与 IMAP 服务器的连接
  38. c, err := client.DialTLS(mailAddress, nil)
  39. if err != nil {
  40. fmt.Printf("连接 IMAP 服务器失败: %+v \n", err)
  41. return
  42. }
  43. // 最后一定不要忘记退出登录
  44. defer func() {
  45. _ = c.Logout()
  46. }()
  47. // 登录
  48. if err = c.Login(userName, password); err != nil {
  49. fmt.Printf("邮箱[%s] 登录失败: %v \n", fmt.Sprintf("%s:%s", userName, mailAddress), err)
  50. return
  51. }
  52. // 列出当前邮箱中的文件夹
  53. mailboxes := make(chan *imap.MailboxInfo, 10)
  54. done := make(chan error, 1) // 记录错误的 chan
  55. go func() {
  56. done <- c.List("", "*", mailboxes)
  57. }()
  58. log.Println("-->当前邮箱的文件夹 Mailboxes:")
  59. var folderExists bool
  60. for m := range mailboxes {
  61. log.Println("* ", m.Name)
  62. if m.Name == folder {
  63. folderExists = true
  64. }
  65. }
  66. err = <-done
  67. if err != nil {
  68. utils.FileLog.Error("列出邮箱列表时,出现错误:%v \n", err)
  69. return
  70. }
  71. log.Println("-->列出邮箱列表完毕!")
  72. if !folderExists {
  73. err = fmt.Errorf(fmt.Sprintf("文件夹[%s] 不存在 \n", folder))
  74. return
  75. }
  76. message.CharsetReader = myCharsetReader
  77. // 选择指定的文件夹
  78. mbox, err := c.Select(folder, false)
  79. if err != nil {
  80. err = fmt.Errorf(fmt.Sprintf("选择邮件箱失败: %+v", err))
  81. return
  82. }
  83. log.Printf("当前文件夹[%s]中,总共有 %d 封邮件 \n", folder, mbox.Messages)
  84. if mbox.Messages == 0 {
  85. return
  86. }
  87. // 创建一个序列集,用于批量读取邮件
  88. seqSet := new(imap.SeqSet)
  89. to := mbox.Messages // 此文件下的邮件总数
  90. var isStopFor bool
  91. step := uint32(1)
  92. for i := to; i >= 1; {
  93. start := i - step + 1
  94. if start < 0 {
  95. start = 1
  96. }
  97. seqSet.Clear()
  98. seqSet.AddRange(start, i) // 添加指定范围内的邮件编号
  99. // 获取整个消息正文
  100. // imap.FetchEnvelope:请求获取邮件的信封数据(例如发件人、收件人、主题等元数据)。
  101. // imap.FetchRFC822:请求获取完整的邮件内容,包括所有头部和正文。
  102. items := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchRFC822, imap.FetchBodyStructure}
  103. // 获取邮件内容 Start
  104. messages := make(chan *imap.Message, readBatchSize) // 创建一个通道,用于接收邮件消息
  105. fetchDone := make(chan error, 1) // 创建一个通道,用于接收错误消息
  106. go func() {
  107. // Fetch方法用于从服务器获取邮件数据,这里请求了邮件的信封和完整内容
  108. fetchDone <- c.Fetch(seqSet, items, messages)
  109. }()
  110. err = <-fetchDone
  111. if err != nil {
  112. utils.FileLog.Error("获取邮件信息出现错误:%v \n", err)
  113. return
  114. }
  115. // 获取邮件内容 End
  116. for msg := range messages {
  117. // 如果需要终止,那么就不处理了
  118. if isStopFor {
  119. continue
  120. }
  121. emailMessage, isRead, tmpErr := readEveryMsg(msg)
  122. if tmpErr != nil {
  123. // 移除本地文件
  124. {
  125. for _, v := range emailMessage.Resources {
  126. os.Remove(v)
  127. }
  128. }
  129. utils.FileLog.Error("读取邮件内容时出现错误:%v \n", tmpErr)
  130. continue
  131. }
  132. // 如果没有取到,那么就过滤
  133. if !isRead {
  134. continue
  135. }
  136. // 判断当前邮件id是否小于等于已经监听到的最小id,如果是,那么就不处理了
  137. if emailMessage.Uid <= uint32(fromEmailIndex) {
  138. isStopFor = true
  139. continue
  140. }
  141. // 如果取到了,那么写入待处理chan
  142. // 写入邮件处理chan
  143. mailMessageChan <- emailMessage
  144. }
  145. if isStopFor {
  146. // 已经找到了最小的邮件id,那么就退出循环了
  147. }
  148. i = i - step
  149. }
  150. log.Println("读取了所有邮件,完毕!")
  151. return
  152. }
  153. // document link: https://github.com/emersion/go-imap/wiki/Fetching-messages
  154. func readEveryMsg(msg *imap.Message) (emailMessage MailMessage, ok bool, err error) {
  155. ok = true
  156. defer func() {
  157. if err != nil {
  158. ok = false
  159. utils.FileLog.Error("邮件读取失败;Err:%s", err.Error())
  160. }
  161. }()
  162. message.CharsetReader = myCharsetReader
  163. emailMessage.Resources = make(map[string]string) // 内嵌资源
  164. emailMessage.Attachment = make(map[string][]byte) // 附件
  165. emailMessage.Uid = msg.Uid
  166. htmlStr := ``
  167. textStr := ``
  168. // 获取邮件正文
  169. r := msg.GetBody(&imap.BodySectionName{})
  170. if r == nil {
  171. utils.FileLog.Info("服务器没有返回消息内容")
  172. }
  173. mr, err := mail.CreateReader(r)
  174. if err != nil {
  175. err = errors.New(fmt.Sprintf("邮件读取时出现错误:%v \n", err))
  176. return
  177. }
  178. // 收件时间
  179. {
  180. date, err := mr.Header.Date()
  181. if err != nil {
  182. log.Println("收件时间 异常:", err.Error())
  183. }
  184. emailMessage.Date = date
  185. }
  186. // 发件人
  187. {
  188. fromStr := mr.Header.Get("From")
  189. // 处理无效地址的情况
  190. if !strings.Contains(fromStr, "@") {
  191. emailMessage.FromAddress = fromStr
  192. emailMessage.From = fromStr
  193. } else {
  194. from, tmpErr := mr.Header.AddressList("From")
  195. if tmpErr != nil {
  196. log.Println("发件人 异常:", err.Error())
  197. }
  198. if len(from) > 0 {
  199. emailMessage.FromAddress = from[0].Address
  200. emailMessage.From = from[0].Name
  201. }
  202. }
  203. }
  204. // 邮件标题
  205. subject, err := mr.Header.Subject()
  206. if err != nil {
  207. log.Println("邮件主题 Subject ERR:", err)
  208. } else {
  209. //log.Println("邮件主题 Subject:", subject)
  210. }
  211. emailMessage.Title = subject
  212. // 过滤
  213. for {
  214. p, tmpErr := mr.NextPart()
  215. if tmpErr == io.EOF {
  216. break
  217. } else if tmpErr != nil {
  218. utils.FileLog.Error("读取邮件内容时出现错误:%v \n", tmpErr)
  219. err = tmpErr
  220. return
  221. }
  222. bodyBytes, _ := io.ReadAll(p.Body)
  223. if err != nil {
  224. //log.Fatalf("读取邮件部分时出现错误:%v \n", err)
  225. err = errors.New(fmt.Sprintf("读取邮件部分时出现错误:%v \n", err))
  226. return
  227. }
  228. switch h := p.Header.(type) {
  229. case *mail.InlineHeader:
  230. // 这是消息的文本(可以是纯文本或 HTML)
  231. contentType := h.Get("Content-Type")
  232. //log.Println("消息内容content-type:", contentType)
  233. if strings.HasPrefix(contentType, "text/plain") {
  234. //log.Printf("得到正文 -> TEXT: %v \n", string(bodyBytes))
  235. textStr += string(bodyBytes)
  236. } else if strings.HasPrefix(contentType, "text/html") {
  237. //log.Printf("得到正文 -> HTML: %v \n", len(b))
  238. //log.Printf("得到正文 -> HTML: %v \n", string(bodyBytes))
  239. htmlStr += string(bodyBytes)
  240. }
  241. // 这是内嵌资源
  242. if cid := p.Header.Get("Content-ID"); cid != "" {
  243. // 确定文件后缀
  244. fileSuffix := determineFileSuffix(bodyBytes)
  245. fileName := fmt.Sprintf("%s%s.%s", utils.MtjhFilePath, cid[1:len(cid)-1], fileSuffix)
  246. err = SaveToFile(bodyBytes, fileName)
  247. if err != nil {
  248. err = errors.New(fmt.Sprintf("保存文件时出现错误:%v \n", err))
  249. return
  250. }
  251. emailMessage.Resources[cid] = fileName
  252. }
  253. case *mail.AttachmentHeader:
  254. // 这是一个附件
  255. filename, _ := h.Filename()
  256. //log.Printf("得到附件: %v,content-type:%s \n", filename, p.Header.Get("Content-Type"))
  257. saveName := fmt.Sprint(msg.SeqNum, utils.MD5(filename), time.Now().Format(utils.FormatDateTimeUnSpace), time.Now().Nanosecond(), path.Ext(filename))
  258. filePath := fmt.Sprintf("%s%s%s%s", utils.MtjhFilePath, `file`, string(os.PathSeparator), saveName)
  259. err = SaveToFile(bodyBytes, filePath)
  260. if err != nil {
  261. err = errors.New(fmt.Sprintf("保存文件时出现错误:%v \n", err))
  262. return
  263. }
  264. // 这是附件资源
  265. if contentDisposition := p.Header.Get("Content-Disposition"); contentDisposition != "" {
  266. if strings.HasPrefix(contentDisposition, "attachment") {
  267. emailMessage.Attachment[filename] = bodyBytes
  268. }
  269. } else if cid := p.Header.Get("Content-ID"); cid != "" {
  270. // 这是内嵌资源
  271. emailMessage.Resources[cid] = filePath
  272. }
  273. //else {
  274. // mailMessage.Attachment[filename] = filePath
  275. //}
  276. default:
  277. utils.FileLog.Info("未知格式:", h)
  278. //log.Println(h)
  279. }
  280. }
  281. emailMessage.Content = htmlStr
  282. if emailMessage.Content == `` {
  283. emailMessage.Content = textStr
  284. }
  285. //log.Println("一封邮件读取完毕")
  286. //log.Printf("------------------------- \n\n")
  287. return
  288. }
  289. // 根据文件内容确定文件后缀
  290. func determineFileSuffix(content []byte) string {
  291. kind, err := filetype.Match(content)
  292. if err != nil {
  293. utils.FileLog.Error("无法确定文件类型:%v \n", err)
  294. return ".bin"
  295. }
  296. return kind.Extension
  297. }
  298. func SaveToFile(content []byte, fileName string) error {
  299. file, err := os.Create(fileName)
  300. if err != nil {
  301. return err
  302. }
  303. defer func() {
  304. _ = file.Close()
  305. }()
  306. _, err = file.Write(content)
  307. if err != nil {
  308. return err
  309. }
  310. return nil
  311. }
  312. // ContainsWholeWord 检查字符串 s 中是否包含完整的单词 word。
  313. // 该函数使用正则表达式来匹配整个单词,确保不会错误地匹配到单词的一部分。
  314. // 参数:
  315. //
  316. // s: 要搜索的字符串
  317. // word: 要查找的完整单词
  318. //
  319. // 返回值:
  320. //
  321. // 如果 s 中包含完整的单词 word,则返回 true;否则返回 false。
  322. func ContainsWholeWord(s string, word string) bool {
  323. pattern := fmt.Sprintf(`\b%s\b`, regexp.QuoteMeta(word))
  324. re := regexp.MustCompile(pattern)
  325. return re.MatchString(s)
  326. }