diff --git a/kmime/kmime_content.cpp b/kmime/kmime_content.cpp index e69734ad1..051b8b36a 100644 --- a/kmime/kmime_content.cpp +++ b/kmime/kmime_content.cpp @@ -1,1172 +1,1209 @@ /* kmime_content.cpp KMime, the KDE internet mail/usenet news message library. Copyright (c) 2001 the KMime authors. See file AUTHORS for details Copyright (c) 2006 Volker Krause 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. */ /** @file This file is part of the API for handling @ref MIME data and defines the Content class. @brief Defines the Content class. @authors the KMime authors (see AUTHORS file), Volker Krause \ */ #include "kmime_content.h" #include "kmime_content_p.h" #include "kmime_parsers.h" #include "kmime_util_p.h" #include #include #include #include #include #include #include #include using namespace KMime; namespace KMime { Content::Content() : d_ptr( new ContentPrivate( this ) ) { } Content::Content( Content *parent ) : d_ptr( new ContentPrivate( this ) ) { d_ptr->parent = parent; } Content::Content( const QByteArray &h, const QByteArray &b ) : d_ptr( new ContentPrivate( this ) ) { d_ptr->head = h; d_ptr->body = b; } Content::Content( const QByteArray &h, const QByteArray &b, Content *parent ) : d_ptr( new ContentPrivate( this ) ) { d_ptr->head = h; d_ptr->body = b; d_ptr->parent = parent; } Content::Content( ContentPrivate *d ) : d_ptr( d ) { } Content::~Content() { qDeleteAll( h_eaders ); h_eaders.clear(); delete d_ptr; d_ptr = 0; } bool Content::hasContent() const { return !d_ptr->head.isEmpty() || !d_ptr->body.isEmpty() || !d_ptr->contents.isEmpty(); } void Content::setContent( const QList &l ) { Q_D(Content); //qDebug("Content::setContent( const QList &l ) : start"); d->head.clear(); d->body.clear(); //usage of textstreams is much faster than simply appending the strings QTextStream hts( &( d->head ), QIODevice::WriteOnly ); QTextStream bts( &( d->body ), QIODevice::WriteOnly ); hts.setCodec( "ISO 8859-1" ); bts.setCodec( "ISO 8859-1" ); bool isHead = true; foreach ( const QByteArray& line, l ) { if ( isHead && line.isEmpty() ) { isHead = false; continue; } if ( isHead ) { hts << line << "\n"; } else { bts << line << "\n"; } } //qDebug("Content::setContent( const QList & l ) : finished"); } void Content::setContent( const QByteArray &s ) { Q_D(Content); d->head.clear(); d->body.clear(); // empty header if ( s.startsWith( '\n' ) ) { d->body = s.right( s.length() - 1 ); return; } int pos = s.indexOf( "\n\n", 0 ); if ( pos > -1 ) { d->head = s.left( ++pos ); //header *must* end with "\n" !! d->body = s.mid( pos + 1, s.length() - pos - 1 ); } else { d->head = s; } } QByteArray Content::head() const { return d_ptr->head; } void Content::setHead( const QByteArray &head ) { d_ptr->head = head; } QByteArray Content::body() const { return d_ptr->body; } void Content::setBody( const QByteArray &body ) { d_ptr->body = body; } //parse the message, split multiple parts void Content::parse() { Q_D(Content); //qDebug("void Content::parse() : start"); qDeleteAll( h_eaders ); h_eaders.clear(); // check this part has already been partioned into subparts. // if this is the case, we will not try to reparse the body // of this part. if ( d->body.size() == 0 && !d->contents.isEmpty() ) { // reparse all sub parts foreach ( Content *c, d->contents ) { c->parse(); } return; } qDeleteAll( d->contents ); d->contents.clear(); Headers::ContentType *ct = contentType(); QByteArray tmp; Content *c; Headers::contentCategory cat; // just "text" as mimetype is suspicious, perhaps this article was // generated by broken software, better check for uuencoded binaries // No mimetype content can also contains such encoded data (unless it is just // a text/plain message --that the test isText() after this block catches--) if ( ct->mimeType() == "text" || ct->isEmpty() ) { //non-mime body => check for uuencoded content Parser::UUEncoded uup( d->body, rawHeader( "Subject" ) ); if ( uup.parse() ) { // yep, it is uuencoded if ( uup.isPartial() ) { //this seems to be only a part of the message so we treat //it as "message/partial" ct->setMimeType( "message/partial" ); //ct->setId( uniqueString() ); not needed yet ct->setPartialParams( uup.partialCount(), uup.partialNumber() ); contentTransferEncoding()->setEncoding( Headers::CE7Bit ); } else { //it's a complete message => treat as "multipart/mixed" //the whole content is now split into single parts, so it's safe //to delete the message-body d->body.clear(); //binary parts for ( int i = 0; i < uup.binaryParts().count(); ++i ) { c = new Content( this ); //generate content with mime-compliant headers tmp = "Content-Type: "; tmp += uup.mimeTypes().at( i ); tmp += "; name=\""; tmp += uup.filenames().at( i ); tmp += "\"\nContent-Transfer-Encoding: x-uuencode\nContent-Disposition: attachment; filename=\""; tmp += uup.filenames().at( i ); tmp += "\"\n\n"; tmp += uup.binaryParts().at( i ); c->setContent( tmp ); addContent( c ); } if ( !d->contents.isEmpty() && d->contents.first() ) { //readd the plain text before the uuencoded part d->contents.first()->setContent( "Content-Type: text/plain\nContent-Transfer-Encoding: 7Bit\n\n" + uup.textPart() ); d->contents.first()->contentType()->setMimeType( "text/plain" ); } } } else { Parser::YENCEncoded yenc( d->body ); if ( yenc.parse() ) { /* If it is partial, just assume there is exactly one decoded part, * and make this that part */ if ( yenc.isPartial() ) { ct->setMimeType( "message/partial" ); //ct->setId( uniqueString() ); not needed yet ct->setPartialParams( yenc.partialCount(), yenc.partialNumber() ); contentTransferEncoding()->setEncoding( Headers::CEbinary ); } else { //it's a complete message => treat as "multipart/mixed" //the whole content is now split into single parts, so it's safe //to delete the message-body d->body.clear(); //binary parts for ( int i=0; isetContent( tmp ); // the bodies of yenc message parts are binary data, not null-terminated strings: c->setBody( yenc.binaryParts()[i] ); addContent( c ); } if ( !d->contents.isEmpty() && d->contents.first() ) { //readd the plain text before the uuencoded part d->contents.first()->setContent( "Content-Type: text/plain\nContent-Transfer-Encoding: 7Bit\n\n" + yenc.textPart() ); d->contents.first()->contentType()->setMimeType( "text/plain" ); } } } } } if ( ct->isText() ) { // default is text/plain return; //nothing to do } if ( ct->isMultipart() ) { //this is a multipart message tmp = ct->boundary(); //get boundary-parameter if ( !tmp.isEmpty() ) { Parser::MultiPart mpp( d->body, tmp ); if ( mpp.parse() ) { //at least one part found if ( ct->isSubtype( "alternative" ) ) { //examine category for the sub-parts cat = Headers::CCalternativePart; } else { cat = Headers::CCmixedPart; //default to "mixed" } QList parts = mpp.parts(); QList::Iterator it; for ( it=parts.begin(); it != parts.end(); ++it ) { //create a new Content for every part c = new Content( this ); c->setContent( *it ); c->parse(); c->contentType()->setCategory( cat ); //set category of the sub-part d->contents.append( c ); //qDebug("part:\n%s\n\n%s", c->h_ead.data(), c->b_ody.left(100).data()); } //the whole content is now split into single parts, so it's safe delete the message-body d->body.clear(); } else { //sh*t, the parsing failed so we have to treat the message as "text/plain" instead ct->setMimeType( "text/plain" ); ct->setCharset( "US-ASCII" ); } } } } + void Content::assemble() { Q_D(Content); QByteArray newHead = assembleHeaders(); + KMime::Content m; + m.setContent( d->fullContent() ); + m.parse(); + foreach ( Headers::Base *h, h_eaders ) { if ( h->isXHeader() ) { - newHead += h->as7BitString() + '\n'; - KMime::removeHeader( d->head, h->type() ); + Headers::Base *oldH = m.headerByType( h->type() ); + if ( !oldH || ( oldH && oldH->as7BitString() != h->as7BitString() ) ) { + newHead += h->as7BitString() + '\n'; + KMime::removeHeader( d->head, h->type() ); + } } } - newHead += d->head; // keep unparsed headers - d->head = newHead; + if ( newHead != d->head ) {//the headers were modified + newHead += d->head; // keep unparsed headers + d->head = newHead; + } foreach ( Content *c, contents() ) { c->assemble(); } } QByteArray Content::assembleHeaders() { Q_D(Content); QByteArray newHead; + KMime::Content m; + m.setContent( d->fullContent() ); + m.parse(); //Content-Type Headers::Base *h = contentType( false ); - if ( h && !h->isEmpty() ) { + if ( h && !h->isEmpty() && h->as7BitString() != m.contentType()->as7BitString() ) { newHead += contentType()->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Content-Transfer-Encoding h = contentTransferEncoding( false ); - if ( h && !h->isEmpty() ) { + if ( h && !h->isEmpty() && h->as7BitString() != m.contentTransferEncoding()->as7BitString() ) { newHead += contentTransferEncoding()->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Content-Description h = contentDescription( false ); - if ( h ) { + if ( h && h->as7BitString() != m.contentDescription()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Content-Disposition h = contentDisposition( false ); - if ( h ) { + if ( h && h->as7BitString() != m.contentDisposition()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } + if ( newHead.isEmpty() ) { + newHead = d->head; + } else { + } + return newHead; } void Content::clear() { Q_D(Content); qDeleteAll( h_eaders ); h_eaders.clear(); clearContents(); d->head.clear(); d->body.clear(); } void Content::clearContents( bool del ) { Q_D(Content); if( del ) { qDeleteAll( d->contents ); } d->contents.clear(); } QByteArray Content::encodedContent( bool useCrLf ) { Q_D(Content); QByteArray e; // hack to convert articles with uuencoded or yencoded binaries into // proper mime-compliant articles if ( !d->contents.isEmpty() ) { bool convertNonMimeBinaries=false; // reencode non-mime binaries... foreach ( Content *c, d->contents ) { if ( ( c->contentTransferEncoding( true )->encoding() == Headers::CEuuenc ) || ( c->contentTransferEncoding( true )->encoding() == Headers::CEbinary ) ) { convertNonMimeBinaries = true; c->setBody( KCodecs::base64Encode( c->decodedContent(), true ) + '\n' ); c->contentTransferEncoding( true )->setEncoding(Headers::CEbase64); c->contentTransferEncoding( true )->setDecoded( false ); c->removeHeader("Content-Description"); c->assemble(); } } // add proper mime headers... if ( convertNonMimeBinaries ) { int beg = 0, end = 0; beg = d->head.indexOf( "MIME-Version: " ); if ( beg >= 0 ) { end = d->head.indexOf( '\n', beg ); } if ( beg >= 0 && end > beg ) { d->head.remove( beg, end - beg ); } beg = d->head.indexOf( "Content-Type: " ); if ( beg >= 0 ) { end = d->head.indexOf( '\n', beg ); } if ( beg >= 0 && end > beg ) { d->head.remove( beg, end - beg ); } beg = d->head.indexOf( "Content-Transfer-Encoding: " ); if ( beg >= 0 ) { end = d->head.indexOf( '\n', beg ); } if ( beg >= 0 && end > beg ) { d->head.remove( beg, end - beg ); } d->head += "MIME-Version: 1.0\n"; d->head += contentType( true )->as7BitString() + '\n'; d->head += contentTransferEncoding( true )->as7BitString() + '\n'; } } //head e = d->head; e += '\n'; //body if ( !d->body.isEmpty() ) { //this message contains only one part Headers::ContentTransferEncoding *enc=contentTransferEncoding(); if (enc->needToEncode()) { if ( enc->encoding() == Headers::CEquPr ) { e += KCodecs::quotedPrintableEncode( d->body, false ); } else { e += KCodecs::base64Encode( d->body, true ); e += '\n'; } } else { e += d->body; } } else if ( !d->contents.isEmpty() ) { //this is a multipart message Headers::ContentType *ct=contentType(); QByteArray boundary = "\n--" + ct->boundary(); //add all (encoded) contents separated by boundaries foreach ( Content *c, d->contents ) { e+=boundary + '\n'; e += c->encodedContent( false ); // don't convert LFs here, we do that later!!!!! } //finally append the closing boundary e += boundary+"--\n"; }; if ( useCrLf ) { return LFtoCRLF( e ); } else { return e; } } QByteArray Content::decodedContent() { QByteArray temp, ret; Headers::ContentTransferEncoding *ec=contentTransferEncoding(); bool removeTrailingNewline=false; int size = d_ptr->body.length(); if ( size == 0 ) { return ret; } temp.resize( size ); memcpy( temp.data(), d_ptr->body.data(), size ); if ( ec->decoded() ) { ret = temp; removeTrailingNewline = true; } else { switch( ec->encoding() ) { case Headers::CEbase64 : KCodecs::base64Decode( temp, ret ); break; case Headers::CEquPr : ret = KCodecs::quotedPrintableDecode( d_ptr->body ); ret.resize( ret.size() - 1 ); // remove null-char removeTrailingNewline = true; break; case Headers::CEuuenc : KCodecs::uudecode( temp, ret ); break; case Headers::CEbinary : ret = temp; removeTrailingNewline = false; break; default : ret = temp; removeTrailingNewline = true; } } if ( removeTrailingNewline && ( ret.size() > 0 ) && ( ret[ret.size()-1] == '\n') ) { ret.resize( ret.size() - 1 ); } return ret; } QString Content::decodedText( bool trimText, bool removeTrailingNewlines ) { if ( !decodeText() ) { //this is not a text content !! return QString(); } bool ok = true; QTextCodec *codec = KGlobal::charsets()->codecForName( contentType()->charset(), ok ); QString s = codec->toUnicode( d_ptr->body.data(), d_ptr->body.length() ); if ( trimText && removeTrailingNewlines ) { int i; for ( i = s.length() - 1; i >= 0; --i ) { if ( !s[i].isSpace() ) { break; } } s.truncate( i + 1 ); } else { if ( s.right( 1 ) == "\n" ) { s.truncate( s.length() - 1 ); // remove trailing new-line } } return s; } void Content::fromUnicodeString( const QString &s ) { bool ok = true; QTextCodec *codec = KGlobal::charsets()->codecForName( contentType()->charset(), ok ); if ( !ok ) { // no suitable codec found => try local settings and hope the best ;-) codec = KGlobal::locale()->codecForEncoding(); QByteArray chset = KGlobal::locale()->encoding(); contentType()->setCharset( chset ); } d_ptr->body = codec->fromUnicode( s ); contentTransferEncoding()->setDecoded( true ); //text is always decoded } Content *Content::textContent() { Content *ret=0; //return the first content with mimetype=text/* if ( contentType()->isText() ) { ret = this; } else { foreach ( Content *c, d_ptr->contents ) { if ( ( ret = c->textContent() ) != 0 ) { break; } } } return ret; } Content::List Content::attachments( bool incAlternatives ) { List attachments; if ( d_ptr->contents.isEmpty() ) { attachments.append( this ); } else { foreach ( Content *c, d_ptr->contents ) { if ( !incAlternatives && c->contentType()->category() == Headers::CCalternativePart ) { continue; } else { attachments += c->attachments( incAlternatives ); } } } if ( isTopLevel() ) { Content *text = textContent(); if ( text ) { attachments.removeAll( text ); } } return attachments; } Content::List Content::contents() const { return d_ptr->contents; } void Content::addContent( Content *c, bool prepend ) { Q_D(Content); if ( d->contents.isEmpty() && !contentType()->isMultipart() ) { // this message is not multipart yet // first we convert the body to a content Content *main = new Content( this ); //the Mime-Headers are needed, so we move them to the new content for ( Headers::Base::List::iterator it = h_eaders.begin(); it != h_eaders.end(); ) { if ( (*it)->isMimeHeader() ) { // append to new content main->h_eaders.append( *it ); // and remove from this content it = h_eaders.erase( it ); } else { ++it; } } //"main" is now part of a multipart/mixed message main->contentType()->setCategory(Headers::CCmixedPart); //the head of "main" is empty, so we assemble it main->assemble(); //now we can copy the body and append the new content; main->setBody( d->body ); d->contents.append( main ); d->body.clear(); //no longer needed //finally we have to convert this article to "multipart/mixed" Headers::ContentType *ct=contentType(); ct->setMimeType( "multipart/mixed" ); ct->setBoundary( multiPartBoundary() ); ct->setCategory( Headers::CCcontainer ); contentTransferEncoding()->clear(); // 7Bit, decoded } //here we actually add the content if ( prepend ) { d->contents.insert( 0, c ); } else { d->contents.append( c ); } if ( c->parent() != this ) c->setParent(this); } void Content::removeContent( Content *c, bool del ) { Q_D(Content); if ( d->contents.isEmpty() ) { // what the .. return; } d->contents.removeAll( c ); if ( del ) { delete c; } else { c->setParent( 0 ); } //only one content left => turn this message in a single-part if ( d->contents.count() == 1 ) { Content *main = d->contents.first(); //first we have to move the mime-headers for ( Headers::Base::List::iterator it = main->h_eaders.begin(); it != main->h_eaders.end(); ) { if ( (*it)->isMimeHeader() ) { kDebug(5320) << "Content::removeContent(Content *c, bool del) : mime-header moved:" << (*it)->as7BitString(); // first remove the old header removeHeader( (*it)->type() ); // then append to new content h_eaders.append( *it ); // and finally remove from this content it = main->h_eaders.erase( it ); } else { ++it; } } //now we can copy the body d->body = main->body(); //finally we can delete the content list qDeleteAll( d->contents ); d->contents.clear(); } } void Content::changeEncoding( Headers::contentEncoding e ) { Headers::ContentTransferEncoding *enc = contentTransferEncoding(); if ( enc->encoding() == e ) { //nothing to do return; } if ( decodeText() ) { enc->setEncoding( e ); // text is not encoded until it's sent or saved // so we just set the new encoding } else { // this content contains non textual data, that has to be re-encoded if ( e != Headers::CEbase64 ) { //kWarning(5003) << "Content::changeEncoding() : non textual data" // << "and encoding != base64 - this should not happen =>" // << "forcing base64"; e = Headers::CEbase64; } if ( enc->encoding() != e ) { // ok, we reencode the content using base64 d_ptr->body = KCodecs::base64Encode( decodedContent(), true ); d_ptr->body.append( "\n" ); enc->setEncoding( e ); //set encoding enc->setDecoded( false ); } } } void Content::toStream( QTextStream &ts, bool scrambleFromLines ) { QByteArray ret = encodedContent( false ); if ( scrambleFromLines ) { // FIXME Why are only From lines with a preceding empty line considered? // And, of course, all lines starting with >*From have to be escaped // because otherwise the transformation is not revertable. ret.replace( "\n\nFrom ", "\n\n>From "); } ts << ret; } Headers::Generic *Content::getNextHeader( QByteArray &head ) { return nextHeader( head ); } Headers::Generic *Content::nextHeader( QByteArray &head ) { int pos1=-1, pos2=0, len=head.length()-1; bool folded( false ); Headers::Generic *header=0; pos1 = head.indexOf( ": " ); if ( pos1 > -1 ) { //there is another header pos2 = pos1 += 2; //skip the name if ( head[pos2] != '\n' ) { // check if the header is not empty while ( 1 ) { pos2 = head.indexOf( '\n', pos2 + 1 ); if ( pos2 == -1 || pos2 == len || ( head[pos2+1] != ' ' && head[pos2+1] != '\t' ) ) { //break if we reach the end of the string, honor folded lines break; } else { folded = true; } } } if ( pos2 < 0 ) { pos2 = len + 1; //take the rest of the string } if ( !folded ) { header = new Headers::Generic(head.left(pos1-2), this, head.mid(pos1, pos2-pos1)); } else { QByteArray hdrValue = head.mid( pos1, pos2 - pos1 ); header = new Headers::Generic( head.left( pos1 - 2 ), this, unfoldHeader( hdrValue ) ); } head.remove( 0, pos2 + 1 ); } else { head = ""; } return header; } Headers::Base *Content::getHeaderByType( const char *type ) { return headerByType( type ); } Headers::Base *Content::headerByType( const char *type ) { if ( !type ) { return 0; } //first we check if the requested header is already cached foreach ( Headers::Base *h, h_eaders ) { if ( h->is( type ) ) { return h; //found } } //now we look for it in the article head Headers::Base *h = 0; QByteArray raw=rawHeader( type ); if ( !raw.isEmpty() ) { //ok, we found it //choose a suitable header class if ( strcasecmp( "Message-Id", type ) == 0 ) { h = new Headers::MessageID( this, raw ); } else if ( strcasecmp( "Subject", type ) == 0 ) { h = new Headers::Subject( this, raw ); } else if ( strcasecmp( "Date", type ) == 0 ) { h = new Headers::Date( this, raw ); } else if ( strcasecmp( "From", type ) == 0 ) { h = new Headers::From( this, raw ); } else if ( strcasecmp( "Organization", type ) == 0 ) { h = new Headers::Organization( this, raw ); } else if ( strcasecmp( "Reply-To", type ) == 0 ) { h = new Headers::ReplyTo( this, raw ); } else if ( strcasecmp( "Mail-Copies-To", type ) == 0 ) { h = new Headers::MailCopiesTo( this, raw ); } else if ( strcasecmp( "To", type ) == 0 ) { h = new Headers::To( this, raw ); } else if ( strcasecmp( "CC", type ) == 0 ) { h = new Headers::Cc( this, raw ); } else if ( strcasecmp( "BCC", type ) == 0 ) { h = new Headers::Bcc( this, raw ); } else if ( strcasecmp( "Newsgroups", type ) == 0 ) { h = new Headers::Newsgroups( this, raw ); } else if ( strcasecmp( "Followup-To", type ) == 0 ) { h = new Headers::FollowUpTo( this, raw ); } else if ( strcasecmp( "References", type ) == 0 ) { h = new Headers::References( this, raw ); } else if ( strcasecmp( "Lines", type ) == 0 ) { h = new Headers::Lines( this, raw ); } else if ( strcasecmp( "Content-Type", type ) == 0 ) { h = new Headers::ContentType( this, raw ); } else if ( strcasecmp( "Content-Transfer-Encoding", type ) == 0 ) { h = new Headers::ContentTransferEncoding( this, raw ); } else if ( strcasecmp( "Content-Disposition", type ) == 0 ) { h = new Headers::ContentDisposition( this, raw ); } else if ( strcasecmp( "Content-Description", type ) == 0 ) { h = new Headers::ContentDescription( this, raw ); } else if ( strcasecmp( "Content-Location", type ) == 0 ) { h = new Headers::ContentLocation( this, raw ); } else if ( strcasecmp( "Sender", type ) == 0 ) { h = new Headers::Sender( this, raw ); } else { h = new Headers::Generic( type, this, raw ); } h_eaders.append( h ); //add to cache return h; } else { return 0; //header not found } } QList Content::headersByType( const char *type ) { QList result; if ( !type ) { return result; } QList raw=rawHeaders( type ); foreach( QByteArray header, raw ) result.append( new Headers::Generic( type, this, header ) ); return result; } void Content::setHeader( Headers::Base *h ) { if ( !h ) { return; } removeHeader( h->type() ); h_eaders.append( h ); } bool Content::removeHeader( const char *type ) { for ( Headers::Base::List::iterator it = h_eaders.begin(); it != h_eaders.end(); ++it ) if ( (*it)->is(type) ) { delete (*it); h_eaders.erase( it ); return true; } return false; } bool Content::hasHeader( const char *type ) { return headerByType( type ) != 0; } Headers::ContentType *Content::contentType( bool create ) { Headers::ContentType *p=0; return headerInstance( p, create ); } Headers::ContentTransferEncoding *Content::contentTransferEncoding( bool create ) { Headers::ContentTransferEncoding *p=0; return headerInstance( p, create ); } Headers::ContentDisposition *Content::contentDisposition( bool create ) { Headers::ContentDisposition *p=0; return headerInstance( p, create ); } Headers::ContentDescription *Content::contentDescription( bool create ) { Headers::ContentDescription *p=0; return headerInstance( p, create ); } Headers::ContentLocation *Content::contentLocation( bool create ) { Headers::ContentLocation *p=0; return headerInstance( p, create ); } int Content::size() { int ret = d_ptr->body.length(); if ( contentTransferEncoding()->encoding() == Headers::CEbase64 ) { return ret * 3 / 4; //base64 => 6 bit per byte } return ret; } int Content::storageSize() const { const Q_D(Content); int s = d->head.size(); if ( d->contents.isEmpty() ) { s += d->body.size(); } else { foreach ( Content *c, d->contents ) { s += c->storageSize(); } } return s; } int Content::lineCount() const { const Q_D(Content); int ret = 0; if ( !isTopLevel() ) { ret += d->head.count( '\n' ); } ret += d->body.count( '\n' ); foreach ( Content *c, d->contents ) { ret += c->lineCount(); } return ret; } QByteArray Content::rawHeader( const char *name ) const { return KMime::extractHeader( d_ptr->head, name ); } QList Content::rawHeaders( const char *name ) const { return KMime::extractHeaders( d_ptr->head, name ); } bool Content::decodeText() { Q_D(Content); Headers::ContentTransferEncoding *enc = contentTransferEncoding(); if ( !contentType()->isText() ) { return false; //non textual data cannot be decoded here => use decodedContent() instead } if ( enc->decoded() ) { return true; //nothing to do } switch( enc->encoding() ) { case Headers::CEbase64 : d->body = KCodecs::base64Decode( d->body ); d->body.append( "\n" ); break; case Headers::CEquPr : d->body = KCodecs::quotedPrintableDecode( d->body ); break; case Headers::CEuuenc : d->body = KCodecs::uudecode( d->body ); d->body.append( "\n" ); break; case Headers::CEbinary : // nothing to decode d->body.append( "\n" ); default : break; } enc->setDecoded( true ); return true; } QByteArray Content::defaultCharset() const { return d_ptr->defaultCS; } void Content::setDefaultCharset( const QByteArray &cs ) { d_ptr->defaultCS = KMime::cachedCharset( cs ); foreach ( Content *c, d_ptr->contents ) { c->setDefaultCharset( cs ); } // reparse the part and its sub-parts in order // to clear cached header values parse(); } bool Content::forceDefaultCharset() const { return d_ptr->forceDefaultCS; } void Content::setForceDefaultCharset( bool b ) { d_ptr->forceDefaultCS = b; foreach ( Content *c, d_ptr->contents ) { c->setForceDefaultCharset( b ); } // reparse the part and its sub-parts in order // to clear cached header values parse(); } Content * KMime::Content::content( const ContentIndex &index ) const { if ( !index.isValid() ) { return const_cast( this ); } ContentIndex idx = index; unsigned int i = idx.pop() - 1; // one-based -> zero-based index if ( i < (unsigned int)d_ptr->contents.size() ) { return d_ptr->contents[i]->content( idx ); } else { return 0; } } ContentIndex KMime::Content::indexForContent( Content * content ) const { int i = d_ptr->contents.indexOf( content ); if ( i >= 0 ) { ContentIndex ci; ci.push( i + 1 ); // zero-based -> one-based index return ci; } // not found, we need to search recursively for ( int i = 0; i < d_ptr->contents.size(); ++i ) { ContentIndex ci = d_ptr->contents[i]->indexForContent( content ); if ( ci.isValid() ) { // found it ci.push( i + 1 ); // zero-based -> one-based index return ci; } } return ContentIndex(); // not found } bool Content::isTopLevel() const { return false; } void Content::setParent( Content* parent ) { //make sure the Content is only in the contents list of one parent object Content *oldParent = d_ptr->parent; if ( oldParent && oldParent->contents().contains( this ) ) { oldParent->removeContent( this ); } d_ptr->parent = parent; if ( parent && !parent->contents().contains( this ) ) { parent->addContent( this ); } } Content* Content::parent() const { return d_ptr->parent; } Content* Content::topLevel() const { Content *top = const_cast(this); Content *c = parent(); while ( c ) { top = c; c = c->parent(); } return top; } ContentIndex Content::index() const { Content* top = topLevel(); if ( top ) { return top->indexForContent( const_cast(this) ); } return indexForContent( const_cast(this) ); } +QByteArray ContentPrivate::fullContent() const +{ + QByteArray content = head + "\n\n" + body; + if ( !contents.isEmpty() ) { //this is a multipart message + const Headers::ContentType *ct = const_cast(q_ptr)->contentType(); + QByteArray boundary = "\n--" + ct->boundary(); + + //add all (encoded) contents separated by boundaries + foreach ( Content *c, contents ) { + content += boundary + '\n'; + content += c->d_ptr->fullContent(); + } + //finally append the closing boundary + content += boundary + "--\n"; + }; + + return content; +} + } // namespace KMime diff --git a/kmime/kmime_content_p.h b/kmime/kmime_content_p.h index ed032bb02..f6483ea01 100644 --- a/kmime/kmime_content_p.h +++ b/kmime/kmime_content_p.h @@ -1,56 +1,59 @@ /* Copyright (c) 2007 Volker Krause 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. */ #ifndef KMIME_CONTENT_P_H #define KMIME_CONTENT_P_H //@cond PRIVATE namespace KMime { class ContentPrivate { public: ContentPrivate( Content *q ) : forceDefaultCS( false ), parent( 0 ), q_ptr( q ) { defaultCS = KMime::cachedCharset( "ISO-8859-1" ); } virtual ~ContentPrivate() { qDeleteAll( contents ); contents.clear(); } + /** Returns the reconstructed content (header + body) for the content and sub-contents */ + QByteArray fullContent() const; + QByteArray head; QByteArray body; Content::List contents; QByteArray defaultCS; bool forceDefaultCS; Content *parent; Content* q_ptr; Q_DECLARE_PUBLIC( Content ) }; } //@endcond #endif diff --git a/kmime/kmime_message.cpp b/kmime/kmime_message.cpp index caa3ad50b..97e8ed1aa 100644 --- a/kmime/kmime_message.cpp +++ b/kmime/kmime_message.cpp @@ -1,280 +1,295 @@ /* kmime_message.cpp KMime, the KDE internet mail/usenet news message library. Copyright (c) 2001 the KMime authors. See file AUTHORS for details 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 "kmime_message.h" #include "kmime_message_p.h" #include "kmime_util_p.h" using namespace KMime; namespace KMime { Message::Message() : Content( new MessagePrivate( this ) ) {} Message::Message(MessagePrivate * d) : Content( d ) {} Message::~Message() {} void Message::parse() { Q_D(Message); Content::parse(); QByteArray raw; if ( !( raw = rawHeader( d->subject.type() ) ).isEmpty() ) d->subject.from7BitString( raw ); if ( !( raw = rawHeader( d->date.type() ) ).isEmpty() ) d->date.from7BitString( raw ); } QByteArray Message::assembleHeaders() { Q_D(Message); Headers::Base *h; QByteArray newHead; + KMime::Message m; + m.setContent( d->fullContent() ); + m.parse(); //Message-ID - if ( ( h = messageID( false ) ) != 0 && !h->isEmpty() ) { + if ( ( h = messageID( false ) ) != 0 && !h->isEmpty() && h->as7BitString() != m.messageID()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //From h = from(); // "From" is mandatory - if ( !h->isEmpty() ) { + if ( !h->isEmpty() && h->as7BitString() != m.from()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Subject h = subject(); // "Subject" is mandatory - if ( !h->isEmpty() ) { + if ( !h->isEmpty() && h->as7BitString() != m.subject()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //To - if ( ( h = to( false )) != 0 && !h->isEmpty() ) { + if ( ( h = to( false )) != 0 && !h->isEmpty() && h->as7BitString() != m.to()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Cc - if ( ( h = cc( false )) != 0 && !h->isEmpty() ) { + if ( ( h = cc( false )) != 0 && !h->isEmpty() && h->as7BitString() != m.cc()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Reply-To - if ( ( h = replyTo( false )) != 0 && !h->isEmpty() ) { + if ( ( h = replyTo( false )) != 0 && !h->isEmpty() && h->as7BitString() != m.replyTo()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Date h = date(); // "Date" is mandatory - if ( !h->isEmpty() ) { + if ( !h->isEmpty() && h->as7BitString() != m.date()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //References - if ( ( h = references( false )) != 0 && !h->isEmpty() ) { + if ( ( h = references( false )) != 0 && !h->isEmpty() && h->as7BitString() != m.references()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Organization - if ( ( h = organization( false )) != 0 && !h->isEmpty() ) { + if ( ( h = organization( false )) != 0 && !h->isEmpty() && h->as7BitString() != m.organization()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //UserAgent - if ( ( h = userAgent( false )) != 0 && !h->isEmpty() ) { + if ( ( h = userAgent( false )) != 0 && !h->isEmpty() && h->as7BitString() != m.userAgent()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } // In-Reply-To - if ( ( h = inReplyTo( false ) ) != 0 && !h->isEmpty() ) { + if ( ( h = inReplyTo( false ) ) != 0 && !h->isEmpty() && h->as7BitString() != m.inReplyTo()->as7BitString() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Mime-Version - newHead += "MIME-Version: 1.0\n"; - KMime::removeHeader( d->head, "MIME-Version" ); + Headers::Base *mimeHeader = m.headerByType("MIME-Version"); + QByteArray oldMimeVersion; + if ( mimeHeader ) + oldMimeVersion = mimeHeader->as7BitString(); + if ( oldMimeVersion != "MIME-Version: 1.0" ) { + newHead += "MIME-Version: 1.0\n"; + KMime::removeHeader( d->head, "MIME-Version" ); + } + + QByteArray newContentHead = Content::assembleHeaders(); - return newHead + Content::assembleHeaders(); + if ( newHead.isEmpty() && newContentHead == d->head ) { //no header was changed + return d->head; + } else { + return newHead + newContentHead; + } } void Message::clear() { Q_D(Message); d->subject.clear(); d->date.clear(); Content::clear(); } Headers::Base *Message::getHeaderByType( const char *type ) { return headerByType( type ); } Headers::Base *Message::headerByType( const char *type ) { Q_D(Message); if ( strcasecmp( "Subject", type ) == 0 ) { if ( d->subject.isEmpty() ) { return 0; } else { return &d->subject; } } else if ( strcasecmp("Date", type ) == 0 ){ if ( d->date.isEmpty() ) { return 0; } else { return &d->date; } } else { return Content::headerByType( type ); } } void Message::setHeader( Headers::Base *h ) { Q_D(Message); bool del = true; if ( h->is( "Subject" ) ) { d->subject.fromUnicodeString( h->asUnicodeString(), h->rfc2047Charset() ); } else if ( h->is( "Date" ) ) { d->date.setDateTime( ( static_cast( h ) )->dateTime() ); } else { del = false; Content::setHeader( h ); } if ( del ) delete h; } bool Message::removeHeader( const char *type ) { Q_D(Message); if ( strcasecmp( "Subject", type ) == 0 ) { d->subject.clear(); } else if ( strcasecmp( "Date", type ) == 0 ) { d->date.clear(); } else { return Content::removeHeader( type ); } return true; } Headers::Subject *Message::subject( bool create ) { Q_D( Message ); if ( !create && d->subject.isEmpty() ) { return 0; } return &d->subject; } Headers::Date *Message::date( bool create ) { Q_D( Message ); if ( !create && d->date.isEmpty() ) { return 0; } return &d->date; } bool Message::isTopLevel() const { return true; } Content *Message::mainBodyPart( const QByteArray &type ) { KMime::Content *c = this; while ( c ) { // not a multipart message if ( !c->contentType()->isMultipart() ) { if ( c->contentType()->mimeType() == type || type.isEmpty() ) { return c; } return 0; } // empty multipart if ( c->contents().count() == 0 ) { return 0; } // multipart/alternative if ( c->contentType()->subType() == "alternative" ) { if ( type.isEmpty() ) { return c->contents().first(); } foreach ( Content *c1, c->contents() ) { if ( c1->contentType()->mimeType() == type ) { return c1; } } return 0; } c = c->contents().first(); } return 0; } // @cond PRIVATE #define kmime_mk_header_accessor( header, method ) \ Headers::header *Message::method( bool create ) { \ Headers::header *p = 0; \ return headerInstance( p, create ); \ } kmime_mk_header_accessor( MessageID, messageID ) kmime_mk_header_accessor( Organization, organization ) kmime_mk_header_accessor( From, from ) kmime_mk_header_accessor( ReplyTo, replyTo ) kmime_mk_header_accessor( To, to ) kmime_mk_header_accessor( Cc, cc ) kmime_mk_header_accessor( Bcc, bcc ) kmime_mk_header_accessor( References, references ) kmime_mk_header_accessor( UserAgent, userAgent ) kmime_mk_header_accessor( InReplyTo, inReplyTo ) kmime_mk_header_accessor( Sender, sender ) #undef kmime_mk_header_accessor // @endcond } diff --git a/kmime/tests/kmime_content_test.cpp b/kmime/tests/kmime_content_test.cpp index f4799acbd..ad0b9a317 100644 --- a/kmime/tests/kmime_content_test.cpp +++ b/kmime/tests/kmime_content_test.cpp @@ -1,446 +1,470 @@ /* Copyright (c) 2006 Volker Krause 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 + #include "kmime_content_test.h" #include #include #include #include using namespace KMime; QTEST_KDEMAIN( KMimeContentTest, NoGUI ) void KMimeContentTest::testGetHeaderInstance( ) { // stuff that looks trivial but breaks if you mess with virtual method signatures (see r534381) Headers::From *myfrom = new Headers::From(); QCOMPARE( myfrom->type(), "From" ); Headers::Base *mybase = myfrom; QCOMPARE( mybase->type(), "From" ); // getHeaderInstance() is protected, so we need to test it via KMime::Message Message *c = new Message(); Headers::From *f1 = c->from( true ); Headers::From *f2 = c->from( true ); QCOMPARE( f1, f2 ); delete c; } void KMimeContentTest::testSetContent() { Content *c = new Content(); QVERIFY( !c->hasContent() ); // head and body present c->setContent( "head1\nhead2\n\nbody1\n\nbody2\n" ); QVERIFY( c->hasContent() ); QCOMPARE( c->head(), QByteArray( "head1\nhead2\n" ) ); QCOMPARE( c->body(), QByteArray( "body1\n\nbody2\n" ) ); QList list; list << "head1" << "head2" << "" << "body1" << "" << "body2"; c->setContent( list ); QVERIFY( c->hasContent() ); QCOMPARE( c->head(), QByteArray( "head1\nhead2\n" ) ); QCOMPARE( c->body(), QByteArray( "body1\n\nbody2\n" ) ); // ### the final \n is questionable // empty content c->setContent( QByteArray() ); QVERIFY( !c->hasContent() ); QVERIFY( c->head().isEmpty() ); QVERIFY( c->body().isEmpty() ); // empty head c->setContent( "\nbody1\n\nbody2\n" ); QVERIFY( c->hasContent() ); QVERIFY( c->head().isEmpty() ); QCOMPARE( c->body(), QByteArray( "body1\n\nbody2\n" ) ); list.clear(); list << "" << "body1" << "" << "body2"; c->setContent( list ); QVERIFY( c->hasContent() ); QVERIFY( c->head().isEmpty() ); QCOMPARE( c->body(), QByteArray( "body1\n\nbody2\n" ) ); // empty body c->setContent( "head1\nhead2\n\n" ); QVERIFY( c->hasContent() ); QCOMPARE( c->head(), QByteArray( "head1\nhead2\n" ) ); QVERIFY( c->body().isEmpty() ); list.clear(); list << "head1" << "head2" << ""; c->setContent( list ); QVERIFY( c->hasContent() ); QCOMPARE( c->head(), QByteArray( "head1\nhead2\n" ) ); QVERIFY( c->body().isEmpty() ); } void KMimeContentTest::testMultipleHeaderExtraction() { QByteArray data = "From: Nathaniel Borenstein \n" "To: Ned Freed \n" "Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST)\n" "Subject: Sample message\n" "Received: from ktown.kde.org ([192.168.100.1])\n" "Received: from dev1.kde.org ([192.168.100.2])\n" "\t by ktown.kde.org ([192.168.100.1])\n" "Received: from dev2.kde.org ([192.168.100.3])\n" " by ktown.kde.org ([192.168.100.1])\n"; Message *msg = new Message(); msg->setContent( data ); // FAILS identically to KMimeContentTest::testMultipartMixed // QCOMPARE( msg->encodedContent(), data ); msg->parse(); QList result = msg->headersByType( "Received" ); QCOMPARE( result.count(), 3 ); QCOMPARE( result[0]->asUnicodeString(), QString("from ktown.kde.org ([192.168.100.1])") ); QCOMPARE( result[1]->asUnicodeString(), QString("from dev1.kde.org ([192.168.100.2]) by ktown.kde.org ([192.168.100.1])") ); QCOMPARE( result[2]->asUnicodeString(), QString("from dev2.kde.org ([192.168.100.3]) by ktown.kde.org ([192.168.100.1])") ); } void KMimeContentTest::testMultipartMixed() { // example taken from RFC 2046, section 5.1.1. QByteArray data = "From: Nathaniel Borenstein \n" "To: Ned Freed \n" "Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST)\n" "Subject: Sample message\n" "MIME-Version: 1.0\n" "Content-type: multipart/mixed; boundary=\"simple boundary\"\n" "\n" "This is the preamble. It is to be ignored, though it\n" "is a handy place for composition agents to include an\n" "explanatory note to non-MIME conformant readers.\n" "\n" "--simple boundary\n" "\n" "This is implicitly typed plain US-ASCII text.\n" "It does NOT end with a linebreak.\n" "--simple boundary\n" "Content-type: text/plain; charset=us-ascii\n" "\n" "This is explicitly typed plain US-ASCII text.\n" "It DOES end with a linebreak.\n" "\n" "--simple boundary--\n" "\n" "This is the epilogue. It is also to be ignored.\n"; QByteArray part1 = "This is implicitly typed plain US-ASCII text.\n" "It does NOT end with a linebreak."; QByteArray part2 = "This is explicitly typed plain US-ASCII text.\n" "It DOES end with a linebreak.\n"; // slightly diffrent from original data QByteArray assembled = "From: Nathaniel Borenstein \n" - "Subject: Sample message\n" "To: Ned Freed \n" - "Date: Sun, 21 Mar 1993 23:56:48 -0800\n" + "Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST)\n" + "Subject: Sample message\n" "MIME-Version: 1.0\n" - "Content-Type: multipart/mixed; boundary=\"simple boundary\"\n" + "Content-type: multipart/mixed; boundary=\"simple boundary\"\n" "\n" "\n" "--simple boundary\n" "\n" "This is implicitly typed plain US-ASCII text.\n" "It does NOT end with a linebreak.\n" "--simple boundary\n" - "Content-Type: text/plain; charset=\"us-ascii\"\n" + "Content-type: text/plain; charset=us-ascii\n" "\n" "This is explicitly typed plain US-ASCII text.\n" "It DOES end with a linebreak.\n" "\n" "--simple boundary--\n"; // test parsing Message *msg = new Message(); msg->setContent( data ); QCOMPARE( msg->encodedContent(), data ); msg->parse(); QVERIFY( msg->contentType()->isMultipart() ); Content::List list = msg->contents(); QCOMPARE( list.count(), 2 ); Content *c = list.takeFirst(); QCOMPARE( c->body(), part1 ); c = list.takeFirst(); QCOMPARE( c->body(), part2 ); // assemble again msg->assemble(); QCOMPARE( msg->encodedContent(), assembled ); delete msg; // assembling from scratch + // slightly diffrent from original data + QByteArray assembled2 = + "From: Nathaniel Borenstein \n" + "Subject: Sample message\n" + "To: Ned Freed \n" + "Date: Sun, 21 Mar 1993 23:56:48 -0800\n" + "MIME-Version: 1.0\n" + "Content-Type: multipart/mixed; boundary=\"simple boundary\"\n" + "\n" + "\n" + "--simple boundary\n" + "\n" + "This is implicitly typed plain US-ASCII text.\n" + "It does NOT end with a linebreak.\n" + "--simple boundary\n" + "Content-Type: text/plain; charset=\"us-ascii\"\n" + "\n" + "This is explicitly typed plain US-ASCII text.\n" + "It DOES end with a linebreak.\n" + "\n" + "--simple boundary--\n"; + msg = new Message(); msg->from()->from7BitString( "Nathaniel Borenstein " ); msg->to()->from7BitString( "Ned Freed " ); msg->subject()->from7BitString( "Sample message" ); msg->date()->from7BitString( "Sun, 21 Mar 1993 23:56:48 -0800 (PST)" ); msg->setBody( part1 ); c = new Content(); c->setBody( part2 ); c->contentType()->setMimeType( "text/plain" ); c->contentType()->setCharset( "us-ascii" ); msg->addContent( c ); msg->contentType()->setBoundary( "simple boundary" ); list = msg->contents(); QCOMPARE( list.count(), 2 ); c = list.takeFirst(); QCOMPARE( c->body(), part1 ); c = list.takeFirst(); QCOMPARE( c->body(), part2 ); msg->assemble(); - QCOMPARE( msg->encodedContent(), assembled ); + QCOMPARE( msg->encodedContent(), assembled2 ); } void KMimeContentTest::testImplicitMultipartGeneration() { Content *c1 = new Content(); c1->contentType()->from7BitString( "text/plain" ); c1->setBody( "textpart" ); Content *c2 = new Content(); c2->contentType()->from7BitString( "text/html" ); c2->setBody( "htmlpart" ); c1->addContent( c2 ); // c1 implicitly converted into a multipart/mixed node QCOMPARE( c1->contentType()->mimeType(), QByteArray("multipart/mixed") ); QVERIFY( c1->body().isEmpty() ); Content *c = c1->contents().at( 0 ); // former c1 QCOMPARE( c->contentType()->mimeType(), QByteArray("text/plain") ); QCOMPARE( c->body(), QByteArray("textpart") ); QCOMPARE( c1->contents().at( 1 ), c2 ); } void KMimeContentTest::testExplicitMultipartGeneration() { Content *c1 = new Content(); c1->contentType()->from7BitString( "multipart/mixed" ); Content *c2 = new Content(); c2->contentType()->from7BitString( "text/plain" ); c2->setBody( "textpart" ); Content *c3 = new Content(); c3->contentType()->from7BitString( "text/html" ); c3->setBody( "htmlpart" ); c1->addContent( c2 ); c1->addContent( c3 ); // c1 should not be changed QCOMPARE( c1->contentType()->mimeType(), QByteArray("multipart/mixed") ); QVERIFY( c1->body().isEmpty() ); QCOMPARE( c1->contents().at( 0 ), c2 ); QCOMPARE( c1->contents().at( 1 ), c3 ); } void KMimeContentTest::testParsingUuencoded() { const QByteArray body = "This is a test message that should appears as a text/plain part\n" "once this message is parsed and convert to a MIME tree.\n" "\n" "\n"; const QString imageName = "Name of the encoded file (oxygen 22x22 kde.png)"; const QByteArray uuencodedMsg = "Path: news.example.net!not-for-mail\n" "From: Coin coin \n" "Newsgroups: test.kmime.uuencoded\n" "Subject: Kmime test\n" "Date: Thu, 14 Apr 2005 20:12:47 -0700\n" "Message-ID: \n" "X-Newsreader: Forte Agent 2.0/32.640\n" "Lines: 1283\n" "Organization: Ament\n" "Xref: news.example.net test.kmime.uuencoded:4584\n" "\n" "This is a test message that should appears as a text/plain part\n" "once this message is parsed and convert to a MIME tree.\n" "\n" "begin 644 Name of the encoded file (oxygen 22x22 kde.png)\n" "MB5!.1PT*&@H````-24A$4@```!8````6\"`8```#$M&P[````!'-\"250(\"`@(\n" "M?`ADB`````EP2%ES```#=@```W8!?=6\"S````!ET15AT4V]F='=A^U1FZM2*X2V035`Q5.9UM3S:>&Z__]?36N1],G.QRQ[_QS:6C!YX\\R)\n" "M;C]FS/&)EU&=IH<.PEQ\"\",LUJMK*2K9VT9UVZ.V9D62)H.TK#RE4:;\n" "M1$1..L\"H&WSY\\)$#K*X-(%0,!ST6!EV&W91>&B-B^=>;.Z@*\"WL'#`8[NIG@$<=8(RXI2@5)MF,8;^'F!B5&$]$HX[9=L')\n" "MCW>)G.'I,W/Z>Q;()QF??K_B;,*Y\\S-^,XGPE&\"3/F`,N\\08*:L\"%8=U\"6H2\n" "M@D3X8&F\"H6@@31+B.*'T4%3*`\\O;1,[AG$.S\"5_\\\n" "MQ\"K.6OYVJ>*5&QT\"!I4(%W>_VZ$,@::%5H6Z5L1:&766K%GJ6R/625TKK4+E\n" "M`[,XY==_N$'9P.E?O,&6ZR`BB`AK*PD@&($B+\\CKFKRNJ6I/6;6(M83E-6O:\n" "MN&>-L\\Q+3]6V%%7#=&/,P;TISYZ]PL4JIJ@;0`#AF>?'/F]QRWWL7N;M7LCW-:)J*-C2`T*>/$5LJ*$$]=963.!\"4M@UL9H:FAK8%\n" "M5064IA5^=/8FEZ[EW/+MS]])K\\SQ=8GW-2)*1H8QA09V^:9\"M>6A9R[RR=.O\n" "M<>'-.7>N6-1;0F,((:\"J&&)<.N+TLY?)2\\^@8WC\\4X?0VA/J$@T!J0G\"L4<&\n" "M@[7C-Q975U,;)0QZ(U96]O/@>X0'/KC,X[_3YC%FVPW2Z1613\n" "M]HSV<77]E3\\\"K:@JLO]#*]W%X\\_W]]YS?[>_)XU=A#,6:P0DH-KB38;7G,CV\n" "MB;1+)%WBN$MD$U2$?+[57+OTTJLW7S_SI6I\\X8*H*K>(B`-&P'Y``.'_H[QE\n" ";$[@.>-WU'U(#IV=EWM`H`````$E%3D2N0F\"\"\n" "`\n" "end\n" "\n"; Message *msg = new Message(); msg->setContent( uuencodedMsg ); msg->parse(); Content::List contents = msg->contents(); // text + image QCOMPARE( contents.size(), 2 ); Content *c = 0; // Check the first text part c = contents.at( 0 ); QVERIFY( c->contentType()->isPlainText() ); QCOMPARE( c->body(), body ); // Check the image part c = contents.at( 1 ); QVERIFY( !c->contentType()->isText() ); QCOMPARE( c->contentType()->name(), imageName ); delete msg; } void KMimeContentTest::testParent() { Content *c1 = new Content(); c1->contentType()->from7BitString( "multipart/mixed" ); Content *c2 = new Content(); c2->contentType()->from7BitString( "text/plain" ); c2->setBody( "textpart" ); Content *c3 = new Content(); c3->contentType()->from7BitString( "text/html" ); c3->setBody( "htmlpart" ); Content *c4 = new Content(); c4->contentType()->from7BitString( "text/html" ); c4->setBody( "htmlpart2" ); Content *c5 = new Content(); c5->contentType()->from7BitString( "multipart/mixed" ); //c2 doesn't have a parent yet QCOMPARE(c2->parent(), (Content*)(0L)); c1->addContent( c2 ); c1->addContent( c3 ); c1->addContent( c4 ); // c1 is the parent of those QCOMPARE( c2->parent(), c1 ); QCOMPARE( c3->parent(), c1 ); //test removal c1->removeContent(c2, false); QCOMPARE(c2->parent(), (Content*)(0L)); QCOMPARE(c1->contents().at( 0 ), c3 ); //check if the content is moved correctly to another parent c5->addContent( c4 ); QCOMPARE(c4->parent(), c5); QCOMPARE(c1->contents().count(), 0); //yes, it should be 0 QCOMPARE(c5->contents().at( 0 ), c4); // example taken from RFC 2046, section 5.1.1. QByteArray data = "From: Nathaniel Borenstein \n" "To: Ned Freed \n" "Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST)\n" "Subject: Sample message\n" "MIME-Version: 1.0\n" "Content-type: multipart/mixed; boundary=\"simple boundary\"\n" "\n" "This is the preamble. It is to be ignored, though it\n" "is a handy place for composition agents to include an\n" "explanatory note to non-MIME conformant readers.\n" "\n" "--simple boundary\n" "\n" "This is implicitly typed plain US-ASCII text.\n" "It does NOT end with a linebreak.\n" "--simple boundary\n" "Content-type: text/plain; charset=us-ascii\n" "\n" "This is explicitly typed plain US-ASCII text.\n" "It DOES end with a linebreak.\n" "\n" "--simple boundary--\n" "\n" "This is the epilogue. It is also to be ignored.\n"; // test parsing Message *msg = new Message(); msg->setContent( data ); msg->parse(); QCOMPARE( msg->parent(), (Content*)(0L)); QCOMPARE( msg->contents().at( 0 )->parent(), msg); QCOMPARE( msg->contents().at( 1 )->parent(), msg); delete msg; } #include "kmime_content_test.moc"