package com.google.sitebricks.mail.imap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import org.apache.james.mime4j.codec.DecodeMonitor;
import org.apache.james.mime4j.codec.DecoderUtil;
import java.util.Collection;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author dhanji@gmail.com (Dhanji R. Prasanna)
*/
public class Parsing {
static List<String> readAddresses(Queue<String> tokens) {
// Weird base case where we don't get nil, but instead get an empty address set.
if (tokens.isEmpty())
return ImmutableList.of();
if (isValid(tokens.peek())) {
eat(tokens, "(");
List<String> addresses = Lists.newArrayList();
while ("(".equals(tokens.peek()))
addresses.add(readAddress(tokens));
eat(tokens, ")");
return addresses;
}
tokens.poll(); // Discard 'NIL'
return null;
}
static String readAddress(Queue<String> tokens) {
// := ( a b c d )
StringBuilder address = new StringBuilder();
eat(tokens, "(");
String namePiece = match(tokens, String.class);
if (namePiece != null)
namePiece = namePiece.replace("\\", "");
String sourceRoute = match(tokens, String.class);
String mailboxName = match(tokens, String.class); // mail username
String hostname = match(tokens, String.class); // domain
eat(tokens, ")");
if (namePiece != null)
address.append('"').append(decode(namePiece)).append("\" ");
// I duno what source route is for ...
return address.append(mailboxName).append('@').append(hostname).toString();
}
@SuppressWarnings("unchecked")
static <T> T match(Queue<String> tokens, Class<T> clazz) {
String token = tokens.poll();
if (!isValid(token))
return null;
if (String.class == clazz) {
if (token.startsWith("\"") && token.endsWith("\""))
return (T)token.substring(1, token.length() - 1);
else
return (T)token;
} else if (int.class == clazz) {
return (T) Integer.valueOf(token);
} else if (long.class == clazz) {
return (T) Long.valueOf(token);
}
throw new IllegalArgumentException("Unsupported type: " + clazz.getName());
}
static String matchAnyOf(Queue<String> tokens, String... match) {
for (String piece : match) {
if (piece.equalsIgnoreCase(tokens.peek())) {
return tokens.poll();
}
}
// None found.
return null;
}
static void eat(Queue<String> tokens, String... match) {
for (String piece : match) {
if (piece.equalsIgnoreCase(tokens.peek())) {
tokens.poll();
} else
throw new IllegalArgumentException("Expected token " + piece + " but found "
+ tokens.peek() +" in [" + tokens + "]");
}
}
static boolean isValid(String token) {
return null != token && !"NIL".equalsIgnoreCase(token);
}
static String normalizeDateToken(String token) {
return token.replaceAll(" \\(.+\\)$", "").replaceAll("[ ]+", " ").trim();
}
static Queue<String> tokenize(String message) {
Queue<String> tokens = new LinkedBlockingQueue<String>();
char[] charArray = message.toCharArray();
boolean inString = false;
StringBuilder currentToken = new StringBuilder();
boolean escaped = false;
for (int i = 0, charArrayLength = charArray.length; i < charArrayLength; i++) {
char c = charArray[i];
if (c == '\\') {
if (escaped) { // i.e. two backlashes in a row..
currentToken.append('\\');
escaped = false;
} else {
escaped = true;
}
continue;
}
// String checks, but only if we're not an escaped quote character.
if (c == '"' && !escaped) {
if (inString) {
inString = false;
// Bake string token.
currentToken.append('"');
bakeToken(tokens, currentToken);
currentToken = new StringBuilder();
} else {
inString = true;
// We've entered a string, so bake whatever has come so far.
bakeToken(tokens, currentToken);
currentToken = new StringBuilder();
currentToken.append('"');
}
continue;
}
if (!inString) {
if (c == '(') {
bakeToken(tokens, currentToken);
tokens.add("(");
currentToken = new StringBuilder();
continue;
} else if (c == ')') {
bakeToken(tokens, currentToken);
tokens.add(")");
currentToken = new StringBuilder();
continue;
// Otherwise whitespace is a delimiter for non-strings. EXCEPT when
// preceeded by '\', which is an escape character.
} else if (c == ' ' && !escaped) {
bakeToken(tokens, currentToken);
currentToken = new StringBuilder();
continue;
}
}
// Only swallow backslashes if this character was escaped inside a string.
if (escaped && !inString) {
currentToken.append('\\');
}
currentToken.append(c);
escaped = false;
}
// Close up dangling tokens.
bakeToken(tokens, currentToken);
return tokens;
}
static void bakeToken(Collection<String> tokens, StringBuilder currentToken) {
String trim = currentToken.toString().trim();
if (trim.length() > 0)
tokens.add(trim);
}
public static boolean startsWithIgnoreCase(String toTest, String prefix) {
if (null == toTest)
return (null == prefix);
return toTest.toLowerCase().startsWith(prefix.toLowerCase());
}
public static String stripQuotes(String var) {
if (var.startsWith("\"") && var.endsWith("\""))
return var.substring(1, var.length() - 1);
return var;
}
public static String decode(String str) {
// decode as per http://www.ietf.org/rfc/rfc2047.txt
return str == null
? null
: str.isEmpty()
? str
: DecoderUtil.decodeEncodedWords(str, DecodeMonitor.SILENT);
}
public static Collection<String> getKeyVariations(Multimap<String, String> headers, String... keys) {
for (String key : keys) {
Collection<String> values = headers.get(key);
if (!values.isEmpty())
return values;
}
return ImmutableList.of();
}
}