使用的依赖如下:
org.springframework.boot:spring-boot-starter-mail -> 2.2.1.RELEASE
该依赖下用于发送邮件的jar包如下:
org.springframework:spring-context-support:5.2.1.RELEASE
com.sun.mail:jakarta.mail:1.6.4
如果你的项目中引入了com.sun.mail:javax.mail:1.5.2及之前的版本,你会发现并不会出现附件中文乱码,当使用了com.sun.mail:javax.mail:1.5.3以上的版本或者使用jakarta.mail的版本,就会出现附件中文乱码。
注意:javax.mail最新版截至1.6.2,之后启用jakarta.mail,其版本从1.6.3开始
作者在网上找到了很多对该问题的解决方案的文章,但是都没有对该问题的出现原因进行分析。下面我就通过源码对该问题做个简单的解释,文章基于你已经完成了邮件的发送。
// 这里设置编码,如果没有设置,也会出现中文乱码,不过这个是最基本的
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
//这里添加附件,fileName文件名称,fileSystemResource为附件
messageHelper.addAttachment(fileName, fileSystemResource);
上面的代码是我们发送邮件添加附件的基本操作,问题就出现在添加附件时,我们进入源码
/**
* org.springframework.mail.javamail.MimeMessageHelper#addAttachment(java.lang.String, org.springframework.core.io.InputStreamSource)
*/
public void addAttachment(String attachmentFilename, InputStreamSource inputStreamSource)
throws MessagingException {
String contentType = getFileTypeMap().getContentType(attachmentFilename);
addAttachment(attachmentFilename, inputStreamSource, contentType);
}
public void addAttachment(
String attachmentFilename, InputStreamSource inputStreamSource, String contentType)
throws MessagingException {
...
DataSource dataSource = createDataSource(inputStreamSource, contentType, attachmentFilename);
addAttachment(attachmentFilename, dataSource);
}
public void addAttachment(String attachmentFilename, DataSource dataSource) throws MessagingException {
...
try {
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeBodyPart.setDisposition(MimeBodyPart.ATTACHMENT);
//问题就出现在这里
mimeBodyPart.setFileName(MimeUtility.encodeText(attachmentFilename));
mimeBodyPart.setDataHandler(new DataHandler(dataSource));
getRootMimeMultipart().addBodyPart(mimeBodyPart);
}
catch (UnsupportedEncodingException ex) {
throw new MessagingException("Failed to encode attachment filename", ex);
}
}
可以看到最终是在addAttachment方法中调用mimeBodyPart.setFileName来设计文件名称,参数是对输入的文件名进行编码。进入mimeBodyPart.setFileName
static void setFileName(MimePart part, String name)
throws MessagingException {
...
// Set the Content-Disposition "filename" parameter
String s = part.getHeader("Content-Disposition", null);
ContentDisposition cd =
new ContentDisposition(s == null ? Part.ATTACHMENT : s);
cd.setParameter("filename", name);
part.setHeader("Content-Disposition", cd.toString());
...
}
可以看到这里是在header中设置Content-Disposition参数,也就是真正设置文件名称的地方,最终调用了ContentDisposition的toString方法
private ParameterList list;
public String toString() {
if (primaryType == null || subType == null) // need both
return "";
StringBuffer sb = new StringBuffer();
sb.append(primaryType).append('/').append(subType);
if (list != null)
//在setFileName时 设置parameter,这里的list就是这个参数,可以理解list存储的就是文件名
//细节可以自己研究,这里不做深入,也就是最终调用list.toString方法
sb.append(list.toString(sb.length() + 14));
return sb.toString();
}
终于进入正题 ParameterList.toString方法,1.5.2版本之前如下:
public String toString(int used) {
ToStringBuffer sb = new ToStringBuffer(used);
Iterator e = list.keySet().iterator();
while (e.hasNext()) {
String name = (String)e.next();
Object v = list.get(name);
if (v instanceof MultiValue) {
MultiValue vv = (MultiValue)v;
String ns = name + "*";
for (int i = 0; i < vv.size(); i++) {
Object va = vv.get(i);
if (va instanceof Value)
sb.addNV(ns + i + "*", ((Value)va).encodedValue);
else
sb.addNV(ns + i, (String)va);
}
} else if (v instanceof Value)
sb.addNV(name + "*", ((Value)v).encodedValue);
else
//最终调用这里
sb.addNV(name, (String)v);
}
return sb.toString();
}
1.5.3版本以上
public String toString(int used) {
ToStringBuffer sb = new ToStringBuffer(used);
Iterator<Map.Entry<String, Object>> e = list.entrySet().iterator();
while (e.hasNext()) {
Map.Entry<String, Object> ent = e.next();
String name = ent.getKey();
String value;
Object v = ent.getValue();
if (v instanceof MultiValue) {
MultiValue vv = (MultiValue)v;
name += "*";
for (int i = 0; i < vv.size(); i++) {
Object va = vv.get(i);
String ns;
if (va instanceof Value) {
ns = name + i + "*";
value = ((Value)va).encodedValue;
} else {
ns = name + i;
value = (String)va;
}
sb.addNV(ns, quote(value));
}
} else if (v instanceof LiteralValue) {
value = ((LiteralValue)v).value;
sb.addNV(name, quote(value));
} else if (v instanceof Value) {
/*
* XXX - We could split the encoded value into multiple
* segments if it's too long, but that's more difficult.
*/
name += "*";
value = ((Value)v).encodedValue;
sb.addNV(name, quote(value));
} else {
value = (String)v;
if (value.length() > 60 &&
splitLongParameters && encodeParameters) {
int seg = 0;
name += "*";
while (value.length() > 60) {
sb.addNV(name + seg, quote(value.substring(0, 60)));
value = value.substring(60);
seg++;
}
if (value.length() > 0)
sb.addNV(name + seg, quote(value));
} else {
sb.addNV(name, quote(value));
}
}
}
return sb.toString();
}
对比两个版本可以发现,1.5.3版本之后多出了一段逻辑,在value.length() > 60 &&
splitLongParameters && encodeParameters 这个条件下会对文件名称做截断,导致文件名乱码,那么 splitLongParameters 和 encodeParameters 参数分别是由哪里控制的
private static final boolean encodeParameters =
PropUtil.getBooleanSystemProperty("mail.mime.encodeparameters", true);
private static final boolean splitLongParameters =
PropUtil.getBooleanSystemProperty(
"mail.mime.splitlongparameters", true);
看到这里,大家就明白,为什么设置 System.getProperties().setProperty(“mail.mime.splitlongparameters”, “false”); 可以解决中文乱码了,其实就是让文件名不要被截断,继续走以前的老逻辑。