imap.go 9.6 KB

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