开发
使用springboot 进行实现邮件发送功能,会简化很多工作。
添加依赖文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
application.properties 配置文件
下面以163邮箱服务器为例进行配置
################# mail 基本 属性配置 #####################
## 超时时间
spring.mail.timeout=10000
## 编码
spring.mail.default-encoding=utf-8
spring.mail.encoding=utf-8
## 校验
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.protocol=smtp
spring.mail.properties.mail.debug=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
# ssl 传输使用ssl协议
# 如果使用了 对应的SSL 协议端口,那么需要 设置为 true
# spring.mail.smtp.ssl.enable=false
# 或者使用 下面的其中一种方式
# spring.mail.properties.mail.smtp.ssl.enable=true
# spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
# spring.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
############ 自定义的邮件发送设置 ##############
# 邮件服务器域名
spring.mail.host=smtp.163.com
# 邮件服务器端口号(默认就是25)
spring.mail.port=25
# 发送邮件的邮箱
spring.mail.username=666666@163.com
# 该邮箱对应的授权码
spring.mail.password=123456
如果需要使用SSL 协议 可以设置以下端口号
spring-boot-starter-mail 会根据application配置文件中配置的属性,创建 JavaMailSender实例(JavaMailSenderImpl)
建议测试的时候,使用硬编码方式,这样可以随时修改参数。
@Controller
public class SenderMailController {
@Autowired
private JavaMailSender sender;//通过application配置文件的属性创建的JavaMailSender 实例。创建实例的实现类是JavaMailSenderImpl
@RequestMapping("/testMail")
public String testMail() throws MessagingException, UnsupportedEncodingException {
//手动创建的实例的属性和配置文件中的大致相同
JavaMailSenderImpl sender=new JavaMailSenderImpl();
sender.setDefaultEncoding("utf8"); //编码
sender.setHost("smtp.163.com");//163 smtp服务器
sender.setPort(25); //端口
sender.setUsername("666666@163.com"); //邮箱
sender.setPassword("123456");//授权码
sender.setProtocol("smtp"); //协议
//配置额外属性
Properties properties=new Properties();//额外设置的属性
properties.setProperty("mail.smtp.auth", "true");//是否需要验证
properties.setProperty("mail.smtp.timeout","2000");//超时
// properties.setProperty("mail.smtp.ssl.enable", "true");//ssl加密
sender.setJavaMailProperties(properties);
//编辑信息
MimeMessage mimeMessage =sender.createMimeMessage();//多媒体信息
MimeMessageHelper helper=new MimeMessageHelper(mimeMessage,true);// 使用MimeMessageHelper类可以简化代码,提高开发效率
helper.setFrom("666666@163.com", "xxx");//设置发送邮件和发送人,该邮箱需要和上面的userName相同
helper.setTo(new String[] {"xxxxx@qq.com"});//目标地址
helper.setSubject("测试!!!");//设置邮件主题
helper.setText("<p>测试呀</p>",true);//正文
File file =new File("D:/", "test.pdf");
helper.addAttachment(MimeUtility.encodeWord("测试.pdf"), file);//添加附件,并使用MimeUtility解决附件名称中文乱码
sender.send(mimeMessage);//发送邮件
}
}
简化版本
@Component
public class SendMailUtils {
@Autowired
private JavaMailSender sender;//有spring容器创建
@Value("${spring.mail.username}")
private String username;//使用值注入,避免邮箱发送地址设置错误
/**
*
* @param to 接受人,邮箱数组字符串
* @param subject 标题
* @param content 文本
* @throws MessagingException
* @throws UnsupportedEncodingException
*/
public void simpleSendMail(String[] to,String subject,String content) throws MessagingException, UnsupportedEncodingException {
MimeMessage simpleMessage =sender.createMimeMessage();
MimeMessageHelper helper=new MimeMessageHelper(simpleMessage,true);
helper.setFrom(username, "邮箱发送人说明");
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content,true);//html 数据
sender.send(simpleMessage);
}
遇到的一些问题
之前简单看过邮件发送的程序,觉得很简单真正开始做的时候,还是遇到了很多问题。
Illegal address 。。 Address Reject
- 邮箱服务器地址不正确
-
邮件服务器拒绝访问,可能服务器对访问ip有限制。
javax.mail.SendFailedException: Invalid Address Address Reject
java.net.SocketTimeoutException: Read timed out
超时异常。
通过mail.smtp.timeout来设置超时时间,它的单位是毫秒。开始我设置为2000报错了
mail.smtp.timeout =3000
org.springframework.mail.MailSendException: Mail server connection failed;
org.springframework.mail.MailSendException: Mail server connection failed;
nested exception is javax.mail.MessagingException:
Could not connect to SMTP host: smtp.exmail.qq.com, port: 465, response: -1.
Failed messages: javax.mail.MessagingException: Could not connect to SMTP host: smtp.exmail.qq.com, port: 465, response: -1;
原因是 使用的端口号是 SSL 协议端口,必须通过ssl验证。
# spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring:
mail:
properties:
mail:
smtp:
auth: true
socketFactory:
class: javax.net.ssl.SSLSocketFactory
或者
# spring.mail.properties.mail.smtp.ssl.enable=true
spring:
mail:
properties:
mail:
smtp:
ssl:
enable: true
认证相关问题
1. mail.smtp.auth 为true ,报错 javax.mail.AuthenticationFailedException: 535 Error: authentication failed
设置属性 ”mail.smtp.auth“ 为true(需要进行用户名授权码验证),测试邮件发送时报错。
报错的原因可能是
- 用户名不正确
-
授权码
不正确
创建JavaMailSender对象的时候需要设置密码。该密码是授权码
JavaMailSenderImpl sender=new JavaMailSenderImpl();
sender.setPassword("") //该密码是授权码,而不是邮箱的登陆密码
启用授权码,避免密码泄漏造成邮箱安全隐患,使用授权码是可以访问邮箱的部分功能(发邮件)。使用授权码是无法登陆邮箱的.
开启授权码需要手机验证,对于163邮箱,如下
2. mail.smtp.auth为false,也会报错 javax.mail.AuthenticationFailedException: 535 Error: authentication failed
其实如果设置 mail.smtp.auth为false,但是如果用户名和密码都不为空,也可能会进行校验的。
因为正是和服务器连接之前,会收集服务器的扩展参数。
如果服务器支持校验而且也设置了用户名和密码,即便mail.smtp.auth为false,也会进行验证。
if ((useAuth || (user != null && password != null)) &&
(supportsExtension("AUTH") ||
supportsExtension("AUTH=LOGIN"))) {
if (logger.isLoggable(Level.FINE))
logger.fine("protocolConnect login" +
", host=" + host +
", user=" + traceUser(user) +
", password=" + tracePassword(password));
connected = authenticate(user, password); //校验用户名 和 授权码
如果有兴趣,可以自己看一下源码,我也是看的模模糊糊。
下面是SMTPTransport.class 的部分代码
//与服务器建立连接,并进行验证
@Override
protected synchronized boolean protocolConnect(String host, int port,
String user, String password)
throws MessagingException {
Properties props = session.getProperties();
// 是否需要校验
boolean useAuth = PropUtil.getBooleanProperty(props,
"mail." + name + ".auth", false);
//根据传入useAuth user和password的值进行判断
if (useAuth && (user == null || password == null)) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("need username and password for authentication");
logger.fine("protocolConnect returning false" +
", host=" + host +
", user=" + traceUser(user) +
", password=" + tracePassword(password));
}
return false;
}
//默认使用true
boolean useEhlo = PropUtil.getBooleanProperty(props,
"mail." + name + ".ehlo", true);
if (logger.isLoggable(Level.FINE))
logger.fine("useEhlo " + useEhlo + ", useAuth " + useAuth);
//设置默认的host和port
if (port == -1)
port = PropUtil.getIntProperty(props,
"mail." + name + ".port", -1);
if (port == -1)
port = defaultPort;
if (host == null || host.length() == 0)
host = "localhost";
boolean connected = false;
try {
//..........开启服务
boolean succeed = false;
if (useEhlo)
succeed = ehlo(getLocalHost());//收集服务器的扩展参数
if (!succeed)
helo(getLocalHost());//
//................
//即便useAuth是false,如果扩展列表的值为true,也会进行验证。这跟邮件服务器的设置有关
if ((useAuth || (user != null && password != null)) &&
(supportsExtension("AUTH") ||
supportsExtension("AUTH=LOGIN"))) {
if (logger.isLoggable(Level.FINE))
logger.fine("protocolConnect login" +
", host=" + host +
", user=" + traceUser(user) +
", password=" + tracePassword(password));
connected = authenticate(user, password); //校验用户名 和 授权码
return connected;
}
//..............异常捕获等代码
}
supportsExtension 方法,就是从extMap这个HashTable中获取数据,extMap中存放的数据是在ehlo方法中添加的
// private Hashtable<String, String> extMap;
public boolean supportsExtension(String ext) {
return extMap != null &&
extMap.get(ext.toUpperCase(Locale.ENGLISH)) != null;
}
与服务器进行连接时,会执行ehlo,获取服务器的服务扩展列表。
protected boolean ehlo(String domain) throws MessagingException {
String cmd;
if (domain != null)
cmd = "EHLO " + domain;
else
cmd = "EHLO";
sendCommand(cmd);
int resp = readServerResponse();
if (resp == 250) {
// extract the supported service extensions
BufferedReader rd =
new BufferedReader(new StringReader(lastServerResponse));
String line;
extMap = new Hashtable<>();
try {
boolean first = true;
while ((line = rd.readLine()) != null) {
if (first) { // skip first line which is the greeting
first = false;
continue;
}
if (line.length() < 5)
continue; // shouldn't happen
line = line.substring(4); // skip response code
int i = line.indexOf(' ');
String arg = "";
if (i > 0) {
arg = line.substring(i + 1);
line = line.substring(0, i);
}
if (logger.isLoggable(Level.FINE))
logger.fine("Found extension \"" +
line + "\", arg \"" + arg + "\"");
extMap.put(line.toUpperCase(Locale.ENGLISH), arg); //存储获取的扩展列表
}
} catch (IOException ex) { } // can't happen
}
return resp == 250;
}
3. com.sun.mail.smtp.SMTPSendFailedException: 553 authentication is required,163 smtp10,DsCowAAXDg8IyftbCG3bBw–.56925S2 1543227657
如果设置mail.smtp.auth为false,并且没有设置密码。
4. javax.mail.AuthenticationFailedException: failed to connect, no password specified?
设置mail.smtp.auth为true,但是没有设置用户名或者密码
下面是 javax.mail.Service中的部分代码
public synchronized void connect(String host, int port,
String user, String password) throws MessagingException {
//省略其他。。。.。。。。。。。。
if (!connected) {
if (authEx != null)
throw authEx;
else if (user == null)
throw new AuthenticationFailedException(
"failed to connect, no user name specified?");
else if (password == null)
throw new AuthenticationFailedException(
"failed to connect, no password specified?");
else
throw new AuthenticationFailedException("failed to connect");
}
}
5. com.sun.mail.smtp.SMTPSendFailedException: 553 Mail from must equal authorized user
553 Mail from must equal authorized user,邮件的发送者必须和认证的用户一致。
MimeMessage message = mailSender.createMimeMessage();
message .setFrom("666666@163.com"); //必须和设置的userName一致
MimeMessage mimeMessage=new MimeMessage ();
mimeMessage.setFrom( new InternetAddress("666666@163.com", "xxx"));
其他问题
1. 添加附件中文名乱码
MimeUtility.encodeWord(fileName)
2. 附件太大也可能会抛出异常
该异常又邮件服务器有关系,一般163,qq等邮件服务器提供的附件大小就能满足大部分情况。
我使用的运维人员搭建的服务器,附件最大只能1M也是笑了。