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