mirror of
https://bitbucket.org/smil3y/kde-playground.git
synced 2025-02-23 18:32:51 +00:00
603 lines
17 KiB
C++
603 lines
17 KiB
C++
/*
|
|
Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
|
|
|
|
This library is free software; you can redistribute it and/or modify it
|
|
under the terms of the GNU Library General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or (at your
|
|
option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful, but WITHOUT
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
|
|
License for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
along with this library; see the file COPYING.LIB. If not, write to the
|
|
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
02110-1301, USA.
|
|
*/
|
|
|
|
#include "fetchjob.h"
|
|
|
|
#include <QtCore/QTimer>
|
|
#include <KDE/KDebug>
|
|
#include <KDE/KLocalizedString>
|
|
|
|
#include "job_p.h"
|
|
#include "message_p.h"
|
|
#include "session_p.h"
|
|
|
|
namespace KIMAP
|
|
{
|
|
class FetchJobPrivate : public JobPrivate
|
|
{
|
|
public:
|
|
FetchJobPrivate( FetchJob *job, Session *session, const QString& name )
|
|
: JobPrivate( session, name )
|
|
, q( job )
|
|
, uidBased( false )
|
|
, gmailEnabled(false)
|
|
{ }
|
|
|
|
~FetchJobPrivate()
|
|
{ }
|
|
|
|
void parseBodyStructure( const QByteArray &structure, int &pos, KMime::Content *content );
|
|
void parsePart( const QByteArray &structure, int &pos, KMime::Content *content );
|
|
QByteArray parseString( const QByteArray &structure, int &pos );
|
|
QByteArray parseSentence( const QByteArray &structure, int &pos );
|
|
void skipLeadingSpaces( const QByteArray &structure, int &pos );
|
|
|
|
void emitPendings()
|
|
{
|
|
if ( pendingUids.isEmpty() ) {
|
|
return;
|
|
}
|
|
|
|
if ( !pendingParts.isEmpty() ) {
|
|
emit q->partsReceived( selectedMailBox,
|
|
pendingUids, pendingParts );
|
|
emit q->partsReceived( selectedMailBox,
|
|
pendingUids, pendingAttributes,
|
|
pendingParts );
|
|
}
|
|
if ( !pendingSizes.isEmpty() || !pendingFlags.isEmpty() ) {
|
|
emit q->headersReceived( selectedMailBox,
|
|
pendingUids, pendingSizes,
|
|
pendingFlags, pendingMessages );
|
|
emit q->headersReceived( selectedMailBox,
|
|
pendingUids, pendingSizes,
|
|
pendingAttributes, pendingFlags,
|
|
pendingMessages );
|
|
}
|
|
if ( !pendingMessages.isEmpty() ) {
|
|
emit q->messagesReceived( selectedMailBox,
|
|
pendingUids, pendingMessages );
|
|
emit q->messagesReceived( selectedMailBox,
|
|
pendingUids, pendingAttributes,
|
|
pendingMessages );
|
|
}
|
|
|
|
pendingUids.clear();
|
|
pendingMessages.clear();
|
|
pendingParts.clear();
|
|
pendingSizes.clear();
|
|
pendingFlags.clear();
|
|
pendingAttributes.clear();
|
|
}
|
|
|
|
FetchJob * const q;
|
|
|
|
ImapSet set;
|
|
bool uidBased;
|
|
FetchJob::FetchScope scope;
|
|
QString selectedMailBox;
|
|
bool gmailEnabled;
|
|
|
|
QTimer emitPendingsTimer;
|
|
QMap<qint64, MessagePtr> pendingMessages;
|
|
QMap<qint64, MessageParts> pendingParts;
|
|
QMap<qint64, MessageFlags> pendingFlags;
|
|
QMap<qint64, MessageAttribute> pendingAttributes;
|
|
QMap<qint64, qint64> pendingSizes;
|
|
QMap<qint64, qint64> pendingUids;
|
|
};
|
|
}
|
|
|
|
using namespace KIMAP;
|
|
|
|
FetchJob::FetchScope::FetchScope():
|
|
mode( FetchScope::Content ),
|
|
changedSince( 0 )
|
|
{
|
|
|
|
}
|
|
|
|
FetchJob::FetchJob( Session *session )
|
|
: Job( *new FetchJobPrivate( this, session, i18n( "Fetch" ) ) )
|
|
{
|
|
Q_D( FetchJob );
|
|
connect( &d->emitPendingsTimer, SIGNAL(timeout()),
|
|
this, SLOT(emitPendings()) );
|
|
}
|
|
|
|
FetchJob::~FetchJob()
|
|
{
|
|
}
|
|
|
|
void FetchJob::setSequenceSet( const ImapSet &set )
|
|
{
|
|
Q_D( FetchJob );
|
|
Q_ASSERT( !set.toImapSequenceSet().trimmed().isEmpty() );
|
|
d->set = set;
|
|
}
|
|
|
|
ImapSet FetchJob::sequenceSet() const
|
|
{
|
|
Q_D( const FetchJob );
|
|
return d->set;
|
|
}
|
|
|
|
void FetchJob::setUidBased(bool uidBased)
|
|
{
|
|
Q_D( FetchJob );
|
|
d->uidBased = uidBased;
|
|
}
|
|
|
|
bool FetchJob::isUidBased() const
|
|
{
|
|
Q_D( const FetchJob );
|
|
return d->uidBased;
|
|
}
|
|
|
|
void FetchJob::setScope( const FetchScope &scope )
|
|
{
|
|
Q_D( FetchJob );
|
|
d->scope = scope;
|
|
}
|
|
|
|
FetchJob::FetchScope FetchJob::scope() const
|
|
{
|
|
Q_D( const FetchJob );
|
|
return d->scope;
|
|
}
|
|
|
|
bool FetchJob::setGmailExtensionsEnabled() const
|
|
{
|
|
Q_D( const FetchJob );
|
|
return d->gmailEnabled;
|
|
}
|
|
|
|
void FetchJob::setGmailExtensionsEnabled( bool enabled )
|
|
{
|
|
Q_D( FetchJob );
|
|
d->gmailEnabled = enabled;
|
|
}
|
|
|
|
QMap<qint64, MessagePtr> FetchJob::messages() const
|
|
{
|
|
return QMap<qint64, MessagePtr>();
|
|
}
|
|
|
|
QMap<qint64, MessageParts> FetchJob::parts() const
|
|
{
|
|
return QMap<qint64, MessageParts>();
|
|
}
|
|
|
|
QMap<qint64, MessageFlags> FetchJob::flags() const
|
|
{
|
|
return QMap<qint64, MessageFlags>();
|
|
}
|
|
|
|
QMap<qint64, qint64> FetchJob::sizes() const
|
|
{
|
|
return QMap<qint64, qint64>();
|
|
}
|
|
|
|
QMap<qint64, qint64> FetchJob::uids() const
|
|
{
|
|
return QMap<qint64, qint64>();
|
|
}
|
|
|
|
void FetchJob::doStart()
|
|
{
|
|
Q_D( FetchJob );
|
|
|
|
QByteArray parameters = d->set.toImapSequenceSet()+' ';
|
|
Q_ASSERT( !parameters.trimmed().isEmpty() );
|
|
|
|
switch ( d->scope.mode ) {
|
|
case FetchScope::Headers:
|
|
if ( d->scope.parts.isEmpty() ) {
|
|
parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID";
|
|
} else {
|
|
parameters += '(';
|
|
foreach ( const QByteArray &part, d->scope.parts ) {
|
|
parameters += "BODY.PEEK[" + part + ".MIME] ";
|
|
}
|
|
parameters += "UID";
|
|
}
|
|
break;
|
|
case FetchScope::Flags:
|
|
parameters += "(FLAGS UID";
|
|
break;
|
|
case FetchScope::Structure:
|
|
parameters += "(BODYSTRUCTURE UID";
|
|
break;
|
|
case FetchScope::Content:
|
|
if ( d->scope.parts.isEmpty() ) {
|
|
parameters += "(BODY.PEEK[] UID";
|
|
} else {
|
|
parameters += '(';
|
|
foreach ( const QByteArray &part, d->scope.parts ) {
|
|
parameters += "BODY.PEEK[" + part + "] ";
|
|
}
|
|
parameters += "UID";
|
|
}
|
|
break;
|
|
case FetchScope::Full:
|
|
parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID";
|
|
break;
|
|
case FetchScope::HeaderAndContent:
|
|
if ( d->scope.parts.isEmpty() ) {
|
|
parameters += "(BODY.PEEK[] FLAGS UID";
|
|
} else {
|
|
parameters += "(BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)]";
|
|
foreach ( const QByteArray &part, d->scope.parts ) {
|
|
parameters += " BODY.PEEK[" + part + ".MIME] BODY.PEEK[" + part + "]"; //krazy:exclude=doublequote_chars
|
|
}
|
|
parameters += " FLAGS UID";
|
|
}
|
|
break;
|
|
case FetchScope::FullHeaders:
|
|
parameters += "(RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER] FLAGS UID";
|
|
break;
|
|
}
|
|
|
|
if ( d->gmailEnabled ) {
|
|
parameters += " X-GM-LABELS X-GM-MSGID X-GM-THRID";
|
|
}
|
|
parameters += ")";
|
|
|
|
if ( d->scope.changedSince > 0 ) {
|
|
parameters += " (CHANGEDSINCE " + QByteArray::number( d->scope.changedSince ) + ")";
|
|
}
|
|
|
|
QByteArray command = "FETCH";
|
|
if ( d->uidBased ) {
|
|
command = "UID " + command;
|
|
}
|
|
|
|
d->emitPendingsTimer.start( 100 );
|
|
d->selectedMailBox = d->m_session->selectedMailBox();
|
|
d->tags << d->sessionInternal()->sendCommand( command, parameters );
|
|
}
|
|
|
|
void FetchJob::handleResponse( const Message &response )
|
|
{
|
|
Q_D( FetchJob );
|
|
|
|
// We can predict it'll be handled by handleErrorReplies() so stop
|
|
// the timer now so that result() will really be the last emitted signal.
|
|
if ( !response.content.isEmpty() &&
|
|
d->tags.size() == 1 &&
|
|
d->tags.contains( response.content.first().toString() ) ) {
|
|
d->emitPendingsTimer.stop();
|
|
d->emitPendings();
|
|
}
|
|
|
|
if ( handleErrorReplies( response ) == NotHandled ) {
|
|
if ( response.content.size() == 4 &&
|
|
response.content[2].toString() == "FETCH" &&
|
|
response.content[3].type() == Message::Part::List ) {
|
|
|
|
qint64 id = response.content[1].toString().toLongLong();
|
|
QList<QByteArray> content = response.content[3].toList();
|
|
|
|
MessagePtr message( new KMime::Message );
|
|
bool shouldParseMessage = false;
|
|
MessageParts parts;
|
|
|
|
for ( QList<QByteArray>::ConstIterator it = content.constBegin();
|
|
it != content.constEnd(); ++it ) {
|
|
QByteArray str = *it;
|
|
++it;
|
|
|
|
if ( it == content.constEnd() ) { // Uh oh, message was truncated?
|
|
kWarning() << "FETCH reply got truncated, skipping.";
|
|
break;
|
|
}
|
|
|
|
if ( str == "UID" ) {
|
|
d->pendingUids[id] = it->toLongLong();
|
|
} else if ( str == "RFC822.SIZE" ) {
|
|
d->pendingSizes[id] = it->toLongLong();
|
|
} else if ( str == "INTERNALDATE" ) {
|
|
message->date()->setDateTime( KDateTime::fromString( QLatin1String(*it), KDateTime::RFCDate ) );
|
|
} else if ( str == "FLAGS" ) {
|
|
if ( ( *it ).startsWith( '(' ) && ( *it ).endsWith( ')' ) ) {
|
|
QByteArray str = *it;
|
|
str.chop( 1 );
|
|
str.remove( 0, 1 );
|
|
d->pendingFlags[id] = str.split( ' ' );
|
|
} else {
|
|
d->pendingFlags[id] << *it;
|
|
}
|
|
} else if ( str == "X-GM-LABELS" ) {
|
|
d->pendingAttributes.insert( id, qMakePair<QByteArray, QVariant>( "X-GM-LABELS", *it ) );
|
|
} else if ( str == "X-GM-THRID" ) {
|
|
d->pendingAttributes.insert( id, qMakePair<QByteArray, QVariant>( "X-GM-THRID", *it ) );
|
|
} else if ( str == "X-GM-MSGID" ) {
|
|
d->pendingAttributes.insert( id, qMakePair<QByteArray, QVariant>( "X-GM-MSGID", *it ) );
|
|
} else if ( str == "BODYSTRUCTURE" ) {
|
|
int pos = 0;
|
|
d->parseBodyStructure( *it, pos, message.get() );
|
|
message->assemble();
|
|
d->pendingMessages[id] = message;
|
|
} else if ( str.startsWith( "BODY[" ) ) { //krazy:exclude=strings
|
|
if ( !str.endsWith( ']' ) ) { // BODY[ ... ] might have been split, skip until we find the ]
|
|
while ( !( *it ).endsWith( ']' ) ) {
|
|
++it;
|
|
}
|
|
++it;
|
|
}
|
|
|
|
int index;
|
|
if ( ( index = str.indexOf( "HEADER" ) ) > 0 || ( index = str.indexOf( "MIME" ) ) > 0 ) { // headers
|
|
if ( str[index-1] == '.' ) {
|
|
QByteArray partId = str.mid( 5, index - 6 );
|
|
if ( !parts.contains( partId ) ) {
|
|
parts[partId] = ContentPtr( new KMime::Content );
|
|
}
|
|
parts[partId]->setHead( *it );
|
|
parts[partId]->parse();
|
|
d->pendingParts[id] = parts;
|
|
} else {
|
|
message->setHead( *it );
|
|
shouldParseMessage = true;
|
|
}
|
|
} else { // full payload
|
|
if ( str == "BODY[]" ) {
|
|
message->setContent( KMime::CRLFtoLF( *it ) );
|
|
shouldParseMessage = true;
|
|
|
|
d->pendingMessages[id] = message;
|
|
} else {
|
|
QByteArray partId = str.mid( 5, str.size() - 6 );
|
|
if ( !parts.contains( partId ) ) {
|
|
parts[partId] = ContentPtr( new KMime::Content );
|
|
}
|
|
parts[partId]->setBody( *it );
|
|
parts[partId]->parse();
|
|
|
|
d->pendingParts[id] = parts;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( shouldParseMessage ) {
|
|
message->parse();
|
|
}
|
|
|
|
// For the headers mode the message is built in several
|
|
// steps, hence why we wait it to be done until putting it
|
|
// in the pending queue.
|
|
if ( d->scope.mode == FetchScope::Headers ||
|
|
d->scope.mode == FetchScope::HeaderAndContent ||
|
|
d->scope.mode == FetchScope::FullHeaders ) {
|
|
d->pendingMessages[id] = message;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FetchJobPrivate::parseBodyStructure(const QByteArray &structure, int &pos, KMime::Content *content)
|
|
{
|
|
skipLeadingSpaces( structure, pos );
|
|
|
|
if ( structure[pos] != '(' ) {
|
|
return;
|
|
}
|
|
|
|
pos++;
|
|
|
|
if ( structure[pos] != '(' ) { // simple part
|
|
pos--;
|
|
parsePart( structure, pos, content );
|
|
} else { // multi part
|
|
content->contentType()->setMimeType( "MULTIPART/MIXED" );
|
|
while ( pos < structure.size() && structure[pos] == '(' ) {
|
|
KMime::Content *child = new KMime::Content;
|
|
content->addContent( child );
|
|
parseBodyStructure( structure, pos, child );
|
|
child->assemble();
|
|
}
|
|
|
|
QByteArray subType = parseString( structure, pos );
|
|
content->contentType()->setMimeType( "MULTIPART/" + subType );
|
|
|
|
QByteArray parameters = parseSentence( structure, pos ); // FIXME: Read the charset
|
|
if ( parameters.contains( "BOUNDARY" ) ) {
|
|
content->contentType()->setBoundary( parameters.remove( 0, parameters.indexOf( "BOUNDARY" ) + 11 ).split( '\"' )[0] );
|
|
}
|
|
|
|
QByteArray disposition = parseSentence( structure, pos );
|
|
if ( disposition.contains( "INLINE" ) ) {
|
|
content->contentDisposition()->setDisposition( KMime::Headers::CDinline );
|
|
} else if ( disposition.contains( "ATTACHMENT" ) ) {
|
|
content->contentDisposition()->setDisposition( KMime::Headers::CDattachment );
|
|
}
|
|
|
|
parseSentence( structure, pos ); // Ditch the body language
|
|
}
|
|
|
|
// Consume what's left
|
|
while ( pos < structure.size() && structure[pos] != ')' ) {
|
|
skipLeadingSpaces( structure, pos );
|
|
parseSentence( structure, pos );
|
|
skipLeadingSpaces( structure, pos );
|
|
}
|
|
|
|
pos++;
|
|
}
|
|
|
|
void FetchJobPrivate::parsePart( const QByteArray &structure, int &pos, KMime::Content *content )
|
|
{
|
|
if ( structure[pos] != '(' ) {
|
|
return;
|
|
}
|
|
|
|
pos++;
|
|
|
|
QByteArray mainType = parseString( structure, pos );
|
|
QByteArray subType = parseString( structure, pos );
|
|
|
|
content->contentType()->setMimeType( mainType + '/' + subType );
|
|
|
|
parseSentence( structure, pos ); // Ditch the parameters... FIXME: Read it to get charset and name
|
|
parseString( structure, pos ); // ... and the id
|
|
|
|
content->contentDescription()->from7BitString( parseString( structure, pos ) );
|
|
|
|
parseString( structure, pos ); // Ditch the encoding too
|
|
parseString( structure, pos ); // ... and the size
|
|
parseString( structure, pos ); // ... and the line count
|
|
|
|
QByteArray disposition = parseSentence( structure, pos );
|
|
if ( disposition.contains( "INLINE" ) ) {
|
|
content->contentDisposition()->setDisposition( KMime::Headers::CDinline );
|
|
} else if ( disposition.contains( "ATTACHMENT" ) ) {
|
|
content->contentDisposition()->setDisposition( KMime::Headers::CDattachment );
|
|
}
|
|
if ( ( content->contentDisposition()->disposition() == KMime::Headers::CDattachment ||
|
|
content->contentDisposition()->disposition() == KMime::Headers::CDinline ) &&
|
|
disposition.contains( "FILENAME" ) ) {
|
|
QByteArray filename = disposition.remove( 0, disposition.indexOf( "FILENAME" ) + 11 ).split( '\"' )[0];
|
|
content->contentDisposition()->setFilename( QLatin1String(filename) );
|
|
}
|
|
|
|
// Consume what's left
|
|
while ( pos < structure.size() && structure[pos] != ')' ) {
|
|
skipLeadingSpaces( structure, pos );
|
|
parseSentence( structure, pos );
|
|
skipLeadingSpaces( structure, pos );
|
|
}
|
|
}
|
|
|
|
QByteArray FetchJobPrivate::parseSentence( const QByteArray &structure, int &pos )
|
|
{
|
|
QByteArray result;
|
|
int stack = 0;
|
|
|
|
skipLeadingSpaces( structure, pos );
|
|
|
|
if ( structure[pos] != '(' ) {
|
|
return parseString( structure, pos );
|
|
}
|
|
|
|
int start = pos;
|
|
|
|
do {
|
|
switch ( structure[pos] ) {
|
|
case '(':
|
|
pos++;
|
|
stack++;
|
|
break;
|
|
case ')':
|
|
pos++;
|
|
stack--;
|
|
break;
|
|
case '[':
|
|
pos++;
|
|
stack++;
|
|
break;
|
|
case ']':
|
|
pos++;
|
|
stack--;
|
|
break;
|
|
default:
|
|
skipLeadingSpaces( structure, pos );
|
|
parseString( structure, pos );
|
|
skipLeadingSpaces( structure, pos );
|
|
break;
|
|
}
|
|
} while ( pos < structure.size() && stack != 0 );
|
|
|
|
result = structure.mid( start, pos - start );
|
|
|
|
return result;
|
|
}
|
|
|
|
QByteArray FetchJobPrivate::parseString( const QByteArray &structure, int &pos )
|
|
{
|
|
QByteArray result;
|
|
|
|
skipLeadingSpaces( structure, pos );
|
|
|
|
int start = pos;
|
|
bool foundSlash = false;
|
|
|
|
// quoted string
|
|
if ( structure[pos] == '"' ) {
|
|
pos++;
|
|
Q_FOREVER {
|
|
if ( structure[pos] == '\\' ) {
|
|
pos += 2;
|
|
foundSlash = true;
|
|
continue;
|
|
}
|
|
if ( structure[pos] == '"' ) {
|
|
result = structure.mid( start + 1, pos - start - 1 );
|
|
pos++;
|
|
break;
|
|
}
|
|
pos++;
|
|
}
|
|
} else { // unquoted string
|
|
Q_FOREVER {
|
|
if ( structure[pos] == ' ' ||
|
|
structure[pos] == '(' ||
|
|
structure[pos] == ')' ||
|
|
structure[pos] == '[' ||
|
|
structure[pos] == ']' ||
|
|
structure[pos] == '\n' ||
|
|
structure[pos] == '\r' ||
|
|
structure[pos] == '"' ) {
|
|
break;
|
|
}
|
|
if ( structure[pos] == '\\' ) {
|
|
foundSlash = true;
|
|
}
|
|
pos++;
|
|
}
|
|
|
|
result = structure.mid( start, pos - start );
|
|
|
|
// transform unquoted NIL
|
|
if ( result == "NIL" ) {
|
|
result.clear();
|
|
}
|
|
}
|
|
|
|
// simplify slashes
|
|
if ( foundSlash ) {
|
|
while ( result.contains( "\\\"" ) ) {
|
|
result.replace( "\\\"", "\"" );
|
|
}
|
|
while ( result.contains( "\\\\" ) ) {
|
|
result.replace( "\\\\", "\\" );
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void FetchJobPrivate::skipLeadingSpaces( const QByteArray &structure, int &pos )
|
|
{
|
|
while ( pos < structure.size() && structure[pos] == ' ' ) {
|
|
pos++;
|
|
}
|
|
}
|
|
|
|
#include "moc_fetchjob.cpp"
|