File indexing completed on 2024-05-12 05:17:13
0001 /* 0002 SPDX-FileCopyrightText: 2009 Kevin Ottens <ervin@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "fetchjob.h" 0008 0009 #include "kimap_debug.h" 0010 #include <KLocalizedString> 0011 #include <QTimer> 0012 0013 #include "job_p.h" 0014 #include "response_p.h" 0015 #include "session_p.h" 0016 0017 namespace KIMAP 0018 { 0019 class FetchJobPrivate : public JobPrivate 0020 { 0021 public: 0022 FetchJobPrivate(FetchJob *job, Session *session, const QString &name) 0023 : JobPrivate(session, name) 0024 , q(job) 0025 { 0026 } 0027 0028 ~FetchJobPrivate() 0029 { 0030 } 0031 0032 void parseBodyStructure(const QByteArray &structure, int &pos, KMime::Content *content); 0033 void parsePart(const QByteArray &structure, int &pos, KMime::Content *content); 0034 QByteArray parseString(const QByteArray &structure, int &pos); 0035 QByteArray parseSentence(const QByteArray &structure, int &pos); 0036 void skipLeadingSpaces(const QByteArray &structure, int &pos); 0037 0038 void emitPendings() 0039 { 0040 if (pendingMsgs.isEmpty()) { 0041 return; 0042 } 0043 0044 Q_EMIT q->messagesAvailable(pendingMsgs); 0045 0046 if (!pendingParts.isEmpty()) { 0047 Q_EMIT q->partsReceived(selectedMailBox, pendingUids, pendingParts); 0048 Q_EMIT q->partsReceived(selectedMailBox, pendingUids, pendingAttributes, pendingParts); 0049 } 0050 if (!pendingSizes.isEmpty() || !pendingFlags.isEmpty() || !pendingMessages.isEmpty()) { 0051 Q_EMIT q->headersReceived(selectedMailBox, pendingUids, pendingSizes, pendingFlags, pendingMessages); 0052 Q_EMIT q->headersReceived(selectedMailBox, pendingUids, pendingSizes, pendingAttributes, pendingFlags, pendingMessages); 0053 } 0054 if (!pendingMessages.isEmpty()) { 0055 Q_EMIT q->messagesReceived(selectedMailBox, pendingUids, pendingMessages); 0056 Q_EMIT q->messagesReceived(selectedMailBox, pendingUids, pendingAttributes, pendingMessages); 0057 } 0058 0059 pendingUids.clear(); 0060 pendingMessages.clear(); 0061 pendingParts.clear(); 0062 pendingSizes.clear(); 0063 pendingFlags.clear(); 0064 pendingAttributes.clear(); 0065 pendingMsgs.clear(); 0066 } 0067 0068 FetchJob *const q; 0069 0070 ImapSet set; 0071 bool uidBased = false; 0072 FetchJob::FetchScope scope; 0073 QString selectedMailBox; 0074 bool gmailEnabled = false; 0075 0076 QTimer emitPendingsTimer; 0077 QMap<qint64, MessagePtr> pendingMessages; 0078 QMap<qint64, MessageParts> pendingParts; 0079 QMap<qint64, MessageFlags> pendingFlags; 0080 QMap<qint64, MessageAttribute> pendingAttributes; 0081 QMap<qint64, qint64> pendingSizes; 0082 QMap<qint64, qint64> pendingUids; 0083 QMap<qint64, Message> pendingMsgs; 0084 }; 0085 } 0086 0087 using namespace KIMAP; 0088 0089 FetchJob::FetchScope::FetchScope() 0090 : mode(FetchScope::Content) 0091 , changedSince(0) 0092 , qresync(false) 0093 { 0094 } 0095 0096 FetchJob::FetchJob(Session *session) 0097 : Job(*new FetchJobPrivate(this, session, i18n("Fetch"))) 0098 { 0099 Q_D(FetchJob); 0100 connect(&d->emitPendingsTimer, &QTimer::timeout, this, [d]() { 0101 d->emitPendings(); 0102 }); 0103 } 0104 0105 void FetchJob::setSequenceSet(const ImapSet &set) 0106 { 0107 Q_D(FetchJob); 0108 Q_ASSERT(!set.isEmpty()); 0109 d->set = set; 0110 } 0111 0112 ImapSet FetchJob::sequenceSet() const 0113 { 0114 Q_D(const FetchJob); 0115 return d->set; 0116 } 0117 0118 void FetchJob::setUidBased(bool uidBased) 0119 { 0120 Q_D(FetchJob); 0121 d->uidBased = uidBased; 0122 } 0123 0124 bool FetchJob::isUidBased() const 0125 { 0126 Q_D(const FetchJob); 0127 return d->uidBased; 0128 } 0129 0130 void FetchJob::setScope(const FetchScope &scope) 0131 { 0132 Q_D(FetchJob); 0133 d->scope = scope; 0134 } 0135 0136 FetchJob::FetchScope FetchJob::scope() const 0137 { 0138 Q_D(const FetchJob); 0139 return d->scope; 0140 } 0141 0142 bool FetchJob::setGmailExtensionsEnabled() const 0143 { 0144 Q_D(const FetchJob); 0145 return d->gmailEnabled; 0146 } 0147 0148 void FetchJob::setGmailExtensionsEnabled(bool enabled) 0149 { 0150 Q_D(FetchJob); 0151 d->gmailEnabled = enabled; 0152 } 0153 0154 QString FetchJob::mailBox() const 0155 { 0156 Q_D(const FetchJob); 0157 return d->selectedMailBox; 0158 } 0159 0160 void FetchJob::doStart() 0161 { 0162 Q_D(FetchJob); 0163 0164 d->set.optimize(); 0165 QByteArray parameters = d->set.toImapSequenceSet() + ' '; 0166 Q_ASSERT(!parameters.trimmed().isEmpty()); 0167 0168 switch (d->scope.mode) { 0169 case FetchScope::Headers: 0170 if (d->scope.parts.isEmpty()) { 0171 parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID"; 0172 } else { 0173 parameters += '('; 0174 for (const QByteArray &part : std::as_const(d->scope.parts)) { 0175 parameters += "BODY.PEEK[" + part + ".MIME] "; 0176 } 0177 parameters += "UID"; 0178 } 0179 break; 0180 case FetchScope::Flags: 0181 parameters += "(FLAGS UID"; 0182 break; 0183 case FetchScope::Structure: 0184 parameters += "(BODYSTRUCTURE UID"; 0185 break; 0186 case FetchScope::Content: 0187 if (d->scope.parts.isEmpty()) { 0188 parameters += "(BODY.PEEK[] UID"; 0189 } else { 0190 parameters += '('; 0191 for (const QByteArray &part : std::as_const(d->scope.parts)) { 0192 parameters += "BODY.PEEK[" + part + "] "; 0193 } 0194 parameters += "UID"; 0195 } 0196 break; 0197 case FetchScope::Full: 0198 parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID"; 0199 break; 0200 case FetchScope::HeaderAndContent: 0201 if (d->scope.parts.isEmpty()) { 0202 parameters += "(BODY.PEEK[] FLAGS UID"; 0203 } else { 0204 parameters += "(BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)]"; 0205 for (const QByteArray &part : std::as_const(d->scope.parts)) { 0206 parameters += " BODY.PEEK[" + part + ".MIME] BODY.PEEK[" + part + "]"; // krazy:exclude=doublequote_chars 0207 } 0208 parameters += " FLAGS UID"; 0209 } 0210 break; 0211 case FetchScope::FullHeaders: 0212 parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER] FLAGS UID"; 0213 break; 0214 } 0215 0216 if (d->gmailEnabled) { 0217 parameters += " X-GM-LABELS X-GM-MSGID X-GM-THRID"; 0218 } 0219 parameters += ")"; 0220 0221 if (d->scope.changedSince > 0) { 0222 parameters += " (CHANGEDSINCE " + QByteArray::number(d->scope.changedSince); 0223 if (d->scope.qresync) { 0224 parameters += " VANISHED"; 0225 } 0226 parameters += ")"; 0227 } 0228 0229 QByteArray command = "FETCH"; 0230 if (d->uidBased) { 0231 command = "UID " + command; 0232 } 0233 0234 d->emitPendingsTimer.start(100); 0235 d->selectedMailBox = d->m_session->selectedMailBox(); 0236 d->tags << d->sessionInternal()->sendCommand(command, parameters); 0237 } 0238 0239 void FetchJob::handleResponse(const Response &response) 0240 { 0241 Q_D(FetchJob); 0242 0243 // We can predict it'll be handled by handleErrorReplies() so stop 0244 // the timer now so that result() will really be the last emitted signal. 0245 if (!response.content.isEmpty() && d->tags.size() == 1 && d->tags.contains(response.content.first().toString())) { 0246 d->emitPendingsTimer.stop(); 0247 d->emitPendings(); 0248 } 0249 0250 if (handleErrorReplies(response) == NotHandled) { 0251 if (response.content.size() == 4 && response.content[1].toString() == "VANISHED") { 0252 const auto vanishedSet = ImapSet::fromImapSequenceSet(response.content[3].toString()); 0253 Q_EMIT messagesVanished(vanishedSet); 0254 } else if (response.content.size() == 4 && response.content[2].toString() == "FETCH" && response.content[3].type() == Response::Part::List) { 0255 const qint64 id = response.content[1].toString().toLongLong(); 0256 const QList<QByteArray> content = response.content[3].toList(); 0257 0258 Message msg; 0259 MessagePtr message(new KMime::Message); 0260 bool shouldParseMessage = false; 0261 MessageParts parts; 0262 0263 for (QList<QByteArray>::ConstIterator it = content.constBegin(); it != content.constEnd(); ++it) { 0264 QByteArray str = *it; 0265 ++it; 0266 0267 if (it == content.constEnd()) { // Uh oh, message was truncated? 0268 qCWarning(KIMAP_LOG) << "FETCH reply got truncated, skipping."; 0269 break; 0270 } 0271 0272 if (str == "UID") { 0273 d->pendingUids[id] = msg.uid = it->toLongLong(); 0274 } else if (str == "RFC822.SIZE") { 0275 d->pendingSizes[id] = msg.size = it->toLongLong(); 0276 } else if (str == "INTERNALDATE") { 0277 message->date()->setDateTime(QDateTime::fromString(QLatin1StringView(*it), Qt::RFC2822Date)); 0278 } else if (str == "FLAGS") { 0279 if ((*it).startsWith('(') && (*it).endsWith(')')) { 0280 QByteArray str = *it; 0281 str.chop(1); 0282 str.remove(0, 1); 0283 const auto flags = str.split(' '); 0284 d->pendingFlags[id] = flags; 0285 msg.flags = flags; 0286 } else { 0287 d->pendingFlags[id] << *it; 0288 msg.flags << *it; 0289 } 0290 } else if (str == "X-GM-LABELS") { 0291 d->pendingAttributes.insert(id, {"X-GM-LABELS", *it}); 0292 msg.attributes.insert("X-GM-LABELS", *it); 0293 } else if (str == "X-GM-THRID") { 0294 d->pendingAttributes.insert(id, {"X-GM-THRID", *it}); 0295 msg.attributes.insert("X-GM-THRID", *it); 0296 } else if (str == "X-GM-MSGID") { 0297 d->pendingAttributes.insert(id, {"X-GM-MSGID", *it}); 0298 msg.attributes.insert("X-GM-MSGID", *it); 0299 } else if (str == "BODYSTRUCTURE") { 0300 int pos = 0; 0301 d->parseBodyStructure(*it, pos, message.data()); 0302 message->assemble(); 0303 d->pendingMessages[id] = message; 0304 msg.message = message; 0305 } else if (str.startsWith("BODY[")) { // krazy:exclude=strings 0306 if (!str.endsWith(']')) { // BODY[ ... ] might have been split, skip until we find the ] 0307 while (!(*it).endsWith(']')) { 0308 ++it; 0309 } 0310 ++it; 0311 } 0312 0313 int index; 0314 if ((index = str.indexOf("HEADER")) > 0 || (index = str.indexOf("MIME")) > 0) { // headers 0315 if (str[index - 1] == '.') { 0316 QByteArray partId = str.mid(5, index - 6); 0317 if (!parts.contains(partId)) { 0318 parts[partId] = ContentPtr(new KMime::Content); 0319 } 0320 parts[partId]->setHead(*it); 0321 parts[partId]->parse(); 0322 d->pendingParts[id] = parts; 0323 msg.parts = parts; 0324 } else { 0325 message->setHead(*it); 0326 shouldParseMessage = true; 0327 } 0328 } else { // full payload 0329 if (str == "BODY[]") { 0330 message->setContent(KMime::CRLFtoLF(*it)); 0331 shouldParseMessage = true; 0332 0333 d->pendingMessages[id] = message; 0334 msg.message = message; 0335 } else { 0336 QByteArray partId = str.mid(5, str.size() - 6); 0337 if (!parts.contains(partId)) { 0338 parts[partId] = ContentPtr(new KMime::Content); 0339 } 0340 parts[partId]->setBody(*it); 0341 parts[partId]->parse(); 0342 0343 d->pendingParts[id] = parts; 0344 msg.parts = parts; 0345 } 0346 } 0347 } 0348 } 0349 0350 if (shouldParseMessage) { 0351 message->parse(); 0352 } 0353 0354 // For the headers mode the message is built in several 0355 // steps, hence why we wait it to be done until putting it 0356 // in the pending queue. 0357 if (d->scope.mode == FetchScope::Headers || d->scope.mode == FetchScope::HeaderAndContent || d->scope.mode == FetchScope::FullHeaders) { 0358 d->pendingMessages[id] = message; 0359 msg.message = message; 0360 } 0361 0362 d->pendingMsgs[id] = msg; 0363 } 0364 } 0365 } 0366 0367 void FetchJobPrivate::parseBodyStructure(const QByteArray &structure, int &pos, KMime::Content *content) 0368 { 0369 skipLeadingSpaces(structure, pos); 0370 0371 if (structure[pos] != '(') { 0372 return; 0373 } 0374 0375 pos++; 0376 0377 if (structure[pos] != '(') { // simple part 0378 pos--; 0379 parsePart(structure, pos, content); 0380 } else { // multi part 0381 content->contentType()->setMimeType("MULTIPART/MIXED"); 0382 while (pos < structure.size() && structure[pos] == '(') { 0383 auto child = new KMime::Content; 0384 content->appendContent(child); 0385 parseBodyStructure(structure, pos, child); 0386 child->assemble(); 0387 } 0388 0389 QByteArray subType = parseString(structure, pos); 0390 content->contentType()->setMimeType("MULTIPART/" + subType); 0391 0392 QByteArray parameters = parseSentence(structure, pos); // FIXME: Read the charset 0393 if (parameters.contains("BOUNDARY")) { 0394 content->contentType()->setBoundary(parameters.remove(0, parameters.indexOf("BOUNDARY") + 11).split('\"')[0]); 0395 } 0396 0397 QByteArray disposition = parseSentence(structure, pos); 0398 if (disposition.contains("INLINE")) { 0399 content->contentDisposition()->setDisposition(KMime::Headers::CDinline); 0400 } else if (disposition.contains("ATTACHMENT")) { 0401 content->contentDisposition()->setDisposition(KMime::Headers::CDattachment); 0402 } 0403 0404 parseSentence(structure, pos); // Ditch the body language 0405 } 0406 0407 // Consume what's left 0408 while (pos < structure.size() && structure[pos] != ')') { 0409 skipLeadingSpaces(structure, pos); 0410 parseSentence(structure, pos); 0411 skipLeadingSpaces(structure, pos); 0412 } 0413 0414 pos++; 0415 } 0416 0417 void FetchJobPrivate::parsePart(const QByteArray &structure, int &pos, KMime::Content *content) 0418 { 0419 if (structure[pos] != '(') { 0420 return; 0421 } 0422 0423 pos++; 0424 0425 QByteArray mainType = parseString(structure, pos); 0426 QByteArray subType = parseString(structure, pos); 0427 0428 content->contentType()->setMimeType(mainType + '/' + subType); 0429 0430 parseSentence(structure, pos); // Ditch the parameters... FIXME: Read it to get charset and name 0431 parseString(structure, pos); // ... and the id 0432 0433 content->contentDescription()->from7BitString(parseString(structure, pos)); 0434 0435 parseString(structure, pos); // Ditch the encoding too 0436 parseString(structure, pos); // ... and the size 0437 parseString(structure, pos); // ... and the line count 0438 0439 QByteArray disposition = parseSentence(structure, pos); 0440 if (disposition.contains("INLINE")) { 0441 content->contentDisposition()->setDisposition(KMime::Headers::CDinline); 0442 } else if (disposition.contains("ATTACHMENT")) { 0443 content->contentDisposition()->setDisposition(KMime::Headers::CDattachment); 0444 } 0445 if ((content->contentDisposition()->disposition() == KMime::Headers::CDattachment 0446 || content->contentDisposition()->disposition() == KMime::Headers::CDinline) 0447 && disposition.contains("FILENAME")) { 0448 QByteArray filename = disposition.remove(0, disposition.indexOf("FILENAME") + 11).split('\"')[0]; 0449 content->contentDisposition()->setFilename(QLatin1StringView(filename)); 0450 } 0451 0452 // Consume what's left 0453 while (pos < structure.size() && structure[pos] != ')') { 0454 skipLeadingSpaces(structure, pos); 0455 parseSentence(structure, pos); 0456 skipLeadingSpaces(structure, pos); 0457 } 0458 } 0459 0460 QByteArray FetchJobPrivate::parseSentence(const QByteArray &structure, int &pos) 0461 { 0462 QByteArray result; 0463 int stack = 0; 0464 0465 skipLeadingSpaces(structure, pos); 0466 0467 if (structure[pos] != '(') { 0468 return parseString(structure, pos); 0469 } 0470 0471 int start = pos; 0472 0473 do { 0474 switch (structure[pos]) { 0475 case '(': 0476 pos++; 0477 stack++; 0478 break; 0479 case ')': 0480 pos++; 0481 stack--; 0482 break; 0483 case '[': 0484 pos++; 0485 stack++; 0486 break; 0487 case ']': 0488 pos++; 0489 stack--; 0490 break; 0491 default: 0492 skipLeadingSpaces(structure, pos); 0493 parseString(structure, pos); 0494 skipLeadingSpaces(structure, pos); 0495 break; 0496 } 0497 } while (pos < structure.size() && stack != 0); 0498 0499 result = structure.mid(start, pos - start); 0500 0501 return result; 0502 } 0503 0504 QByteArray FetchJobPrivate::parseString(const QByteArray &structure, int &pos) 0505 { 0506 QByteArray result; 0507 0508 skipLeadingSpaces(structure, pos); 0509 0510 int start = pos; 0511 bool foundSlash = false; 0512 0513 // quoted string 0514 if (structure[pos] == '"') { 0515 pos++; 0516 for (;;) { 0517 if (structure[pos] == '\\') { 0518 pos += 2; 0519 foundSlash = true; 0520 continue; 0521 } 0522 if (structure[pos] == '"') { 0523 result = structure.mid(start + 1, pos - start - 1); 0524 pos++; 0525 break; 0526 } 0527 pos++; 0528 } 0529 } else { // unquoted string 0530 for (;;) { 0531 if (structure[pos] == ' ' || structure[pos] == '(' || structure[pos] == ')' || structure[pos] == '[' || structure[pos] == ']' 0532 || structure[pos] == '\n' || structure[pos] == '\r' || structure[pos] == '"') { 0533 break; 0534 } 0535 if (structure[pos] == '\\') { 0536 foundSlash = true; 0537 } 0538 pos++; 0539 } 0540 0541 result = structure.mid(start, pos - start); 0542 0543 // transform unquoted NIL 0544 if (result == "NIL") { 0545 result.clear(); 0546 } 0547 } 0548 0549 // simplify slashes 0550 if (foundSlash) { 0551 while (result.contains("\\\"")) { 0552 result.replace("\\\"", "\""); 0553 } 0554 while (result.contains("\\\\")) { 0555 result.replace("\\\\", "\\"); 0556 } 0557 } 0558 0559 return result; 0560 } 0561 0562 void FetchJobPrivate::skipLeadingSpaces(const QByteArray &structure, int &pos) 0563 { 0564 while (pos < structure.size() && structure[pos] == ' ') { 0565 pos++; 0566 } 0567 } 0568 0569 #include "moc_fetchjob.cpp"