SearchCommandParser.java
/*
* Copyright (c) 2014 Wael Chatila / Icegreen Technologies. All Rights Reserved.
* This software is released under the Apache license 2.0
* This file has been modified by the copyright holder.
* Original file can be found at http://james.apache.org
*/
package com.icegreen.greenmail.imap.commands;
import com.icegreen.greenmail.imap.ImapRequestLineReader;
import com.icegreen.greenmail.imap.ProtocolException;
import jakarta.mail.search.AndTerm;
import jakarta.mail.search.NotTerm;
import jakarta.mail.search.OrTerm;
import jakarta.mail.search.SearchTerm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Deque;
import java.util.LinkedList;
import static com.icegreen.greenmail.imap.commands.IdRange.SEQUENCE;
/**
* Handles processing for the SEARCH imap command.
* <p>
* <a href="https://tools.ietf.org/html/rfc3501#section-6.4.4">https://tools.ietf.org/html/rfc3501#section-6.4.4</a>
*
* @author Darrell DeBoer <darrell@apache.org>
* @author Marcel May
*/
class SearchCommandParser extends CommandParser {
private final Logger log = LoggerFactory.getLogger(SearchCommandParser.class);
private static final String CHARSET_TOKEN = "CHARSET";
/**
* Marker for stack when parsing search
*/
protected enum SearchOperator {
AND, OR, NOT, GROUP /* Pseudo operator */
}
/**
* IMAP <a href="https://www.rfc-editor.org/rfc/rfc3501.html#section-4">RFC 3501 Data Formats</a>
*/
protected enum DataFormats {
ATOM,
STRING
// Complete further types on demand
}
/**
* Parses the request argument into a valid search term. Not yet fully implemented - see SearchKey enum.
* <p>
* Other searches will return everything for now.
* Throws an UnsupportedCharsetException if provided CHARSET is not supported.
*/
public SearchTerm searchTerm(ImapRequestLineReader request)
throws ProtocolException {
Charset charset = StandardCharsets.US_ASCII; // Default
// Stack contains mix of SearchOperator and SearchTerm instances
// and will be processed in two steps
Deque<Object> stack = new LinkedList<>();
stack.push(SearchOperator.GROUP); // So that the final list of terms will be wrapped in an AndTerm
// Phase one : parse search query into SearchOperators/simple search terms and put them on the stack.
char next;
while ((next = request.nextChar()) != '\n' && next != CHR_CR /* \r */) {
next = request.consumeAll(CHR_SPACE);
if (isAtomSpecial(next)) {
// Parentheses?
if (next == '(') {
request.consume();
request.consumeAll(CHR_SPACE);
stack.push(SearchOperator.GROUP);
} else if (next == ')') {
request.consume();
request.consumeAll(CHR_SPACE);
// Resolve group to single term
handleGroup(stack);
} else {
throw new IllegalStateException("Unsupported atom special char <" + next + ">");
}
} else {
String token = atomOnly(request);
// Sequence-set?
if (SEQUENCE.matcher(token).matches()) {
stack.push(SearchTermBuilder.create(SearchKey.SEQUENCE_SET).addParameter(token).build());
}
// Charset?
else if (CHARSET_TOKEN.equals(token)) {
// If the server does not support the specified [CHARSET], it MUST
// return a tagged NO response (not a BAD). This response SHOULD
// contain the BADCHARSET response code, which MAY list the
// [CHARSET]s supported by the server.
request.consumeAll(CHR_SPACE);
final String charsetName = astring(request);
try {
charset = Charset.forName(charsetName);
} catch (UnsupportedCharsetException ex) {
log.error("Unsupported charset '{}'", charsetName);
throw ex;
}
} else {
// Term?
SearchKey key = SearchKey.valueOf(token);
// Operator?
if (key == SearchKey.NOT) {
stack.push(SearchOperator.NOT);
} else if (key == SearchKey.OR) {
stack.push(SearchOperator.OR);
} else {
// No operator
SearchTermBuilder b = SearchTermBuilder.create(key);
if (b.expectsParameter()) {
for (int pi = 0; pi < key.getNumberOfParameters(); pi++) {
request.consumeAll(CHR_SPACE);
handleSearchArg(request, key, b, charset);
}
}
stack.push(b.build());
}
}
}
}
// Phase two : Build search terms by operators
return handleOperators(stack);
}
private void handleGroup(Deque<Object> stack) {
Deque<Object> groupItems = new LinkedList<>();
Object item;
while ((item = stack.pop()) != SearchOperator.GROUP) {
groupItems.addLast(item);
}
final SearchTerm groupTerm = handleOperators(groupItems);
stack.push(groupTerm);
}
private void handleSearchArg(ImapRequestLineReader request, SearchKey key, SearchTermBuilder searchTermBuilder, Charset charset) throws ProtocolException {
String paramValue;
switch (key.getArgDataFormat()) {
case STRING:
paramValue = string(request, charset);
break;
case ATOM:
paramValue = atomOnly(request);
break;
default:
throw new IllegalStateException("Argument type " + key.getArgDataFormat() + " not implemented for key " + key);
}
searchTermBuilder.addParameter(paramValue);
}
private SearchTerm handleOperators(Deque<Object> stack) {
// Must be single term
if (stack.size() == 1) {
return (SearchTerm) stack.pop();
}
LinkedList<SearchTerm> params = new LinkedList<>();
while (!stack.isEmpty()) {
final Object o = stack.pop();
if (SearchOperator.OR == o) {
final SearchTerm term1 = params.pop();
final SearchTerm term2 = params.pop();
params.push(new OrTerm(term1, term2));
} else if (SearchOperator.NOT == o) {
params.push(new NotTerm(params.pop()));
} else if (SearchOperator.AND == o) {
final SearchTerm term1 = params.pop();
final SearchTerm term2 = params.pop();
params.push(new AndTerm(term1, term2));
} else if (SearchOperator.GROUP == o) {
// Size 0: Empty braces? Do nothing.
// Size 1: Do nothing, keep item on params stack, no wrapping needed
// Size > 1 : Wrap with AndTerm
if (params.size() > 1) {
SearchTerm[] items = params.toArray(new SearchTerm[0]);
params.clear();
params.push(new AndTerm(items));
}
} else if (o instanceof SearchTerm) {
params.push((SearchTerm) o);
} else {
throw new IllegalStateException("Unsupported stack item " + o);
}
}
if (params.size() > 1) {
SearchTerm[] items = params.toArray(new SearchTerm[0]);
return new AndTerm(items);
} else {
return params.pop();
}
}
}