1 问题描述
生产服务器发送通知邮件,之前一直都是正常的。可突然有一天业务同事反馈收不到通知邮件了。经过查看生产运行日志,发现是由于出现无效邮件地址导致的,而核心异常日志信息如下:
javax.mail.SendFailedException: Invalid Addresses
at com.sun.mail.smtp.SMTPTransport.rcptTo(SMTPTransport.java:1862) ~[mail-1.4.7.jar:1.4.7]
at com.sun.mail.smtp.SMTPTransport.sendMessage(SMTPTransport.java:1118) ~[mail-1.4.7.jar:1.4.7]
at com.crtrust.trustonline.utils.MailUtils.sendHtmlEmail(MailUtils.java:167) [classes/:?]
at com.crtrust.trustonline.utils.MailUtilsTest2.main(MailUtilsTest2.java:47) [classes/:?]
Caused by: com.sun.mail.smtp.SMTPAddressFailedException: 550 User suspended: panting@email.com
at com.sun.mail.smtp.SMTPTransport.rcptTo(SMTPTransport.java:1715) ~[mail-1.4.7.jar:1.4.7]
... 3 more
Caused by: com.sun.mail.smtp.SMTPAddressFailedException: 550 User not found: panting1@email.com
at com.sun.mail.smtp.SMTPTransport.rcptTo(SMTPTransport.java:1715) ~[mail-1.4.7.jar:1.4.7]
... 3 more
2 问题解析与方案分析
最初我还纳闷,之前邮件通知程序一直都是在正常运行的,咋会突然莫名其妙地出现bug呢,还以为是发版上线引起了。经过分析排查定位,最终确认是由于员工离职邮箱账号被停用导致的邮箱地址无效。
我就在想不能因为账号被禁用就导致群发邮件失败啊,要不然出现一个员工离职就需要调整邮件列表配置或是重启服务,那样也太不灵活了吧,所以肯定还是需要完善一下的。
经过上网搜索及分析排查,发现在抛出的异常类SMTPAddressFailedException中包含有无效的邮箱地址,而通过方法getInvalidAddresses()即可获取到无效的邮箱地址列表,而在获取到无效的邮箱地址之后从原来的邮箱地址列表中剔除掉再重新发送一次,即可把问题完美解决掉。
题外话:其实事后再回想一下,既然异常日志中已明确指明无效的邮件地址,那就说明可以通过某种方式获取到无效的邮件地址列表,而这就应该是我分析定位的方向。可惜最初自己并没有看到这个方向,而是一心想着去网上搜解决方案,也幸亏最后找到了解决方案。在这个问题解决的过程中,我也在反思自己解决问题的技术思维和能力还非常有待进一步提升。
3 实例代码
废话不啰嗦,直接上代码。注意,代码中的邮件服务器地址和账号密码已经隐去,大家换成自己家的配置即可。
package com.demo.utils;
import com.alibaba.fastjson.JSON;
import com.sun.mail.smtp.SMTPAddressFailedException;
import org.apache.commons.lang3.StringUtils;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.*;
import javax.mail.internet.*;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
public class MailUtils {
private static String mailHost = "*.*.*.*";
private static String mailName = "mailname@email.com";
private static String hostPort = "25";
private static String password = "password";
public static void main(String[] args) {
try {
File attachment = null;
String mailSubject = "Email Subject Demo";
String mailHtmlConent = "Dear:This is mail content。";
String emailReceiveList = null;
String emailCcList = null;
//测试用例:
// 发送人和抄送人均为空
//发送人为空,抄送人有效
//发送人为空,抄送人无效
//发送人为空,抄送人包含无效
//抄送人为空,发送人有效
//抄送人为空,发送人无效
//抄送人为空,发送人包含无效
//发送人和抄送人同时无效
//发送人和抄送人同时包含无效
emailReceiveList = "XIAGUANGHUI@email.hk,panting@email.com";
emailCcList = "8888@qq.com,panting1@email.com";
String[] mailReceiveArr = StringUtils.split(emailReceiveList, ",");
//使用StringUtils.split方法而不是String自带的split方法,是为了防止切分后去除产生空串的情况
String[] emailCcArr = StringUtils.split(emailCcList, ",");
boolean sendEmailBoolean = MailUtils.sendHtmlEmail(mailReceiveArr, emailCcArr, mailSubject, mailHtmlConent, attachment,
"smtp", mailHost, hostPort, mailName, password);
System.out.println("执行结果=======" + sendEmailBoolean);
} catch (Exception e) {
System.out.println(e);
}
}
/**
* 发送邮件
*
* @param subject:邮件主题
* @param sendHtmlContent:邮件内容
* @param attachment: 附件
* @param mailProtocol:邮件服务协议
* @param mailHost:邮件服务地址
* @param mailPort:邮件服务端口
* @param senderUsername 发件人用户名
* @param senderPassword 发件人密码
* @Param userArr:收件人列表
* @Param ccUserArr:抄送人列表
*/
public static boolean sendHtmlEmail(String[] userArr, String[] ccUserArr, String subject, String sendHtmlContent, File attachment,
String mailProtocol, String mailHost, String mailPort, String senderUsername, String senderPassword
) {
Transport transport = null;
try {
Properties props = new Properties();
props.put("mail.transport.protocol", mailProtocol);
props.put("mail.host", mailHost);
props.put("mail.from", senderUsername);
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.socketFactory.fallback", "false");
props.put("mail.smtp.socketFactory.port", mailPort);
Session session = Session.getInstance(props);
session.setDebug(true);// 开启后有调试信息
MimeMessage message = new MimeMessage(session);
// 发件人
InternetAddress from = new InternetAddress(senderUsername);
message.setFrom(from);
//设置多发件人
if (userArr != null && userArr.length > 0) {
message.addRecipients(Message.RecipientType.TO, setRecipientT0(userArr));
}
//设置抄送地址
if (ccUserArr != null && ccUserArr.length > 0) {
message.addRecipients(Message.RecipientType.CC, setRecipientT0(ccUserArr));
}
Address[] allRecipients = message.getAllRecipients();
System.out.println("邮件接收人:" + JSON.toJSONString(allRecipients));
if (allRecipients == null || allRecipients.length == 0) {
throw new RuntimeException("邮件接收人列表为空!");
}
// 邮件主题
message.setSubject(subject);
// 向multipart对象中添加邮件的各个部分内容,包括文本内容和附件
Multipart multipart = new MimeMultipart();
// 添加邮件正文
BodyPart contentPart = new MimeBodyPart();
contentPart.setContent(sendHtmlContent, "text/html;charset=UTF-8");
multipart.addBodyPart(contentPart);
// 添加附件的内容
if (attachment != null) {
BodyPart attachmentBodyPart = new MimeBodyPart();
DataSource source = new FileDataSource(attachment);
attachmentBodyPart.setDataHandler(new DataHandler(source));
//MimeUtility.encodeWord可以避免文件名乱码
attachmentBodyPart.setFileName(MimeUtility.encodeWord(attachment.getName()));
multipart.addBodyPart(attachmentBodyPart);
}
// 将multipart对象放到message中
message.setContent(multipart);
// 保存邮件
message.saveChanges();
transport = session.getTransport(mailProtocol);
// smtp验证,就是你用来发邮件的邮箱用户名密码
transport.connect(mailHost, senderUsername, senderPassword);
// 发送
transport.sendMessage(message, message.getAllRecipients());
return true;
} catch (Exception e) {
System.out.println("邮件发送异常," + e);
if (e instanceof SMTPAddressFailedException) {
Address[] invalidAddresses = ((SMTPAddressFailedException) e).getInvalidAddresses();
return removeInvalidAddressesAndResendEmail(userArr, ccUserArr, subject, sendHtmlContent, attachment, mailProtocol, mailHost, mailPort, senderUsername, senderPassword, invalidAddresses);
} else if (e instanceof SendFailedException) {
Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
return removeInvalidAddressesAndResendEmail(userArr, ccUserArr, subject, sendHtmlContent, attachment, mailProtocol, mailHost, mailPort, senderUsername, senderPassword, invalidAddresses);
}
} finally {
if (transport != null) {
try {
transport.close();
} catch (MessagingException e) {
System.out.println("邮件发送后,Transport关闭失败," + e);
}
}
}
return false;
}
/*** 设置收件人/抄送人/密送人地址信息*/
private static InternetAddress[] setRecipientT0(String[] recipientT0Arr) throws MessagingException, UnsupportedEncodingException {
if (recipientT0Arr.length > 0) {
InternetAddress[] sendTo = new InternetAddress[recipientT0Arr.length];
for (int i = 0; i < recipientT0Arr.length; i++) {
System.out.println("发送到:" + recipientT0Arr[i]);
sendTo[i] = new InternetAddress(recipientT0Arr[i], "", "UTF-8");
}
return sendTo;
}
return null;
}
public static boolean removeInvalidAddressesAndResendEmail(String[] userArr, String[] ccUserArr, String subject, String sendHtmlContent, File attachment, String mailProtocol, String mailHost, String mailPort, String senderUsername, String senderPassword, Address[] invalidAddresses) {
System.out.println("无效的邮件地址:" + JSON.toJSONString(invalidAddresses));
List<String> invalidAddressList = new ArrayList<>();
for (Address address : invalidAddresses) {
invalidAddressList.add(address.toString());
userArr = remove(userArr, address.toString());
ccUserArr = remove(ccUserArr, address.toString());
}
System.out.println("移除无效邮件列表:(" + JSON.toJSONString(invalidAddressList) + ")后,重新进行发送。。。");
return sendHtmlEmail(userArr, ccUserArr, subject, sendHtmlContent, attachment,
mailProtocol, mailHost, mailPort, senderUsername, senderPassword);
}
/**
* 从数组中移除指定元素,并返回一个新数组
* @param arr
* @param removeElement
* @return
*/
private static String[] remove(String[] arr, String removeElement) {
int count = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(removeElement)) {
count++;
}
}
String[] newArr = new String[arr.length - count];
int index = 0;
for (int i = 0; i < arr.length; i++) {
if (!arr[i].equals(removeElement)) {
newArr[index] = arr[i];
index++;
}
}
return newArr;
}
}