使用IMAP协议实现邮件提醒

  
  其实这篇文章是不太想写的,因为我自己在平时貌似已经没有什么场景会用邮箱了。但是在比较正式的场合邮件还是”地位稳固”,比如公司内部还是会用企业邮箱,与客户发一些重要文件、资料等。邮件是比较正式的一个东西。
  之前再服务器上做过定时发邮件的功能,不过由于是在spring框架下做的,开发的时候其实很简单,几乎没遇到什么问题,甚至感觉自己没做什么功能就写完了….而至于收邮件,我是没有做过的。这几天写了个桌面插件,(点我去看看,记得点个星),在想要加点什么功能时,我就想到邮件来件提示。好了不多废话,开始了

协议

  首先要了解邮件的协议,SMTP,IMAP, POP3 是几个大家耳熟能详的的协议了。先简介一下吧,做邮件相关功能,肯定得至少了解一下的。

协议简介

SMTP

   Simple Mail Transfer Protocol. 简单邮件传输协议。 这是一个相对简单的基于文本的协议。 而我们主要用这个协议来发邮件
  它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。SMTP 服务器就是遵循 SMTP 协议的发送邮件服务器。
  SMTP 认证,简单地说就是要求必须在提供了账户名和密码之后才可以登录 SMTP 服务器,这就使得那些垃圾邮件的散播者无可乘之机。
  增加 SMTP 认证的目的是为了使用户避免受到垃圾邮件的侵扰。

POP3

   Post Office Protocol - Version 3. 邮局协议版本3。 我们用来收邮件
   POP3规定怎样将个人计算机连接到Internet的邮件服务器和下载电子邮件的电子协议。它是因特网电子邮件的第一个离线协议标准,POP3允许用户从服务器上把邮件存储到本地主机(即自己的计算机)上,同时删除保存在邮件服务器上的邮件,而POP3服务器则是遵循POP3协议的接收邮件服务器,用来接收电子邮件的。

IMAP

  Internet Mail Access Protocol. 交互式邮件存取协议。 我们用来收邮件
  IMAP是跟POP3类似邮件访问标准协议之一。不同的是,开启了IMAP后,您在电子邮件客户端收取的邮件仍然保留在服务器上,同时在客户端上的操作都会反馈到服务器上,如:删除邮件,标记已读等,服务器上的邮件也会做相应的动作。所以无论从浏览器登录邮箱或者客户端软件登录邮箱,看到的邮件以及状态都是一致的。

区别

  自己看上面3个协议的简介的话,其实已经可以看出来了,这里总结一下。
   SMTP 管发 , POP3/IMAP 管收。所以在想完整的实现一个邮件客户端的时候,一般是这样组合的 :SMTP+POP3 或 SMTP+IMAP。
  而IMAP 和POP3 都负责收件,具体的区别在于:

操作位置 操作内容 IMAP POP3
收件箱 阅读、标记、移动、删除邮件等 客户端与邮件更新同步 仅客户端内
发件箱 保存到已发送 客户端与邮件更新同步 仅客户端内
创建文件夹 新建自定义的文件夹 客户端与邮件更新同步 仅客户端内
草稿 保存草稿 客户端与邮件更新同步 仅客户端内
垃圾文件夹 接受误移入垃圾文件夹的邮件 支持 不支持
广告邮件 接受被移入广告文件夹的邮件 支持 不支持

  总之,IMAP 整体上为用户带来更为便捷和可靠的体验。POP3 更易丢失邮件或多次下载相同的邮件,但 IMAP 通过邮件客户端与webmail 之间的双向同步功能很好地避免了这些问题。

实现

目标

  我要实现什么样的功能?首先作为来件提示,最最重要的就是,在收到邮件后能否即时的通知。其次,并非仅仅监听一个邮箱罢了,作为一个客户端,需要的是能够自己配置想要监听的邮箱。一句话概括:可配置监听邮箱,即时通知

问题分析

  需要自定义配置,那么properties很好的可以实现。而即时这个事情其实就有点麻烦了,怎么才能即时?定点x时间去获取一下邮箱内的邮件量,如果收件箱中邮件数量变化,则判断有新邮件? 这是第一反应,但是当然不能这么做(其实也可以,但这是下下策):

  • 1,定点x时间轮询,并不是真正的即时。
  • 2,可以通过缩小x,提高即时感。但代价却是性能的开销,开发中需要避免无意义的轮询,能回调通知就不要轮询!!!
  • 3,也就是最重要的一点:在x时间段内,我收件箱删了一封邮件,又收到了一封新的。总数没变,怎么算?有人会杠,那就算一下垃圾箱里有没有多邮件呗? x时间内,收件箱多了5封,垃圾箱多了2封,其他邮件箱少了4封 …这要考虑的情况就更复杂了
      回到第二条,我想到了接受通知,在查阅了一些资料后,我了解到了IDLE。IDLE是RFC 2177中描述的一项IMAP功能,它允许客户端向服务器表明它已准备好接受实时通知。在IMAP4扩展协议中提供了这功能,于是乎,我在POP3与IMAP中选择了IMAP。但是并非所有的IMAP服务提供者都提供了这种功能,比较主流的gmail,qq邮箱,腾讯企业邮箱提供了这一服务。而sina免费邮箱,网易163邮箱没有提供…其余的邮箱自己有兴趣可以测试一下,在本文最后我会给出如何查看提供服务的方法。

代码

  说了这么多,不废话了,上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import javax.mail.Folder;
import javax.mail.FolderClosedException;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.event.ConnectionEvent;
import javax.mail.event.ConnectionListener;
import javax.mail.event.MessageCountAdapter;
import javax.mail.event.MessageCountEvent;

//下面2个是我自己的其他部分的功能,源码去看前面放出的桌面插件
import com.magic.ApiService;
import com.magic.SingleThreadPool;

import com.sun.mail.iap.ProtocolException;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IdleManager;
import com.sun.mail.imap.protocol.IMAPProtocol;


/**
* 所有mail实例的父级抽象类 统一使用imap 协议
* @author chenhaoyu
*/
public abstract class Mail implements MailInterface {
// handler 构造方法中直接传递
protected ApiService handler;

// 账号密码
protected String userName;
protected String passWord;

// 以下 session 和store 等 独立使用 后期自己初始化
protected Session session;
protected Store store;
protected IdleManager idleManager;
protected List<IMAPFolder> folderObjects;

public Mail(ApiService handler, String userName, String passWord, List<String> folders) {
this.handler = handler;
this.userName = userName;
this.passWord = passWord;

try {
initStore();
createFoldersListen(folders);
heartWork();
} catch (Exception e) {
e.printStackTrace();
handler.setModelText("监听" + getMailName() + "失败", 0);
}
}

// 初始化session store idleManager folderObjects
public void initStore() throws Exception {
String protocol = "imap";// 使用的协议
Properties props = getProperties();
session = Session.getInstance(props);
store = session.getStore(protocol);
// 方便调试 加上监听
store.addConnectionListener(new ConnectionListener() {
@Override
public void opened(ConnectionEvent e) {
System.out.println("邮箱" + getMailName() + " opened");
}
@Override
public void disconnected(ConnectionEvent e) {
System.out.println("邮箱" + getMailName() + " disconnected");
}
@Override
public void closed(ConnectionEvent e) {
System.out.println("邮箱" + getMailName() + " closed");
}
});
store.connect(userName, passWord);
idleManager = new IdleManager(session, SingleThreadPool.getInstance().threadPool());
folderObjects = new ArrayList<IMAPFolder>();
};

public abstract Properties getProperties();

public abstract String getMailName();

// 创建监听
public void createFoldersListen(List<String> folders) throws Exception {
for (String folderName : folders) {
IMAPFolder temp = (IMAPFolder) store.getFolder(folderName);
// 为了调试加上监听
temp.addConnectionListener(new ConnectionListener() {
@Override
public void opened(ConnectionEvent e) {
System.out.println("文件夹:" + getMailName() + folderName + " opened");
}
@Override
public void disconnected(ConnectionEvent e) {
System.out.println("文件夹:" + getMailName() + folderName + " disconnected");
}
@Override
public void closed(ConnectionEvent e) {
System.out.println("文件夹:" + getMailName() + folderName + " closed");
}
});

temp.open(Folder.READ_ONLY);// 在这一步,收件箱所有邮件将被下载到本地

// Message message = folder.getMessage(size);//取得最新的那个邮件
System.out.println("Size:" + temp.getMessageCount());
System.out.println("unread:" + temp.getUnreadMessageCount());
System.out.println("new:" + temp.getNewMessageCount());
System.out.println();

temp.addMessageCountListener(new MessageCountAdapter() {
@Override
public void messagesAdded(MessageCountEvent e) {
super.messagesAdded(e);
IMAPFolder folder = (IMAPFolder) e.getSource();
Message[] msgs = e.getMessages();
for (int i = 0; i < msgs.length; i++) {
try {
handler.setModelTextWithHoldTime(getMailName() + "有新邮件!标题:" + msgs[i].getSubject(), 0, 10);
System.out.println("新邮件主题" + msgs[i].getSubject());
} catch (MessagingException e1) {
e1.printStackTrace();
}
}
try {
idleManager.watch(folder);
} catch (MessagingException e2) {
e2.printStackTrace();
}
}
});
idleManager.watch(temp);
folderObjects.add(temp);
handler.setModelText(getMailName() + " " + folderName + " 已关注", 0);
}
}

/**
* 心跳
*/
public void heartWork() {
SingleThreadPool.getInstance().scheduledThreadPool().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "————" + getMailName());
for (IMAPFolder imapFolder : folderObjects) {
try {
imapFolder.doCommand(new IMAPFolder.ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol arg0) throws ProtocolException {
arg0.simpleCommand("NOOP", null);
return null;
}
});
} catch (FolderClosedException cfe) {
cfe.printStackTrace();
try {
if (!imapFolder.isOpen()) {
imapFolder.open(Folder.READ_ONLY);
}
} catch (MessagingException e1) {
e1.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
//一定要重新watch
try {
idleManager.watch(imapFolder);
} catch (MessagingException e) {
e.printStackTrace();
}
}
}
}, 9, 9, TimeUnit.MINUTES);
}

// 关闭 监听 文件夹 store
@Override
public void close() {
try {
if (idleManager != null && idleManager.isRunning()) {
idleManager.stop();
}
for (Folder folder : folderObjects) {
if (folder != null) {
folder.close(false);
}
}
if (store != null) {
store.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}

}


代码解释

  在写这个功能的时候,真的遇到了挺多坑。这里就分析一下吧,顺便讲讲那些坑 (代码行标 以上面的代码为基准)

为什么用抽象类

   考虑到会适配多个邮箱,那么其实大部分邮箱的功能是一样的,不同的仅仅是邮箱的账号密码,服务器地址,端口等。作为客户端,账户密码肯定来源用户,那么我就从外部获取。而邮箱服务器配置写个子类实现一下抽象父类的钩子就可以了。比如实现qq邮箱就像下面,是不是很方便呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class QQMailPro extends Mail {

public QQMailPro(ApiService handler, String userName, String passWord, List<String> folders) {
super(handler, userName, passWord, folders);
}

@Override
public Properties getProperties() {
String host = "imap.qq.com";// QQ邮箱的imap服务器
int port = 993;// 端口
Properties props = new Properties();
props.put("mail.imap.ssl.enable", "true");
props.put("mail.imap.host", host);
props.put("mail.imap.port", port);
props.put("mail.transport.protocol", "imap");
props.put("mail.imap.usesocketchannels", "true");
return props;
}

@Override
public String getMailName() {
return "QQ邮箱";
}
}

handshake_failure 是什么问题?

   我用的是mac os,jdk是1.8.0_121。 第一次运行的时候 直接就甩了这个异常给我。一脸懵,几经波折,好像是ssl上的问题,java的一个bug,需要在官方下载2个jar包替换。相对的jre运行的时候,如果jre版本较早的话,也会出这个问题。反正用最新的jre就不会有事了。

64行 获得session的实例

  这个问题一开始没有遇到,当时只适配了一个qq邮箱,而在我适配多个邮箱之后,就出现了一些问题;

1
session = Session.getDefaultInstance(props);

  这是我一开始用的方法,在获得多个session实例情况下,后获取的实例连接新邮箱时,会随着邮件服务商不同报出各种问题,于是下载了源码看了看(javax.mail.Session),好家伙,原来有2个方法 getDefaultInstance得到的始终是该方法初次创建的缺省的对象,而getInstance得到的始终是新的对象。所以之前一只获得了相同的对象,而用了不同的邮件账号密码去登录,当然会出错了…

118行 增加事件监听

  一开始其实我并不知道idle这个东西,folder的方法中直接就有很多addListener,那感觉就很简单,直接加监听就完事了呗~

1
2
3
4
5
6
7
public synchronized void addMessageCountListener(MessageCountListener l);

public synchronized void addConnectionListener(ConnectionListener l);

public synchronized void addFolderListener(FolderListener l);

public synchronized void addMessageChangedListener(MessageChangedListener l);

  第一个是监听文件夹内邮件增减的监听器,里面监听事件有2个 messagesAdded,messagesRemoved,而且这个监听是IMAPFolder类独有的,这很明显了,我们就该使用这个监听。
  顺便说一下,下面3个监听都是IMAPFolder的父类 Folder有的方法。第二个是监听连接事件,比如连接通道的open,失去连接disconnect,关闭连接closed。第三个是文件夹状态的监听,比如文件夹的创建,删除,重命名。第四个是邮件变化的监听,监听的是MessageChangedEvent 这事件对象,对象中用type具体分类不同的变化。有兴趣的可以自己下载源码看看,这里不多展开。
  当我乐呵呵的添加了MessageCountListener之后,赶紧发了一封邮件试试。果然没有收到…. 为什么呢,明明添加了监听,缺没有收到通知。于是开始查阅资料…原来需要用imap idle的支持 才能接收到服务器的通知…

IdleManager的使用

  接上文,有了头绪就好做很多了。几经查阅了解到,在JavaMail 1.5.2. 版本时,新推出了一个IdleManager。听名字就知道是用来管理idle的,一般来说,用官方推出的manager管理总比自己写好一些,比如线程 和 线程池…下面列出IdleManager中public的方法,对于内部的一些实现有兴趣的可以看看,这边讲一下使用点到为止。

1
2
3
4
5
6
7
public IdleManager(Session session, Executor es) throws IOException;

public boolean isRunning();

public void watch(Folder folder) throws MessagingException;

public synchronized void stop();

  第一个是IdleManager的构造方法,第一个参数是邮件会话的session实例,就是上文提到过的那个。说白点就是 你要关注的那个邮箱连接。第二个参数是一个线程池。盲猜一下,应该是用来执行监听任务分配线程的。
  第二个方法和第四个一起说,太见名知意了。isRunning毫无疑问就是看看现在这个IdleManager对象有没有的运行,而stop就是停止当前对象。需要注意的一点是,在IdleManager调用stop()方法停止后,是无法重启的。
  第三个watch就是关键了,接受的参数就是你需要关注(监听)的那个收件夹。而且需要注意: Watch the Folder for new messages and other events using the IMAP IDLE command.这个是源码方法上的注释,最初不知道,后来下载源码看了才知道,给我带来了惨痛的后果,之后会说。
  当用IdleManager关注你的folder之后,就可以收到邮件了。然而比较有趣的是:只能收到一封,后面又收不到了。带着操蛋的心情我只能又去看看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Watch the Folder for new messages and other events using the IMAP IDLE
* command.
*
* @param folder the folder to watch
* @exception MessagingException for errors related to the folder
*/
public void watch(Folder folder) throws MessagingException {
if (die) // XXX - should be IllegalStateException?
throw new MessagingException("IdleManager is not running");
if (!(folder instanceof IMAPFolder))
throw new MessagingException("Can only watch IMAP folders");
IMAPFolder ifolder = (IMAPFolder)folder;
SocketChannel sc = ifolder.getChannel();
if (sc == null) {
if (folder.isOpen())
throw new MessagingException("Folder is not using SocketChannels");
else
throw new MessagingException("Folder is not open");
}
if (logger.isLoggable(Level.FINEST))
logger.log(Level.FINEST, "IdleManager watching {0}",folderName(ifolder));
// keep trying to start the IDLE command until we're successful.
// may block if we're in the middle of aborting an IDLE command.
int tries = 0;
while (!ifolder.startIdle(this)) {
if (logger.isLoggable(Level.FINEST))
logger.log(Level.FINEST, "IdleManager.watch startIdle failed for {0}", folderName(ifolder));
tries++;
}
if (logger.isLoggable(Level.FINEST)) {
if (tries > 0)
logger.log(Level.FINEST,
"IdleManager.watch startIdle succeeded for {0}" +
" after " + tries + " tries",
folderName(ifolder));
else
logger.log(Level.FINEST,
"IdleManager.watch startIdle succeeded for {0}",
folderName(ifolder));
}
synchronized (this) {
toWatch.add(ifolder);
selector.wakeup();
}
}

  接收到的folder,在13行 被强转了 IMAPFolder ifolder = (IMAPFolder)folder; 然后一路看到最后,跳过大部分的抛异常,打log。看到比较关键的地方toWatch.add(ifolder); 他把ifolder添加到了toWatch中,这toWatch又是啥?

1
private Queue<IMAPFolder> toWatch = new ConcurrentLinkedQueue<>();

  原来是内部私有的一个队列…这问题就很显然了,这么一个流程:一开始open了folder,然后IdleManager.watch了folder,folder被转成IMAPFolder加入队列,收到邮件任务被执行,队列里的任务出队列,队列里没有了任务…说的通俗一点就是 watch方法是个消耗品,触发过监听回调的就不在监听了。于是乎在监听事件中又继续加上watch(代码133行),发了几封测试一下,解决问题。

心跳大坑

  这个问题是折腾了我很久的一个问题,其实有几个问题交织在一起,但是他们产生的结果的现象是一样的。
  现在确实可以即时的收到邮件,但是!但是!并不能持续长久,过了一会就失效了。作为写过很多socket的人想都不用想,心跳嘛~ easy~。啥是心跳,自己百度。虽说心跳随便发点什么都行(一般来说发空就行),但是也不能乱发啊,万一服务器接收到错误格式的数据直接把你咔嚓了也是有可能的(以前就遇到过一个服务器心跳只接受 heart,多发几次乱七八糟的字符直接暴毙…)
  有过翻车经验的我就直接去找imap协议了,网上很多一看就知…NOOP命令:NOOP命令什么也不做,用来向服务器发送自动命令,防止因长时间处于不活动状态而导致连接中断,服务器对该命令的响应始终为肯定。无参数。
  于是只要定时给服务器 发noop就可以了 (关键代码155-161,为了看起来方便,下方再贴一下)

1
2
3
4
5
6
7
ImapFolder.doCommand(new IMAPFolder.ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol arg0) throws ProtocolException {
arg0.simpleCommand("NOOP", null);
return null;
}
});

  写完心跳我测试了一下,没有成功….这里开始就坑了,测起来太麻烦了,因为一般保持连接默认是30分钟,所以很无奈,只能30分钟测一次。心跳发出去了为什么还是断开了连接?于是我在代码中给所有store和folder和添加了连接监听,开着log挂机测试…结果是啥?并没有断开连接,也就是说,连接还在,但是监听失效?
  为了定位问题,我把心跳发送时间间隔缩短至30秒。测试下来,30秒前可以正常收到监听,在心跳出去之后,无法收到…wtf?啥玩意啊?咋回事啊?那咋整啊?心跳本身让监听失效,开什么玩笑?我就发了个noop啊!我已经忘记我是怎么注意到这个问题的关键点了,只能说是灵光一现吧….
  记得上面说的那个惨痛的后果么,就是这里了。Watch the Folder for new messages and other events using the IMAP IDLE command。在偶然看源代码的时候注意到了…这个是IdleManager的watch方法的注释。翻译一下:使用imap idle命令监视文件夹中的新邮件和其他事件。 新邮件和其他事件。其他事件!!! 所以说我tmd那个noop也是其他事件?结合上一节发现的问题,watch是消耗品。于是推测心跳的command动作让watch被消耗掉了,这一后果就是 连接 还在,但是监听断了。就是这个现象!!于是在ImapFolder.doCommand动作后补上folder的watch(175-179行)。

  • 测试noop心跳问题:启动,发送noop,发测试件,收到监听,成功!
  • 再测试心跳连接:启动,挂机1小时,发测试件,收到监听,成功!

  以为这就完了?并没有!!!在实际使用中,发现过了很长一段时间后,收件提示失效….我已经欲哭无泪了。难道超时时间不是半小时?我的心跳其实压根没用?在几经怀疑人生的情况下,我开始了对照测试:同时启2份代码,一个有9分钟一次的心跳(gmail的超时时间是10分钟,所以心跳设置了9分钟) ;另一个没有心跳,用qq邮箱测试。在过了40分钟后,有心跳的哪一个可以收到通知,而没有心跳的收不到。这不是没问题么?心跳确实生效了啊!
  于是又想到了一个问题,线程 与 os休眠的问题。想象以下场景: 我与服务器的超时时间是 10分钟,我每隔9分钟执行一次心跳任务保持连接。在5分钟的时候,电脑休眠了,这时候所有线程被快照保存下来。过了20分钟,电脑恢复使用,线程重新被唤醒,线程以为只过了5分钟继续从5分钟开始走,(这时候由于25分钟没有心跳,连接肯定是断开了。)又过了4分钟,任务线程9分钟被触发发送心跳,但是实际时间已经过了29分钟了,已经断开了。
  这个问题我目前不知道怎么解决…但是我得排除一下试试。心如死灰的关闭电脑休眠功能,然后测试,过了很久很久,邮件监听失效,这反而让我有一些高兴,暂且排除os休眠产生的问题(其实我也不确定那样到底会不会出这个问题,但是等遇到了再说吧,心太累了)于是乎开启调试模式,所有监听全开,异常全部打出,挂机….终于!!!在2个小时左右 抛出了异常!

1
javax.mail.FolderClosedException: * BYE JavaMail Exception: java.io.IOException: **Connection dropped by server?**

  嗯??啥 bye?Connection dropped by server? 最后这个问号啥意思,到底是不是by server 倒是说清楚啊….同时我的folder closed监听被触发。注意是folder 不是store,也就说 连接还在,但是folder被关闭了,为什么会这样 不知道…没办法 只能google一下。最后找到了相关资料,一样的问题,不能根本上解决,只能写一个补偿吧…捕获异常,异常关闭后再次打开即可。注意不能再监听的closed事件中重新打开,不然在正常关闭的时候有你受的… 补偿代码(162-171行)

1
2
3
4
5
6
7
8
9
10
11
12
try{
.....
}catch (FolderClosedException cfe) {
cfe.printStackTrace();
try {
if (!imapFolder.isOpen()) {
imapFolder.open(Folder.READ_ONLY);
}
} catch (MessagingException e1) {
e1.printStackTrace();
}
}

2019年06月12日补充
  后来仔细想了想,其实在close监听事件中重新打开也没有什么问题。额外设定一个flag标记,当true的时候需要重新打开文件夹,而认为手动关闭的时候将这个flag设false就好了。这样做可以应对一些未知问题引起的异常关闭。

附件 查看IMAP服务器提供的服务

  主要其实可以看IMAP4 RFC3501协议,这边就说一下怎么看邮件服务器提供的服务,这里用qq邮箱为例,首先打开shell,用的是telnet

telnet连接邮件服务器

  qq邮箱的服务地址是imap.qq.com,imap端口143

1
2
3
$ telnet imap.qq.com 143

* OK [CAPABILITY IMAP4 IMAP4rev1 ID AUTH=LOGIN NAMESPACE] QQMail IMAP4Server ready

  当你看到 * OK [CAPABILITY IMAP4 IMAP4rev1 ID AUTH=LOGIN NAMESPACE] QQMail IMAP4Server ready 这条说明成功

登录

  格式:$ login 账号 密码 ,有些邮箱用的是授权码,比如这里的qq就是。所以像下面那样

1
2
3
$ A01 login xxxxx@qq.com zxcvbasdfg

A01 OK Success login ok

  当你看到 A01 OK Success login ok ,说明登录成功

查看提供的服务

  使用 $ A02 CAPABILITY 即可查看

1
2
3
4
 $ A02 CAPABILITY

* CAPABILITY IMAP4 IMAP4rev1 XLIST MOVE IDLE XAPPLEPUSHSERVICE NAMESPACE CHILDREN ID UIDPLUS
a02 OK CAPABILITY Completed

  上面是qq邮箱返回的信息,我们可以看到支持IDLE。
  有人会问 这个a01 a02是什么东西,不打怎么就报错了,其实你看了协议就知道了。imap的命令以一个 标记tag 开头,可以是a01 a02 等,服务器的应答是对应该tag的处理结果(OK / NO)详情就作为拓展自己看协议吧。

后记

  写完之后就好像把坑又复习了一遍,看起来一下子就知道了,但中间踩坑排查的过程真的是….刺激…
  本文的源码在这里,不要脸的求个star,(*/ω\*)