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"