标签归档:javamail

javamail接收时邮件标题乱码解决方案

通过javamail接收邮件时,获取到Message后,通过Message#getSubject()获取邮件标题时出现中文乱码,如:[�求�� 5068761788:�I家已收到退款],这种情况只有在对方标题内容有繁体字,而且使用的编码是GB2312时就出现,把编码改为GBK后显示正常,这种情况是GB2312没有收录这部分繁体字符而无法显示导致的。

GB2312(1980年)共7445个字符,GBK(1995年)共21886个字符,GBK字符编码向上兼容GB2312,这部分无法显示的繁体字符在GBK收录的范围,因此GBK能正常显示。

查看Email的源码可以看到类似 Subject: =?GB2312?B?1tA=?= 的字符,其中第一个 ?GB2312?是指字符编码,第二个?B?,表示内容的编码方式,如果是B则表示Base64编码,如果是“Q”则代表 Quoted-Printable编码。

javamail在Message#getSubject()的方法中,通过MimeUtility#decodeText()对邮件消息的标题进行解码 ,通过MimeUtility#javaCharset(String charset)这个方法获取真正的字符集编码,代码如下:


public static String javaCharset(String charset) {
 if (mime2java == null || charset == null)
 // no mapping table, or charset parameter is null
 return charset;

String alias =
 (String)mime2java.get(charset.toLowerCase(Locale.ENGLISH));
 return alias == null ? charset : alias;
 }

可以看到最终会通过mime2java这个对象获取编码,mimi2java是一个Hashtable对象,在类加载的时候进行初始化,代码如下:


InputStream is =
 javax.mail.internet.MimeUtility.class.getResourceAsStream(
 "/META-INF/javamail.charset.map");

if (is != null) {
 try {
 is = new LineInputStream(is);

// Load the JDK-to-MIME charset mapping table
 loadMappings((LineInputStream)is, java2mime);

// Load the MIME-to-JDK charset mapping table
 loadMappings((LineInputStream)is, mime2java);

if (java2mime.isEmpty()) {

...

}

if (mime2java.isEmpty()) {

mime2java.put("iso-2022-cn", "ISO2022CN");

......

}

程序会先在”/META-INF/javamail.charset.map”这个文件中加载字符的映射关系,如果没有则添加默认值。到这里已经找到问题的解决办法了。

在项目下添加”/META-INF/javamail.charset.map”这个文件,文件内容为默认的映射关系+GB2312->GBK的映射关系,让程序通过GBK去解码全部GB2312的内容:


---------mimi2java(required)--------------
iso-2022-cn ISO2022CN
iso-2022-kr ISO2022KR
utf-8 UTF8
utf8 UTF8
ja_jp.iso2022-7 ISO2022JP
ja_jp.eucjp EUCJIS
euc-kr KSC5601
euckr KSC5601
us-ascii ISO-8859-1
x-us-ascii ISO-8859-1
gb2312 GBK

ps:这里第一行带有“——“的内容必须有,否则系统会把这些内容赋值给java2mime对象,而不对赋值给mime2java对象,程序是通过”–”标识或者空行来分割的.

每行内容必须用一个tab分开,因为javamail是通过”\t”来分割key和value的.源代码如下:


while (true) {

... ...

if (currLine == null) // end of file, stop
 break;
 if (currLine.startsWith("--") && currLine.endsWith("--"))
 // end of this table
 break;

... ...

StringTokenizer tk = new StringTokenizer(currLine, " \t");

... ...

}

javamail通过代理服务器发送邮件

这段时间在写一个邮件系统,因为可能有很多帐号,需要使用不同的代理服务器发送,在网上查了很多资料,很多demo基本上都是没用,真不知道哪些家伙刷那么多垃圾帖干嘛,自己都没测试就贴出来,浪费老子时间,真想扇他们几个耳刮子。

不想浪费在这海量垃圾资料的查阅中,只能自己动手丰衣足食了。通过跟了大半天的源码终于成功了。

javamail只有1.4.5及以上版本才支持代理发送,而且支持socks代理,http代理不支持,网上有很多类似的代码如:


System.getProperties().put("http.proxySet","true");
System.getProperties().put("http.proxyHost","127.0.0.1");
System.getProperties().put("http.proxyPort","8098");

这些我尝试都不成功,JAVAMAIL API FAQ中也有提到:Note that setting these properties directs all TCP sockets to the SOCKS proxy, which may have negative impact on other aspects of your application.这个会代理所有的tcp socket,可能会影响到其他程序,而且不能做得每个帐号不同的代理,也会有并发的问题,显然这不是我想要的。

JAVAMAIL API FAQ提到了通过设置:mail.smtp.socks.host属性来做到session级别的代理,但是经过尝试还是没有成功,代码如下:


properties.put("mail.smtp.socks.host","127.0.0.1");
properties.put("mail.smtp.socks.port","8098");

通过一步一步代码跟踪,发现在 com.sun.mail.util.SocketFetcher#createSocket(InetAddress localaddr, int localport,String host, int port, int cto, int to,Properties props, String prefix,SocketFactory sf, boolean useSSL) 这个方法中开始出现了使用socks的代码:


String socksHost = props.getProperty(prefix + ".socks.host", null);

调用这个方法的代码:


serverSocket = SocketFetcher.getSocket(host, port,
props, "mail." + name, isSSL);

可以知道,prefix为”mail.”+name

name在SMTPSSLTransport的构造方法中定义,值为“smtps”,代码如下:


public SMTPSSLTransport(Session session, URLName urlname) {
 super(session, urlname, "smtps", true);
 }

这里调用SMTPSSLTransport的构造方法是由配置:

 properties.put("mail.transport.protocol", "smtps");

来决定的;

在session初始化的时候:

mailSession = Session.getInstance(properties);

会调用 Session#loadProviders 方法


addProvider(new Provider(Provider.Type.TRANSPORT,
 "smtps", "com.sun.mail.smtp.SMTPSSLTransport",
 "Sun Microsystems, Inc.", Version.version));

把相应的Provider初始化好,在获取Transport的时候会根据配置获取相应的Provider, Session#getTransport:

getTransport(getProperty("mail.transport.protocol"))

因此这里的设置代理的时候name为stmps而不是stmp:

properties.put("mail.smtps.socks.host","127.0.0.1");
properties.put("mail.smtps.socks.port","8098");

这样设置后虽然能正确获取host和端口了,但是最终还是没有使用代理,继续跟踪发现socket在这里创建:


if (sf != null)
 socket = sf.createSocket();
if (socket == null) {
 ......
 // get & invoke the getSocket(host, port) method
 Method mthGetSocket = proxySupport.getMethod("getSocket",
 new Class[] { String.class, int.class });
 socket = (Socket)mthGetSocket.invoke(new Object(),
 ......
 }

这个sf即配置的socketFactory,具体代码在方法:SocketFetcher#getSocket(String host, int port, Properties props,String prefix, boolean useSSL) 中,

因此只有不配置socketFactory就会使用配置的代理去生成socket;

于是乎把socketFactory相关配置,如:

 properties.put("mail.imaps.ssl.socketFactory.class", "javax.net.ssl.SSLSocketFactory");

去掉,再运行,完美运行。

关于测试是否使用了代理发送,在收到邮件后,在foxmail中右键邮件->更多操作->查看邮件源码:


Return-Path: <xxxx@gmail.com>
Received: from xxxx  ([my proxy ip])

这里my proxy ip就是代理的ip了;