File indexing completed on 2024-09-15 04:36:25

0001 /*
0002     SPDX-FileCopyrightText: 2006-2007 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "imapparser_p.h"
0008 
0009 #include <QDateTime>
0010 #include <QTimeZone>
0011 
0012 #include <ctype.h>
0013 
0014 using namespace Akonadi;
0015 
0016 class Akonadi::ImapParserPrivate
0017 {
0018 public:
0019     QByteArray tagBuffer;
0020     QByteArray dataBuffer;
0021     int parenthesesCount;
0022     qint64 literalSize;
0023     bool continuation;
0024 
0025     // returns true if readBuffer contains a literal start and sets
0026     // parser state accordingly
0027     bool checkLiteralStart(const QByteArray &readBuffer, int pos = 0)
0028     {
0029         if (readBuffer.trimmed().endsWith('}')) {
0030             const int begin = readBuffer.lastIndexOf('{');
0031             const int end = readBuffer.lastIndexOf('}');
0032 
0033             // new literal in previous literal data block
0034             if (begin < pos) {
0035                 return false;
0036             }
0037 
0038             // TODO error handling
0039             literalSize = readBuffer.mid(begin + 1, end - begin - 1).toLongLong();
0040 
0041             // empty literal
0042             if (literalSize == 0) {
0043                 return false;
0044             }
0045 
0046             continuation = true;
0047             dataBuffer.reserve(dataBuffer.size() + static_cast<int>(literalSize) + 1);
0048             return true;
0049         }
0050         return false;
0051     }
0052 };
0053 
0054 namespace
0055 {
0056 template<typename T>
0057 int parseParenthesizedListHelper(const QByteArray &data, T &result, int start)
0058 {
0059     result.clear();
0060     if (start >= data.length()) {
0061         return data.length();
0062     }
0063 
0064     const int begin = data.indexOf('(', start);
0065     if (begin < 0) {
0066         return start;
0067     }
0068 
0069     result.reserve(16);
0070 
0071     int count = 0;
0072     int sublistBegin = start;
0073     bool insideQuote = false;
0074     for (int i = begin + 1; i < data.length(); ++i) {
0075         const char currentChar = data[i];
0076         if (currentChar == '(' && !insideQuote) {
0077             ++count;
0078             if (count == 1) {
0079                 sublistBegin = i;
0080             }
0081 
0082             continue;
0083         }
0084 
0085         if (currentChar == ')' && !insideQuote) {
0086             if (count <= 0) {
0087                 return i + 1;
0088             }
0089 
0090             if (count == 1) {
0091                 result.append(data.mid(sublistBegin, i - sublistBegin + 1));
0092             }
0093 
0094             --count;
0095             continue;
0096         }
0097 
0098         if (currentChar == ' ' || currentChar == '\n' || currentChar == '\r') {
0099             continue;
0100         }
0101 
0102         if (count == 0) {
0103             QByteArray ba;
0104             const int consumed = ImapParser::parseString(data, ba, i);
0105             i = consumed - 1; // compensate for the for loop increment
0106             result.append(ba);
0107         } else if (count > 0) {
0108             if (currentChar == '"') {
0109                 insideQuote = !insideQuote;
0110             } else if (currentChar == '\\' && insideQuote) {
0111                 ++i;
0112                 continue;
0113             }
0114         }
0115     }
0116 
0117     return data.length();
0118 }
0119 
0120 } // namespace
0121 
0122 int ImapParser::parseParenthesizedList(const QByteArray &data, QVarLengthArray<QByteArray, 16> &result, int start)
0123 {
0124     return parseParenthesizedListHelper(data, result, start);
0125 }
0126 
0127 int ImapParser::parseParenthesizedList(const QByteArray &data, QList<QByteArray> &result, int start)
0128 {
0129     return parseParenthesizedListHelper(data, result, start);
0130 }
0131 
0132 int ImapParser::parseString(const QByteArray &data, QByteArray &result, int start)
0133 {
0134     int begin = stripLeadingSpaces(data, start);
0135     result.clear();
0136     if (begin >= data.length()) {
0137         return data.length();
0138     }
0139 
0140     // literal string
0141     // TODO: error handling
0142     if (data[begin] == '{') {
0143         int end = data.indexOf('}', begin);
0144         Q_ASSERT(end > begin);
0145         int size = data.mid(begin + 1, end - begin - 1).toInt();
0146 
0147         // strip CRLF
0148         begin = end + 1;
0149         if (begin < data.length() && data[begin] == '\r') {
0150             ++begin;
0151         }
0152         if (begin < data.length() && data[begin] == '\n') {
0153             ++begin;
0154         }
0155 
0156         end = begin + size;
0157         result = data.mid(begin, end - begin);
0158         return end;
0159     }
0160 
0161     // quoted string
0162     return parseQuotedString(data, result, begin);
0163 }
0164 
0165 int ImapParser::parseQuotedString(const QByteArray &data, QByteArray &result, int start)
0166 {
0167     int begin = stripLeadingSpaces(data, start);
0168     int end = begin;
0169     result.clear();
0170     if (begin >= data.length()) {
0171         return data.length();
0172     }
0173 
0174     bool foundSlash = false;
0175     // quoted string
0176     if (data[begin] == '"') {
0177         ++begin;
0178         result.reserve(qMin(32, data.size() - begin));
0179         for (int i = begin; i < data.length(); ++i) {
0180             const char ch = data.at(i);
0181             if (foundSlash) {
0182                 foundSlash = false;
0183                 if (ch == 'r') {
0184                     result += '\r';
0185                 } else if (ch == 'n') {
0186                     result += '\n';
0187                 } else if (ch == '\\') {
0188                     result += '\\';
0189                 } else if (ch == '\"') {
0190                     result += '\"';
0191                 } else {
0192                     // TODO: this is actually an error
0193                     result += ch;
0194                 }
0195                 continue;
0196             }
0197             if (ch == '\\') {
0198                 foundSlash = true;
0199                 continue;
0200             }
0201             if (ch == '"') {
0202                 end = i + 1; // skip the '"'
0203                 break;
0204             }
0205             result += ch;
0206         }
0207     } else {
0208         // unquoted string
0209         bool reachedInputEnd = true;
0210         for (int i = begin; i < data.length(); ++i) {
0211             const char ch = data.at(i);
0212             if (ch == ' ' || ch == '(' || ch == ')' || ch == '\n' || ch == '\r') {
0213                 end = i;
0214                 reachedInputEnd = false;
0215                 break;
0216             }
0217             if (ch == '\\') {
0218                 foundSlash = true;
0219             }
0220         }
0221         if (reachedInputEnd) {
0222             end = data.length();
0223         }
0224         result = data.mid(begin, end - begin);
0225 
0226         // transform unquoted NIL
0227         if (result == "NIL") {
0228             result.clear();
0229         }
0230 
0231         // strip quotes
0232         if (foundSlash) {
0233             while (result.contains("\\\"")) {
0234                 result.replace("\\\"", "\"");
0235             }
0236             while (result.contains("\\\\")) {
0237                 result.replace("\\\\", "\\");
0238             }
0239         }
0240     }
0241 
0242     return end;
0243 }
0244 
0245 int ImapParser::stripLeadingSpaces(const QByteArray &data, int start)
0246 {
0247     for (int i = start; i < data.length(); ++i) {
0248         if (data[i] != ' ') {
0249             return i;
0250         }
0251     }
0252 
0253     return data.length();
0254 }
0255 
0256 int ImapParser::parenthesesBalance(const QByteArray &data, int start)
0257 {
0258     int count = 0;
0259     bool insideQuote = false;
0260     for (int i = start; i < data.length(); ++i) {
0261         const char ch = data[i];
0262         if (ch == '"') {
0263             insideQuote = !insideQuote;
0264             continue;
0265         }
0266         if (ch == '\\' && insideQuote) {
0267             ++i;
0268             continue;
0269         }
0270         if (ch == '(' && !insideQuote) {
0271             ++count;
0272             continue;
0273         }
0274         if (ch == ')' && !insideQuote) {
0275             --count;
0276             continue;
0277         }
0278     }
0279     return count;
0280 }
0281 
0282 QByteArray ImapParser::join(const QList<QByteArray> &list, const QByteArray &separator)
0283 {
0284     // shortcuts for the easy cases
0285     if (list.isEmpty()) {
0286         return QByteArray();
0287     }
0288     if (list.size() == 1) {
0289         return list.first();
0290     }
0291 
0292     // avoid expensive realloc's by determining the size beforehand
0293     QList<QByteArray>::const_iterator it = list.constBegin();
0294     const QList<QByteArray>::const_iterator endIt = list.constEnd();
0295     int resultSize = (list.size() - 1) * separator.size();
0296     for (; it != endIt; ++it) {
0297         resultSize += (*it).size();
0298     }
0299 
0300     QByteArray result;
0301     result.reserve(resultSize);
0302     it = list.constBegin();
0303     result += (*it);
0304     ++it;
0305     for (; it != endIt; ++it) {
0306         result += separator;
0307         result += (*it);
0308     }
0309 
0310     return result;
0311 }
0312 
0313 QByteArray ImapParser::join(const QSet<QByteArray> &set, const QByteArray &separator)
0314 {
0315     const QList<QByteArray> list(set.begin(), set.end());
0316 
0317     return ImapParser::join(list, separator);
0318 }
0319 
0320 int ImapParser::parseString(const QByteArray &data, QString &result, int start)
0321 {
0322     QByteArray tmp;
0323     const int end = parseString(data, tmp, start);
0324     result = QString::fromUtf8(tmp);
0325     return end;
0326 }
0327 
0328 int ImapParser::parseNumber(const QByteArray &data, qint64 &result, bool *ok, int start)
0329 {
0330     if (ok) {
0331         *ok = false;
0332     }
0333 
0334     int pos = stripLeadingSpaces(data, start);
0335     if (pos >= data.length()) {
0336         return data.length();
0337     }
0338 
0339     int begin = pos;
0340     for (; pos < data.length(); ++pos) {
0341         if (!isdigit(data.at(pos))) {
0342             break;
0343         }
0344     }
0345 
0346     const QByteArray tmp = data.mid(begin, pos - begin);
0347     result = tmp.toLongLong(ok);
0348 
0349     return pos;
0350 }
0351 
0352 QByteArray ImapParser::quote(const QByteArray &data)
0353 {
0354     if (data.isEmpty()) {
0355         static const QByteArray empty("\"\"");
0356         return empty;
0357     }
0358 
0359     const int inputLength = data.length();
0360     int stuffToQuote = 0;
0361     for (int i = 0; i < inputLength; ++i) {
0362         const char ch = data.at(i);
0363         if (ch == '"' || ch == '\\' || ch == '\n' || ch == '\r') {
0364             ++stuffToQuote;
0365         }
0366     }
0367 
0368     QByteArray result;
0369     result.reserve(inputLength + stuffToQuote + 2);
0370     result += '"';
0371 
0372     // shortcut for the case that we don't need to quote anything at all
0373     if (stuffToQuote == 0) {
0374         result += data;
0375     } else {
0376         for (int i = 0; i < inputLength; ++i) {
0377             const char ch = data.at(i);
0378             if (ch == '\n') {
0379                 result += "\\n";
0380                 continue;
0381             }
0382 
0383             if (ch == '\r') {
0384                 result += "\\r";
0385                 continue;
0386             }
0387 
0388             if (ch == '"' || ch == '\\') {
0389                 result += '\\';
0390             }
0391 
0392             result += ch;
0393         }
0394     }
0395 
0396     result += '"';
0397     return result;
0398 }
0399 
0400 int ImapParser::parseSequenceSet(const QByteArray &data, ImapSet &result, int start)
0401 {
0402     int begin = stripLeadingSpaces(data, start);
0403     qint64 value = -1;
0404     qint64 lower = -1;
0405     qint64 upper = -1;
0406     for (int i = begin; i < data.length(); ++i) {
0407         if (data[i] == '*') {
0408             value = 0;
0409         } else if (data[i] == ':') {
0410             lower = value;
0411         } else if (isdigit(data[i])) {
0412             bool ok = false;
0413             i = parseNumber(data, value, &ok, i);
0414             Q_ASSERT(ok); // TODO handle error
0415             --i;
0416         } else {
0417             upper = value;
0418             if (lower < 0) {
0419                 lower = value;
0420             }
0421             result.add(ImapInterval(lower, upper));
0422             lower = -1;
0423             upper = -1; // NOLINT(clang-analyzer-deadcode.DeadStores) // false positive?
0424             value = -1;
0425             if (data[i] != ',') {
0426                 return i;
0427             }
0428         }
0429     }
0430     // take care of left-overs at input end
0431     upper = value;
0432     if (lower < 0) {
0433         lower = value;
0434     }
0435 
0436     if (lower >= 0 && upper >= 0) {
0437         result.add(ImapInterval(lower, upper));
0438     }
0439 
0440     return data.length();
0441 }
0442 
0443 int ImapParser::parseDateTime(const QByteArray &data, QDateTime &dateTime, int start)
0444 {
0445     // Syntax:
0446     // date-time      = DQUOTE date-day-fixed "-" date-month "-" date-year
0447     //                  SP time SP zone DQUOTE
0448     // date-day-fixed = (SP DIGIT) / 2DIGIT
0449     //                    ; Fixed-format version of date-day
0450     // date-month     = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
0451     //                  "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
0452     // date-year      = 4DIGIT
0453     // time           = 2DIGIT ":" 2DIGIT ":" 2DIGIT
0454     //                    ; Hours minutes seconds
0455     // zone           = ("+" / "-") 4DIGIT
0456     //                    ; Signed four-digit value of hhmm representing
0457     //                    ; hours and minutes east of Greenwich (that is,
0458     //                    ; the amount that the given time differs from
0459     //                    ; Universal Time).  Subtracting the timezone
0460     //                    ; from the given time will give the UT form.
0461     //                    ; The Universal Time zone is "+0000".
0462     // Example : "28-May-2006 01:03:35 +0200"
0463     // Position: 0123456789012345678901234567
0464     //                     1         2
0465 
0466     int pos = stripLeadingSpaces(data, start);
0467     if (data.length() <= pos) {
0468         return pos;
0469     }
0470 
0471     bool quoted = false;
0472     if (data[pos] == '"') {
0473         quoted = true;
0474         ++pos;
0475 
0476         if (data.length() <= pos + 26) {
0477             return start;
0478         }
0479     } else {
0480         if (data.length() < pos + 26) {
0481             return start;
0482         }
0483     }
0484 
0485     bool ok = true;
0486     const int day = (data[pos] == ' ' ? data[pos + 1] - '0' // single digit day
0487                                       : data.mid(pos, 2).toInt(&ok));
0488     if (!ok) {
0489         return start;
0490     }
0491 
0492     pos += 3;
0493     static const QByteArray shortMonthNames("janfebmaraprmayjunjulaugsepoctnovdec");
0494     int month = shortMonthNames.indexOf(data.mid(pos, 3).toLower());
0495     if (month == -1) {
0496         return start;
0497     }
0498 
0499     month = month / 3 + 1;
0500     pos += 4;
0501     const int year = data.mid(pos, 4).toInt(&ok);
0502     if (!ok) {
0503         return start;
0504     }
0505 
0506     pos += 5;
0507     const int hours = data.mid(pos, 2).toInt(&ok);
0508     if (!ok) {
0509         return start;
0510     }
0511 
0512     pos += 3;
0513     const int minutes = data.mid(pos, 2).toInt(&ok);
0514     if (!ok) {
0515         return start;
0516     }
0517 
0518     pos += 3;
0519     const int seconds = data.mid(pos, 2).toInt(&ok);
0520     if (!ok) {
0521         return start;
0522     }
0523 
0524     pos += 4;
0525     const int tzhh = data.mid(pos, 2).toInt(&ok);
0526     if (!ok) {
0527         return start;
0528     }
0529 
0530     pos += 2;
0531     const int tzmm = data.mid(pos, 2).toInt(&ok);
0532     if (!ok) {
0533         return start;
0534     }
0535 
0536     int tzsecs = tzhh * 60 * 60 + tzmm * 60;
0537     if (data[pos - 3] == '-') {
0538         tzsecs = -tzsecs;
0539     }
0540 
0541     const QDate date(year, month, day);
0542     const QTime time(hours, minutes, seconds);
0543     dateTime = QDateTime(date, time, QTimeZone::UTC);
0544     if (!dateTime.isValid()) {
0545         return start;
0546     }
0547 
0548     dateTime = dateTime.addSecs(-tzsecs);
0549 
0550     pos += 2;
0551     if (data.length() <= pos || !quoted) {
0552         return pos;
0553     }
0554 
0555     if (data[pos] == '"') {
0556         ++pos;
0557     }
0558 
0559     return pos;
0560 }
0561 
0562 void ImapParser::splitVersionedKey(const QByteArray &data, QByteArray &key, int &version)
0563 {
0564     const int startPos = data.indexOf('[');
0565     const int endPos = data.indexOf(']');
0566     if (startPos != -1 && endPos != -1) {
0567         if (endPos > startPos) {
0568             bool ok = false;
0569 
0570             version = data.mid(startPos + 1, endPos - startPos - 1).toInt(&ok);
0571             if (!ok) {
0572                 version = 0;
0573             }
0574 
0575             key = data.left(startPos);
0576         }
0577     } else {
0578         key = data;
0579         version = 0;
0580     }
0581 }
0582 
0583 ImapParser::ImapParser()
0584     : d(new ImapParserPrivate)
0585 {
0586     reset();
0587 }
0588 
0589 ImapParser::~ImapParser() = default;
0590 
0591 bool ImapParser::parseNextLine(const QByteArray &readBuffer)
0592 {
0593     d->continuation = false;
0594 
0595     // first line, get the tag
0596     if (d->tagBuffer.isEmpty()) {
0597         const int startOfData = ImapParser::parseString(readBuffer, d->tagBuffer);
0598         if (startOfData < readBuffer.length() && startOfData >= 0) {
0599             d->dataBuffer = readBuffer.mid(startOfData + 1);
0600         }
0601 
0602     } else {
0603         d->dataBuffer += readBuffer;
0604     }
0605 
0606     // literal read in progress
0607     if (d->literalSize > 0) {
0608         d->literalSize -= readBuffer.size();
0609 
0610         // still not everything read
0611         if (d->literalSize > 0) {
0612             return false;
0613         }
0614 
0615         // check the remaining (non-literal) part for parentheses
0616         if (d->literalSize < 0) {
0617             // the following looks strange but works since literalSize can be negative here
0618             d->parenthesesCount += ImapParser::parenthesesBalance(readBuffer, readBuffer.length() + static_cast<int>(d->literalSize));
0619 
0620             // check if another literal read was started
0621             if (d->checkLiteralStart(readBuffer, readBuffer.length() + static_cast<int>(d->literalSize))) {
0622                 return false;
0623             }
0624         }
0625 
0626         // literal string finished but still open parentheses
0627         if (d->parenthesesCount > 0) {
0628             return false;
0629         }
0630 
0631     } else {
0632         // open parentheses
0633         d->parenthesesCount += ImapParser::parenthesesBalance(readBuffer);
0634 
0635         // start new literal read
0636         if (d->checkLiteralStart(readBuffer)) {
0637             return false;
0638         }
0639 
0640         // still open parentheses
0641         if (d->parenthesesCount > 0) {
0642             return false;
0643         }
0644 
0645         // just a normal response, fall through
0646     }
0647 
0648     return true;
0649 }
0650 
0651 void ImapParser::parseBlock(const QByteArray &data)
0652 {
0653     Q_ASSERT(d->literalSize >= data.size());
0654     d->literalSize -= data.size();
0655     d->dataBuffer += data;
0656 }
0657 
0658 QByteArray ImapParser::tag() const
0659 {
0660     return d->tagBuffer;
0661 }
0662 
0663 QByteArray ImapParser::data() const
0664 {
0665     return d->dataBuffer;
0666 }
0667 
0668 void ImapParser::reset()
0669 {
0670     d->dataBuffer.clear();
0671     d->tagBuffer.clear();
0672     d->parenthesesCount = 0;
0673     d->literalSize = 0;
0674     d->continuation = false;
0675 }
0676 
0677 bool ImapParser::continuationStarted() const
0678 {
0679     return d->continuation;
0680 }
0681 
0682 qint64 ImapParser::continuationSize() const
0683 {
0684     return d->literalSize;
0685 }