diff --git a/kmime/kmime_content.cpp b/kmime/kmime_content.cpp index 451bb6b5b..96e89f970 100644 --- a/kmime/kmime_content.cpp +++ b/kmime/kmime_content.cpp @@ -1,1100 +1,1163 @@ /* 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; } 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(); + 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(); + 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(); foreach ( Headers::Base *h, h_eaders ) { if ( h->isXHeader() ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } } newHead += d->head; // keep unparsed headers d->head = newHead; foreach ( Content *c, contents() ) { c->assemble(); } } QByteArray Content::assembleHeaders() { Q_D(Content); QByteArray newHead; //Content-Type Headers::Base *h = contentType( false ); if ( h && !h->isEmpty() ) { newHead += contentType()->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Content-Transfer-Encoding h = contentTransferEncoding( false ); if ( h && !h->isEmpty() ) { newHead += contentTransferEncoding()->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Content-Description h = contentDescription( false ); if ( h ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } //Content-Disposition h = contentDisposition( false ); if ( h ) { newHead += h->as7BitString() + '\n'; KMime::removeHeader( d->head, h->type() ); } return newHead; } void Content::clear() { Q_D(Content); qDeleteAll( h_eaders ); h_eaders.clear(); qDeleteAll( d->contents ); d->contents.clear(); d->head.clear(); d->body.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') ) { + 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(); + 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 = 0; + 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) ); +} + + + } // namespace KMime diff --git a/kmime/kmime_content.h b/kmime/kmime_content.h index 95ba15162..1a5dedd7d 100644 --- a/kmime/kmime_content.h +++ b/kmime/kmime_content.h @@ -1,443 +1,488 @@ /* kmime_content.h 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 \ TODO: possible glossary terms: content encoding, transfer type, disposition, description header body attachment charset article */ #ifndef __KMIME_CONTENT_H__ #define __KMIME_CONTENT_H__ #include #include #include #include "kmime_export.h" #include "kmime_contentindex.h" #include "kmime_util.h" #include "kmime_headers.h" namespace KMime { class ContentPrivate; /** @brief A class that encapsulates @ref MIME encoded Content. It parses the given data and creates a tree-like structure that represents the structure of the message. */ class KMIME_EXPORT Content { public: typedef QList List; /** Creates an empty Content object. */ Content(); + /** + Creates an empty Content object with a specified parent. + @param parent the parent Content object + @since 4.3 + */ + explicit Content( Content* parent ); //TODO: Merge with the above + /** Creates a Content object containing the given raw data. @param head is a QByteArray containing the header data. @param body is a QByteArray containing the body data. */ Content( const QByteArray &head, const QByteArray &body ); + /** + Creates a Content object containing the given raw data. + + @param head is a QByteArray containing the header data. + @param body is a QByteArray containing the body data. + @param parent the parent Content object + @since 4.3 + */ + Content( const QByteArray &head, const QByteArray &body, Content *parent ); //TODO: merge with the above + /** Destroys this Content object. */ virtual ~Content(); /** Returns true if this Content object is not empty. */ bool hasContent() const; /** Sets the Content to the given raw data, containing the Content head and body separated by two linefeeds. @param l is a line-splitted list of the raw Content data. */ void setContent( const QList &l ); /** Sets the Content to the given raw data, containing the Content head and body separated by two linefeeds. @param s is a QByteArray containing the raw Content data. */ void setContent( const QByteArray &s ); /** Parses the Contents, splitting into multiple sub-Contents. */ virtual void parse(); /** Call to generate the MIME structure of the message. */ virtual void assemble(); /** Clears the complete message and deletes all sub-Contents. */ virtual void clear(); /** Returns the Content header raw data. @see setHead(). */ QByteArray head() const; /** Sets the Content header raw data. @param head is a QByteArray containing the header data. @see head(). */ void setHead( const QByteArray &head ); /** Extracts and removes the next header from @p head. The caller is responsible for deleting the returned header. @deprecated Use nextHeader( QByteArray ) @param head is a QByteArray containing the header data. */ KDE_DEPRECATED Headers::Generic *getNextHeader( QByteArray &head ); /** Extracts and removes the next header from @p head. The caller is responsible for deleting the returned header. @since 4.2 @param head is a QByteArray containing the header data. */ Headers::Generic *nextHeader( QByteArray &head ); /** Tries to find a @p type header in the message and returns it. @deprecated Use headerByType( const char * ) */ KDE_DEPRECATED virtual Headers::Base *getHeaderByType( const char *type ); /** Tries to find a @p type header in the message and returns it. @since 4.2 */ virtual Headers::Base *headerByType( const char *type ); /** Tries to find all the @p type headers in the message and returns it. Take care that this result is not cached, so could be slow. @since 4.2 */ virtual QList headersByType( const char *type ); virtual void setHeader( Headers::Base *h ); virtual bool removeHeader( const char *type ); bool hasHeader( const char *type ); /** Returns the Content type header. @param create if true, create the header if it doesn't exist yet. */ Headers::ContentType *contentType( bool create=true ); /** Returns the Content transfer encoding. @param create if true, create the header if it doesn't exist yet. */ Headers::ContentTransferEncoding *contentTransferEncoding( bool create=true ); /** Returns the Content disposition. @param create if true, create the header if it doesn't exist yet. */ Headers::ContentDisposition *contentDisposition( bool create=true ); /** Returns the Content description. @param create if true, create the header if it doesn't exist yet. */ Headers::ContentDescription *contentDescription( bool create=true ); /** Returns the Content location. @param create if true, create the header if it doesn't exist yet. @since 4.2 */ Headers::ContentLocation *contentLocation( bool create=true ); /** Returns the size of the Content body after encoding. */ int size(); /** Returns the size of this Content and all sub-Contents. */ int storageSize() const; /** Line count of this Content and all sub-Contents. */ int lineCount() const; /** Returns the Content body raw data. @see setBody(). */ QByteArray body() const; /** Sets the Content body raw data. @param body is a QByteArray containing the body data. @see body(). */ void setBody( const QByteArray &body ); /** Returns a QByteArray containing the encoded Content, including the Content header and all sub-Contents. @param useCrLf if true, use @ref CRLF instead of @ref LF for linefeeds. */ QByteArray encodedContent( bool useCrLf = false ); /** Returns the decoded Content body. */ QByteArray decodedContent(); /** Returns the decoded text. Additional to decodedContent(), this also applies charset decoding. If this is not a text Content, decodedText() returns an empty QString. @param trimText if true, then the decoded text will have all trailing whitespace removed. @param removeTrailingNewlines if true, then the decoded text will have all consecutive trailing newlines removed. The last trailing new line of the decoded text is always removed. */ QString decodedText( bool trimText = false, bool removeTrailingNewlines = false ); /** Sets the Content body to the given string using the current charset. @param s Unicode-encoded string. */ void fromUnicodeString( const QString &s ); /** Returns the first Content with mimetype text/. */ Content *textContent(); /** Returns a list of attachments. @param incAlternatives if true, include multipart/alternative parts. */ List attachments( bool incAlternatives = false ); /** Returns a list of sub-Contents. */ List contents() const; /** Adds a new sub-Content, the current Content object is converted into a - multipart/mixed Content node if it has been a single-part Content. + multipart/mixed Content node if it has been a single-part Content. If the sub-Content + is already in another Content object, it is removed from there and its parent is + updated. @param c The new sub-Content. @param prepend if true, prepend to the Content list; else append to the Content list. @see removeContent(). */ void addContent( Content *c, bool prepend = false ); /** Removes the given sub-Content, the current Content object is converted into a single-port Content if only one sub-Content is left. @param c The Content to remove. - @param del if true, delete the removed Content object. + @param del if true, delete the removed Content object. Otherwise its parent is set to NULL. @see addContent(). */ void removeContent( Content *c, bool del = false ); void changeEncoding( Headers::contentEncoding e ); /** Saves the encoded Content to the given textstream @param ts is the stream where the Content should be written to. @param scrambleFromLines: if true, replace "\nFrom " with "\n>From " in the stream. This is needed to avoid problem with mbox-files */ void toStream( QTextStream &ts, bool scrambleFromLines = false ); /** Returns the charset that is used for all headers and the body if the charset is not declared explictly. @see setDefaultCharset() */ QByteArray defaultCharset() const; /** Sets the default charset. @param cs is a QByteArray containing the new default charset. @see defaultCharset(). */ void setDefaultCharset( const QByteArray &cs ); /** Use the default charset even if a different charset is declared in the article. @see setForceDefaultCharset(). */ bool forceDefaultCharset() const; /** Enables/disables the force mode, housekeeping. works correctly only when the article is completely empty or completely loaded. @param b if true, force the default charset to be used. @see forceDefaultCharset(). */ virtual void setForceDefaultCharset( bool b ); /** Returns the Content specified by the given index. If the index doesn't point to a Content, 0 is returned, if the index is invalid (empty), this Content is returned. @param index the Content index */ Content *content( const ContentIndex &index ) const; /** Returns the ContentIndex for the given Content, an invalid index if the Content is not found withing the hierarchy. @param content the Content object to search. */ ContentIndex indexForContent( Content *content ) const; /** Returns true if this is the top-level node in the MIME tree, ie. if this is actually a message or news article. */ virtual bool isTopLevel() const; + /** + * Sets a new parent to the Content and add to its contents list. If it already had a parent, it is removed from the + * old parents contents list. + * @param parent the new parent + * @since 4.3 + */ + void setParent( Content *parent ); + + /** + * Returns the parent content object, or NULL if the content doesn't have a parent. + * @since 4.3 + */ + Content* parent() const; + + /** + * Returns the toplevel content object, NULL if there is no such object. + * @since 4.3 + */ + Content* topLevel() const; + + /** + * Returns the index of this Content based on the topLevel() object. + * @since 4.3 + */ + ContentIndex index() const; + protected: /** Reimplement this method if you need to assemble additional headers in a derived class. Don't forget to call the implementation of the base class. @return The raw, assembled headers. */ virtual QByteArray assembleHeaders(); QByteArray rawHeader( const char *name ) const; QList rawHeaders( const char *name ) const; bool decodeText(); template T *headerInstance( T *ptr, bool create ); Headers::Base::List h_eaders; //@cond PRIVATE ContentPrivate *d_ptr; explicit Content( ContentPrivate *d ); //@endcond private: Q_DECLARE_PRIVATE( Content ) Q_DISABLE_COPY( Content ) }; // some compilers (for instance Compaq C++) need template inline functions // here rather than in the *.cpp file template T *Content::headerInstance( T *ptr, bool create ) { T dummy; //needed to access virtual member T::type() ptr=static_cast ( headerByType( dummy.type() ) ); if ( !ptr && create ) { //no such header found, but we need one => create it ptr = new T( this ); h_eaders.append( ptr ); } return ptr; } } // namespace KMime #endif // __KMIME_CONTENT_H__ diff --git a/kmime/kmime_content_p.h b/kmime/kmime_content_p.h index 6227d9589..ed032bb02 100644 --- a/kmime/kmime_content_p.h +++ b/kmime/kmime_content_p.h @@ -1,55 +1,56 @@ /* 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 ), q_ptr( q ) + ContentPrivate( Content *q ) : forceDefaultCS( false ), parent( 0 ), q_ptr( q ) { defaultCS = KMime::cachedCharset( "ISO-8859-1" ); } virtual ~ContentPrivate() { qDeleteAll( contents ); contents.clear(); } 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/tests/contentindextest.cpp b/kmime/tests/contentindextest.cpp index 734109236..c1dfcff4f 100644 --- a/kmime/tests/contentindextest.cpp +++ b/kmime/tests/contentindextest.cpp @@ -1,90 +1,91 @@ /* 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 "contentindextest.h" #include #include using namespace KMime; #include QTEST_KDEMAIN( ContentIndexTest, NoGUI ) void ContentIndexTest::testToString() { KMime::ContentIndex ci; QCOMPARE( ci.toString(), QString() ); ci.push( 1 ); QCOMPARE( ci.toString(), QString( "1" ) ); ci.push( 2 ); QCOMPARE( ci.toString(), QString( "2.1" ) ); } void ContentIndexTest::testFromString( ) { ContentIndex ci1; QVERIFY( !ci1.isValid() ); ContentIndex ci2( "1.2.bla" ); QVERIFY( !ci2.isValid() ); ContentIndex ci3( "1" ); QVERIFY( ci3.isValid() ); QCOMPARE( ci3.pop(), 1u ); QVERIFY( !ci3.isValid() ); ContentIndex ci4( "3.2" ); QVERIFY( ci4.isValid() ); QCOMPARE( ci4.pop(), 3u ); QCOMPARE( ci4.pop(), 2u ); QVERIFY( !ci4.isValid() ); } void ContentIndexTest::testContent( ) { Content *c1 = new Content(); QCOMPARE( c1->content( ContentIndex() ), c1 ); QCOMPARE( c1->content( ContentIndex( "1" ) ), (Content*)0 ); QCOMPARE( c1->indexForContent( c1 ), ContentIndex() ); Content *c11 = new Content(); // this makes c1 multipart/mixed, ie. c11 will be the second child! c1->addContent( c11 ); QCOMPARE( c1->content( ContentIndex( "2" ) ), c11 ); QCOMPARE( c1->content( ContentIndex( "3" ) ), (Content*)0 ); QCOMPARE( c1->content( ContentIndex( "2.1" ) ), (Content*)0 ); QCOMPARE( c1->indexForContent( c1 ), ContentIndex() ); QCOMPARE( c1->indexForContent( c11 ), ContentIndex( "2" ) ); QCOMPARE( c1->indexForContent( c1->contents().first() ), ContentIndex( "1" ) ); Content *c12 = new Content(); c1->addContent( c12 ); // c12 becomes multipart/mixed, ie. c12 will be the second child! Content *c121 = new Content(); c12->addContent( c121 ); QCOMPARE( c1->content( ContentIndex( "3" ) ), c12 ); QCOMPARE( c1->content( ContentIndex( "3.2" ) ), c121 ); QCOMPARE( c1->indexForContent( c121 ), ContentIndex( "3.2" ) ); + QCOMPARE( c121->index(), ContentIndex( "3.2" ) ); QCOMPARE( c1->indexForContent( (Content*)0 ), ContentIndex() ); } #include "contentindextest.moc" diff --git a/kmime/tests/kmime_content_test.cpp b/kmime/tests/kmime_content_test.cpp index b6c8b9584..a531ef845 100644 --- a/kmime/tests/kmime_content_test.cpp +++ b/kmime/tests/kmime_content_test.cpp @@ -1,364 +1,446 @@ /* 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 "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" "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"; // 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 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 ); } 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" diff --git a/kmime/tests/kmime_content_test.h b/kmime/tests/kmime_content_test.h index 1ec651c1d..0085cda2a 100644 --- a/kmime/tests/kmime_content_test.h +++ b/kmime/tests/kmime_content_test.h @@ -1,42 +1,43 @@ /* 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. */ #ifndef KMIME_CONTENT_TEST_H #define KMIME_CONTENT_TEST_H #include class KMimeContentTest : public QObject { Q_OBJECT private Q_SLOTS: void testGetHeaderInstance(); void testSetContent(); void testMultipartMixed(); void testImplicitMultipartGeneration(); void testExplicitMultipartGeneration(); void testMultipleHeaderExtraction(); /** Tests that a message with uuencoded content is parsed correctly and if a corresponding MIME structure is created. */ void testParsingUuencoded(); + void testParent(); }; #endif