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

KDE's Doxygen guidelines are available online.