KIMAP

fetchjob.cpp
1 /*
2  SPDX-FileCopyrightText: 2009 Kevin Ottens <[email protected]>
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 
17 namespace KIMAP
18 {
19 class FetchJobPrivate : public JobPrivate
20 {
21 public:
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 
87 using namespace KIMAP;
88 
89 FetchJob::FetchScope::FetchScope()
90  : mode(FetchScope::Content)
91  , changedSince(0)
92  , qresync(false)
93 {
94 }
95 
96 FetchJob::FetchJob(Session *session)
97  : Job(*new FetchJobPrivate(this, session, i18n("Fetch")))
98 {
99  Q_D(FetchJob);
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 
118 void 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 
130 void FetchJob::setScope(const FetchScope &scope)
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 
160 void 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) {
169  case FetchScope::Headers:
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;
180  case FetchScope::Flags:
181  parameters += "(FLAGS UID";
182  break;
184  parameters += "(BODYSTRUCTURE UID";
185  break;
186  case FetchScope::Content:
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 
239 void 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;
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(QLatin1String(*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 
367 void 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->addContent(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 
417 void 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(QLatin1String(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 
460 QByteArray 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 
504 QByteArray 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 
562 void 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"
void setSequenceSet(const ImapSet &set)
Set which messages to fetch data for.
Definition: fetchjob.cpp:105
bool contains(const Key &key) const const
void messagesVanished(const KIMAP::ImapSet &uids)
Provides vanished messages.
@ FullHeaders
Fetch message size (in octets), internal date of the message, flags, UID and all RFC822 headers.
Definition: fetchjob.h:143
void setScope(const FetchScope &scope)
Sets what data should be fetched.
Definition: fetchjob.cpp:130
Q_EMITQ_EMIT
int indexOf(char ch, int from) const const
static ImapSet fromImapSequenceSet(const QByteArray &sequence)
Return the set corresponding to the given IMAP-compatible QByteArray representation.
Definition: imapset.cpp:285
Q_SCRIPTABLE Q_NOREPLY void start()
QByteArray number(int n, int base)
QList< QByteArray > split(char sep) const const
QByteArray trimmed() const const
@ HeaderAndContent
Fetch the message MIME headers and the content of parts specified in the parts field.
Definition: fetchjob.h:133
QList::const_iterator constBegin() const const
void setMimeType(const QByteArray &mimeType)
void clear()
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Fetch message data from the server.
Definition: fetchjob.h:59
Used to indicate what message data should be fetched.
Definition: fetchjob.h:74
ImapSet sequenceSet() const
The messages that will be fetched.
Definition: fetchjob.cpp:112
QByteArray & remove(int pos, int len)
qlonglong toLongLong(bool *ok, int base) const const
void chop(int n)
QString i18n(const char *text, const TYPE &arg...)
@ Content
Fetch the message content (the UID is also fetched)
Definition: fetchjob.h:111
contentDisposition disposition() const
QByteArray mid(int pos, int len) const const
void timeout()
Represents a set of natural numbers (1->∞) in a as compact as possible form.
Definition: imapset.h:126
Headers::ContentDisposition * contentDisposition(bool create=true)
bool startsWith(const QByteArray &ba) const const
bool setGmailExtensionsEnabled() const
Returns whether Gmail support is enabled.
Definition: fetchjob.cpp:142
void setUidBased(bool uidBased)
Set how the sequence set should be interpreted.
Definition: fetchjob.cpp:118
QDateTime fromString(const QString &string, Qt::DateFormat format)
bool contains(char ch) const const
void insert(int i, const T &value)
QByteArray & replace(int pos, int len, const char *after)
void setDisposition(contentDisposition disp)
void addContent(Content *content, bool prepend=false)
@ Structure
Fetch the MIME message body structure (the UID is also fetched)
Definition: fetchjob.h:104
QString mailBox() const
Returns the name of the mailbox the fetch job is executed on.
Definition: fetchjob.cpp:154
bool isEmpty() const const
@ Headers
Fetch RFC-2822 or MIME message headers.
Definition: fetchjob.h:96
QList::const_iterator constEnd() const const
const char * name(StandardAction id)
virtual void from7BitString(const char *s, size_t len)
bool isUidBased() const
How to interpret the sequence set.
Definition: fetchjob.cpp:124
bool endsWith(const QByteArray &ba) const const
int size() const const
QChar * data()
@ Flags
Fetch the message flags (the UID is also fetched)
Definition: fetchjob.h:100
Headers::ContentType * contentType(bool create=true)
FetchScope scope() const
Specifies what data will be fetched.
Definition: fetchjob.cpp:136
void setFilename(const QString &filename)
@ Full
Fetch the complete message.
Definition: fetchjob.h:115
bool isEmpty() const
Returns true if this set doesn't contains any values.
Definition: imapset.cpp:305
QString message
Headers::ContentDescription * contentDescription(bool create=true)
RFC2822Date
Q_D(Todo)
void setBoundary(const QByteArray &s)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Dec 10 2023 03:48:59 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.