All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.angus.mail.imap.IdleManager Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2014, 2023 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.eclipse.angus.mail.imap;

import jakarta.mail.Folder;
import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import org.eclipse.angus.mail.util.MailLogger;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.Socket;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.logging.Level;

/**
 * IdleManager uses the optional IMAP IDLE command
 * (RFC 2177)
 * to watch multiple folders for new messages.
 * IdleManager uses an Executor to execute tasks in separate threads.
 * An Executor is typically provided by an ExecutorService.
 * For example, for a Java SE application:
 * 
 * 	ExecutorService es = Executors.newCachedThreadPool();
 * 	final IdleManager idleManager = new IdleManager(session, es);
 * 
* For a Java EE 7 application: *
 *    {@literal @}Resource
 * 	ManagedExecutorService es;
 * 	final IdleManager idleManager = new IdleManager(session, es);
 * 
* 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(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");
 * 		try {
 * 		    // process new messages
 * 		    idleManager.watch(folder); // keep watching for new messages
 *        } catch (MessagingException mex) {
 * 		    // handle exception related to the Folder
 *        }
 *        }
 *    });
 * 	idleManager.watch(folder);
 * 
* This delivers the events for each folder in a separate thread, NOT * using the Executor. To deliver all events in a single thread * using the Executor, set the following properties for the Session * (once), and then add listeners and watch the folder as above. *
 * 	// the following should be done once...
 * 	Properties props = session.getProperties();
 * 	props.put("mail.event.scope", "session"); // or "application"
 * 	props.put("mail.event.executor", es);
 * 
* 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 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. (Or mail.imaps.usesocketchannels if * you're using the "imaps" protocol.) This will effect all connections in * that Session, but you can create another Session without this property set * if you need to use the features that are incompatible with SocketChannels. *

* NOTE: The IdleManager, and all APIs and properties related to it, should * be considered EXPERIMENTAL. They may be changed in the * future in ways that are incompatible with applications using the * current APIs. * * @since JavaMail 1.5.2 */ public class IdleManager { private Executor es; private Selector selector; private MailLogger logger; private volatile boolean die = false; private volatile boolean running; private Queue toWatch = new ConcurrentLinkedQueue<>(); private Queue toAbort = new ConcurrentLinkedQueue<>(); /** * Create an IdleManager. The Session is used only to configure * debugging output. The Executor is used to create the * "select" thread. * * @param session the Session containing configuration information * @param es the Executor used to create threads * @exception IOException for Selector failures */ public IdleManager(Session session, Executor es) throws IOException { this.es = es; logger = new MailLogger(this.getClass(), "DEBUG IMAP", session.getDebug(), session.getDebugOut()); selector = Selector.open(); es.execute(new Runnable() { @Override public void run() { logger.fine("IdleManager select starting"); try { running = true; select(); } finally { running = false; logger.fine("IdleManager select terminating"); } } }); } /** * Is the IdleManager currently running? The IdleManager starts * running when the Executor schedules its task. The IdleManager * stops running after its task detects the stop request from the * {@link #stop stop} method, or if it terminates abnormally due * to an unexpected error. * * @return true if the IdleMaanger is running * @since JavaMail 1.5.5 */ public boolean isRunning() { return running; } /** * 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(); } } /** * 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. */ 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. */ private void select() { 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 || Thread.currentThread().isInterrupted()) 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 * selection key 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. This should be "fair" since each * selection key is used only once before being added * to the toWatch list. */ do { processKeys(); } while (selector.selectNow() > 0 || !toAbort.isEmpty()); } } catch (InterruptedIOException ex) { logger.log(Level.FINEST, "IdleManager interrupted", ex); } catch (IOException ex) { logger.log(Level.FINEST, "IdleManager got I/O exception", ex); } catch (Exception ex) { logger.log(Level.FINEST, "IdleManager got exception", ex); } finally { die = true; // prevent new watches in case of exception logger.finest("IdleManager unwatchAll"); try { unwatchAll(); selector.close(); } catch (IOException ex2) { // nothing to do... logger.log(Level.FINEST, "IdleManager unwatch exception", ex2); } 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() { /* * Pull each of the folders from the toWatch queue * and register it. */ IMAPFolder folder; while ((folder = toWatch.poll()) != null) { if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "IdleManager adding {0} to selector", folderName(folder)); try { SocketChannel sc = folder.getChannel(); if (sc == null) continue; // 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 (CancelledKeyException ex) { // this should never happen logger.log(Level.FINEST, "IdleManager can't register folder", ex); } } } /** * Process the selected keys. */ private void processKeys() throws IOException { IMAPFolder folder; /* * First, process any channels with data to read. */ Set selectedKeys = selector.selectedKeys(); /* * XXX - this is simpler, but it can fail with * ConcurrentModificationException * for (SelectionKey sk : selectedKeys) { selectedKeys.remove(sk); // only process each key once ... } */ Iterator it = selectedKeys.iterator(); while (it.hasNext()) { SelectionKey sk = it.next(); it.remove(); // only process each key once // have to cancel so we can switch back to blocking I/O mode sk.cancel(); folder = (IMAPFolder) sk.attachment(); if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "IdleManager selected folder: {0}", folderName(folder)); SelectableChannel sc = sk.channel(); // switch back to blocking to allow normal I/O sc.configureBlocking(true); try { if (folder.handleIdle(false)) { if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "IdleManager continue watching folder {0}", folderName(folder)); // more to do with this folder, select on it again toWatch.add(folder); } else { // done watching this folder, if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "IdleManager done watching folder {0}", folderName(folder)); } } catch (MessagingException ex) { // something went wrong, stop watching this folder logger.log(Level.FINEST, "IdleManager got exception for folder: " + folderName(folder), ex); } } /* * Now, process any folders that we need to abort. */ while ((folder = toAbort.poll()) != null) { if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "IdleManager aborting IDLE for folder: {0}", folderName(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); // if there's a read timeout, have to do the abort in a new thread Socket sock = sc.socket(); if (sock != null && sock.getSoTimeout() > 0) { logger.finest("IdleManager requesting DONE with timeout"); toWatch.remove(folder); final IMAPFolder folder0 = folder; es.execute(new Runnable() { @Override public void run() { // send the DONE and wait for the response folder0.idleAbortWait(); } }); } else { folder.idleAbort(); // send the DONE message // watch for OK response to DONE // XXX - what if we also added it above? should be a nop toWatch.add(folder); } } } /** * Stop watching all folders. Cancel any selection keys and, * most importantly, switch the channel back to blocking mode. * If there's any folders waiting to be watched, need to abort * them too. */ private void unwatchAll() { IMAPFolder folder; Set keys = selector.keys(); for (SelectionKey sk : keys) { // have to cancel so we can switch back to blocking I/O mode sk.cancel(); folder = (IMAPFolder) sk.attachment(); if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "IdleManager no longer watching folder: {0}", folderName(folder)); SelectableChannel sc = sk.channel(); // switch back to blocking to allow normal I/O try { sc.configureBlocking(true); folder.idleAbortWait(); // send the DONE message and wait } catch (IOException ex) { // ignore it, channel might be closed logger.log(Level.FINEST, "IdleManager exception while aborting idle for folder: " + folderName(folder), ex); } } /* * Finally, process any folders waiting to be watched. */ while ((folder = toWatch.poll()) != null) { if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "IdleManager aborting IDLE for unwatched folder: {0}", folderName(folder)); SocketChannel sc = folder.getChannel(); if (sc == null) continue; try { // channel should still be in blocking mode, but make sure sc.configureBlocking(true); folder.idleAbortWait(); // send the DONE message and wait } catch (IOException ex) { // ignore it, channel might be closed logger.log(Level.FINEST, "IdleManager exception while aborting idle for folder: " + folderName(folder), ex); } } } /** * Stop the IdleManager. The IdleManager can not be restarted. */ public synchronized void stop() { die = true; logger.fine("IdleManager stopping"); selector.wakeup(); } /** * Return the fully qualified name of the folder, for use in log messages. * Essentially just the getURLName method, but ignoring the * MessagingException that can never happen. */ private static String folderName(Folder folder) { try { return folder.getURLName().toString(); } catch (MessagingException mex) { // can't happen return folder.getStore().toString() + "/" + folder.toString(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy