需求描述:公司最近有个项目邮件通知功能,但是客户上传的邮件地址并不一定存在,以及其他的各种问题。所有希望发送通知后有个回执,及时发现地址存在问题的邮箱。
需求分析:经过分析JavaMail可以读取收件箱邮件,我们可以通过对应通知的退信来回写通知状态。那么问题来了,发送通知和退信如何建立映射?经过调研,最终确定采用以下方案解决。
映射方案:
- 在发送邮件通知时在Header中指定自定义的Message_Id,作为唯一标示,本系统中采用UUID。
- 定时任务扫描服务器邮箱的收件箱,本系统我们搜索收件箱中前30分钟内的主题为:“来自postmaster@net.cn的退信”,的退信邮件。
- 分析退信附件,退信关联邮件信息存在附件中,我们需要的Message_Id也在其中,解析附件获取Message_Id回写通知状态。
核心代码:
邮件搜索
1 package com.yinghuo.yingxinxin.notification.service;
2
3 import com.yinghuo.yingxinxin.notification.domain.PayrollNotificationEntity;
4 import com.yinghuo.yingxinxin.notification.domain.valobj.EmailNotificationStatus;
5 import com.yinghuo.yingxinxin.notification.repository.NotificationRepository;
6 import com.yinghuo.yingxinxin.notification.util.DateUtil;
7 import com.yinghuo.yingxinxin.notification.util.EmailUtil;
8 import com.yinghuo.yingxinxin.notification.util.StringUtil;
9 import lombok.Data;
10 import lombok.extern.slf4j.Slf4j;
11 import org.apache.commons.lang.exception.ExceptionUtils;
12 import org.springframework.boot.context.properties.ConfigurationProperties;
13 import org.springframework.stereotype.Service;
14 import org.springframework.transaction.annotation.Transactional;
15
16 import javax.mail.*;
17 import javax.mail.search.AndTerm;
18 import javax.mail.search.ComparisonTerm;
19 import javax.mail.search.SearchTerm;
20 import javax.mail.search.SentDateTerm;
21 import javax.mail.search.SubjectTerm;
22 import java.util.Arrays;
23 import java.util.Calendar;
24 import java.util.Date;
25 import java.util.Properties;
26
27 @Service
28 @Slf4j
29 @Data
30 @ConfigurationProperties(prefix = "spring.mail")
31 public class EmailBounceScanService {
32 private final static String subjectKeyword = "来自postmaster@net.cn的退信";
33
34 private String popHost;
35 private String username;
36 private String password;
37 private Integer timeOffset;
38 private final NotificationRepository payrollSendRecordRepository;
39
40 private Properties buildInboxProperties() {
41 Properties properties = new Properties();
42 properties.setProperty("mail.store.protocol", "pop3");
43 properties.setProperty("mail.pop3.host", popHost);
44 properties.setProperty("mail.pop3.auth", "true");
45 properties.setProperty("mail.pop3.default-encoding", "UTF-8");
46 return properties;
47 }
48
49 public void searchInboxEmail() {
50 Session session = Session.getInstance(this.buildInboxProperties());
51 Store store = null;
52 Folder receiveFolder = null;
53 try {
54 store = session.getStore("pop3");
55 store.connect(username, password);
56 receiveFolder = store.getFolder("inbox");
57 receiveFolder.open(Folder.READ_ONLY);
58
59 int messageCount = receiveFolder.getMessageCount();
60 if (messageCount > 0) {
61 Date now = Calendar.getInstance().getTime();
62 Date timeOffsetAgo = DateUtil.nextXMinute(now, timeOffset);
63 SearchTerm comparisonTermGe = new SentDateTerm(ComparisonTerm.GE, timeOffsetAgo);
64 SearchTerm search = new AndTerm(new SubjectTerm(subjectKeyword), comparisonTermGe);
65
66 Message[] messages = receiveFolder.search(search);
67 if (messages.length == 0) {
68 log.info("No bounce email was found.");
69 return;
70 }
71 this.messageHandler(messages);
72 }
73 } catch (MessagingException e) {
74 log.error("Exception in searchInboxEmail {}", ExceptionUtils.getFullStackTrace(e));
75 e.printStackTrace();
76 } finally {
77 try {
78 if (receiveFolder != null) {
79 receiveFolder.close(true);
80 }
81 if (store != null) {
82 store.close();
83 }
84 } catch (MessagingException e) {
85 log.error("Exception in searchInboxEmail {}", ExceptionUtils.getFullStackTrace(e));
86 e.printStackTrace();
87 }
88 }
89 }
90
91 @Transactional
92 public void messageHandler(Message[] messageArray) {
93 Arrays.stream(messageArray).filter(EmailUtil::isContainAttachment).forEach((message -> {
94 String messageId = null;
95 try {
96 messageId = EmailUtil.getMessageId(message);
97 } catch (Exception e) {
98 log.error("getMessageId:", ExceptionUtils.getFullStackTrace(e));
99 e.printStackTrace();
100 }
101 if (StringUtil.isEmpty(messageId)) return;
102
103 PayrollNotificationEntity payrollNotificationEntity = payrollSendRecordRepository.findFirstByMessageId(messageId);
104 if (payrollNotificationEntity == null || EmailNotificationStatus.BOUNCE.getStatus() == payrollNotificationEntity.getStatus()) {
105 log.warn("not found payrollNotificationEntity by messageId:{}", messageId);
106 return;
107 }
108
109 payrollNotificationEntity.setStatus(EmailNotificationStatus.BOUNCE.getStatus());
110 payrollNotificationEntity.setErrorMessage(EmailNotificationStatus.BOUNCE.getErrorMessage());
111 payrollSendRecordRepository.save(payrollNotificationEntity);
112 }));
113 }
114 }
附件解析
1 package com.yinghuo.yingxinxin.notification.util;
2
3 import lombok.extern.slf4j.Slf4j;
4
5 import javax.mail.BodyPart;
6 import javax.mail.MessagingException;
7 import javax.mail.Multipart;
8 import javax.mail.Part;
9 import java.io.BufferedReader;
10 import java.io.IOException;
11 import java.io.InputStream;
12 import java.io.InputStreamReader;
13
14 @Slf4j
15 public final class EmailUtil {
16 private static final String multipart = "multipart/*";
17
18 public static String getMessageId(Part part) throws Exception {
19 if (!part.isMimeType(multipart)) {
20 return "";
21 }
22
23 Multipart multipart = (Multipart) part.getContent();
24 for (int i = 0; i < multipart.getCount(); i++) {
25 BodyPart bodyPart = multipart.getBodyPart(i);
26
27 if (part.isMimeType("message/rfc822")) {
28 return getMessageId((Part) part.getContent());
29 }
30 InputStream inputStream = bodyPart.getInputStream();
31
32 try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
33 String strLine;
34 while ((strLine = br.readLine()) != null) {
35 if (strLine.startsWith("Message_Id:")) {
36 String[] split = strLine.split("Message_Id:");
37 return split.length > 1 ? split[1].trim() : null;
38 }
39 }
40 }
41 }
42
43 return "";
44 }
45
46 public static boolean isContainAttachment(Part part) {
47 boolean attachFlag = false;
48 try {
49 if (part.isMimeType(multipart)) {
50 Multipart mp = (Multipart) part.getContent();
51 for (int i = 0; i < mp.getCount(); i++) {
52 BodyPart mpart = mp.getBodyPart(i);
53 String disposition = mpart.getDisposition();
54 if ((disposition != null) && ((disposition.equals(Part.ATTACHMENT)) || (disposition.equals(Part.INLINE))))
55 attachFlag = true;
56 else if (mpart.isMimeType(multipart)) {
57 attachFlag = isContainAttachment((Part) mpart);
58 } else {
59 String contype = mpart.getContentType();
60 if (contype.toLowerCase().contains("application"))
61 attachFlag = true;
62 if (contype.toLowerCase().contains("name"))
63 attachFlag = true;
64 }
65 }
66 } else if (part.isMimeType("message/rfc822")) {
67 attachFlag = isContainAttachment((Part) part.getContent());
68 }
69 } catch (MessagingException | IOException e) {
70 e.printStackTrace();
71 }
72 return attachFlag;
73 }
74 }