/* Copyright (c) 2006 - 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "imapparser_p.h" #include #include #include using namespace Akonadi; class ImapParser::Private { public: QByteArray tagBuffer; QByteArray dataBuffer; int parenthesesCount; qint64 literalSize; bool continuation; // returns true if readBuffer contains a literal start and sets // parser state accordingly bool checkLiteralStart( const QByteArray &readBuffer, int pos = 0 ) { if ( readBuffer.trimmed().endsWith( '}' ) ) { const int begin = readBuffer.lastIndexOf( '{' ); const int end = readBuffer.lastIndexOf( '}' ); // new literal in previous literal data block if ( begin < pos ) { return false; } // TODO error handling literalSize = readBuffer.mid( begin + 1, end - begin - 1 ).toLongLong(); // empty literal if ( literalSize == 0 ) { return false; } continuation = true; dataBuffer.reserve( dataBuffer.size() + literalSize + 1 ); return true; } return false; } }; namespace { template int parseParenthesizedListHelper( const QByteArray &data, T &result, int start ) { result.clear(); if ( start >= data.length() ) { return data.length(); } const int begin = data.indexOf( '(', start ); if ( begin < 0 ) { return start; } int count = 0; int sublistBegin = start; bool insideQuote = false; for ( int i = begin + 1; i < data.length(); ++i ) { const char currentChar = data[i]; if ( currentChar == '(' && !insideQuote ) { ++count; if ( count == 1 ) { sublistBegin = i; } continue; } if ( currentChar == ')' && !insideQuote ) { if ( count <= 0 ) { return i + 1; } if ( count == 1 ) { result.append( data.mid( sublistBegin, i - sublistBegin + 1 ) ); } --count; continue; } if ( currentChar == ' ' || currentChar == '\n' || currentChar == '\r' ) { continue; } if ( count == 0 ) { QByteArray ba; const int consumed = ImapParser::parseString( data, ba, i ); i = consumed - 1; // compensate for the for loop increment result.append( ba ); } else if ( count > 0 ) { if ( currentChar == '"' ) { insideQuote = !insideQuote; } else if ( currentChar == '\\' && insideQuote ) { ++i; continue; } } } return data.length(); } } int ImapParser::parseParenthesizedList( const QByteArray &data, QVarLengthArray &result, int start ) { return parseParenthesizedListHelper( data, result, start ); } int ImapParser::parseParenthesizedList( const QByteArray &data, QList &result, int start ) { return parseParenthesizedListHelper( data, result, start ); } int ImapParser::parseString( const QByteArray &data, QByteArray &result, int start ) { int begin = stripLeadingSpaces( data, start ); result.clear(); if ( begin >= data.length() ) { return data.length(); } // literal string // TODO: error handling if ( data[begin] == '{' ) { int end = data.indexOf( '}', begin ); Q_ASSERT( end > begin ); int size = data.mid( begin + 1, end - begin - 1 ).toInt(); // strip CRLF begin = end + 1; if ( begin < data.length() && data[begin] == '\r' ) { ++begin; } if ( begin < data.length() && data[begin] == '\n' ) { ++begin; } end = begin + size; result = data.mid( begin, end - begin ); return end; } // quoted string return parseQuotedString( data, result, begin ); } int ImapParser::parseQuotedString( const QByteArray &data, QByteArray &result, int start ) { int begin = stripLeadingSpaces( data, start ); int end = begin; result.clear(); if ( begin >= data.length() ) { return data.length(); } bool foundSlash = false; // quoted string if ( data[begin] == '"' ) { ++begin; for ( int i = begin; i < data.length(); ++i ) { const char ch = data.at( i ); if ( foundSlash ) { foundSlash = false; if ( ch == 'r' ) { result += '\r'; } else if ( ch == 'n' ) { result += '\n'; } else if ( ch == '\\' ) { result += '\\'; } else if ( ch == '\"' ) { result += '\"'; } else { //TODO: this is actually an error result += ch; } continue; } if ( ch == '\\' ) { foundSlash = true; continue; } if ( ch == '"' ) { end = i + 1; // skip the '"' break; } result += ch; } } // unquoted string else { bool reachedInputEnd = true; for ( int i = begin; i < data.length(); ++i ) { const char ch = data.at( i ); if ( ch == ' ' || ch == '(' || ch == ')' || ch == '\n' || ch == '\r' ) { end = i; reachedInputEnd = false; break; } if ( ch == '\\' ) { foundSlash = true; } } if ( reachedInputEnd ) { end = data.length(); } result = data.mid( begin, end - begin ); // transform unquoted NIL if ( result == "NIL" ) { result.clear(); } // strip quotes if ( foundSlash ) { while ( result.contains( "\\\"" ) ) { result.replace( "\\\"", "\"" ); } while ( result.contains( "\\\\" ) ) { result.replace( "\\\\", "\\" ); } } } return end; } int ImapParser::stripLeadingSpaces( const QByteArray &data, int start ) { for ( int i = start; i < data.length(); ++i ) { if ( data[i] != ' ' ) { return i; } } return data.length(); } int ImapParser::parenthesesBalance( const QByteArray &data, int start ) { int count = 0; bool insideQuote = false; for ( int i = start; i < data.length(); ++i ) { const char ch = data[i]; if ( ch == '"' ) { insideQuote = !insideQuote; continue; } if ( ch == '\\' && insideQuote ) { ++i; continue; } if ( ch == '(' && !insideQuote ) { ++count; continue; } if ( ch == ')' && !insideQuote ) { --count; continue; } } return count; } QByteArray ImapParser::join( const QList &list, const QByteArray &separator ) { // shortcuts for the easy cases if ( list.isEmpty() ) { return QByteArray(); } if ( list.size() == 1 ) { return list.first(); } // avoid expensive realloc's by determining the size beforehand QList::const_iterator it = list.constBegin(); const QList::const_iterator endIt = list.constEnd(); int resultSize = ( list.size() - 1 ) * separator.size(); for ( ; it != endIt; ++it ) { resultSize += ( *it ).size(); } QByteArray result; result.reserve( resultSize ); it = list.constBegin(); result += ( *it ); ++it; for ( ; it != endIt; ++it ) { result += separator; result += ( *it ); } return result; } QByteArray ImapParser::join( const QSet &set, const QByteArray &separator ) { const QList list = QList::fromSet( set ); return ImapParser::join( list, separator ); } int ImapParser::parseString( const QByteArray &data, QString &result, int start ) { QByteArray tmp; const int end = parseString( data, tmp, start ); result = QString::fromUtf8( tmp ); return end; } int ImapParser::parseNumber( const QByteArray &data, qint64 &result, bool *ok, int start ) { if ( ok ) { *ok = false; } int pos = stripLeadingSpaces( data, start ); if ( pos >= data.length() ) { return data.length(); } int begin = pos; for ( ; pos < data.length(); ++pos ) { if ( !isdigit( data.at( pos ) ) ) { break; } } const QByteArray tmp = data.mid( begin, pos - begin ); result = tmp.toLongLong( ok ); return pos; } QByteArray ImapParser::quote( const QByteArray &data ) { if ( data.isEmpty() ) { return QByteArray( "\"\"" ); } const int inputLength = data.length(); int stuffToQuote = 0; for ( int i = 0; i < inputLength; ++i ) { const char ch = data.at( i ); if ( ch == '"' || ch == '\\' || ch == '\n' || ch == '\r' ) { ++stuffToQuote; } } QByteArray result; result.reserve( inputLength + stuffToQuote + 2 ); result += '"'; // shortcut for the case that we don't need to quote anything at all if ( stuffToQuote == 0 ) { result += data; } else { for ( int i = 0; i < inputLength; ++i ) { const char ch = data.at( i ); if ( ch == '\n' ) { result += "\\n"; continue; } if ( ch == '\r' ) { result += "\\r"; continue; } if ( ch == '"' || ch == '\\' ) { result += '\\'; } result += ch; } } result += '"'; return result; } int ImapParser::parseSequenceSet( const QByteArray &data, ImapSet &result, int start ) { int begin = stripLeadingSpaces( data, start ); qint64 value = -1, lower = -1, upper = -1; for ( int i = begin; i < data.length(); ++i ) { if ( data[i] == '*' ) { value = 0; } else if ( data[i] == ':' ) { lower = value; } else if ( isdigit( data[i] ) ) { bool ok = false; i = parseNumber( data, value, &ok, i ); Q_ASSERT( ok ); // TODO handle error --i; } else { upper = value; if ( lower < 0 ) { lower = value; } result.add( ImapInterval( lower, upper ) ); lower = -1; upper = -1; value = -1; if ( data[i] != ',' ) { return i; } } } // take care of left-overs at input end upper = value; if ( lower < 0 ) { lower = value; } if ( lower >= 0 && upper >= 0 ) { result.add( ImapInterval( lower, upper ) ); } return data.length(); } int ImapParser::parseDateTime( const QByteArray &data, QDateTime &dateTime, int start ) { // Syntax: // date-time = DQUOTE date-day-fixed "-" date-month "-" date-year // SP time SP zone DQUOTE // date-day-fixed = (SP DIGIT) / 2DIGIT // ; Fixed-format version of date-day // date-month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / // "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec" // date-year = 4DIGIT // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT // ; Hours minutes seconds // zone = ("+" / "-") 4DIGIT // ; Signed four-digit value of hhmm representing // ; hours and minutes east of Greenwich (that is, // ; the amount that the given time differs from // ; Universal Time). Subtracting the timezone // ; from the given time will give the UT form. // ; The Universal Time zone is "+0000". // Example : "28-May-2006 01:03:35 +0200" // Position: 0123456789012345678901234567 // 1 2 int pos = stripLeadingSpaces( data, start ); if ( data.length() <= pos ) { return pos; } bool quoted = false; if ( data[pos] == '"' ) { quoted = true; ++pos; if ( data.length() <= pos + 26 ) { return start; } } else { if ( data.length() < pos + 26 ) { return start; } } bool ok = true; const int day = ( data[pos] == ' ' ? data[pos + 1] - '0' // single digit day : data.mid( pos, 2 ).toInt( &ok ) ); if ( !ok ) { return start; } pos += 3; const QByteArray shortMonthNames( "janfebmaraprmayjunjulaugsepoctnovdec" ); int month = shortMonthNames.indexOf( data.mid( pos, 3 ).toLower() ); if ( month == -1 ) { return start; } month = month / 3 + 1; pos += 4; const int year = data.mid( pos, 4 ).toInt( &ok ); if ( !ok ) { return start; } pos += 5; const int hours = data.mid( pos, 2 ).toInt( &ok ); if ( !ok ) { return start; } pos += 3; const int minutes = data.mid( pos, 2 ).toInt( &ok ); if ( !ok ) { return start; } pos += 3; const int seconds = data.mid( pos, 2 ).toInt( &ok ); if ( !ok ) { return start; } pos += 4; const int tzhh = data.mid( pos, 2 ).toInt( &ok ); if ( !ok ) { return start; } pos += 2; const int tzmm = data.mid( pos, 2 ).toInt( &ok ); if ( !ok ) { return start; } int tzsecs = tzhh * 60 * 60 + tzmm * 60; if ( data[pos - 3] == '-' ) { tzsecs = -tzsecs; } const QDate date( year, month, day ); const QTime time( hours, minutes, seconds ); dateTime = QDateTime( date, time, Qt::UTC ); if ( !dateTime.isValid() ) { return start; } dateTime = dateTime.addSecs( -tzsecs ); pos += 2; if ( data.length() <= pos || !quoted ) { return pos; } if ( data[pos] == '"' ) { ++pos; } return pos; } void ImapParser::splitVersionedKey( const QByteArray &data, QByteArray &key, int &version ) { if ( data.contains( '[' ) && data.contains( ']' ) ) { const int startPos = data.indexOf( '[' ); const int endPos = data.indexOf( ']' ); if ( startPos != -1 && endPos != -1 && endPos > startPos ) { bool ok = false; version = data.mid( startPos + 1, endPos - startPos - 1 ).toInt( &ok ); if ( !ok ) { version = 0; } key = data.left( startPos ); } } else { key = data; version = 0; } } ImapParser::ImapParser() : d ( new Private ) { reset(); } ImapParser::~ImapParser() { delete d; } bool ImapParser::parseNextLine( const QByteArray &readBuffer ) { d->continuation = false; // first line, get the tag if ( d->tagBuffer.isEmpty() ) { const int startOfData = ImapParser::parseString( readBuffer, d->tagBuffer ); if ( startOfData < readBuffer.length() && startOfData >= 0 ) { d->dataBuffer = readBuffer.mid( startOfData + 1 ); } } else { d->dataBuffer += readBuffer; } // literal read in progress if ( d->literalSize > 0 ) { d->literalSize -= readBuffer.size(); // still not everything read if ( d->literalSize > 0 ) { return false; } // check the remaining (non-literal) part for parentheses if ( d->literalSize < 0 ) { // the following looks strange but works since literalSize can be negative here d->parenthesesCount += ImapParser::parenthesesBalance( readBuffer, readBuffer.length() + d->literalSize ); // check if another literal read was started if ( d->checkLiteralStart( readBuffer, readBuffer.length() + d->literalSize ) ) { return false; } } // literal string finished but still open parentheses if ( d->parenthesesCount > 0 ) { return false; } } else { // open parentheses d->parenthesesCount += ImapParser::parenthesesBalance( readBuffer ); // start new literal read if ( d->checkLiteralStart( readBuffer ) ) { return false; } // still open parentheses if ( d->parenthesesCount > 0 ) { return false; } // just a normal response, fall through } return true; } void ImapParser::parseBlock( const QByteArray &data ) { Q_ASSERT( d->literalSize >= data.size() ); d->literalSize -= data.size(); d->dataBuffer += data; } QByteArray ImapParser::tag() const { return d->tagBuffer; } QByteArray ImapParser::data() const { return d->dataBuffer; } void ImapParser::reset() { d->dataBuffer.clear(); d->tagBuffer.clear(); d->parenthesesCount = 0; d->literalSize = 0; d->continuation = false; } bool ImapParser::continuationStarted() const { return d->continuation; } qint64 ImapParser::continuationSize() const { return d->literalSize; }