Akonadi

imapparser.cpp
1/*
2 SPDX-FileCopyrightText: 2006-2007 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "imapparser_p.h"
8
9#include <QDateTime>
10#include <QTimeZone>
11
12#include <ctype.h>
13
14using namespace Akonadi;
15
16class Akonadi::ImapParserPrivate
17{
18public:
19 QByteArray tagBuffer;
20 QByteArray dataBuffer;
21 int parenthesesCount;
22 qint64 literalSize;
23 bool continuation;
24
25 // returns true if readBuffer contains a literal start and sets
26 // parser state accordingly
27 bool checkLiteralStart(const QByteArray &readBuffer, int pos = 0)
28 {
29 if (readBuffer.trimmed().endsWith('}')) {
30 const int begin = readBuffer.lastIndexOf('{');
31 const int end = readBuffer.lastIndexOf('}');
32
33 // new literal in previous literal data block
34 if (begin < pos) {
35 return false;
36 }
37
38 // TODO error handling
39 literalSize = readBuffer.mid(begin + 1, end - begin - 1).toLongLong();
40
41 // empty literal
42 if (literalSize == 0) {
43 return false;
44 }
45
46 continuation = true;
47 dataBuffer.reserve(dataBuffer.size() + static_cast<int>(literalSize) + 1);
48 return true;
49 }
50 return false;
51 }
52};
53
54namespace
55{
56template<typename T>
57int parseParenthesizedListHelper(const QByteArray &data, T &result, int start)
58{
59 result.clear();
60 if (start >= data.length()) {
61 return data.length();
62 }
63
64 const int begin = data.indexOf('(', start);
65 if (begin < 0) {
66 return start;
67 }
68
69 result.reserve(16);
70
71 int count = 0;
72 int sublistBegin = start;
73 bool insideQuote = false;
74 for (int i = begin + 1; i < data.length(); ++i) {
75 const char currentChar = data[i];
76 if (currentChar == '(' && !insideQuote) {
77 ++count;
78 if (count == 1) {
79 sublistBegin = i;
80 }
81
82 continue;
83 }
84
85 if (currentChar == ')' && !insideQuote) {
86 if (count <= 0) {
87 return i + 1;
88 }
89
90 if (count == 1) {
91 result.append(data.mid(sublistBegin, i - sublistBegin + 1));
92 }
93
94 --count;
95 continue;
96 }
97
98 if (currentChar == ' ' || currentChar == '\n' || currentChar == '\r') {
99 continue;
100 }
101
102 if (count == 0) {
103 QByteArray ba;
104 const int consumed = ImapParser::parseString(data, ba, i);
105 i = consumed - 1; // compensate for the for loop increment
106 result.append(ba);
107 } else if (count > 0) {
108 if (currentChar == '"') {
109 insideQuote = !insideQuote;
110 } else if (currentChar == '\\' && insideQuote) {
111 ++i;
112 continue;
113 }
114 }
115 }
116
117 return data.length();
118}
119
120} // namespace
121
122int ImapParser::parseParenthesizedList(const QByteArray &data, QVarLengthArray<QByteArray, 16> &result, int start)
123{
124 return parseParenthesizedListHelper(data, result, start);
125}
126
127int ImapParser::parseParenthesizedList(const QByteArray &data, QList<QByteArray> &result, int start)
128{
129 return parseParenthesizedListHelper(data, result, start);
130}
131
132int ImapParser::parseString(const QByteArray &data, QByteArray &result, int start)
133{
134 int begin = stripLeadingSpaces(data, start);
135 result.clear();
136 if (begin >= data.length()) {
137 return data.length();
138 }
139
140 // literal string
141 // TODO: error handling
142 if (data[begin] == '{') {
143 int end = data.indexOf('}', begin);
144 Q_ASSERT(end > begin);
145 int size = data.mid(begin + 1, end - begin - 1).toInt();
146
147 // strip CRLF
148 begin = end + 1;
149 if (begin < data.length() && data[begin] == '\r') {
150 ++begin;
151 }
152 if (begin < data.length() && data[begin] == '\n') {
153 ++begin;
154 }
155
156 end = begin + size;
157 result = data.mid(begin, end - begin);
158 return end;
159 }
160
161 // quoted string
162 return parseQuotedString(data, result, begin);
163}
164
165int ImapParser::parseQuotedString(const QByteArray &data, QByteArray &result, int start)
166{
167 int begin = stripLeadingSpaces(data, start);
168 int end = begin;
169 result.clear();
170 if (begin >= data.length()) {
171 return data.length();
172 }
173
174 bool foundSlash = false;
175 // quoted string
176 if (data[begin] == '"') {
177 ++begin;
178 result.reserve(qMin(32, data.size() - begin));
179 for (int i = begin; i < data.length(); ++i) {
180 const char ch = data.at(i);
181 if (foundSlash) {
182 foundSlash = false;
183 if (ch == 'r') {
184 result += '\r';
185 } else if (ch == 'n') {
186 result += '\n';
187 } else if (ch == '\\') {
188 result += '\\';
189 } else if (ch == '\"') {
190 result += '\"';
191 } else {
192 // TODO: this is actually an error
193 result += ch;
194 }
195 continue;
196 }
197 if (ch == '\\') {
198 foundSlash = true;
199 continue;
200 }
201 if (ch == '"') {
202 end = i + 1; // skip the '"'
203 break;
204 }
205 result += ch;
206 }
207 } else {
208 // unquoted string
209 bool reachedInputEnd = true;
210 for (int i = begin; i < data.length(); ++i) {
211 const char ch = data.at(i);
212 if (ch == ' ' || ch == '(' || ch == ')' || ch == '\n' || ch == '\r') {
213 end = i;
214 reachedInputEnd = false;
215 break;
216 }
217 if (ch == '\\') {
218 foundSlash = true;
219 }
220 }
221 if (reachedInputEnd) {
222 end = data.length();
223 }
224 result = data.mid(begin, end - begin);
225
226 // transform unquoted NIL
227 if (result == "NIL") {
228 result.clear();
229 }
230
231 // strip quotes
232 if (foundSlash) {
233 while (result.contains("\\\"")) {
234 result.replace("\\\"", "\"");
235 }
236 while (result.contains("\\\\")) {
237 result.replace("\\\\", "\\");
238 }
239 }
240 }
241
242 return end;
243}
244
245int ImapParser::stripLeadingSpaces(const QByteArray &data, int start)
246{
247 for (int i = start; i < data.length(); ++i) {
248 if (data[i] != ' ') {
249 return i;
250 }
251 }
252
253 return data.length();
254}
255
256int ImapParser::parenthesesBalance(const QByteArray &data, int start)
257{
258 int count = 0;
259 bool insideQuote = false;
260 for (int i = start; i < data.length(); ++i) {
261 const char ch = data[i];
262 if (ch == '"') {
263 insideQuote = !insideQuote;
264 continue;
265 }
266 if (ch == '\\' && insideQuote) {
267 ++i;
268 continue;
269 }
270 if (ch == '(' && !insideQuote) {
271 ++count;
272 continue;
273 }
274 if (ch == ')' && !insideQuote) {
275 --count;
276 continue;
277 }
278 }
279 return count;
280}
281
282QByteArray ImapParser::join(const QList<QByteArray> &list, const QByteArray &separator)
283{
284 // shortcuts for the easy cases
285 if (list.isEmpty()) {
286 return QByteArray();
287 }
288 if (list.size() == 1) {
289 return list.first();
290 }
291
292 // avoid expensive realloc's by determining the size beforehand
295 int resultSize = (list.size() - 1) * separator.size();
296 for (; it != endIt; ++it) {
297 resultSize += (*it).size();
298 }
299
300 QByteArray result;
301 result.reserve(resultSize);
302 it = list.constBegin();
303 result += (*it);
304 ++it;
305 for (; it != endIt; ++it) {
306 result += separator;
307 result += (*it);
308 }
309
310 return result;
311}
312
313QByteArray ImapParser::join(const QSet<QByteArray> &set, const QByteArray &separator)
314{
315 const QList<QByteArray> list(set.begin(), set.end());
316
317 return ImapParser::join(list, separator);
318}
319
320int ImapParser::parseString(const QByteArray &data, QString &result, int start)
321{
322 QByteArray tmp;
323 const int end = parseString(data, tmp, start);
324 result = QString::fromUtf8(tmp);
325 return end;
326}
327
328int ImapParser::parseNumber(const QByteArray &data, qint64 &result, bool *ok, int start)
329{
330 if (ok) {
331 *ok = false;
332 }
333
334 int pos = stripLeadingSpaces(data, start);
335 if (pos >= data.length()) {
336 return data.length();
337 }
338
339 int begin = pos;
340 for (; pos < data.length(); ++pos) {
341 if (!isdigit(data.at(pos))) {
342 break;
343 }
344 }
345
346 const QByteArray tmp = data.mid(begin, pos - begin);
347 result = tmp.toLongLong(ok);
348
349 return pos;
350}
351
352QByteArray ImapParser::quote(const QByteArray &data)
353{
354 if (data.isEmpty()) {
355 static const QByteArray empty("\"\"");
356 return empty;
357 }
358
359 const int inputLength = data.length();
360 int stuffToQuote = 0;
361 for (int i = 0; i < inputLength; ++i) {
362 const char ch = data.at(i);
363 if (ch == '"' || ch == '\\' || ch == '\n' || ch == '\r') {
364 ++stuffToQuote;
365 }
366 }
367
368 QByteArray result;
369 result.reserve(inputLength + stuffToQuote + 2);
370 result += '"';
371
372 // shortcut for the case that we don't need to quote anything at all
373 if (stuffToQuote == 0) {
374 result += data;
375 } else {
376 for (int i = 0; i < inputLength; ++i) {
377 const char ch = data.at(i);
378 if (ch == '\n') {
379 result += "\\n";
380 continue;
381 }
382
383 if (ch == '\r') {
384 result += "\\r";
385 continue;
386 }
387
388 if (ch == '"' || ch == '\\') {
389 result += '\\';
390 }
391
392 result += ch;
393 }
394 }
395
396 result += '"';
397 return result;
398}
399
400int ImapParser::parseSequenceSet(const QByteArray &data, ImapSet &result, int start)
401{
402 int begin = stripLeadingSpaces(data, start);
403 qint64 value = -1;
404 qint64 lower = -1;
405 qint64 upper = -1;
406 for (int i = begin; i < data.length(); ++i) {
407 if (data[i] == '*') {
408 value = 0;
409 } else if (data[i] == ':') {
410 lower = value;
411 } else if (isdigit(data[i])) {
412 bool ok = false;
413 i = parseNumber(data, value, &ok, i);
414 Q_ASSERT(ok); // TODO handle error
415 --i;
416 } else {
417 upper = value;
418 if (lower < 0) {
419 lower = value;
420 }
421 result.add(ImapInterval(lower, upper));
422 lower = -1;
423 upper = -1; // NOLINT(clang-analyzer-deadcode.DeadStores) // false positive?
424 value = -1;
425 if (data[i] != ',') {
426 return i;
427 }
428 }
429 }
430 // take care of left-overs at input end
431 upper = value;
432 if (lower < 0) {
433 lower = value;
434 }
435
436 if (lower >= 0 && upper >= 0) {
437 result.add(ImapInterval(lower, upper));
438 }
439
440 return data.length();
441}
442
443int ImapParser::parseDateTime(const QByteArray &data, QDateTime &dateTime, int start)
444{
445 // Syntax:
446 // date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
447 // SP time SP zone DQUOTE
448 // date-day-fixed = (SP DIGIT) / 2DIGIT
449 // ; Fixed-format version of date-day
450 // date-month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
451 // "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
452 // date-year = 4DIGIT
453 // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
454 // ; Hours minutes seconds
455 // zone = ("+" / "-") 4DIGIT
456 // ; Signed four-digit value of hhmm representing
457 // ; hours and minutes east of Greenwich (that is,
458 // ; the amount that the given time differs from
459 // ; Universal Time). Subtracting the timezone
460 // ; from the given time will give the UT form.
461 // ; The Universal Time zone is "+0000".
462 // Example : "28-May-2006 01:03:35 +0200"
463 // Position: 0123456789012345678901234567
464 // 1 2
465
466 int pos = stripLeadingSpaces(data, start);
467 if (data.length() <= pos) {
468 return pos;
469 }
470
471 bool quoted = false;
472 if (data[pos] == '"') {
473 quoted = true;
474 ++pos;
475
476 if (data.length() <= pos + 26) {
477 return start;
478 }
479 } else {
480 if (data.length() < pos + 26) {
481 return start;
482 }
483 }
484
485 bool ok = true;
486 const int day = (data[pos] == ' ' ? data[pos + 1] - '0' // single digit day
487 : data.mid(pos, 2).toInt(&ok));
488 if (!ok) {
489 return start;
490 }
491
492 pos += 3;
493 static const QByteArray shortMonthNames("janfebmaraprmayjunjulaugsepoctnovdec");
494 int month = shortMonthNames.indexOf(data.mid(pos, 3).toLower());
495 if (month == -1) {
496 return start;
497 }
498
499 month = month / 3 + 1;
500 pos += 4;
501 const int year = data.mid(pos, 4).toInt(&ok);
502 if (!ok) {
503 return start;
504 }
505
506 pos += 5;
507 const int hours = data.mid(pos, 2).toInt(&ok);
508 if (!ok) {
509 return start;
510 }
511
512 pos += 3;
513 const int minutes = data.mid(pos, 2).toInt(&ok);
514 if (!ok) {
515 return start;
516 }
517
518 pos += 3;
519 const int seconds = data.mid(pos, 2).toInt(&ok);
520 if (!ok) {
521 return start;
522 }
523
524 pos += 4;
525 const int tzhh = data.mid(pos, 2).toInt(&ok);
526 if (!ok) {
527 return start;
528 }
529
530 pos += 2;
531 const int tzmm = data.mid(pos, 2).toInt(&ok);
532 if (!ok) {
533 return start;
534 }
535
536 int tzsecs = tzhh * 60 * 60 + tzmm * 60;
537 if (data[pos - 3] == '-') {
538 tzsecs = -tzsecs;
539 }
540
541 const QDate date(year, month, day);
542 const QTime time(hours, minutes, seconds);
543 dateTime = QDateTime(date, time, QTimeZone::UTC);
544 if (!dateTime.isValid()) {
545 return start;
546 }
547
548 dateTime = dateTime.addSecs(-tzsecs);
549
550 pos += 2;
551 if (data.length() <= pos || !quoted) {
552 return pos;
553 }
554
555 if (data[pos] == '"') {
556 ++pos;
557 }
558
559 return pos;
560}
561
562void ImapParser::splitVersionedKey(const QByteArray &data, QByteArray &key, int &version)
563{
564 const int startPos = data.indexOf('[');
565 const int endPos = data.indexOf(']');
566 if (startPos != -1 && endPos != -1) {
567 if (endPos > startPos) {
568 bool ok = false;
569
570 version = data.mid(startPos + 1, endPos - startPos - 1).toInt(&ok);
571 if (!ok) {
572 version = 0;
573 }
574
575 key = data.left(startPos);
576 }
577 } else {
578 key = data;
579 version = 0;
580 }
581}
582
583ImapParser::ImapParser()
584 : d(new ImapParserPrivate)
585{
586 reset();
587}
588
589ImapParser::~ImapParser() = default;
590
591bool ImapParser::parseNextLine(const QByteArray &readBuffer)
592{
593 d->continuation = false;
594
595 // first line, get the tag
596 if (d->tagBuffer.isEmpty()) {
597 const int startOfData = ImapParser::parseString(readBuffer, d->tagBuffer);
598 if (startOfData < readBuffer.length() && startOfData >= 0) {
599 d->dataBuffer = readBuffer.mid(startOfData + 1);
600 }
601
602 } else {
603 d->dataBuffer += readBuffer;
604 }
605
606 // literal read in progress
607 if (d->literalSize > 0) {
608 d->literalSize -= readBuffer.size();
609
610 // still not everything read
611 if (d->literalSize > 0) {
612 return false;
613 }
614
615 // check the remaining (non-literal) part for parentheses
616 if (d->literalSize < 0) {
617 // the following looks strange but works since literalSize can be negative here
618 d->parenthesesCount += ImapParser::parenthesesBalance(readBuffer, readBuffer.length() + static_cast<int>(d->literalSize));
619
620 // check if another literal read was started
621 if (d->checkLiteralStart(readBuffer, readBuffer.length() + static_cast<int>(d->literalSize))) {
622 return false;
623 }
624 }
625
626 // literal string finished but still open parentheses
627 if (d->parenthesesCount > 0) {
628 return false;
629 }
630
631 } else {
632 // open parentheses
633 d->parenthesesCount += ImapParser::parenthesesBalance(readBuffer);
634
635 // start new literal read
636 if (d->checkLiteralStart(readBuffer)) {
637 return false;
638 }
639
640 // still open parentheses
641 if (d->parenthesesCount > 0) {
642 return false;
643 }
644
645 // just a normal response, fall through
646 }
647
648 return true;
649}
650
651void ImapParser::parseBlock(const QByteArray &data)
652{
653 Q_ASSERT(d->literalSize >= data.size());
654 d->literalSize -= data.size();
655 d->dataBuffer += data;
656}
657
658QByteArray ImapParser::tag() const
659{
660 return d->tagBuffer;
661}
662
663QByteArray ImapParser::data() const
664{
665 return d->dataBuffer;
666}
667
668void ImapParser::reset()
669{
670 d->dataBuffer.clear();
671 d->tagBuffer.clear();
672 d->parenthesesCount = 0;
673 d->literalSize = 0;
674 d->continuation = false;
675}
676
677bool ImapParser::continuationStarted() const
678{
679 return d->continuation;
680}
681
682qint64 ImapParser::continuationSize() const
683{
684 return d->literalSize;
685}
Q_SCRIPTABLE Q_NOREPLY void start()
Helper integration between Akonadi and Qt.
KDB_EXPORT KDbVersionInfo version()
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KGuiItem reset()
const QList< QKeySequence > & begin()
const QList< QKeySequence > & end()
char at(qsizetype i) const const
void clear()
bool contains(QByteArrayView bv) const const
bool endsWith(QByteArrayView bv) const const
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
bool isEmpty() const const
qsizetype lastIndexOf(QByteArrayView bv) const const
QByteArray left(qsizetype len) const const
qsizetype length() const const
QByteArray mid(qsizetype pos, qsizetype len) const const
QByteArray & replace(QByteArrayView before, QByteArrayView after)
void reserve(qsizetype size)
qsizetype size() const const
int toInt(bool *ok, int base) const const
qlonglong toLongLong(bool *ok, int base) const const
QByteArray toLower() const const
QByteArray trimmed() const const
QDateTime addSecs(qint64 s) const const
bool isValid() const const
const_iterator constBegin() const const
const_iterator constEnd() const const
T & first()
bool isEmpty() const const
qsizetype size() const const
iterator begin()
iterator end()
QString fromUtf8(QByteArrayView str)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:38 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.