JavaMail发送邮件,附件中文乱码原因解析及解决方案

背景

使用的依赖如下:
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”); 可以解决中文乱码了,其实就是让文件名不要被截断,继续走以前的老逻辑。

你可能感兴趣的:(java,spring,spring,boot)