File indexing completed on 2024-11-10 04:40:48
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 }