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 | |
27 | using namespace Akonadi; |
28 | |
29 | class ImapParser::Private |
30 | { |
31 | public: |
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 | |
67 | namespace { |
68 | |
69 | template <typename T> |
70 | int 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 | |
133 | int ImapParser::parseParenthesizedList(const QByteArray &data, QVarLengthArray<QByteArray, 16> &result, int start) |
134 | { |
135 | return parseParenthesizedListHelper(data, result, start); |
136 | } |
137 | |
138 | int ImapParser::parseParenthesizedList(const QByteArray &data, QList<QByteArray> &result, int start) |
139 | { |
140 | return parseParenthesizedListHelper(data, result, start); |
141 | } |
142 | |
143 | int 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 | |
176 | int 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 | |
255 | int 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 | |
266 | int 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 | |
292 | QByteArray 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 | |
323 | QByteArray 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 | |
330 | int 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 | |
338 | int 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 | |
362 | QByteArray 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 | |
409 | int 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 | |
450 | int 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 | |
569 | void 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 | |
590 | ImapParser::ImapParser() |
591 | : d(new Private) |
592 | { |
593 | reset(); |
594 | } |
595 | |
596 | ImapParser::~ImapParser() |
597 | { |
598 | delete d; |
599 | } |
600 | |
601 | bool 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 | |
662 | void ImapParser::parseBlock(const QByteArray &data) |
663 | { |
664 | Q_ASSERT(d->literalSize >= data.size()); |
665 | d->literalSize -= data.size(); |
666 | d->dataBuffer += data; |
667 | } |
668 | |
669 | QByteArray ImapParser::tag() const |
670 | { |
671 | return d->tagBuffer; |
672 | } |
673 | |
674 | QByteArray ImapParser::data() const |
675 | { |
676 | return d->dataBuffer; |
677 | } |
678 | |
679 | void 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 | |
688 | bool ImapParser::continuationStarted() const |
689 | { |
690 | return d->continuation; |
691 | } |
692 | |
693 | qint64 ImapParser::continuationSize() const |
694 | { |
695 | return d->literalSize; |
696 | } |
697 | |