Akonadi

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

KDE's Doxygen guidelines are available online.