<D> ChannelFuture send(Command command, String args, SettableFuture<D> valueFuture) { Long seq = sequence.incrementAndGet(); String commandString = seq + " " + command.toString() + (null == args ? "" : " " + args) + "\r\n"; // Log the command but clip the \r\n log.debug("Sending {} to server...", commandString.substring(0, commandString.length() - 2)); Boolean toStdOut = logAllMessagesForUsers.get(config.getUsername()); if (toStdOut != null) { if (toStdOut) System.out.println( "IMAPsnd[" + config.getUsername() + "]: " + commandString.substring(0, commandString.length() - 2)); else log.info( "IMAPsnd[{}]: {}", config.getUsername(), commandString.substring(0, commandString.length() - 2)); } // Enqueue command. mailClientHandler.enqueue(new CommandCompletion(command, seq, valueFuture, commandString)); return channel.write(commandString); }
/** This is synchronized to ensure that we process the queue serially. */ private synchronized void complete(String message) { // This is a weird problem with writing stuff while idling. Need to investigate it more, but // for now just ignore it. if (MESSAGE_COULDNT_BE_FETCHED_REGEX.matcher(message).matches()) { log.warn( "Some messages in the batch could not be fetched for {}\n" + "---cmd---\n{}\n---wire---\n{}\n---end---\n", new Object[] {config.getUsername(), getCommandTrace(), getWireTrace()}); errorStack.push(new Error(completions.peek(), message, wireTrace.list())); throw new RuntimeException( "Some messages in the batch could not be fetched for user " + config.getUsername()); } CommandCompletion completion = completions.peek(); if (completion == null) { if ("+ idling".equalsIgnoreCase(message)) { synchronized (idleMutex) { idler.idleStart(); log.trace("IDLE entered."); idleAcknowledged.set(true); } } else { log.error("Could not find the completion for message {} (Was it ever issued?)", message); errorStack.push(new Error(null, "No completion found!", wireTrace.list())); } return; } if (completion.complete(message)) { completions.poll(); } }
/** Connects to the IMAP server logs in with the given credentials. */ @Override public synchronized boolean connect(final DisconnectListener listener) { reset(); ChannelFuture future = bootstrap.connect(new InetSocketAddress(config.getHost(), config.getPort())); Channel channel = future.awaitUninterruptibly().getChannel(); if (!future.isSuccess()) { throw new RuntimeException("Could not connect channel", future.getCause()); } this.channel = channel; this.disconnectListener = listener; if (null != listener) { // https://issues.jboss.org/browse/NETTY-47?page=com.atlassian.jirafisheyeplugin%3Afisheye-issuepanel#issue-tabs channel .getCloseFuture() .addListener( new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { mailClientHandler.idleAcknowledged.set(false); mailClientHandler.disconnected(); listener.disconnected(); } }); } return login(); }
public MailClientHandler(Idler idler, MailClientConfig config) { this.idler = idler; this.config = config; mBeanRegistration = JmxUtil.registerMBean( this, "com.google.sitebricks", "MailClientHandler", config.getUsername()); }
@Override public ListenableFuture<List<MessageStatus>> listUidThin( Folder folder, List<Sequence> sequences) { Preconditions.checkState( mailClientHandler.isLoggedIn(), "Can't execute command because client is not logged in"); Preconditions.checkState( !mailClientHandler.idleRequested.get(), "Can't execute command while idling (are you watching a folder?)"); checkCurrentFolder(folder); SettableFuture<List<MessageStatus>> valueFuture = SettableFuture.create(); // -ve end range means get everything (*). String extensions = config.useGmailExtensions() ? " X-GM-MSGID X-GM-THRID X-GM-LABELS UID" : ""; StringBuilder argsBuilder = new StringBuilder(); // Emit ranges. for (int i = 0, sequencesSize = sequences.size(); i < sequencesSize; i++) { Sequence seq = sequences.get(i); argsBuilder.append(toUpperBound(seq.start)); if (seq.end != 0) argsBuilder.append(':').append(toUpperBound(seq.end)); if (i < sequencesSize - 1) argsBuilder.append(','); } argsBuilder.append(" (FLAGS" + extensions + ")"); send(Command.FETCH_THIN_HEADERS_UID, argsBuilder.toString(), valueFuture); return valueFuture; }
@Override public ListenableFuture<List<MessageStatus>> list(Folder folder, int start, int end) { Preconditions.checkState( mailClientHandler.isLoggedIn(), "Can't execute command because client is not logged in"); Preconditions.checkState( !mailClientHandler.idleRequested.get(), "Can't execute command while idling (are you watching a folder?)"); checkCurrentFolder(folder); checkRange(start, end); Preconditions.checkArgument( start > 0, "Start must be greater than zero (IMAP uses 1-based " + "indexing)"); SettableFuture<List<MessageStatus>> valueFuture = SettableFuture.create(); // -ve end range means get everything (*). String extensions = config.useGmailExtensions() ? " X-GM-MSGID X-GM-THRID X-GM-LABELS UID" : ""; String args = start + ":" + toUpperBound(end) + " (RFC822.SIZE INTERNALDATE FLAGS ENVELOPE" + extensions + ")"; send(Command.FETCH_HEADERS, args, valueFuture); return valueFuture; }
/** * Logs out of the current IMAP session and releases all resources, including executor services. */ @Override public synchronized void disconnect() { try { // If there is an error with the handler, dont bother logging out. if (!mailClientHandler.isHalted()) { if (mailClientHandler.idleRequested.get()) { log.warn("Disconnect called while IDLE, leaving idle and logging out."); done(); } // Log out of the IMAP Server. channel.write(". logout\n"); } currentFolder = null; } catch (Exception e) { // swallow any exceptions. } finally { // Shut down all channels and exit (leave threadpools as is--for reconnects). // The Netty channel close listener will fire a disconnect event to our client, // automatically. See connect() for details. try { channel.close().awaitUninterruptibly(config.getTimeout(), TimeUnit.MILLISECONDS); } catch (Exception e) { // swallow any exceptions. } finally { mailClientHandler.idleAcknowledged.set(false); mailClientHandler.disconnected(); if (disconnectListener != null) disconnectListener.disconnected(); } } }
private boolean login() { try { channel.write(". CAPABILITY\r\n"); if (config.getPassword() != null) channel.write(". login " + config.getUsername() + " " + config.getPassword() + "\r\n"); else { // Use xoauth login instead. OAuthConfig oauth = config.getOAuthConfig(); Preconditions.checkArgument( oauth != null, "Must specify a valid oauth config if not using password auth"); //noinspection ConstantConditions String oauthString = new XoauthSasl(config.getUsername(), oauth.clientId, oauth.clientSecret) .build(Protocol.IMAP, oauth.accessToken, oauth.tokenSecret); channel.write(". AUTHENTICATE XOAUTH " + oauthString + "\r\n"); } return mailClientHandler.awaitLogin(); } catch (Exception e) { // Capture the wire trace and log it for some extra context here. StringBuilder trace = new StringBuilder(); for (String line : mailClientHandler.getWireTrace()) { trace.append(line).append("\n"); } log.warn( "Could not oauth or login for {}. Partial trace follows:\n" + "----begin wiretrace----\n{}\n----end wiretrace----", new Object[] {config.getUsername(), trace.toString(), e}); } return false; }
@Override public ListenableFuture<List<Integer>> searchUid(Folder folder, String query, Date since) { Preconditions.checkState( mailClientHandler.isLoggedIn(), "Can't execute command because client is not logged in"); Preconditions.checkState( !mailClientHandler.idleRequested.get(), "Can't execute command while idling (are you watching a folder?)"); checkCurrentFolder(folder); SettableFuture<List<Integer>> valueFuture = SettableFuture.create(); StringBuilder argsBuilder = new StringBuilder(); if (config.useGmailExtensions()) { argsBuilder.append("X-GM-RAW \"").append(query).append('"'); } else argsBuilder.append(query); if (since != null) argsBuilder.append(" since ").append(SINCE_FORMAT.format(since)); send(Command.SEARCH_RAW_UID, argsBuilder.toString(), valueFuture); return valueFuture; }
@ManagedOperation public void logAllMessages(boolean b) { log.info("logAllMessagesForUsers[" + config.getUsername() + "] = " + b); if (b) logAllMessagesForUsers.put(config.getUsername(), false); else logAllMessagesForUsers.remove(config.getUsername()); }
private void processMessage(String message) throws Exception { Boolean toStdOut = logAllMessagesForUsers.get(config.getUsername()); if (toStdOut != null) { if (toStdOut) System.out.println("IMAPrcv[" + config.getUsername() + "]: " + message); else log.info("IMAPrcv[{}]: {}", config.getUsername(), message); } wireTrace.add(message); log.trace(message); if (SYSTEM_ERROR_REGEX.matcher(message).matches() || ". NO [ALERT] Account exceeded command or bandwidth limits. (Failure)" .equalsIgnoreCase(message.trim())) { log.warn( "{} disconnected by IMAP Server due to system error: {}", config.getUsername(), message); disconnectAbnormally(message); return; } try { if (halt) { log.error( "Mail client for {} is halted but continues to receive messages, ignoring!", config.getUsername()); return; } if (loginSuccess.getCount() > 0) { if (message.startsWith(CAPABILITY_PREFIX)) { this.capabilities = Arrays.asList(message.substring(CAPABILITY_PREFIX.length() + 1).split("[ ]+")); return; } else if (AUTH_SUCCESS_REGEX.matcher(message).matches()) { log.info("Authentication success for user {}", config.getUsername()); loginSuccess.countDown(); } else { Matcher matcher = COMMAND_FAILED_REGEX.matcher(message); if (matcher.find()) { // WARNING: DO NOT COUNTDOWN THE LOGIN LATCH ON FAILURE!!! log.warn("Authentication failed for {} due to: {}", config.getUsername(), message); errorStack.push( new Error( null /* logins have no completion */, extractError(matcher), wireTrace.list())); disconnectAbnormally(message); } } return; } // Copy to local var as the value can change underneath us. FolderObserver observer = this.observer; if (idleRequested.get() || idleAcknowledged.get()) { synchronized (idleMutex) { if (IDLE_ENDED_REGEX.matcher(message).matches()) { idleRequested.compareAndSet(true, false); idleAcknowledged.set(false); // Now fire the events. PushedData data = pushedData; pushedData = null; idler.idleEnd(); observer.changed( data.pushAdds.isEmpty() ? null : data.pushAdds, data.pushRemoves.isEmpty() ? null : data.pushRemoves); return; } // Queue up any push notifications to publish to the client in a second. Matcher existsMatcher = IDLE_EXISTS_REGEX.matcher(message); boolean matched = false; if (existsMatcher.matches()) { int number = Integer.parseInt(existsMatcher.group(1)); pushedData.pushAdds.add(number); pushedData.pushRemoves.remove(number); matched = true; } else { Matcher expungeMatcher = IDLE_EXPUNGE_REGEX.matcher(message); if (expungeMatcher.matches()) { int number = Integer.parseInt(expungeMatcher.group(1)); pushedData.pushRemoves.add(number); pushedData.pushAdds.remove(number); matched = true; } } // Stop idling, when we get the idle ended message (next cycle) we can publish what's been // gathered. if (matched) { if (!pushedData.idleExitSent) { idler.done(); pushedData.idleExitSent = true; } return; } } } complete(message); } catch (Exception ex) { CommandCompletion completion = completions.poll(); if (completion != null) completion.error(message, ex); else { log.error( "Strange exception during mail processing (no completions available!): {}", message, ex); errorStack.push(new Error(null, "No completions available!", wireTrace.list())); } throw ex; } }
public void enableSendLogging(boolean enable) { log.info("Logging of sent IMAP commands for user {} = {}", config.getUsername(), enable); if (enable) logAllMessagesForUsers.put(config.getUsername(), false); else logAllMessagesForUsers.remove(config.getUsername()); }
@Override public synchronized void updateOAuthAccessToken(String accessToken, String tokenSecret) { config.getOAuthConfig().accessToken = accessToken; config.getOAuthConfig().tokenSecret = tokenSecret; }