/* ====================================================================
* The Apache Software License, Version 1.1
*
* Copyright (c) 2000-2003 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution,
* if any, must include the following acknowledgment:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgment may appear in the software itself,
* if and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Apache", "Jakarta", "JAMES" and "Apache Software Foundation"
* must not be used to endorse or promote products derived from this
* software without prior written permission. For written
* permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache",
* nor may "Apache" appear in their name, without prior written
* permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
* Portions of this software are based upon public domain software
* originally written at the National Center for Supercomputing Applications,
* University of Illinois, Urbana-Champaign.
*/
package org.apache.james.imapserver.commands;
import org.apache.james.imapserver.ProtocolException;
import org.apache.james.imapserver.ImapRequestLineReader;
import org.apache.james.imapserver.ImapConstants;
import org.apache.james.imapserver.store.MessageFlags;
import org.apache.james.util.Assert;
import java.util.Date;
import java.util.TimeZone;
import java.util.List;
import java.util.ArrayList;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.ParseException;
/**
*
* @author Darrell DeBoer <darrell@apache.org>
*
* @version $Revision: 1.5.2.2 $
*/
public class CommandParser
{
private static final char[] EMPTY_CHAR_ARRAY = new char[0];
/**
* Reads an argument of type "atom" from the request.
*/
public String atom( ImapRequestLineReader request ) throws ProtocolException
{
return consumeWord( request, new ATOM_CHARValidator() );
}
/**
* Reads a command "tag" from the request.
*/
public String tag(ImapRequestLineReader request) throws ProtocolException
{
CharacterValidator validator = new TagCharValidator();
return consumeWord( request, validator );
}
/**
* Reads an argument of type "astring" from the request.
*/
public String astring(ImapRequestLineReader request) throws ProtocolException
{
char next = request.nextWordChar();
switch ( next ) {
case '"':
return consumeQuoted( request );
case '{':
return consumeLiteral( request );
default:
return atom( request );
}
}
/**
* Reads an argument of type "nstring" from the request.
*/
public String nstring( ImapRequestLineReader request ) throws ProtocolException
{
char next = request.nextWordChar();
switch ( next ) {
case '"':
return consumeQuoted( request );
case '{':
return consumeLiteral( request );
default:
String value = atom( request );
if ( "NIL".equals( value ) ) {
return null;
}
else {
throw new ProtocolException( "Invalid nstring value: valid values are '\"...\"', '{12} CRLF *CHAR8', and 'NIL'." );
}
}
}
/**
* Reads a "mailbox" argument from the request. Not implemented *exactly* as per spec,
* since a quoted or literal "inbox" still yeilds "INBOX"
* (ie still case-insensitive if quoted or literal). I think this makes sense.
*
* mailbox ::= "INBOX" / astring
* ;; INBOX is case-insensitive. All case variants of
* ;; INBOX (e.g. "iNbOx") MUST be interpreted as INBOX
* ;; not as an astring.
*/
public String mailbox( ImapRequestLineReader request ) throws ProtocolException
{
String mailbox = astring( request );
if ( mailbox.equalsIgnoreCase( ImapConstants.INBOX_NAME ) ) {
return ImapConstants.INBOX_NAME;
}
else {
return mailbox;
}
}
/**
* Reads a "date-time" argument from the request.
* TODO handle timezones properly
*/
public Date dateTime( ImapRequestLineReader request ) throws ProtocolException
{
char next = request.nextWordChar();
String dateString;
if ( next == '"' ) {
dateString = consumeQuoted( request );
}
else {
throw new ProtocolException( "DateTime values must be quoted." );
}
DateFormat dateFormat = new SimpleDateFormat( "dd-MMM-yyyy hh:mm:ss zzzz" );
try {
return dateFormat.parse( dateString );
}
catch ( ParseException e ) {
throw new ProtocolException( "Invalid date format." );
}
}
/**
* Reads a "date" argument from the request.
* TODO handle timezones properly
*/
public Date date( ImapRequestLineReader request ) throws ProtocolException
{
char next = request.nextWordChar();
String dateString;
if ( next == '"' ) {
dateString = consumeQuoted( request );
}
else {
dateString = atom( request );
}
DateFormat dateFormat = new SimpleDateFormat( "dd-MMM-yyyy" );
try {
return dateFormat.parse( dateString );
}
catch ( ParseException e ) {
throw new ProtocolException( "Invalid date format." );
}
}
/**
* Reads the next "word from the request, comprising all characters up to the next SPACE.
* Characters are tested by the supplied CharacterValidator, and an exception is thrown
* if invalid characters are encountered.
*/
protected String consumeWord( ImapRequestLineReader request,
CharacterValidator validator )
throws ProtocolException
{
StringBuffer atom = new StringBuffer();
char next = request.nextWordChar();
while( ! isWhitespace( next ) ) {
if ( validator.isValid( next ) )
{
atom.append( next );
request.consume();
}
else {
throw new ProtocolException( "Invalid character: '" + next + "'" );
}
next = request.nextChar();
}
return atom.toString();
}
private boolean isWhitespace( char next )
{
return ( next == ' ' || next == '\n' || next == '\r' || next == '\t' );
}
/**
* Reads an argument of type "literal" from the request, in the format:
* "{" charCount "}" CRLF *CHAR8
* Note before calling, the request should be positioned so that nextChar
* is '{'. Leading whitespace is not skipped in this method.
*/
protected String consumeLiteral( ImapRequestLineReader request )
throws ProtocolException
{
// The 1st character must be '{'
consumeChar( request, '{' );
StringBuffer digits = new StringBuffer();
char next = request.nextChar();
while ( next != '}' && next != '+' )
{
digits.append( next );
request.consume();
next = request.nextChar();
}
// If the number is *not* suffixed with a '+', we *are* using a synchronized literal,
// and we need to send command continuation request before reading data.
boolean synchronizedLiteral = true;
// '+' indicates a non-synchronized literal (no command continuation request)
if ( next == '+' ) {
synchronizedLiteral = false;
consumeChar(request, '+' );
}
// Consume the '}' and the newline
consumeChar( request, '}' );
consumeCRLF( request );
if ( synchronizedLiteral ) {
request.commandContinuationRequest();
}
int size = Integer.parseInt( digits.toString() );
byte[] buffer = new byte[size];
request.read( buffer );
return new String( buffer );
}
/**
* Consumes a CRLF from the request.
* TODO we're being liberal, the spec insists on \r\n for new lines.
* @param request
* @throws ProtocolException
*/
private void consumeCRLF( ImapRequestLineReader request )
throws ProtocolException
{
char next = request.nextChar();
if ( next != '\n' ) {
consumeChar( request, '\r' );
}
consumeChar( request, '\n' );
}
/**
* Consumes the next character in the request, checking that it matches the
* expected one. This method should be used when the
*/
protected void consumeChar( ImapRequestLineReader request, char expected )
throws ProtocolException
{
char consumed = request.consume();
if ( consumed != expected ) {
throw new ProtocolException( "Expected:'" + expected + "' found:'" + consumed + "'" );
}
}
/**
* Reads a quoted string value from the request.
*/
protected String consumeQuoted( ImapRequestLineReader request )
throws ProtocolException
{
// The 1st character must be '"'
consumeChar(request, '"' );
StringBuffer quoted = new StringBuffer();
char next = request.nextChar();
while( next != '"' ) {
if ( next == '\\' ) {
request.consume();
next = request.nextChar();
if ( ! isQuotedSpecial( next ) ) {
throw new ProtocolException( "Invalid escaped character in quote: '" +
next + "'" );
}
}
quoted.append( next );
request.consume();
next = request.nextChar();
}
consumeChar( request, '"' );
return quoted.toString();
}
/**
* Reads a base64 argument from the request.
*/
public byte[] base64( ImapRequestLineReader request ) throws ProtocolException
{
return null;
}
/**
* Reads a "flags" argument from the request.
*/
public MessageFlags flagList( ImapRequestLineReader request ) throws ProtocolException
{
MessageFlags flags = new MessageFlags();
request.nextWordChar();
consumeChar( request, '(' );
CharacterValidator validator = new NoopCharValidator();
String nextWord = consumeWord( request, validator );
while ( ! nextWord.endsWith(")" ) ) {
setFlag( nextWord, flags );
nextWord = consumeWord( request, validator );
}
// Got the closing ")", may be attached to a word.
if ( nextWord.length() > 1 ) {
setFlag( nextWord.substring(0, nextWord.length() - 1 ), flags );
}
return flags;
}
public void setFlag( String flagString, MessageFlags flags ) throws ProtocolException
{
if ( flagString.equalsIgnoreCase( MessageFlags.ANSWERED ) ) {
flags.setAnswered( true );
}
else if ( flagString.equalsIgnoreCase( MessageFlags.DELETED ) ) {
flags.setDeleted( true );
}
else if ( flagString.equalsIgnoreCase( MessageFlags.DRAFT ) ) {
flags.setDraft( true );
}
else if ( flagString.equalsIgnoreCase( MessageFlags.FLAGGED ) ) {
flags.setFlagged( true );
}
else if ( flagString.equalsIgnoreCase( MessageFlags.SEEN ) ) {
flags.setSeen( true );
}
else {
throw new ProtocolException( "Invalid flag string." );
}
}
/**
* Reads an argument of type "number" from the request.
*/
public long number( ImapRequestLineReader request ) throws ProtocolException
{
String digits = consumeWord( request, new DigitCharValidator() );
return Long.parseLong( digits );
}
/**
* Reads an argument of type "nznumber" (a non-zero number)
* (NOTE this isn't strictly as per the spec, since the spec disallows
* numbers such as "0123" as nzNumbers (although it's ok as a "number".
* I think the spec is a bit shonky.)
*/
public long nzNumber( ImapRequestLineReader request ) throws ProtocolException
{
long number = number( request );
if ( number == 0 ) {
throw new ProtocolException( "Zero value not permitted." );
}
return number;
}
private boolean isCHAR( char chr )
{
return ( chr >= 0x01 && chr <= 0x7f );
}
private boolean isCHAR8( char chr )
{
return ( chr >= 0x01 && chr <= 0xff );
}
protected boolean isListWildcard( char chr )
{
return ( chr == '*' || chr == '%' );
}
private boolean isQuotedSpecial( char chr )
{
return ( chr == '"' || chr == '\\' );
}
/**
* Consumes the request up to and including the eno-of-line.
* @param request The request
* @throws ProtocolException If characters are encountered before the endLine.
*/
public void endLine( ImapRequestLineReader request ) throws ProtocolException
{
request.eol();
}
/**
* Reads a "message set" argument, and parses into an IdSet.
* Currently only supports a single range of values.
*/
public IdSet set( ImapRequestLineReader request )
throws ProtocolException
{
CharacterValidator validator = new MessageSetCharValidator();
String nextWord = consumeWord( request, validator );
int commaPos = nextWord.indexOf( ',' );
if ( commaPos == -1 ) {
return singleRangeSet( nextWord );
}
CompoundIdSet compoundSet = new CompoundIdSet();
int pos = 0;
while ( commaPos != -1 ) {
String range = nextWord.substring( pos, commaPos );
IdSet set = singleRangeSet( range );
compoundSet.addIdSet( set );
pos = commaPos + 1;
commaPos = nextWord.indexOf( ',', pos );
}
String range = nextWord.substring( pos );
compoundSet.addIdSet( singleRangeSet( range ) );
return compoundSet;
}
private IdSet singleRangeSet( String range ) throws ProtocolException
{
int pos = range.indexOf( ':' );
try {
if ( pos == -1 ) {
long value = parseLong( range );
return new HighLowIdSet( value, value );
}
else {
long lowVal = parseLong( range.substring(0, pos ) );
long highVal = parseLong( range.substring( pos + 1 ) );
return new HighLowIdSet( lowVal, highVal );
}
}
catch ( NumberFormatException e ) {
throw new ProtocolException( "Invalid message set.");
}
}
private long parseLong( String value ) {
if ( value.length() == 1 && value.charAt(0) == '*' ) {
return Long.MAX_VALUE;
}
return Long.parseLong( value );
}
/**
* Provides the ability to ensure characters are part of a permitted set.
*/
protected interface CharacterValidator
{
/**
* Validates the supplied character.
* @param chr The character to validate.
* @return <code>true</code> if chr is valid, <code>false</code> if not.
*/
boolean isValid( char chr );
}
protected class NoopCharValidator implements CharacterValidator
{
public boolean isValid( char chr )
{
return true;
}
}
protected class ATOM_CHARValidator implements CharacterValidator
{
public boolean isValid( char chr )
{
return ( isCHAR( chr ) && !isAtomSpecial( chr ) &&
!isListWildcard( chr ) && !isQuotedSpecial( chr ) );
}
private boolean isAtomSpecial( char chr )
{
return ( chr == '(' ||
chr == ')' ||
chr == '{' ||
chr == ' ' ||
chr == Character.CONTROL );
}
}
protected class DigitCharValidator implements CharacterValidator
{
public boolean isValid( char chr )
{
return ( ( chr >= '0' && chr <= '9' ) ||
chr == '*' );
}
}
private class TagCharValidator extends ATOM_CHARValidator
{
public boolean isValid( char chr )
{
if ( chr == '+' ) return false;
return super.isValid( chr );
}
}
private class MessageSetCharValidator implements CharacterValidator
{
public boolean isValid( char chr )
{
return ( isDigit( chr ) ||
chr == ':' ||
chr == '*' ||
chr == ',' );
}
private boolean isDigit( char chr )
{
return '0' <= chr && chr <= '9';
}
}
private class HighLowIdSet implements IdSet
{
private long lowVal;
private long highVal;
public HighLowIdSet( long lowVal, long highVal )
{
this.lowVal = lowVal;
this.highVal = highVal;
}
public boolean includes( long value ) {
return ( lowVal <= value ) && ( value <= highVal );
}
}
private class CompoundIdSet implements IdSet
{
private List idSets = new ArrayList();
void addIdSet( IdSet set ) {
idSets.add( set );
}
public boolean includes( long value )
{
for ( int i = 0; i < idSets.size(); i++ ) {
IdSet idSet = ( IdSet ) idSets.get( i );
if ( idSet.includes( value ) ) {
return true;
}
}
return false;
}
}
}