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