imap.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. package mail
  2. import (
  3. "errors"
  4. "eta/eta_email_analysis/global"
  5. "eta/eta_email_analysis/models/eta"
  6. "eta/eta_email_analysis/models/report"
  7. "eta/eta_email_analysis/utils"
  8. "fmt"
  9. "github.com/emersion/go-imap"
  10. "github.com/emersion/go-imap/client"
  11. "github.com/emersion/go-message"
  12. "github.com/emersion/go-message/mail"
  13. "github.com/goccy/go-json"
  14. "github.com/h2non/filetype"
  15. "io"
  16. "log"
  17. "os"
  18. "path"
  19. "strings"
  20. "time"
  21. )
  22. type MailMessage struct {
  23. Date time.Time `description:"收件时间"`
  24. Uid uint32 `description:"该邮件在邮箱中的唯一id"`
  25. FromAddress string `description:"发件人邮箱"`
  26. From string `description:"发件人名称"`
  27. Title string `description:"邮件标题"`
  28. Content string `description:"邮件主体正文"`
  29. Resources map[string]string `description:"正文内嵌资源"`
  30. Attachment map[string]string `description:"附件资源"`
  31. }
  32. func ListenMail(mailAddress, folder, userName, password string, readBatchSize, fromEmailIndex int, mailMessageChan chan MailMessage, mailMessageDoneChan chan bool) (err error) { // 收件箱
  33. defer func() {
  34. // 处理结束
  35. mailMessageDoneChan <- true
  36. if err != nil {
  37. fmt.Println("err:", err.Error())
  38. }
  39. }()
  40. // 建立与 IMAP 服务器的连接
  41. c, err := client.DialTLS(mailAddress, nil)
  42. if err != nil {
  43. fmt.Printf("连接 IMAP 服务器失败: %+v \n", err)
  44. return
  45. }
  46. // 最后一定不要忘记退出登录
  47. defer func() {
  48. _ = c.Logout()
  49. }()
  50. // 登录
  51. if err = c.Login(userName, password); err != nil {
  52. fmt.Printf("邮箱[%s] 登录失败: %v \n", fmt.Sprintf("%s:%s", userName, mailAddress), err)
  53. return
  54. }
  55. // 列出当前邮箱中的文件夹
  56. mailboxes := make(chan *imap.MailboxInfo, 10)
  57. done := make(chan error, 1) // 记录错误的 chan
  58. go func() {
  59. done <- c.List("", "*", mailboxes)
  60. }()
  61. log.Println("-->当前邮箱的文件夹 Mailboxes:")
  62. var folderExists bool
  63. for m := range mailboxes {
  64. log.Println("* ", m.Name)
  65. if m.Name == folder {
  66. folderExists = true
  67. }
  68. }
  69. err = <-done
  70. if err != nil {
  71. global.LOG.Errorf("列出邮箱列表时,出现错误:%v \n", err)
  72. return
  73. }
  74. log.Println("-->列出邮箱列表完毕!")
  75. if !folderExists {
  76. err = errors.New(fmt.Sprintf("文件夹[%s] 不存在 \n", folder))
  77. return
  78. }
  79. message.CharsetReader = myCharsetReader
  80. // 选择指定的文件夹
  81. mbox, err := c.Select(folder, false)
  82. if err != nil {
  83. err = errors.New(fmt.Sprintf("选择邮件箱失败: %+v", err))
  84. return
  85. }
  86. //log.Printf("mbox %+v \n", mbox)
  87. log.Printf("当前文件夹[%s]中,总共有 %d 封邮件 \n", folder, mbox.Messages)
  88. if mbox.Messages == 0 {
  89. //log.Fatalf("当前文件夹[%s]中没有邮件", folder)
  90. return
  91. }
  92. // 创建一个序列集,用于批量读取邮件
  93. seqSet := new(imap.SeqSet)
  94. to := mbox.Messages // 此文件下的邮件总数
  95. //minIndex := uint32(5)
  96. //// 假设需要获取最后4封邮件时
  97. //if fromEmailIndex > 0 {
  98. // minIndex = uint32(fromEmailIndex)
  99. //} else {
  100. // var maxNum uint32
  101. // //该次监听获取的最大数量
  102. // maxNum = 20000
  103. // //获取开始的邮件编号
  104. // if to > maxNum {
  105. // minIndex = to - maxNum + 1
  106. // }
  107. //}
  108. //from = 310
  109. var isStopFor bool
  110. step := uint32(1)
  111. for i := to; i >= 1; {
  112. start := i - step + 1
  113. if start < 0 {
  114. start = 1
  115. }
  116. //fmt.Printf("当前剩余%d封邮件待处理\n", i-minIndex+1)
  117. seqSet.Clear()
  118. seqSet.AddRange(start, i) // 添加指定范围内的邮件编号
  119. // 获取整个消息正文
  120. // imap.FetchEnvelope:请求获取邮件的信封数据(例如发件人、收件人、主题等元数据)。
  121. // imap.FetchRFC822:请求获取完整的邮件内容,包括所有头部和正文。
  122. items := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchRFC822}
  123. // 获取邮件内容 Start
  124. messages := make(chan *imap.Message, readBatchSize) // 创建一个通道,用于接收邮件消息
  125. fetchDone := make(chan error, 1) // 创建一个通道,用于接收错误消息
  126. go func() {
  127. // Fetch方法用于从服务器获取邮件数据,这里请求了邮件的信封和完整内容
  128. fetchDone <- c.Fetch(seqSet, items, messages)
  129. }()
  130. err = <-fetchDone
  131. if err != nil {
  132. global.LOG.Errorf("获取邮件信息出现错误:%v \n", err)
  133. return
  134. }
  135. // 获取邮件内容 End
  136. // 获取规则
  137. ruleJson, e := eta.GetBusinessConfByKey("MailCheckRule")
  138. if e != nil {
  139. err = e
  140. global.FILE_LOG.Error("获取规则配置失败:%v", err)
  141. return
  142. }
  143. var ruleList []report.MailRule
  144. err = json.Unmarshal([]byte(ruleJson.ConfVal), &ruleList)
  145. if err != nil {
  146. global.FILE_LOG.Error("解析规则配置失败:%v", err)
  147. return
  148. }
  149. //log.Println("开始读取邮件内容")
  150. for msg := range messages {
  151. // 如果需要终止,那么就不处理了
  152. if isStopFor {
  153. continue
  154. }
  155. emailMessage, isRead, tmpErr := readEveryMsg(ruleList, msg)
  156. if tmpErr != nil {
  157. // 移除本地文件
  158. {
  159. for _, v := range emailMessage.Attachment {
  160. os.Remove(v)
  161. }
  162. for _, v := range emailMessage.Resources {
  163. os.Remove(v)
  164. }
  165. }
  166. global.FILE_LOG.Errorf("读取邮件内容时出现错误:%v \n", tmpErr)
  167. continue
  168. }
  169. // 如果没有取到,那么就过滤
  170. if !isRead {
  171. continue
  172. }
  173. // 判断当前邮件id是否小于等于已经监听到的最小id,如果是,那么就不处理了
  174. if emailMessage.Uid <= uint32(fromEmailIndex) {
  175. isStopFor = true
  176. continue
  177. }
  178. // 如果取到了,那么写入待处理chan
  179. // 写入邮件处理chan
  180. mailMessageChan <- emailMessage
  181. }
  182. if isStopFor {
  183. // 已经找到了最小的邮件id,那么就退出循环了
  184. }
  185. //time.Sleep(time.Second * 5) // 休眠10秒
  186. i = i - step
  187. }
  188. log.Println("读取了所有邮件,完毕!")
  189. return
  190. }
  191. // document link: https://github.com/emersion/go-imap/wiki/Fetching-messages
  192. func readEveryMsg(ruleList []report.MailRule, msg *imap.Message) (emailMessage MailMessage, ok bool, err error) {
  193. ok = true
  194. defer func() {
  195. if err != nil {
  196. ok = false
  197. global.FILE_LOG.Errorf("邮件读取失败;Err:%s", err.Error())
  198. }
  199. }()
  200. message.CharsetReader = myCharsetReader
  201. emailMessage.Resources = make(map[string]string) // 内嵌资源
  202. emailMessage.Attachment = make(map[string]string) // 附件
  203. emailMessage.Uid = msg.Uid
  204. htmlStr := ``
  205. textStr := ``
  206. //log.Printf("当前邮件的消息序列号 %+v \n", msg.SeqNum)
  207. //log.Println("-------------------------")
  208. // 获取邮件正文
  209. r := msg.GetBody(&imap.BodySectionName{})
  210. if r == nil {
  211. global.FILE_LOG.Info("服务器没有返回消息内容")
  212. }
  213. mr, err := mail.CreateReader(r)
  214. if err != nil {
  215. //log.Fatalf("邮件读取时出现错误: %v \n", err)
  216. err = errors.New(fmt.Sprintf("邮件读取时出现错误:%v \n", err))
  217. return
  218. }
  219. // 收件时间
  220. {
  221. date, err := mr.Header.Date()
  222. if err != nil {
  223. log.Println("收件时间 异常:", err.Error())
  224. }
  225. emailMessage.Date = date
  226. //log.Println("收件时间 Date:", date)
  227. }
  228. // 发件人
  229. {
  230. fromStr := mr.Header.Get("From")
  231. //fmt.Println(fromStr)
  232. // 处理无效地址的情况
  233. if !strings.Contains(fromStr, "@") {
  234. emailMessage.FromAddress = fromStr
  235. emailMessage.From = fromStr
  236. } else {
  237. from, tmpErr := mr.Header.AddressList("From")
  238. if tmpErr != nil {
  239. log.Println("发件人 异常:", err.Error())
  240. }
  241. if len(from) > 0 {
  242. emailMessage.FromAddress = from[0].Address
  243. emailMessage.From = from[0].Name
  244. //mailMessage.From = from[0].String()
  245. //log.Println("发件人 From:", from)
  246. }
  247. }
  248. }
  249. //if to, err := mr.Header.AddressList("To"); err == nil {
  250. // log.Println("收件人 To:", to)
  251. //}
  252. //log.Printf("抄送 Cc: %+v \n", msg.Envelope.Cc)
  253. // 邮件标题
  254. subject, err := mr.Header.Subject()
  255. if err != nil {
  256. log.Println("邮件主题 Subject ERR:", err)
  257. } else {
  258. //log.Println("邮件主题 Subject:", subject)
  259. }
  260. emailMessage.Title = subject
  261. if !RuleCheck(ruleList, emailMessage) {
  262. ok = false
  263. return
  264. }
  265. // 过滤
  266. if isIgnore(emailMessage) {
  267. ok = false
  268. return
  269. }
  270. //fmt.Println("当前邮件Uid:", emailMessage.Uid)
  271. //ok = false
  272. //return
  273. for {
  274. p, tmpErr := mr.NextPart()
  275. if tmpErr == io.EOF {
  276. break
  277. } else if tmpErr != nil {
  278. global.FILE_LOG.Errorf("读取邮件内容时出现错误:%v \n", tmpErr)
  279. err = tmpErr
  280. return
  281. }
  282. bodyBytes, _ := io.ReadAll(p.Body)
  283. if err != nil {
  284. //log.Fatalf("读取邮件部分时出现错误:%v \n", err)
  285. err = errors.New(fmt.Sprintf("读取邮件部分时出现错误:%v \n", err))
  286. return
  287. }
  288. switch h := p.Header.(type) {
  289. case *mail.InlineHeader:
  290. // 这是消息的文本(可以是纯文本或 HTML)
  291. contentType := h.Get("Content-Type")
  292. //log.Println("消息内容content-type:", contentType)
  293. if strings.HasPrefix(contentType, "text/plain") {
  294. //log.Printf("得到正文 -> TEXT: %v \n", string(bodyBytes))
  295. textStr += string(bodyBytes)
  296. } else if strings.HasPrefix(contentType, "text/html") {
  297. //log.Printf("得到正文 -> HTML: %v \n", len(b))
  298. //log.Printf("得到正文 -> HTML: %v \n", string(bodyBytes))
  299. htmlStr += string(bodyBytes)
  300. }
  301. // 这是内嵌资源
  302. if cid := p.Header.Get("Content-ID"); cid != "" {
  303. // 确定文件后缀
  304. fileSuffix := determineFileSuffix(bodyBytes)
  305. fileName := fmt.Sprintf("%s%s.%s", global.CONFIG.Serve.StaticDir, cid[1:len(cid)-1], fileSuffix)
  306. err = utils.SaveToFile(bodyBytes, fileName)
  307. if err != nil {
  308. //log.Fatalf("保存文件时出现错误:%v \n", err)
  309. err = errors.New(fmt.Sprintf("保存文件时出现错误:%v \n", err))
  310. return
  311. }
  312. emailMessage.Resources[cid] = fileName
  313. }
  314. break
  315. case *mail.AttachmentHeader:
  316. // 这是一个附件
  317. filename, _ := h.Filename()
  318. //log.Printf("得到附件: %v,content-type:%s \n", filename, p.Header.Get("Content-Type"))
  319. saveName := fmt.Sprint(msg.SeqNum, utils.MD5(filename), time.Now().Format(utils.FormatDateTimeUnSpace), time.Now().Nanosecond(), path.Ext(filename))
  320. filePath := fmt.Sprintf("%s%s%s%s", global.CONFIG.Serve.StaticDir, `file`, string(os.PathSeparator), saveName)
  321. err = utils.SaveToFile(bodyBytes, filePath)
  322. if err != nil {
  323. //log.Fatalf("保存文件时出现错误:%v \n", err)
  324. err = errors.New(fmt.Sprintf("保存文件时出现错误:%v \n", err))
  325. return
  326. }
  327. // 这是附件资源
  328. if contentDisposition := p.Header.Get("Content-Disposition"); contentDisposition != "" {
  329. if strings.HasPrefix(contentDisposition, "attachment") {
  330. emailMessage.Attachment[filename] = filePath
  331. }
  332. } else if cid := p.Header.Get("Content-ID"); cid != "" {
  333. // 这是内嵌资源
  334. emailMessage.Resources[cid] = filePath
  335. }
  336. //else {
  337. // mailMessage.Attachment[filename] = filePath
  338. //}
  339. break
  340. default:
  341. global.FILE_LOG.Info("未知格式:", h)
  342. //log.Println(h)
  343. }
  344. }
  345. emailMessage.Content = htmlStr
  346. if emailMessage.Content == `` {
  347. emailMessage.Content = textStr
  348. }
  349. //log.Println("一封邮件读取完毕")
  350. //log.Printf("------------------------- \n\n")
  351. return
  352. }
  353. // 根据文件内容确定文件后缀
  354. func determineFileSuffix(content []byte) string {
  355. kind, err := filetype.Match(content)
  356. if err != nil {
  357. global.FILE_LOG.Error("无法确定文件类型:%v \n", err)
  358. return ".bin"
  359. }
  360. return kind.Extension
  361. }
  362. // isIgnore
  363. // @Description: 校验是否忽略的邮件
  364. // @author: Roc
  365. // @datetime 2024-09-30 16:09:34
  366. // @param emailMessage MailMessage
  367. // @return bool
  368. func isIgnore(emailMessage MailMessage) bool {
  369. // 发件人中包含待过滤的字符串,那么就过滤
  370. lowerFrom := strings.ToLower(emailMessage.From)
  371. for _, email := range global.CONFIG.Email.IgnoreEmail {
  372. if utils.ContainsWholeWord(lowerFrom, email) {
  373. global.FILE_LOG.Infof("发件人包含%s,过滤掉,发件人:%s;标题:%s;所属下标:%d", email, emailMessage.From, emailMessage.Title, emailMessage.Uid)
  374. return true
  375. }
  376. }
  377. // 邮件标题中包含待过滤的字符串(大小写敏感的标题),那么就过滤
  378. for _, email := range global.CONFIG.Email.IgnoreEmailCaseSensitive {
  379. if utils.ContainsWholeWord(emailMessage.From, email) {
  380. global.FILE_LOG.Infof("发件人包含%s,过滤掉,发件人:%s;标题:%s;所属下标:%d", email, emailMessage.From, emailMessage.Title, emailMessage.Uid)
  381. return true
  382. }
  383. }
  384. // 发件人地址中包含待过滤的字符串,那么就过滤
  385. lowerFromAddress := strings.ToLower(emailMessage.FromAddress)
  386. for _, emailAddress := range global.CONFIG.Email.IgnoreEmailAddress {
  387. if utils.ContainsWholeWord(lowerFromAddress, emailAddress) {
  388. global.FILE_LOG.Infof("发件人邮箱包含%s,过滤掉,发件人邮箱地址:%s;标题:%s;所属下标:%d", emailAddress, emailMessage.FromAddress, emailMessage.Title, emailMessage.Uid)
  389. return true
  390. }
  391. }
  392. // 邮件地址中包含待过滤的字符串(大小写敏感的标题),那么就过滤
  393. for _, emailAddress := range global.CONFIG.Email.IgnoreEmailAddressCaseSensitive {
  394. if utils.ContainsWholeWord(emailMessage.FromAddress, emailAddress) {
  395. global.FILE_LOG.Infof("发件人邮箱包含%s,过滤掉,发件人邮箱地址:%s;标题:%s;所属下标:%d", emailAddress, emailMessage.FromAddress, emailMessage.Title, emailMessage.Uid)
  396. return true
  397. }
  398. }
  399. // 邮件标题中包含待过滤的字符串,那么就过滤
  400. lowerTitle := strings.ToLower(emailMessage.Title)
  401. for _, title := range global.CONFIG.Email.IgnoreEmailTitle {
  402. if utils.ContainsWholeWord(lowerTitle, title) {
  403. global.FILE_LOG.Infof("邮件标题包含%s,过滤掉,标题:%s,所属下标:%d", title, emailMessage.Title, emailMessage.Uid)
  404. return true
  405. }
  406. }
  407. // 邮件标题中包含待过滤的字符串(大小写敏感的标题),那么就过滤
  408. for _, title := range global.CONFIG.Email.IgnoreEmailTitleCaseSensitive {
  409. if utils.ContainsWholeWord(emailMessage.Title, title) {
  410. global.FILE_LOG.Infof("邮件标题包含%s,过滤掉,标题:%s,所属下标:%d", title, emailMessage.Title, emailMessage.Uid)
  411. return true
  412. }
  413. }
  414. return false
  415. }
  416. func RuleCheck(ruleList []report.MailRule, emailMessage MailMessage) bool {
  417. for _, v := range ruleList {
  418. title := strings.ToLower(emailMessage.Title)
  419. rule := strings.ToLower(v.Rule)
  420. if strings.Contains(title, rule){
  421. return true
  422. }
  423. }
  424. return false
  425. }