File indexing completed on 2024-05-12 05:17:22
0001 /* 0002 Copyright (c) 2009 Kevin Ottens <ervin@kde.org> 0003 Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com> 0004 0005 This library is free software; you can redistribute it and/or modify it 0006 under the terms of the GNU Library General Public License as published by 0007 the Free Software Foundation; either version 2 of the License, or (at your 0008 option) any later version. 0009 0010 This library is distributed in the hope that it will be useful, but WITHOUT 0011 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 0012 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 0013 License for more details. 0014 0015 You should have received a copy of the GNU Library General Public License 0016 along with this library; see the file COPYING.LIB. If not, write to the 0017 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0018 02110-1301, USA. 0019 */ 0020 0021 #include "fetchjob.h" 0022 0023 #include "kimap_debug.h" 0024 0025 #include "job_p.h" 0026 #include "message_p.h" 0027 #include "session_p.h" 0028 0029 namespace KIMAP2 0030 { 0031 class FetchJobPrivate : public JobPrivate 0032 { 0033 public: 0034 FetchJobPrivate(FetchJob *job, Session *session, const QString &name) 0035 : JobPrivate(session, name) 0036 , q(job) 0037 , uidBased(false) 0038 , avoidParsing(false) 0039 { } 0040 0041 ~FetchJobPrivate() 0042 { } 0043 0044 void parseBodyStructure(const QByteArray &structure, int &pos, KMime::Content *content); 0045 void parsePart(const QByteArray &structure, int &pos, KMime::Content *content); 0046 QByteArray parseString(const QByteArray &structure, int &pos); 0047 QByteArray parseSentence(const QByteArray &structure, int &pos); 0048 void skipLeadingSpaces(const QByteArray &structure, int &pos); 0049 0050 FetchJob *const q; 0051 0052 ImapSet set; 0053 bool uidBased; 0054 FetchJob::FetchScope scope; 0055 QString selectedMailBox; 0056 bool avoidParsing; 0057 }; 0058 } 0059 0060 using namespace KIMAP2; 0061 0062 FetchJob::FetchScope::FetchScope(): 0063 mode(FetchScope::Content), 0064 changedSince(0), 0065 gmailExtensionsEnabled(false) 0066 { 0067 0068 } 0069 0070 FetchJob::FetchJob(Session *session) 0071 : Job(*new FetchJobPrivate(this, session, "Fetch")) 0072 { 0073 } 0074 0075 FetchJob::~FetchJob() 0076 { 0077 } 0078 0079 void FetchJob::setAvoidParsing(bool avoid) 0080 { 0081 Q_D(FetchJob); 0082 d->avoidParsing = avoid; 0083 } 0084 0085 void FetchJob::setSequenceSet(const ImapSet &set) 0086 { 0087 Q_D(FetchJob); 0088 Q_ASSERT(!set.isEmpty()); 0089 d->set = set; 0090 } 0091 0092 ImapSet FetchJob::sequenceSet() const 0093 { 0094 Q_D(const FetchJob); 0095 return d->set; 0096 } 0097 0098 void FetchJob::setUidBased(bool uidBased) 0099 { 0100 Q_D(FetchJob); 0101 d->uidBased = uidBased; 0102 } 0103 0104 bool FetchJob::isUidBased() const 0105 { 0106 Q_D(const FetchJob); 0107 return d->uidBased; 0108 } 0109 0110 void FetchJob::setScope(const FetchScope &scope) 0111 { 0112 Q_D(FetchJob); 0113 d->scope = scope; 0114 } 0115 0116 FetchJob::FetchScope FetchJob::scope() const 0117 { 0118 Q_D(const FetchJob); 0119 return d->scope; 0120 } 0121 0122 void FetchJob::doStart() 0123 { 0124 Q_D(FetchJob); 0125 0126 d->set.optimize(); 0127 QByteArray parameters = d->set.toImapSequenceSet() + ' '; 0128 Q_ASSERT(!parameters.trimmed().isEmpty()); 0129 0130 switch (d->scope.mode) { 0131 case FetchScope::Headers: 0132 if (d->scope.parts.isEmpty()) { 0133 parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID"; 0134 } else { 0135 parameters += '('; 0136 foreach (const QByteArray &part, d->scope.parts) { 0137 parameters += "BODY.PEEK[" + part + ".MIME] "; 0138 } 0139 parameters += "UID"; 0140 } 0141 break; 0142 case FetchScope::Flags: 0143 parameters += "(FLAGS UID"; 0144 break; 0145 case FetchScope::Structure: 0146 parameters += "(BODYSTRUCTURE UID"; 0147 break; 0148 case FetchScope::Content: 0149 if (d->scope.parts.isEmpty()) { 0150 parameters += "(BODY.PEEK[] UID"; 0151 } else { 0152 parameters += '('; 0153 foreach (const QByteArray &part, d->scope.parts) { 0154 parameters += "BODY.PEEK[" + part + "] "; 0155 } 0156 parameters += "UID"; 0157 } 0158 break; 0159 case FetchScope::Full: 0160 parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID"; 0161 break; 0162 case FetchScope::HeaderAndContent: 0163 if (d->scope.parts.isEmpty()) { 0164 parameters += "(BODY.PEEK[] FLAGS UID"; 0165 } else { 0166 parameters += "(BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)]"; 0167 foreach (const QByteArray &part, d->scope.parts) { 0168 parameters += " BODY.PEEK[" + part + ".MIME] BODY.PEEK[" + part + "]"; //krazy:exclude=doublequote_chars 0169 } 0170 parameters += " FLAGS UID"; 0171 } 0172 break; 0173 case FetchScope::FullHeaders: 0174 parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER] FLAGS UID"; 0175 break; 0176 } 0177 0178 if (d->scope.gmailExtensionsEnabled) { 0179 parameters += " X-GM-LABELS X-GM-MSGID X-GM-THRID"; 0180 } 0181 parameters += ")"; 0182 0183 if (d->scope.changedSince > 0) { 0184 parameters += " (CHANGEDSINCE " + QByteArray::number(d->scope.changedSince) + ")"; 0185 } 0186 0187 QByteArray command = "FETCH"; 0188 if (d->uidBased) { 0189 command = "UID " + command; 0190 } 0191 0192 d->selectedMailBox = d->m_session->selectedMailBox(); 0193 d->sendCommand(command, parameters); 0194 } 0195 0196 void FetchJob::handleResponse(const Message &response) 0197 { 0198 Q_D(FetchJob); 0199 0200 if (handleErrorReplies(response) == NotHandled) { 0201 if (response.content.size() == 4 && 0202 response.content[2].toString() == "FETCH" && 0203 response.content[3].type() == Message::Part::List) { 0204 0205 const QList<QByteArray> content = response.content[3].toList(); 0206 0207 Result result; 0208 result.sequenceNumber = response.content[1].toString().toLongLong(); 0209 bool shouldParseMessage = false; 0210 for (QList<QByteArray>::ConstIterator it = content.constBegin(); 0211 it != content.constEnd(); ++it) { 0212 QByteArray str = *it; 0213 ++it; 0214 if (it == content.constEnd()) { // Uh oh, message was truncated? 0215 qCWarning(KIMAP2_LOG) << "FETCH reply got truncated, skipping."; 0216 qCWarning(KIMAP2_LOG) << response.toString(); 0217 qCWarning(KIMAP2_LOG) << result.sequenceNumber; 0218 qCWarning(KIMAP2_LOG) << content; 0219 qCWarning(KIMAP2_LOG) << str; 0220 break; 0221 } 0222 0223 if (str == "UID") { 0224 result.uid = it->toLongLong(); 0225 } else if (str == "RFC822.SIZE") { 0226 result.size = it->toLongLong(); 0227 } else if (str == "INTERNALDATE") { 0228 if (!result.message) { 0229 result.message = MessagePtr(new KMime::Message); 0230 } 0231 result.message->date()->setDateTime(QDateTime::fromString(QLatin1String(*it), Qt::RFC2822Date)); 0232 } else if (str == "FLAGS") { 0233 if ((*it).startsWith('(') && (*it).endsWith(')')) { 0234 QByteArray str = *it; 0235 str.chop(1); 0236 str.remove(0, 1); 0237 result.flags = str.split(' '); 0238 } else { 0239 result.flags << *it; 0240 } 0241 } else if (str == "X-GM-LABELS") { 0242 result.attributes << qMakePair<QByteArray, QVariant>("X-GM-LABELS", *it); 0243 } else if (str == "X-GM-THRID") { 0244 result.attributes << qMakePair<QByteArray, QVariant>("X-GM-THRID", *it); 0245 } else if (str == "X-GM-MSGID") { 0246 result.attributes << qMakePair<QByteArray, QVariant>("X-GM-MSGID", *it); 0247 } else if (str == "BODYSTRUCTURE") { 0248 if (!result.message) { 0249 result.message = MessagePtr(new KMime::Message); 0250 } 0251 int pos = 0; 0252 d->parseBodyStructure(*it, pos, result.message.data()); 0253 result.message->assemble(); 0254 } else if (str.startsWith("BODY[")) { //krazy:exclude=strings 0255 if (!str.endsWith(']')) { // BODY[ ... ] might have been split, skip until we find the ] 0256 while (it != content.constEnd() && !(*it).endsWith(']')) { 0257 ++it; 0258 } 0259 } 0260 0261 int index; 0262 if ((index = str.indexOf("HEADER")) > 0 || (index = str.indexOf("MIME")) > 0) { // headers 0263 if (str[index - 1] == '.') { 0264 QByteArray partId = str.mid(5, index - 6); 0265 if (!result.parts.contains(partId)) { 0266 result.parts[partId] = ContentPtr(new KMime::Content); 0267 } 0268 result.parts[partId]->setHead(*it); 0269 result.parts[partId]->parse(); 0270 } else { 0271 if (!result.message) { 0272 result.message = MessagePtr(new KMime::Message); 0273 } 0274 shouldParseMessage = true; 0275 result.message->setHead(*it); 0276 } 0277 } else { // full payload 0278 if (str == "BODY[]") { 0279 if (!result.message) { 0280 result.message = MessagePtr(new KMime::Message); 0281 } 0282 shouldParseMessage = true; 0283 result.message->setContent(KMime::CRLFtoLF(*it)); 0284 } else { 0285 QByteArray partId = str.mid(5, str.size() - 6); 0286 if (!result.parts.contains(partId)) { 0287 result.parts[partId] = ContentPtr(new KMime::Content); 0288 } 0289 result.parts[partId]->setBody(*it); 0290 result.parts[partId]->parse(); 0291 } 0292 } 0293 } 0294 } 0295 0296 if (result.message && shouldParseMessage && !d->avoidParsing) { 0297 result.message->parse(); 0298 } 0299 emit resultReceived(result); 0300 } 0301 } 0302 } 0303 0304 void FetchJobPrivate::parseBodyStructure(const QByteArray &structure, int &pos, KMime::Content *content) 0305 { 0306 skipLeadingSpaces(structure, pos); 0307 0308 if (structure[pos] != '(') { 0309 return; 0310 } 0311 0312 pos++; 0313 0314 if (structure[pos] != '(') { // simple part 0315 pos--; 0316 parsePart(structure, pos, content); 0317 } else { // multi part 0318 content->contentType()->setMimeType("MULTIPART/MIXED"); 0319 while (pos < structure.size() && structure[pos] == '(') { 0320 KMime::Content *child = new KMime::Content; 0321 content->addContent(child); 0322 parseBodyStructure(structure, pos, child); 0323 child->assemble(); 0324 } 0325 0326 QByteArray subType = parseString(structure, pos); 0327 content->contentType()->setMimeType("MULTIPART/" + subType); 0328 0329 QByteArray parameters = parseSentence(structure, pos); // FIXME: Read the charset 0330 if (parameters.contains("BOUNDARY")) { 0331 content->contentType()->setBoundary(parameters.remove(0, parameters.indexOf("BOUNDARY") + 11).split('\"')[0]); 0332 } 0333 0334 QByteArray disposition = parseSentence(structure, pos); 0335 if (disposition.contains("INLINE")) { 0336 content->contentDisposition()->setDisposition(KMime::Headers::CDinline); 0337 } else if (disposition.contains("ATTACHMENT")) { 0338 content->contentDisposition()->setDisposition(KMime::Headers::CDattachment); 0339 } 0340 0341 parseSentence(structure, pos); // Ditch the body language 0342 } 0343 0344 // Consume what's left 0345 while (pos < structure.size() && structure[pos] != ')') { 0346 skipLeadingSpaces(structure, pos); 0347 parseSentence(structure, pos); 0348 skipLeadingSpaces(structure, pos); 0349 } 0350 0351 pos++; 0352 } 0353 0354 void FetchJobPrivate::parsePart(const QByteArray &structure, int &pos, KMime::Content *content) 0355 { 0356 if (structure[pos] != '(') { 0357 return; 0358 } 0359 0360 pos++; 0361 0362 QByteArray mainType = parseString(structure, pos); 0363 QByteArray subType = parseString(structure, pos); 0364 0365 content->contentType()->setMimeType(mainType + '/' + subType); 0366 0367 parseSentence(structure, pos); // Ditch the parameters... FIXME: Read it to get charset and name 0368 parseString(structure, pos); // ... and the id 0369 0370 content->contentDescription()->from7BitString(parseString(structure, pos)); 0371 0372 parseString(structure, pos); // Ditch the encoding too 0373 parseString(structure, pos); // ... and the size 0374 parseString(structure, pos); // ... and the line count 0375 0376 QByteArray disposition = parseSentence(structure, pos); 0377 if (disposition.contains("INLINE")) { 0378 content->contentDisposition()->setDisposition(KMime::Headers::CDinline); 0379 } else if (disposition.contains("ATTACHMENT")) { 0380 content->contentDisposition()->setDisposition(KMime::Headers::CDattachment); 0381 } 0382 if ((content->contentDisposition()->disposition() == KMime::Headers::CDattachment || 0383 content->contentDisposition()->disposition() == KMime::Headers::CDinline) && 0384 disposition.contains("FILENAME")) { 0385 QByteArray filename = disposition.remove(0, disposition.indexOf("FILENAME") + 11).split('\"')[0]; 0386 content->contentDisposition()->setFilename(QLatin1String(filename)); 0387 } 0388 0389 // Consume what's left 0390 while (pos < structure.size() && structure[pos] != ')') { 0391 skipLeadingSpaces(structure, pos); 0392 parseSentence(structure, pos); 0393 skipLeadingSpaces(structure, pos); 0394 } 0395 } 0396 0397 QByteArray FetchJobPrivate::parseSentence(const QByteArray &structure, int &pos) 0398 { 0399 QByteArray result; 0400 int stack = 0; 0401 0402 skipLeadingSpaces(structure, pos); 0403 0404 if (structure[pos] != '(') { 0405 return parseString(structure, pos); 0406 } 0407 0408 int start = pos; 0409 0410 do { 0411 switch (structure[pos]) { 0412 case '(': 0413 pos++; 0414 stack++; 0415 break; 0416 case ')': 0417 pos++; 0418 stack--; 0419 break; 0420 case '[': 0421 pos++; 0422 stack++; 0423 break; 0424 case ']': 0425 pos++; 0426 stack--; 0427 break; 0428 default: 0429 skipLeadingSpaces(structure, pos); 0430 parseString(structure, pos); 0431 skipLeadingSpaces(structure, pos); 0432 break; 0433 } 0434 } while (pos < structure.size() && stack != 0); 0435 0436 result = structure.mid(start, pos - start); 0437 0438 return result; 0439 } 0440 0441 QByteArray FetchJobPrivate::parseString(const QByteArray &structure, int &pos) 0442 { 0443 QByteArray result; 0444 0445 skipLeadingSpaces(structure, pos); 0446 0447 int start = pos; 0448 bool foundSlash = false; 0449 0450 // quoted string 0451 if (structure[pos] == '"') { 0452 pos++; 0453 Q_FOREVER { 0454 if (structure[pos] == '\\') 0455 { 0456 pos += 2; 0457 foundSlash = true; 0458 continue; 0459 } 0460 if (structure[pos] == '"') 0461 { 0462 result = structure.mid(start + 1, pos - start - 1); 0463 pos++; 0464 break; 0465 } 0466 pos++; 0467 } 0468 } else { // unquoted string 0469 Q_FOREVER { 0470 if (structure[pos] == ' ' || 0471 structure[pos] == '(' || 0472 structure[pos] == ')' || 0473 structure[pos] == '[' || 0474 structure[pos] == ']' || 0475 structure[pos] == '\n' || 0476 structure[pos] == '\r' || 0477 structure[pos] == '"') 0478 { 0479 break; 0480 } 0481 if (structure[pos] == '\\') 0482 { 0483 foundSlash = true; 0484 } 0485 pos++; 0486 } 0487 0488 result = structure.mid(start, pos - start); 0489 0490 // transform unquoted NIL 0491 if (result == "NIL") { 0492 result.clear(); 0493 } 0494 } 0495 0496 // simplify slashes 0497 if (foundSlash) { 0498 while (result.contains("\\\"")) { 0499 result.replace("\\\"", "\""); 0500 } 0501 while (result.contains("\\\\")) { 0502 result.replace("\\\\", "\\"); 0503 } 0504 } 0505 0506 return result; 0507 } 0508 0509 void FetchJobPrivate::skipLeadingSpaces(const QByteArray &structure, int &pos) 0510 { 0511 while (pos < structure.size() && structure[pos] == ' ') { 0512 pos++; 0513 } 0514 } 0515 0516 #include "moc_fetchjob.cpp"