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

com.google.sitebricks.mail.MailClientHandler Maven / Gradle / Ivy

The newest version!
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 [email protected] (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 logAllMessagesForUsers = new ConcurrentHashMap();

  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 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 errorStack = new LinkedBlockingDeque();
  private final Queue completions =
      new ConcurrentLinkedQueue();
  private volatile PushedData pushedData;

  private final BoundedDiscardingList commandTrace = new BoundedDiscardingList(10);
  private final BoundedDiscardingList wireTrace = new BoundedDiscardingList(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 getLogAllMessagesFor() {
    return logAllMessagesForUsers.keySet();
  }

  public List getCommandTrace() {
    return commandTrace.list();
  }

  public List getWireTrace() {
    return wireTrace.list();
  }

  public boolean isLoggedIn() {
    return loginSuccess.getCount() == 0;
  }

  private static class PushedData {
    volatile boolean idleExitSent = false;
    // guarded by idleMutex.
  	final ArrayList pushAdds = new ArrayList();
    // guarded by idleMutex.
  	final ArrayList pushRemoves = new ArrayList();
  }

  // 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 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 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 wireTrace;

    Error(CommandCompletion completion, String error, List wireTrace) {
      this.completion = completion;
      this.error = error;
      this.wireTrace = wireTrace;
    }

    @Override
    public String message() {
      return error;
    }

    @Override
    public List 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 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 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;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy