com.sun.mail.imap.IdleManager.2 Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2014 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.mail.imap;
import java.io.IOException;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;
import java.util.logging.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import javax.mail.*;
import javax.mail.event.*;
import com.sun.mail.imap.protocol.IMAPProtocol;
import com.sun.mail.util.MailLogger;
/**
* The IdleManager uses the optional IMAP IDLE command
* (RFC 2177)
* to watch multiple folders for new messages.
* First, create a Thread and start the IdleManager running in the Thread.
* For example, for a Java SE application:
*
* final IdleManager idleManager = new IdleManager(session);
* Thread idleThread = new Thread(idleManager);
* idleThread.setDaemon(true);
* idlethread.start();
*
* To watch for new messages in a folder, open the folder, register a listener,
* and ask the IdleManager to watch the folder:
*
* Folder folder = store.getFolder("INBOX");
* folder.open(Folder.READ_WRITE);
* folder.addMessageCountListener
* folder.addMessageCountListener(new MessageCountAdapter() {
* public void messagesAdded(MessageCountEvent ev) {
* Folder folder = (Folder)ev.getSource();
* Message[] msgs = ev.getMessages();
* System.out.println("Folder: " + folder +
* " got " + msgs.length + " new messages");
* // process new messages
* idleManager.watch(folder); // keep watching for new messages
* }
* });
* idleManager.watch(folder);
*
* Note that, after processing new messages in your listener, or doing any
* other operations on the folder in any other thread, you need to tell
* the IdleManager to watch for more new messages. Unless, of course, you
* close the folder.
*
* The IdleManager is created with a Session, which it uses only to control
* debug output. A single IdleManager instance can watch multiple Folders
* from multiple Stores and multiple Sessions.
*
* Due to limitations in the Java SE nio support, a
* {@link java.nio.channels.SocketChannel SocketChannel} must be used instead
* of a {@link java.net.Socket Socket} to connect to the server. However,
* SocketChannels don't support all the features of Sockets, such as connecting
* through a SOCKS proxy server. SocketChannels also don't support
* simultaneous read and write, which means that the
* {@link com.sun.mail.imap.IMAPFolder#idle idle} method can't be used if
* SocketChannels are being used; use this IdleManager instead.
* To enable support for SocketChannels instead of Sockets, set the
* mail.imap.usesocketchannels
property in the Session used to
* access the IMAP Folder.
*/
/*
* XXX - TODO
* This reduces the number of threads per folder from two to one.
* The one thread is the event thread created per folder to deliver events.
* Should consider a different IMAP-specific notification method that works
* synchronously using a listener registered on this class.
*/
public class IdleManager implements Runnable {
private ExecutorService es;
private Selector selector;
private MailLogger logger;
private volatile boolean die = false;
private Queue toWatch = new ConcurrentLinkedQueue();
private Queue toAbort = new ConcurrentLinkedQueue();
private Queue events = new ConcurrentLinkedQueue();
private boolean eventDispatcherStarted = false;
private Runnable eventDispatcher;
/**
* Create an IdleManager. The Session is used only to configure
* debugging output.
*/
public IdleManager(Session session, ExecutorService es) throws IOException {
logger = new MailLogger(this.getClass(), "DEBUG IMAP", session);
this.es = es;
selector = Selector.open();
eventDispatcher = new Runnable() {
public void run() {
dispatchEvents();
}
};
es.submit(this);
}
/**
* Create an IdleManager. The Session is used only to configure
* debugging output.
*/
public IdleManager(Session session) throws IOException {
logger = new MailLogger(this.getClass(), "DEBUG IMAP", session);
selector = Selector.open();
}
/**
* Watch the Folder for new messages and other events using the IMAP IDLE
* command.
*/
public synchronized void watch(Folder f)
throws IOException, MessagingException {
if (!(f instanceof IMAPFolder))
throw new MessagingException("Can only watch IMAP folders");
IMAPFolder folder = (IMAPFolder)f;
SocketChannel sc = folder.getChannel();
if (sc == null)
throw new MessagingException("Folder is not using SocketChannels");
logger.log(Level.FINEST, "IdleManager watching {0}", folder);
folder.startIdle(this);
toWatch.add(folder);
selector.wakeup();
}
// MessageCount listeners
private List messageCountListeners =
new ArrayList();
/**
* Add a listener for MessageCount events.
*
* @param l the Listener for MessageCount events
* @see javax.mail.event.MessageCountEvent
*/
public synchronized void addMessageCountListener(MessageCountListener l) {
messageCountListeners.add(l);
}
/**
* Remove a MessageCount listener.
*
* @param l the listener
* @see #addMessageCountListener
*/
public synchronized void
removeMessageCountListener(MessageCountListener l) {
messageCountListeners.remove(l);
}
// MessageChanged listeners.
private List messageChangedListeners =
new ArrayList();
/**
* Add a listener for MessageChanged events.
*
* @param l the Listener for MessageChanged events
* @see javax.mail.event.MessageChangedEvent
*/
public synchronized void
addMessageChangedListener(MessageChangedListener l) {
messageChangedListeners.add(l);
}
/**
* Remove a MessageChanged listener.
*
* @param l the listener
* @see #addMessageChangedListener
*/
public synchronized void
removeMessageChangedListener(MessageChangedListener l) {
messageChangedListeners.remove(l);
}
/**
* Request that the specified folder abort an IDLE command.
* We can't do the abort directly because the DONE message needs
* to be sent through the (potentially) SSL socket, which means
* we need to be in blocking I/O mode. We can only switch to
* blocking I/O mode when not selecting, so wake up the selector,
* which will process this request when it wakes up.
*/
synchronized void requestAbort(IMAPFolder folder) {
toAbort.add(folder);
selector.wakeup();
}
/**
* Run the {@link java.nio.channels.Selector#select select} loop
* to poll each watched folder for events sent from the server.
*/
//@Override
public void run() {
die = false;
try {
while (!die) {
watchAll();
logger.finest("IdleManager waiting...");
int ns = selector.select();
if (logger.isLoggable(Level.FINEST))
logger.log(Level.FINEST,
"IdleManager selected {0} channels", ns);
if (die)
break;
/*
* Process any selected folders. We cancel the
* selection key for any selected folder, so if we
* need to continue watching that folder it's added
* to the toWatch list again. We can't actually
* register that folder again until the previous
* selectionkey is cancelled, so we call selectNow()
* just for the side effect of cancelling the selection
* keys. But if selectNow() selects something, we
* process it before adding folders from the toWatch
* queue. And so on until there is nothing to do, at
* which point it's safe to register folders from the
* toWatch queue.
*/
while (processKeys() && selector.selectNow() > 0)
;
}
} catch (IOException ex) {
logger.log(Level.FINE, "IdleManager got exception", ex);
} finally {
try {
unwatchAll();
selector.close();
} catch (IOException ex2) {
// nothing to do...
}
}
logger.fine("IdleManager exiting");
}
/**
* Register all of the folders in the queue with the selector,
* switching them to nonblocking I/O mode first.
*/
private void watchAll() {
try {
/*
* Pull each of the folders from the toWatch queue
* and register it. The only way to tell that we've
* emptied the queue is to get the exception. We
* can't check if the queue is empty before pulling
* items from the queue because that introduces a race
* between the two operations.
*/
for (;;) {
IMAPFolder folder = toWatch.remove();
logger.log(Level.FINEST,
"IdleManager adding {0} to selector", folder);
SocketChannel sc = folder.getChannel();
if (sc == null)
continue;
try {
// has to be non-blocking to select
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ, folder);
} catch (IOException ex) {
// oh well, nothing to do
logger.log(Level.FINEST,
"IdleManager can't register folder", ex);
}
}
} catch (NoSuchElementException ex) {
// no more elements in the queue
}
}
/**
* Process the selected keys, returning true if any folders have
* been added to the watch list.
*/
private boolean processKeys() throws IOException {
boolean more = false;
/*
* First, process any folders that we need to abort.
*/
try {
for (;;) {
IMAPFolder folder = toAbort.remove();
logger.log(Level.FINE,
"IdleManager aborting IDLE for folder: {0}", folder);
SocketChannel sc = folder.getChannel();
if (sc == null)
continue;
SelectionKey sk = sc.keyFor(selector);
// have to cancel so we can switch back to blocking I/O mode
if (sk != null)
sk.cancel();
// switch back to blocking to allow normal I/O
sc.configureBlocking(true);
folder.idleAbort(); // send the DONE message
// watch for OK response to DONE
toWatch.add(folder);
more = true;
}
} catch (NoSuchElementException ex) {
// no more elements in the queue
}
/*
* Now, process any channels with data to read.
*/
Set selectedKeys = selector.selectedKeys();
for (SelectionKey sk : selectedKeys) {
selectedKeys.remove(sk); // only process each key once
// have to cancel so we can switch back to blocking I/O mode
sk.cancel();
IMAPFolder folder = (IMAPFolder)sk.attachment();
logger.log(Level.FINE,
"IdleManager selected folder: {0}", folder);
SelectableChannel sc = sk.channel();
// switch back to blocking to allow normal I/O
sc.configureBlocking(true);
try {
List evs;
if ((evs = folder.handleIdleEvents()) != null) {
logger.log(Level.FINEST,
"IdleManager got {0} events", events.size());
// more to do with this folder, select on it again
// XXX - what if we also added it above?
toWatch.add(folder);
more = true;
events.addAll(evs);
startEventDispatcher();
} else {
// done watching this folder,
logger.log(Level.FINE,
"IdleManager done watching folder {0}", folder);
}
} catch (MessagingException ex) {
// something went wrong, stop watching this folder
logger.log(Level.FINE,
"IdleManager got exception for folder: " + folder,
ex);
}
}
return more;
}
/**
* Stop watching all folders. Cancel any selection keys and,
* most importantly, switch the channel back to blocking mode.
*/
private void unwatchAll() {
Set keys = selector.keys();
for (SelectionKey sk : keys) {
// have to cancel so we can switch back to blocking I/O mode
sk.cancel();
IMAPFolder folder = (IMAPFolder)sk.attachment();
logger.log(Level.FINE,
"IdleManager no longer watching folder: {0}", folder);
SelectableChannel sc = sk.channel();
// switch back to blocking to allow normal I/O
try {
sc.configureBlocking(true);
} catch (IOException ex) {
// ignore it, channel might be closed
}
}
}
private synchronized void startEventDispatcher() {
if (!eventDispatcherStarted) {
es.submit(eventDispatcher);
eventDispatcherStarted = true;
}
}
private synchronized void eventDispatcherStopped() {
eventDispatcherStarted = false;
}
private void dispatchEvents() {
for (;;) {
MailEvent e;
synchronized (this) {
try {
e = events.remove();
} catch (NoSuchElementException ex) {
// no more events in the queue
eventDispatcherStarted = false;
return;
}
}
logger.log(Level.FINEST, "IdleManager deliver event: {0}", e);
if (e instanceof MessageCountEvent) {
for (MessageCountListener l : messageCountListeners) {
try {
e.dispatch(l);
} catch (Throwable t) {
logger.log(Level.FINEST,
"IdleManager listener failed!", t);
// ignore anything thrown by the listener
}
}
} else if (e instanceof MessageChangedEvent) {
for (MessageChangedListener l : messageChangedListeners) {
try {
e.dispatch(l);
} catch (Throwable t) {
logger.log(Level.FINEST,
"IdleManager listener failed!", t);
// ignore anything thrown by the listener
}
}
} else {
logger.finest("IdleManager unknown event type!");
// can't happen, I hope
}
}
}
/**
* Stop the IdleManager. The IdleManager can not be restarted.
*/
public synchronized void stop() {
die = true;
logger.finest("IdleManager stopping");
selector.wakeup();
// XXX - close selector and clean up everything?
}
}