KIMAP

fetchjob.cpp
1/*
2 SPDX-FileCopyrightText: 2009 Kevin Ottens <ervin@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "fetchjob.h"
8
9#include "kimap_debug.h"
10#include <KLocalizedString>
11#include <QTimer>
12
13#include "job_p.h"
14#include "response_p.h"
15#include "session_p.h"
16
17namespace KIMAP
18{
19class FetchJobPrivate : public JobPrivate
20{
21public:
22 FetchJobPrivate(FetchJob *job, Session *session, const QString &name)
23 : JobPrivate(session, name)
24 , q(job)
25 {
26 }
27
28 ~FetchJobPrivate()
29 {
30 }
31
32 void parseBodyStructure(const QByteArray &structure, int &pos, KMime::Content *content);
33 void parsePart(const QByteArray &structure, int &pos, KMime::Content *content);
34 QByteArray parseString(const QByteArray &structure, int &pos);
35 QByteArray parseSentence(const QByteArray &structure, int &pos);
36 void skipLeadingSpaces(const QByteArray &structure, int &pos);
37
38 void emitPendings()
39 {
40 if (pendingMsgs.isEmpty()) {
41 return;
42 }
43
44 Q_EMIT q->messagesAvailable(pendingMsgs);
45
46 if (!pendingParts.isEmpty()) {
47 Q_EMIT q->partsReceived(selectedMailBox, pendingUids, pendingParts);
48 Q_EMIT q->partsReceived(selectedMailBox, pendingUids, pendingAttributes, pendingParts);
49 }
50 if (!pendingSizes.isEmpty() || !pendingFlags.isEmpty() || !pendingMessages.isEmpty()) {
51 Q_EMIT q->headersReceived(selectedMailBox, pendingUids, pendingSizes, pendingFlags, pendingMessages);
52 Q_EMIT q->headersReceived(selectedMailBox, pendingUids, pendingSizes, pendingAttributes, pendingFlags, pendingMessages);
53 }
54 if (!pendingMessages.isEmpty()) {
55 Q_EMIT q->messagesReceived(selectedMailBox, pendingUids, pendingMessages);
56 Q_EMIT q->messagesReceived(selectedMailBox, pendingUids, pendingAttributes, pendingMessages);
57 }
58
59 pendingUids.clear();
60 pendingMessages.clear();
61 pendingParts.clear();
62 pendingSizes.clear();
63 pendingFlags.clear();
64 pendingAttributes.clear();
65 pendingMsgs.clear();
66 }
67
68 FetchJob *const q;
69
70 ImapSet set;
71 bool uidBased = false;
73 QString selectedMailBox;
74 bool gmailEnabled = false;
75
76 QTimer emitPendingsTimer;
77 QMap<qint64, MessagePtr> pendingMessages;
78 QMap<qint64, MessageParts> pendingParts;
79 QMap<qint64, MessageFlags> pendingFlags;
80 QMap<qint64, MessageAttribute> pendingAttributes;
81 QMap<qint64, qint64> pendingSizes;
82 QMap<qint64, qint64> pendingUids;
83 QMap<qint64, Message> pendingMsgs;
84};
85}
86
87using namespace KIMAP;
88
89FetchJob::FetchScope::FetchScope()
90 : mode(FetchScope::Content)
91 , changedSince(0)
92 , qresync(false)
93{
94}
95
96FetchJob::FetchJob(Session *session)
97 : Job(*new FetchJobPrivate(this, session, i18n("Fetch")))
98{
100 connect(&d->emitPendingsTimer, &QTimer::timeout, this, [d]() {
101 d->emitPendings();
102 });
103}
104
106{
107 Q_D(FetchJob);
108 Q_ASSERT(!set.isEmpty());
109 d->set = set;
110}
111
113{
114 Q_D(const FetchJob);
115 return d->set;
116}
117
118void FetchJob::setUidBased(bool uidBased)
119{
120 Q_D(FetchJob);
121 d->uidBased = uidBased;
122}
123
125{
126 Q_D(const FetchJob);
127 return d->uidBased;
128}
129
131{
132 Q_D(FetchJob);
133 d->scope = scope;
134}
135
137{
138 Q_D(const FetchJob);
139 return d->scope;
140}
141
143{
144 Q_D(const FetchJob);
145 return d->gmailEnabled;
146}
147
149{
150 Q_D(FetchJob);
151 d->gmailEnabled = enabled;
152}
153
155{
156 Q_D(const FetchJob);
157 return d->selectedMailBox;
158}
159
160void FetchJob::doStart()
161{
162 Q_D(FetchJob);
163
164 d->set.optimize();
165 QByteArray parameters = d->set.toImapSequenceSet() + ' ';
166 Q_ASSERT(!parameters.trimmed().isEmpty());
167
168 switch (d->scope.mode) {
170 if (d->scope.parts.isEmpty()) {
171 parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID";
172 } else {
173 parameters += '(';
174 for (const QByteArray &part : std::as_const(d->scope.parts)) {
175 parameters += "BODY.PEEK[" + part + ".MIME] ";
176 }
177 parameters += "UID";
178 }
179 break;
181 parameters += "(FLAGS UID";
182 break;
184 parameters += "(BODYSTRUCTURE UID";
185 break;
187 if (d->scope.parts.isEmpty()) {
188 parameters += "(BODY.PEEK[] UID";
189 } else {
190 parameters += '(';
191 for (const QByteArray &part : std::as_const(d->scope.parts)) {
192 parameters += "BODY.PEEK[" + part + "] ";
193 }
194 parameters += "UID";
195 }
196 break;
197 case FetchScope::Full:
198 parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID";
199 break;
201 if (d->scope.parts.isEmpty()) {
202 parameters += "(BODY.PEEK[] FLAGS UID";
203 } else {
204 parameters += "(BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)]";
205 for (const QByteArray &part : std::as_const(d->scope.parts)) {
206 parameters += " BODY.PEEK[" + part + ".MIME] BODY.PEEK[" + part + "]"; // krazy:exclude=doublequote_chars
207 }
208 parameters += " FLAGS UID";
209 }
210 break;
212 parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER] FLAGS UID";
213 break;
214 }
215
216 if (d->gmailEnabled) {
217 parameters += " X-GM-LABELS X-GM-MSGID X-GM-THRID";
218 }
219 parameters += ")";
220
221 if (d->scope.changedSince > 0) {
222 parameters += " (CHANGEDSINCE " + QByteArray::number(d->scope.changedSince);
223 if (d->scope.qresync) {
224 parameters += " VANISHED";
225 }
226 parameters += ")";
227 }
228
229 QByteArray command = "FETCH";
230 if (d->uidBased) {
231 command = "UID " + command;
232 }
233
234 d->emitPendingsTimer.start(100);
235 d->selectedMailBox = d->m_session->selectedMailBox();
236 d->tags << d->sessionInternal()->sendCommand(command, parameters);
237}
238
239void FetchJob::handleResponse(const Response &response)
240{
241 Q_D(FetchJob);
242
243 // We can predict it'll be handled by handleErrorReplies() so stop
244 // the timer now so that result() will really be the last emitted signal.
245 if (!response.content.isEmpty() && d->tags.size() == 1 && d->tags.contains(response.content.first().toString())) {
246 d->emitPendingsTimer.stop();
247 d->emitPendings();
248 }
249
250 if (handleErrorReplies(response) == NotHandled) {
251 if (response.content.size() == 4 && response.content[1].toString() == "VANISHED") {
252 const auto vanishedSet = ImapSet::fromImapSequenceSet(response.content[3].toString());
253 Q_EMIT messagesVanished(vanishedSet);
254 } else if (response.content.size() == 4 && response.content[2].toString() == "FETCH" && response.content[3].type() == Response::Part::List) {
255 const qint64 id = response.content[1].toString().toLongLong();
256 const QList<QByteArray> content = response.content[3].toList();
257
258 Message msg;
259 MessagePtr message(new KMime::Message);
260 bool shouldParseMessage = false;
261 MessageParts parts;
262
263 for (QList<QByteArray>::ConstIterator it = content.constBegin(); it != content.constEnd(); ++it) {
264 QByteArray str = *it;
265 ++it;
266
267 if (it == content.constEnd()) { // Uh oh, message was truncated?
268 qCWarning(KIMAP_LOG) << "FETCH reply got truncated, skipping.";
269 break;
270 }
271
272 if (str == "UID") {
273 d->pendingUids[id] = msg.uid = it->toLongLong();
274 } else if (str == "RFC822.SIZE") {
275 d->pendingSizes[id] = msg.size = it->toLongLong();
276 } else if (str == "INTERNALDATE") {
277 message->date()->setDateTime(QDateTime::fromString(QLatin1StringView(*it), Qt::RFC2822Date));
278 } else if (str == "FLAGS") {
279 if ((*it).startsWith('(') && (*it).endsWith(')')) {
280 QByteArray str = *it;
281 str.chop(1);
282 str.remove(0, 1);
283 const auto flags = str.split(' ');
284 d->pendingFlags[id] = flags;
285 msg.flags = flags;
286 } else {
287 d->pendingFlags[id] << *it;
288 msg.flags << *it;
289 }
290 } else if (str == "X-GM-LABELS") {
291 d->pendingAttributes.insert(id, {"X-GM-LABELS", *it});
292 msg.attributes.insert("X-GM-LABELS", *it);
293 } else if (str == "X-GM-THRID") {
294 d->pendingAttributes.insert(id, {"X-GM-THRID", *it});
295 msg.attributes.insert("X-GM-THRID", *it);
296 } else if (str == "X-GM-MSGID") {
297 d->pendingAttributes.insert(id, {"X-GM-MSGID", *it});
298 msg.attributes.insert("X-GM-MSGID", *it);
299 } else if (str == "BODYSTRUCTURE") {
300 int pos = 0;
301 d->parseBodyStructure(*it, pos, message.data());
302 message->assemble();
303 d->pendingMessages[id] = message;
304 msg.message = message;
305 } else if (str.startsWith("BODY[")) { // krazy:exclude=strings
306 if (!str.endsWith(']')) { // BODY[ ... ] might have been split, skip until we find the ]
307 while (!(*it).endsWith(']')) {
308 ++it;
309 }
310 ++it;
311 }
312
313 int index;
314 if ((index = str.indexOf("HEADER")) > 0 || (index = str.indexOf("MIME")) > 0) { // headers
315 if (str[index - 1] == '.') {
316 QByteArray partId = str.mid(5, index - 6);
317 if (!parts.contains(partId)) {
318 parts[partId] = ContentPtr(new KMime::Content);
319 }
320 parts[partId]->setHead(*it);
321 parts[partId]->parse();
322 d->pendingParts[id] = parts;
323 msg.parts = parts;
324 } else {
325 message->setHead(*it);
326 shouldParseMessage = true;
327 }
328 } else { // full payload
329 if (str == "BODY[]") {
330 message->setContent(KMime::CRLFtoLF(*it));
331 shouldParseMessage = true;
332
333 d->pendingMessages[id] = message;
334 msg.message = message;
335 } else {
336 QByteArray partId = str.mid(5, str.size() - 6);
337 if (!parts.contains(partId)) {
338 parts[partId] = ContentPtr(new KMime::Content);
339 }
340 parts[partId]->setBody(*it);
341 parts[partId]->parse();
342
343 d->pendingParts[id] = parts;
344 msg.parts = parts;
345 }
346 }
347 }
348 }
349
350 if (shouldParseMessage) {
351 message->parse();
352 }
353
354 // For the headers mode the message is built in several
355 // steps, hence why we wait it to be done until putting it
356 // in the pending queue.
357 if (d->scope.mode == FetchScope::Headers || d->scope.mode == FetchScope::HeaderAndContent || d->scope.mode == FetchScope::FullHeaders) {
358 d->pendingMessages[id] = message;
359 msg.message = message;
360 }
361
362 d->pendingMsgs[id] = msg;
363 }
364 }
365}
366
367void FetchJobPrivate::parseBodyStructure(const QByteArray &structure, int &pos, KMime::Content *content)
368{
369 skipLeadingSpaces(structure, pos);
370
371 if (structure[pos] != '(') {
372 return;
373 }
374
375 pos++;
376
377 if (structure[pos] != '(') { // simple part
378 pos--;
379 parsePart(structure, pos, content);
380 } else { // multi part
381 content->contentType()->setMimeType("MULTIPART/MIXED");
382 while (pos < structure.size() && structure[pos] == '(') {
383 auto child = new KMime::Content;
384 content->appendContent(child);
385 parseBodyStructure(structure, pos, child);
386 child->assemble();
387 }
388
389 QByteArray subType = parseString(structure, pos);
390 content->contentType()->setMimeType("MULTIPART/" + subType);
391
392 QByteArray parameters = parseSentence(structure, pos); // FIXME: Read the charset
393 if (parameters.contains("BOUNDARY")) {
394 content->contentType()->setBoundary(parameters.remove(0, parameters.indexOf("BOUNDARY") + 11).split('\"')[0]);
395 }
396
397 QByteArray disposition = parseSentence(structure, pos);
398 if (disposition.contains("INLINE")) {
399 content->contentDisposition()->setDisposition(KMime::Headers::CDinline);
400 } else if (disposition.contains("ATTACHMENT")) {
401 content->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
402 }
403
404 parseSentence(structure, pos); // Ditch the body language
405 }
406
407 // Consume what's left
408 while (pos < structure.size() && structure[pos] != ')') {
409 skipLeadingSpaces(structure, pos);
410 parseSentence(structure, pos);
411 skipLeadingSpaces(structure, pos);
412 }
413
414 pos++;
415}
416
417void FetchJobPrivate::parsePart(const QByteArray &structure, int &pos, KMime::Content *content)
418{
419 if (structure[pos] != '(') {
420 return;
421 }
422
423 pos++;
424
425 QByteArray mainType = parseString(structure, pos);
426 QByteArray subType = parseString(structure, pos);
427
428 content->contentType()->setMimeType(mainType + '/' + subType);
429
430 parseSentence(structure, pos); // Ditch the parameters... FIXME: Read it to get charset and name
431 parseString(structure, pos); // ... and the id
432
433 content->contentDescription()->from7BitString(parseString(structure, pos));
434
435 parseString(structure, pos); // Ditch the encoding too
436 parseString(structure, pos); // ... and the size
437 parseString(structure, pos); // ... and the line count
438
439 QByteArray disposition = parseSentence(structure, pos);
440 if (disposition.contains("INLINE")) {
441 content->contentDisposition()->setDisposition(KMime::Headers::CDinline);
442 } else if (disposition.contains("ATTACHMENT")) {
443 content->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
444 }
445 if ((content->contentDisposition()->disposition() == KMime::Headers::CDattachment
446 || content->contentDisposition()->disposition() == KMime::Headers::CDinline)
447 && disposition.contains("FILENAME")) {
448 QByteArray filename = disposition.remove(0, disposition.indexOf("FILENAME") + 11).split('\"')[0];
449 content->contentDisposition()->setFilename(QLatin1StringView(filename));
450 }
451
452 // Consume what's left
453 while (pos < structure.size() && structure[pos] != ')') {
454 skipLeadingSpaces(structure, pos);
455 parseSentence(structure, pos);
456 skipLeadingSpaces(structure, pos);
457 }
458}
459
460QByteArray FetchJobPrivate::parseSentence(const QByteArray &structure, int &pos)
461{
462 QByteArray result;
463 int stack = 0;
464
465 skipLeadingSpaces(structure, pos);
466
467 if (structure[pos] != '(') {
468 return parseString(structure, pos);
469 }
470
471 int start = pos;
472
473 do {
474 switch (structure[pos]) {
475 case '(':
476 pos++;
477 stack++;
478 break;
479 case ')':
480 pos++;
481 stack--;
482 break;
483 case '[':
484 pos++;
485 stack++;
486 break;
487 case ']':
488 pos++;
489 stack--;
490 break;
491 default:
492 skipLeadingSpaces(structure, pos);
493 parseString(structure, pos);
494 skipLeadingSpaces(structure, pos);
495 break;
496 }
497 } while (pos < structure.size() && stack != 0);
498
499 result = structure.mid(start, pos - start);
500
501 return result;
502}
503
504QByteArray FetchJobPrivate::parseString(const QByteArray &structure, int &pos)
505{
506 QByteArray result;
507
508 skipLeadingSpaces(structure, pos);
509
510 int start = pos;
511 bool foundSlash = false;
512
513 // quoted string
514 if (structure[pos] == '"') {
515 pos++;
516 for (;;) {
517 if (structure[pos] == '\\') {
518 pos += 2;
519 foundSlash = true;
520 continue;
521 }
522 if (structure[pos] == '"') {
523 result = structure.mid(start + 1, pos - start - 1);
524 pos++;
525 break;
526 }
527 pos++;
528 }
529 } else { // unquoted string
530 for (;;) {
531 if (structure[pos] == ' ' || structure[pos] == '(' || structure[pos] == ')' || structure[pos] == '[' || structure[pos] == ']'
532 || structure[pos] == '\n' || structure[pos] == '\r' || structure[pos] == '"') {
533 break;
534 }
535 if (structure[pos] == '\\') {
536 foundSlash = true;
537 }
538 pos++;
539 }
540
541 result = structure.mid(start, pos - start);
542
543 // transform unquoted NIL
544 if (result == "NIL") {
545 result.clear();
546 }
547 }
548
549 // simplify slashes
550 if (foundSlash) {
551 while (result.contains("\\\"")) {
552 result.replace("\\\"", "\"");
553 }
554 while (result.contains("\\\\")) {
555 result.replace("\\\\", "\\");
556 }
557 }
558
559 return result;
560}
561
562void FetchJobPrivate::skipLeadingSpaces(const QByteArray &structure, int &pos)
563{
564 while (pos < structure.size() && structure[pos] == ' ') {
565 pos++;
566 }
567}
568
569#include "moc_fetchjob.cpp"
Used to indicate what message data should be fetched.
Definition fetchjob.h:75
@ FullHeaders
Fetch message size (in octets), internal date of the message, flags, UID and all RFC822 headers.
Definition fetchjob.h:143
@ Full
Fetch the complete message.
Definition fetchjob.h:115
@ Headers
Fetch RFC-2822 or MIME message headers.
Definition fetchjob.h:96
@ Structure
Fetch the MIME message body structure (the UID is also fetched)
Definition fetchjob.h:104
@ Content
Fetch the message content (the UID is also fetched)
Definition fetchjob.h:111
@ HeaderAndContent
Fetch the message MIME headers and the content of parts specified in the parts field.
Definition fetchjob.h:133
@ Flags
Fetch the message flags (the UID is also fetched)
Definition fetchjob.h:100
Fetch message data from the server.
Definition fetchjob.h:60
void messagesAvailable(const QMap< qint64, KIMAP::Message > &messages)
Provides received messages.
void setUidBased(bool uidBased)
Set how the sequence set should be interpreted.
Definition fetchjob.cpp:118
ImapSet sequenceSet() const
The messages that will be fetched.
Definition fetchjob.cpp:112
KIMAP_DEPRECATED void messagesReceived(const QString &mailBox, const QMap< qint64, qint64 > &uids, const QMap< qint64, KIMAP::MessagePtr > &messages)
Provides header and message results.
KIMAP_DEPRECATED void partsReceived(const QString &mailBox, const QMap< qint64, qint64 > &uids, const QMap< qint64, KIMAP::MessageParts > &parts)
Provides header and message results.
KIMAP_DEPRECATED void headersReceived(const QString &mailBox, const QMap< qint64, qint64 > &uids, const QMap< qint64, qint64 > &sizes, const QMap< qint64, KIMAP::MessageFlags > &flags, const QMap< qint64, KIMAP::MessagePtr > &messages)
Provides header and message results.
void messagesVanished(const KIMAP::ImapSet &uids)
Provides vanished messages.
FetchScope scope() const
Specifies what data will be fetched.
Definition fetchjob.cpp:136
void setSequenceSet(const ImapSet &set)
Set which messages to fetch data for.
Definition fetchjob.cpp:105
QString mailBox() const
Returns the name of the mailbox the fetch job is executed on.
Definition fetchjob.cpp:154
void setScope(const FetchScope &scope)
Sets what data should be fetched.
Definition fetchjob.cpp:130
bool isUidBased() const
How to interpret the sequence set.
Definition fetchjob.cpp:124
bool setGmailExtensionsEnabled() const
Returns whether Gmail support is enabled.
Definition fetchjob.cpp:142
Represents a set of natural numbers (1->∞) in a as compact as possible form.
Definition imapset.h:127
bool isEmpty() const
Returns true if this set doesn't contains any values.
Definition imapset.cpp:305
static ImapSet fromImapSequenceSet(const QByteArray &sequence)
Return the set corresponding to the given IMAP-compatible QByteArray representation.
Definition imapset.cpp:285
Headers::ContentDescription * contentDescription(bool create=true)
Headers::ContentType * contentType(bool create=true)
Headers::ContentDisposition * contentDisposition(bool create=true)
void appendContent(Content *content)
void setDisposition(contentDisposition disp)
contentDisposition disposition() const
void setFilename(const QString &filename)
void setMimeType(const QByteArray &mimeType)
void setBoundary(const QByteArray &s)
virtual void from7BitString(const char *s, size_t len)
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18n(const char *text, const TYPE &arg...)
QString name(StandardShortcut id)
void chop(qsizetype n)
void clear()
bool contains(QByteArrayView bv) const const
bool endsWith(QByteArrayView bv) const const
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
bool isEmpty() const const
QByteArray mid(qsizetype pos, qsizetype len) const const
QByteArray number(double n, char format, int precision)
QByteArray & remove(qsizetype pos, qsizetype len)
QByteArray & replace(QByteArrayView before, QByteArrayView after)
qsizetype size() const const
QList< QByteArray > split(char sep) const const
bool startsWith(QByteArrayView bv) const const
QByteArray trimmed() const const
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
QList< T > toList() const const
const_iterator constBegin() const const
const_iterator constEnd() const const
iterator insert(const_iterator before, parameter_type value)
void clear()
bool contains(const Key &key) const const
iterator insert(const Key &key, const T &value)
bool isEmpty() const const
Q_EMITQ_EMIT
RFC2822Date
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:37 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.