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 | |
30 | namespace 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 | |
107 | using namespace KIMAP; |
108 | |
109 | FetchJob::FetchScope::FetchScope(): |
110 | mode( FetchScope::Content ), |
111 | changedSince( 0 ) |
112 | { |
113 | |
114 | } |
115 | |
116 | FetchJob::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 | |
124 | FetchJob::~FetchJob() |
125 | { |
126 | } |
127 | |
128 | void FetchJob::setSequenceSet( const ImapSet &set ) |
129 | { |
130 | Q_D( FetchJob ); |
131 | Q_ASSERT( !set.toImapSequenceSet().trimmed().isEmpty() ); |
132 | d->set = set; |
133 | } |
134 | |
135 | ImapSet FetchJob::sequenceSet() const |
136 | { |
137 | Q_D( const FetchJob ); |
138 | return d->set; |
139 | } |
140 | |
141 | void FetchJob::setUidBased(bool uidBased) |
142 | { |
143 | Q_D( FetchJob ); |
144 | d->uidBased = uidBased; |
145 | } |
146 | |
147 | bool FetchJob::isUidBased() const |
148 | { |
149 | Q_D( const FetchJob ); |
150 | return d->uidBased; |
151 | } |
152 | |
153 | void FetchJob::setScope( const FetchScope &scope ) |
154 | { |
155 | Q_D( FetchJob ); |
156 | d->scope = scope; |
157 | } |
158 | |
159 | FetchJob::FetchScope FetchJob::scope() const |
160 | { |
161 | Q_D( const FetchJob ); |
162 | return d->scope; |
163 | } |
164 | |
165 | bool FetchJob::setGmailExtensionsEnabled() const |
166 | { |
167 | Q_D( const FetchJob ); |
168 | return d->gmailEnabled; |
169 | } |
170 | |
171 | void FetchJob::setGmailExtensionsEnabled( bool enabled ) |
172 | { |
173 | Q_D( FetchJob ); |
174 | d->gmailEnabled = enabled; |
175 | } |
176 | |
177 | QMap<qint64, MessagePtr> FetchJob::messages() const |
178 | { |
179 | return QMap<qint64, MessagePtr>(); |
180 | } |
181 | |
182 | QMap<qint64, MessageParts> FetchJob::parts() const |
183 | { |
184 | return QMap<qint64, MessageParts>(); |
185 | } |
186 | |
187 | QMap<qint64, MessageFlags> FetchJob::flags() const |
188 | { |
189 | return QMap<qint64, MessageFlags>(); |
190 | } |
191 | |
192 | QMap<qint64, qint64> FetchJob::sizes() const |
193 | { |
194 | return QMap<qint64, qint64>(); |
195 | } |
196 | |
197 | QMap<qint64, qint64> FetchJob::uids() const |
198 | { |
199 | return QMap<qint64, qint64>(); |
200 | } |
201 | |
202 | void 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 | |
276 | void 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 | |
395 | void 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 | |
445 | void 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 | |
488 | QByteArray 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 | |
532 | QByteArray 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 | |
596 | void 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 | |