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

KDE's Doxygen guidelines are available online.