/sitebricks-mail/src/main/java/com/google/sitebricks/mail/MailClientHandler.java
http://github.com/dhanji/sitebricks · Java · 424 lines · 333 code · 63 blank · 28 comment · 43 complexity · a287ddcc2df15eaca842d4711ecd7250 MD5 · raw file
- package com.google.sitebricks.mail;
- import com.google.common.annotations.VisibleForTesting;
- import com.google.common.base.Preconditions;
- import com.google.common.collect.ImmutableList;
- import com.google.common.collect.Lists;
- import com.google.sitebricks.util.BoundedDiscardingList;
- import org.jboss.netty.channel.ChannelHandlerContext;
- import org.jboss.netty.channel.ExceptionEvent;
- import org.jboss.netty.channel.MessageEvent;
- import org.jboss.netty.channel.SimpleChannelHandler;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import java.util.*;
- import java.util.concurrent.*;
- import java.util.concurrent.atomic.AtomicBoolean;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
- /**
- * A command/response handler for a single mail connection/user.
- *
- * @author dhanji@gmail.com (Dhanji R. Prasanna)
- */
- class MailClientHandler extends SimpleChannelHandler {
- private static final Logger log = LoggerFactory.getLogger(MailClientHandler.class);
- private static boolean ENABLE_WIRE_TRACE = true;
- private static final Map<String, Boolean> logAllMessagesForUsers = new ConcurrentHashMap<String, Boolean>();
- public static final String CAPABILITY_PREFIX = "* CAPABILITY";
- static final Pattern GMAIL_AUTH_SUCCESS_REGEX =
- Pattern.compile("[.] OK .*@.* \\(Success\\)", Pattern.CASE_INSENSITIVE);
-
- static final Pattern IMAP_AUTH_SUCCESS_REGEX =
- Pattern.compile("[.] OK (.*)", Pattern.CASE_INSENSITIVE);
- static final Pattern COMMAND_FAILED_REGEX =
- Pattern.compile("^[.] (NO|BAD) (.*)", Pattern.CASE_INSENSITIVE);
- static final Pattern MESSAGE_COULDNT_BE_FETCHED_REGEX =
- Pattern.compile("^\\d+ no some messages could not be fetched \\(failure\\)\\s*",
- Pattern.CASE_INSENSITIVE);
- static final Pattern SYSTEM_ERROR_REGEX = Pattern.compile("[*]\\s*bye\\s*system\\s*error\\s*",
- Pattern.CASE_INSENSITIVE);
- static final Pattern IDLE_ENDED_REGEX = Pattern.compile(".* OK IDLE terminated \\(success\\)\\s*",
- Pattern.CASE_INSENSITIVE);
- static final Pattern IDLE_EXISTS_REGEX = Pattern.compile("\\* (\\d+) exists\\s*",
- Pattern.CASE_INSENSITIVE);
- static final Pattern IDLE_EXPUNGE_REGEX = Pattern.compile("\\* (\\d+) expunge\\s*",
- Pattern.CASE_INSENSITIVE);
- private final Idler idler;
- private final MailClientConfig config;
- private final CountDownLatch loginSuccess = new CountDownLatch(1);
- private volatile List<String> capabilities;
- private volatile FolderObserver observer;
- final AtomicBoolean idleRequested = new AtomicBoolean();
- final AtomicBoolean idleAcknowledged = new AtomicBoolean();
- private final Object idleMutex = new Object();
- // Panic button.
- private volatile boolean halt = false;
- private final LinkedBlockingDeque<Error> errorStack = new LinkedBlockingDeque<Error>();
- private final Queue<CommandCompletion> completions =
- new ConcurrentLinkedQueue<CommandCompletion>();
- private volatile PushedData pushedData;
- private final BoundedDiscardingList<String> commandTrace = new BoundedDiscardingList<String>(10);
- private final BoundedDiscardingList<String> wireTrace = new BoundedDiscardingList<String>(25);
- private final InputBuffer inputBuffer = new InputBuffer();
- public MailClientHandler(Idler idler, MailClientConfig config) {
- this.idler = idler;
- this.config = config;
- }
- // For debugging, use with caution!
- public static void addUserForVerboseLogging(String username, boolean toStdOut) {
- logAllMessagesForUsers.put(username, toStdOut);
- }
- public void setReceiveLogging(boolean b) {
- log.info("setReceiveLogging[" + config.getUsername() + "] = " + b);
- if (b)
- logAllMessagesForUsers.put(config.getUsername(), false);
- else
- logAllMessagesForUsers.remove(config.getUsername());
- }
- public Set<String> getLogAllMessagesFor() {
- return logAllMessagesForUsers.keySet();
- }
- public List<String> getCommandTrace() {
- return commandTrace.list();
- }
- public List<String> getWireTrace() {
- return wireTrace.list();
- }
- public boolean isLoggedIn() {
- return loginSuccess.getCount() == 0;
- }
- private static class PushedData {
- volatile boolean idleExitSent = false;
- // guarded by idleMutex.
- final ArrayList<Integer> pushAdds = new ArrayList<Integer>();
- // guarded by idleMutex.
- final ArrayList<Integer> pushRemoves = new ArrayList<Integer>();
- }
- // DO NOT synchronize!
- public void enqueue(CommandCompletion completion) {
- completions.add(completion);
- commandTrace.add(new Date().toString() + " " + completion.toString());
- }
- @Override
- public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
- String message = e.getMessage().toString();
- for (String input : inputBuffer.processMessage(message)) {
- processMessage(input);
- }
- }
- 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);
- }
- if (ENABLE_WIRE_TRACE) {
- 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 (GMAIL_AUTH_SUCCESS_REGEX.matcher(message).matches() || IMAP_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);
- matched = true;
- } else {
- Matcher expungeMatcher = IDLE_EXPUNGE_REGEX.matcher(message);
- if (expungeMatcher.matches()) {
- int number = Integer.parseInt(expungeMatcher.group(1));
- pushedData.pushRemoves.add(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;
- }
- }
- private void disconnectAbnormally(String message) {
- try {
- halt();
- // Disconnect abnormally. The user code should reconnect using the mail client.
- errorStack.push(new Error(completions.poll(), message, wireTrace.list()));
- idler.disconnectAsync();
- } finally {
- disconnected();
- }
- }
- private String extractError(Matcher matcher) {
- return (matcher.groupCount()) > 1 ? matcher.group(2) : matcher.group();
- }
- /**
- * 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()));
- final CommandCompletion completion = completions.peek();
- String errorMsg = "Some messages in the batch could not be fetched for user " + config.getUsername();
- RuntimeException ex = new RuntimeException(errorMsg);
- if (completion != null) {
- completion.error(errorMsg, new MailHandlingException(getWireTrace(), errorMsg, ex));
- completions.poll();
- } else {
- throw ex;
- }
- }
- 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();
- }
- }
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
- log.error("Exception caught! Disconnecting...", e.getCause());
- disconnectAbnormally(e.getCause().getMessage());
- }
- public List<String> getCapabilities() {
- return capabilities;
- }
- boolean awaitLogin() {
- try {
- if (!loginSuccess.await(10L, TimeUnit.SECONDS)) {
- disconnectAbnormally("Timed out waiting for login response");
- throw new RuntimeException("Timed out waiting for login response");
- }
- return isLoggedIn();
- } catch (InterruptedException e) {
- errorStack.push(new Error(null, e.getMessage(), wireTrace.list()));
- throw new RuntimeException("Interruption while awaiting server login", e);
- }
- }
- MailClient.WireError lastError() {
- return errorStack.peek() != null ? errorStack.pop() : null;
- }
- List<String> getLastTrace() {
- return wireTrace.list();
- }
- /**
- * Registers a FolderObserver to receive events happening with a particular
- * folder. Typically an IMAP IDLE feature. If called multiple times, will
- * overwrite the currently set observer.
- */
- void observe(FolderObserver observer) {
- synchronized (idleMutex) {
- this.observer = observer;
- pushedData = new PushedData();
- idleAcknowledged.set(false);
- }
- }
- void halt() {
- halt = true;
- }
- public boolean isHalted() {
- return halt;
- }
- public void disconnected() {
- }
- static class Error implements MailClient.WireError {
- final CommandCompletion completion;
- final String error;
- final List<String> wireTrace;
- Error(CommandCompletion completion, String error, List<String> wireTrace) {
- this.completion = completion;
- this.error = error;
- this.wireTrace = wireTrace;
- }
- @Override
- public String message() {
- return error;
- }
- @Override
- public List<String> trace() {
- return wireTrace;
- }
- @Override
- public String expected() {
- return completion == null ? null : completion.toString();
- }
- @Override
- public String toString() {
- StringBuilder sout = new StringBuilder();
- sout.append("WireError: ");
- sout.append("Completion=").append(completion);
- sout.append(", Error: ").append(error);
- sout.append(", Trace:\n");
- for (String s : wireTrace) {
- sout.append(" ").append(s).append("\n");
- }
- return sout.toString();
- }
- }
- @VisibleForTesting
- static class InputBuffer {
- volatile private StringBuilder buffer = new StringBuilder();
- @VisibleForTesting
- List<String> processMessage(String message) {
- // Nuke all CR characters, and we'll only count LF.
- message = message.replaceAll("\r", "");
- // Split leaves a trailing empty line if there's a terminating newline.
- ArrayList<String> split = Lists.newArrayList(message.split("\n", -1));
- Preconditions.checkArgument(split.size() > 0);
- synchronized (this) {
- buffer.append(split.get(0));
- if (split.size() == 1) // no newlines.
- return ImmutableList.of();
- split.set(0, buffer.toString());
- buffer = new StringBuilder().append(split.remove(split.size() - 1));
- }
- return split;
- }
- }
- }