关于JavaMail发送邮件的技术分享
代码中发送邮件的需求我们可能会经常遇到,这里记录并分享一些在发送邮件时遇到的问题;
JavaMail附件乱码问题
在发送携带附件的邮件时,部分类型邮箱收到的附件名会出现乱码问题如:
部分类型邮箱(如163邮箱)收到的附件名又会变成“AT XXX”如:
甚至有些附件收到之后连后缀名都变了;
追踪源码
接下来我们来看一下JavaMail发送邮件的源码;既然是附件名称的问题我们就先点进添加附件的方法看一下:
public void addAttachment(String attachmentFilename, DataSource dataSource) throws MessagingException {
Assert.notNull(attachmentFilename, "Attachment filename must not be null");
Assert.notNull(dataSource, "DataSource must not be null");
try {
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeBodyPart.setDisposition("attachment");
mimeBodyPart.setFileName(MimeUtility.encodeText(attachmentFilename));
mimeBodyPart.setDataHandler(new DataHandler(dataSource));
this.getRootMimeMultipart().addBodyPart(mimeBodyPart);
} catch (UnsupportedEncodingException var4) {
throw new MessagingException("Failed to encode attachment filename", var4);
}
}
可以看到,这里对文件名称做了两件事:
-
对文件名称进行转码;
-
将文件名称赋值给MimeBodyPart对象;
所以当我们点进mimeBodyPart.setFileName()方法时拿到的name已经是转码之后的值:
此时它的长度已经是72,这将影响之后的操作;而我们原始的文件名长度只有28;
static void setFileName(MimePart part, String name) throws MessagingException {
//前略
...
ContentDisposition cd = new ContentDisposition(s == null ? "attachment" : s);
String charset = MimeUtility.getDefaultMIMECharset();
ParameterList p = cd.getParameterList();
if (p == null) {
p = new ParameterList();
cd.setParameterList(p);
}
if (encodeFileName) {
p.setLiteral("filename", name);
} else {
p.set("filename", name, charset);
}
part.setHeader("Content-Disposition", cd.toString());
//后略
...
}
这里我们可以看到,它是将文件名设置给了ParameterList这个对象,而在其内部有一个map;这里的set其底层实际上是将其put进入了这个map:
到这里我们就知道它将文件名称存入哪里了;
它接下来调用了part.setHeader("Content-Disposition", cd.toString());
将整个ContentDisposition对象toString并放入了part的header当中;问题就出现在这里;ContentDisposition对象重写了toString方法,我们点进cd.toString():
//class ContentDisposition
public String toString() {
if (this.disposition == null) {
return "";
} else if (this.list == null) {
return this.disposition;
} else {
StringBuilder sb = new StringBuilder(this.disposition);
sb.append(this.list.toString(sb.length() + 21));
return sb.toString();
}
}
可以看到它这里是调用了this.list.toString(int used)方法,而它的list属性正是ParameterList对象;点进这个toString方法可以看到这样一段代码:
这里的意思就是如果value的长度大于60并且splitLongParameters
和encodeParameters
都为true的时候,则会将value分割成多个长度不超过60的字符;而splitLongParameters
和encodeParameters
的值如下:
可以看到他们的默认值都是true;所以我们在不进行设置的情况下,转码之后的名称很可能会超过60,则就会被分割;而part的header中存的值就是分割之后的值:
由于不同的邮件服务商对attachment解析的兼容度不一致,有些邮箱可以兼容这种写法,而有些邮箱则不兼容,就会导致部分邮箱在识别名文件名称时出现各种各样的错误,像163邮箱就不兼容,但是他们如果解析不了,则会使用自己生成的附件名去代替;就像我们上面见到的"ATXXX"等。
解决办法
我们上面看到它在分割文件名称时会判断两个值是否为true,即splitLongParameters
和encodeParameters
:
private static final boolean encodeParameters = PropUtil.getBooleanSystemProperty("mail.mime.encodeparameters", true);
private static final boolean splitLongParameters = PropUtil.getBooleanSystemProperty("mail.mime.splitlongparameters", true);
可以看到这两个值是通过PropUtil.getBooleanSystemProperty()
方法取值,并且都传了一个默认值;
而PropUtil.getBooleanSystemProperty()
方法则是从System.getProperties()
中获取,如果获取不到则会返回默认值:
public static boolean getBooleanSystemProperty(String name, boolean def) {
try {
return getBoolean(getProp(System.getProperties(), name), def);
} catch (SecurityException var4) {
try {
String value = System.getProperty(name);
if (value == null) {
return def;
} else if (def) {
return !value.equalsIgnoreCase("false");
} else {
return value.equalsIgnoreCase("true");
}
} catch (SecurityException var3) {
return def;
}
}
}
所以我们可以通过下列代码来阻止它对文件名进行分割;
System.getProperties().setProperty("mail.mime.splitlongparameters", "false");