PageRenderTime 26ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/sitebricks-mail/src/main/java/com/google/sitebricks/mail/NettyImapClient.java

https://github.com/juven/sitebricks
Java | 285 lines | 208 code | 55 blank | 22 comment | 14 complexity | d379d6c224a70eebb680ec94162f1bfd MD5 | raw file
  1. package com.google.sitebricks.mail;
  2. import com.google.common.base.Preconditions;
  3. import com.google.common.util.concurrent.ListenableFuture;
  4. import com.google.common.util.concurrent.SettableFuture;
  5. import com.google.sitebricks.mail.imap.*;
  6. import org.jboss.netty.bootstrap.ClientBootstrap;
  7. import org.jboss.netty.channel.Channel;
  8. import org.jboss.netty.channel.ChannelFuture;
  9. import org.jboss.netty.channel.ChannelFutureListener;
  10. import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
  11. import org.slf4j.Logger;
  12. import org.slf4j.LoggerFactory;
  13. import java.net.InetSocketAddress;
  14. import java.util.List;
  15. import java.util.concurrent.ExecutionException;
  16. import java.util.concurrent.ExecutorService;
  17. import java.util.concurrent.TimeUnit;
  18. import java.util.concurrent.atomic.AtomicLong;
  19. /**
  20. * @author dhanji@gmail.com (Dhanji R. Prasanna)
  21. */
  22. class NettyImapClient implements MailClient {
  23. private static final Logger log = LoggerFactory.getLogger(NettyImapClient.class);
  24. private final ExecutorService workerPool;
  25. private final ExecutorService bossPool;
  26. private final MailClientConfig config;
  27. // Connection variables.
  28. private volatile ClientBootstrap bootstrap;
  29. private volatile MailClientHandler mailClientHandler;
  30. // State variables:
  31. private final AtomicLong sequence = new AtomicLong();
  32. private volatile Channel channel;
  33. private volatile Folder currentFolder = null;
  34. private volatile boolean idling = false;
  35. private volatile boolean loggedIn = false;
  36. public NettyImapClient(MailClientConfig config,
  37. ExecutorService bossPool,
  38. ExecutorService workerPool) {
  39. this.workerPool = workerPool;
  40. this.bossPool = bossPool;
  41. this.config = config;
  42. }
  43. static {
  44. System.setProperty("mail.mime.decodetext.strict", "false");
  45. }
  46. public boolean isConnected() {
  47. return channel != null && channel.isConnected() && channel.isOpen();
  48. }
  49. private void reset() {
  50. Preconditions.checkState(!isConnected(),
  51. "Cannot reset while mail client is still connected (call disconnect() first).");
  52. // Just to be on the safe side.
  53. if (mailClientHandler != null)
  54. mailClientHandler.halt();
  55. this.mailClientHandler = new MailClientHandler();
  56. MailClientPipelineFactory pipelineFactory =
  57. new MailClientPipelineFactory(mailClientHandler, config);
  58. this.bootstrap = new ClientBootstrap(new NioClientSocketChannelFactory(bossPool, workerPool));
  59. this.bootstrap.setPipelineFactory(pipelineFactory);
  60. // Reset state (helps if this is a reconnect).
  61. this.currentFolder = null;
  62. this.sequence.set(0L);
  63. this.idling = false;
  64. }
  65. @Override
  66. public boolean connect() {
  67. return connect(null);
  68. }
  69. /**
  70. * Connects to the IMAP server logs in with the given credentials.
  71. */
  72. @Override
  73. public synchronized boolean connect(final DisconnectListener listener) {
  74. reset();
  75. ChannelFuture future = bootstrap.connect(new InetSocketAddress(config.getHost(),
  76. config.getPort()));
  77. Channel channel = future.awaitUninterruptibly().getChannel();
  78. if (!future.isSuccess()) {
  79. throw new RuntimeException("Could not connect channel", future.getCause());
  80. }
  81. this.channel = channel;
  82. if (null != listener) {
  83. // https://issues.jboss.org/browse/NETTY-47?page=com.atlassian.jirafisheyeplugin%3Afisheye-issuepanel#issue-tabs
  84. channel.getCloseFuture().addListener(new ChannelFutureListener() {
  85. @Override public void operationComplete(ChannelFuture future) throws Exception {
  86. listener.disconnected();
  87. }
  88. });
  89. }
  90. return login();
  91. }
  92. private boolean login() {
  93. channel.write(". CAPABILITY\r\n");
  94. channel.write(". login " + config.getUsername() + " " + config.getPassword() + "\r\n");
  95. return loggedIn = mailClientHandler.awaitLogin();
  96. }
  97. @Override public String lastError() {
  98. return mailClientHandler.lastError().error;
  99. }
  100. /**
  101. * Logs out of the current IMAP session and releases all resources, including
  102. * executor services.
  103. */
  104. @Override
  105. public synchronized void disconnect() {
  106. Preconditions.checkState(!idling, "Can't execute command while idling (are you watching a folder?)");
  107. currentFolder = null;
  108. // Log out of the IMAP Server.
  109. channel.write(". logout\n");
  110. // Shut down all thread pools and exit.
  111. channel.close().awaitUninterruptibly(config.getTimeout(), TimeUnit.MILLISECONDS);
  112. }
  113. <D> ChannelFuture send(Command command, String args, SettableFuture<D> valueFuture) {
  114. Long seq = sequence.incrementAndGet();
  115. String commandString = seq + " " + command.toString()
  116. + (null == args ? "" : " " + args)
  117. + "\r\n";
  118. // Log the command but clip the \r\n
  119. log.debug("Sending {} to server...", commandString.substring(0, commandString.length() - 2));
  120. // Enqueue command.
  121. mailClientHandler.enqueue(new CommandCompletion(command, seq, valueFuture, commandString));
  122. return channel.write(commandString);
  123. }
  124. @Override
  125. public List<String> capabilities() {
  126. return mailClientHandler.getCapabilities();
  127. }
  128. @Override
  129. public ListenableFuture<List<String>> listFolders() {
  130. Preconditions.checkState(loggedIn, "Can't execute command because client is not logged in");
  131. Preconditions.checkState(!idling, "Can't execute command while idling (are you watching a folder?)");
  132. SettableFuture<List<String>> valueFuture = SettableFuture.create();
  133. // TODO Should we use LIST "[Gmail]" % here instead? That will only fetch top-level folders.
  134. send(Command.LIST_FOLDERS, "\"[Gmail]\" \"*\"", valueFuture);
  135. return valueFuture;
  136. }
  137. @Override
  138. public ListenableFuture<FolderStatus> statusOf(String folder) {
  139. Preconditions.checkState(loggedIn, "Can't execute command because client is not logged in");
  140. SettableFuture<FolderStatus> valueFuture = SettableFuture.create();
  141. String args = '"' + folder + "\" (UIDNEXT RECENT MESSAGES UNSEEN)";
  142. send(Command.FOLDER_STATUS, args, valueFuture);
  143. return valueFuture;
  144. }
  145. @Override
  146. public ListenableFuture<Folder> open(String folder) {
  147. return open(folder, false);
  148. }
  149. @Override
  150. public ListenableFuture<Folder> open(String folder, boolean readWrite) {
  151. Preconditions.checkState(loggedIn, "Can't execute command because client is not logged in");
  152. Preconditions.checkState(!idling, "Can't execute command while idling (are you watching a folder?)");
  153. final SettableFuture<Folder> valueFuture = SettableFuture.create();
  154. valueFuture.addListener(new Runnable() {
  155. @Override
  156. public void run() {
  157. try {
  158. currentFolder = valueFuture.get();
  159. } catch (InterruptedException e) {
  160. log.error("Interrupted while attempting to open a folder", e);
  161. } catch (ExecutionException e) {
  162. log.error("Execution exception while attempting to open a folder", e);
  163. }
  164. }
  165. }, workerPool);
  166. String args = '"' + folder + "\"";
  167. send(readWrite ? Command.FOLDER_OPEN : Command.FOLDER_EXAMINE, args, valueFuture);
  168. return valueFuture;
  169. }
  170. @Override
  171. public ListenableFuture<List<MessageStatus>> list(Folder folder, int start, int end) {
  172. Preconditions.checkState(loggedIn, "Can't execute command because client is not logged in");
  173. Preconditions.checkState(!idling, "Can't execute command while idling (are you watching a folder?)");
  174. checkCurrentFolder(folder);
  175. Preconditions.checkArgument(start <= end, "Start must be <= end");
  176. Preconditions.checkArgument(start > 0, "Start must be greater than zero (IMAP uses 1-based " +
  177. "indexing)");
  178. SettableFuture<List<MessageStatus>> valueFuture = SettableFuture.create();
  179. // -ve end range means get everything (*).
  180. String args = start + ":" + toUpperBound(end) + " all";
  181. send(Command.FETCH_HEADERS, args, valueFuture);
  182. return valueFuture;
  183. }
  184. private static String toUpperBound(int end) {
  185. return (end > 0)
  186. ? Integer.toString(end)
  187. : "*";
  188. }
  189. @Override
  190. public ListenableFuture<List<Message>> fetch(Folder folder, int start, int end) {
  191. Preconditions.checkState(loggedIn, "Can't execute command because client is not logged in");
  192. Preconditions.checkState(!idling, "Can't execute command while idling (are you watching a folder?)");
  193. checkCurrentFolder(folder);
  194. Preconditions.checkArgument(start <= end, "Start must be <= end");
  195. Preconditions.checkArgument(start > 0, "Start must be greater than zero (IMAP uses 1-based " +
  196. "indexing)");
  197. SettableFuture<List<Message>> valueFuture = SettableFuture.create();
  198. String args = start + ":" + toUpperBound(end) + " body[]";
  199. send(Command.FETCH_BODY, args, valueFuture);
  200. return valueFuture;
  201. }
  202. @Override
  203. public void watch(Folder folder, FolderObserver observer) {
  204. Preconditions.checkState(loggedIn, "Can't execute command because client is not logged in");
  205. checkCurrentFolder(folder);
  206. Preconditions.checkState(!idling, "Already idling...");
  207. idling = true;
  208. send(Command.IDLE, null, SettableFuture.<Object>create());
  209. mailClientHandler.observe(observer);
  210. }
  211. @Override
  212. public void unwatch() {
  213. if (!idling)
  214. return;
  215. // Stop watching folders.
  216. mailClientHandler.observe(null);
  217. channel.write(". DONE");
  218. idling = false;
  219. }
  220. private void checkCurrentFolder(Folder folder) {
  221. Preconditions.checkState(folder.equals(currentFolder), "You must have opened folder %s" +
  222. " before attempting to read from it (%s is currently open).", folder.getName(),
  223. (currentFolder == null ? "No folder" : currentFolder.getName()));
  224. }
  225. }