/* kmime_content.cpp KMime, the KDE internet mail/usenet news message library. Copyright (c) 2001 the KMime authors. See file AUTHORS for details This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, US */ #include "kmime_content.h" #include "kmime_parsers.h" #include #include #include #include #include #include using namespace KMime; namespace KMime { Content::Content() : c_ontents(0), h_eaders(0), f_orceDefaultCS(false) { d_efaultCS = cachedCharset("ISO-8859-1"); } Content::Content(const TQCString &h, const TQCString &b) : c_ontents(0), h_eaders(0), f_orceDefaultCS(false) { d_efaultCS = cachedCharset("ISO-8859-1"); h_ead=h.copy(); b_ody=b.copy(); } Content::~Content() { delete c_ontents; delete h_eaders; } void Content::setContent(TQStrList *l) { //tqDebug("Content::setContent(TQStrList *l) : start"); h_ead.resize(0); b_ody.resize(0); //usage of textstreams is much faster than simply appending the strings TQTextStream hts(h_ead, IO_WriteOnly), bts(b_ody, IO_WriteOnly); hts.setEncoding(TQTextStream::Latin1); bts.setEncoding(TQTextStream::Latin1); bool isHead=true; for(char *line=l->first(); line; line=l->next()) { if(isHead && line[0]=='\0') { isHead=false; continue; } if(isHead) hts << line << "\n"; else bts << line << "\n"; } //terminate strings hts << '\0'; bts << '\0'; //tqDebug("Content::setContent(TQStrList *l) : finished"); } void Content::setContent(const TQCString &s) { int pos=s.find("\n\n", 0); if(pos>-1) { h_ead=s.left(++pos); //header *must* end with "\n" !! b_ody=s.mid(pos+1, s.length()-pos-1); } else h_ead=s; } //parse the message, split multiple parts void Content::parse() { //tqDebug("void Content::parse() : start"); delete h_eaders; h_eaders=0; // 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 ((b_ody.size() == 0) && (c_ontents != 0) && !c_ontents->isEmpty()) { // reparse all sub parts for(Content *c=c_ontents->first(); c; c=c_ontents->next()) c->parse(); return; } delete c_ontents; c_ontents=0; Headers::ContentType *ct=contentType(); TQCString 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 if (ct->mimeType()=="text") ct->setMimeType("invalid/invalid"); if(ct->isText()) return; //nothing to do if(ct->isMultipart()) { //this is a multipart message tmp=ct->boundary(); //get boundary-parameter if(!tmp.isEmpty()) { Parser::MultiPart mpp(b_ody, tmp); if(mpp.parse()) { //at least one part found c_ontents=new List(); c_ontents->setAutoDelete(true); if(ct->isSubtype("alternative")) //examine category for the sub-parts cat=Headers::CCalternativePart; else cat=Headers::CCmixedPart; //default to "mixed" QCStringList parts=mpp.parts(); QCStringList::Iterator it; for(it=parts.begin(); it!=parts.end(); ++it) { //create a new Content for every part c=new Content(); c->setContent(*it); c->parse(); c->contentType()->setCategory(cat); //set category of the sub-part c_ontents->append(c); //tqDebug("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 b_ody.resize(0); } 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"); } } } else if (ct->mimeType()=="invalid/invalid") { //non-mime body => check for uuencoded content Parser::UUEncoded uup(b_ody, 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()->setCte(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 b_ody.resize(0); //binary parts for (unsigned int i=0;isetContent(tmp); addContent(c); } if(c_ontents && c_ontents->first()) { //readd the plain text before the uuencoded part c_ontents->first()->setContent("Content-Type: text/plain\nContent-Transfer-Encoding: 7Bit\n\n"+uup.textPart()); c_ontents->first()->contentType()->setMimeType("text/plain"); } } } else { Parser::YENCEncoded yenc(b_ody); 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()->setCte(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 b_ody.resize(0); //binary parts for (unsigned int i=0;isetContent(tmp); // the bodies of yenc message parts are binary data, not null-terminated strings: TQByteArray body = yenc.binaryParts()[i]; TQCString body_string(body.size()); memcpy(body_string.data(), body.data(), body.size()); c->setBody(body_string); addContent(c); } if(c_ontents && c_ontents->first()) { //readd the plain text before the uuencoded part c_ontents->first()->setContent("Content-Type: text/plain\nContent-Transfer-Encoding: 7Bit\n\n"+yenc.textPart()); c_ontents->first()->contentType()->setMimeType("text/plain"); } } } else { //no, this doesn't look like uuencoded stuff => we treat it as "text/plain" ct->setMimeType("text/plain"); } } } //tqDebug("void Content::parse() : finished"); } void Content::assemble() { TQCString newHead=""; //Content-Type newHead+=contentType()->as7BitString()+"\n"; //Content-Transfer-Encoding newHead+=contentTransferEncoding()->as7BitString()+"\n"; //Content-Description Headers::Base *h=contentDescription(false); if(h) newHead+=h->as7BitString()+"\n"; //Content-Disposition h=contentDisposition(false); if(h) newHead+=h->as7BitString()+"\n"; h_ead=newHead; } void Content::clear() { delete h_eaders; h_eaders=0; delete c_ontents; c_ontents=0; h_ead.resize(0); b_ody.resize(0); } TQCString Content::encodedContent(bool useCrLf) { TQCString e; // hack to convert articles with uuencoded or yencoded binaries into // proper mime-compliant articles if(c_ontents && !c_ontents->isEmpty()) { bool convertNonMimeBinaries=false; // reencode non-mime binaries... for(Content *c=c_ontents->first(); c; c=c_ontents->next()) { if ((c->contentTransferEncoding(true)->cte()==Headers::CEuuenc) || (c->contentTransferEncoding(true)->cte()==Headers::CEbinary)) { convertNonMimeBinaries=true; c->b_ody = KCodecs::base64Encode(c->decodedContent(), true); c->b_ody.append("\n"); c->contentTransferEncoding(true)->setCte(Headers::CEbase64); c->contentTransferEncoding(true)->setDecoded(false); c->removeHeader("Content-Description"); c->assemble(); } } // add proper mime headers... if (convertNonMimeBinaries) { h_ead.replace(TQRegExp("MIME-Version: .*\\n"),""); h_ead.replace(TQRegExp("Content-Type: .*\\n"),""); h_ead.replace(TQRegExp("Content-Transfer-Encoding: .*\\n"),""); h_ead+="MIME-Version: 1.0\n"; h_ead+=contentType(true)->as7BitString()+"\n"; h_ead+=contentTransferEncoding(true)->as7BitString()+"\n"; } } //head e=h_ead.copy(); e+="\n"; //body if(!b_ody.isEmpty()) { //this message contains only one part Headers::CTEncoding *enc=contentTransferEncoding(); if(enc->needToEncode()) { if(enc->cte()==Headers::CEquPr) { TQByteArray temp(b_ody.length()); memcpy(temp.data(), b_ody.data(), b_ody.length()); e+=KCodecs::quotedPrintableEncode(temp, false); } else { e+=KCodecs::base64Encode(b_ody, true); e+="\n"; } } else e+=b_ody; } else if(c_ontents && !c_ontents->isEmpty()) { //this is a multipart message Headers::ContentType *ct=contentType(); TQCString boundary="\n--"+ct->boundary(); //add all (encoded) contents separated by boundaries for(Content *c=c_ontents->first(); c; c=c_ontents->next()) { 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; } TQByteArray Content::decodedContent() { TQByteArray temp, ret; Headers::CTEncoding *ec=contentTransferEncoding(); bool removeTrailingNewline=false; int size=ec->cte()==Headers::CEbinary ? b_ody.size() : b_ody.length(); if (size==0) return ret; temp.resize(size); memcpy(temp.data(), b_ody.data(), size); if(ec->decoded()) { ret = temp; removeTrailingNewline=true; } else { switch(ec->cte()) { case Headers::CEbase64 : KCodecs::base64Decode(temp, ret); break; case Headers::CEquPr : ret = KCodecs::quotedPrintableDecode(b_ody); 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; } void Content::decodedText(TQString &s, bool trimText, bool removeTrailingNewlines) { if(!decodeText()) //this is not a text content !! return; bool ok=true; TQTextCodec *codec=TDEGlobal::charsets()->codecForName(contentType()->charset(),ok); s=codec->toUnicode(b_ody.data(), b_ody.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 } } void Content::decodedText(TQStringList &l, bool trimText, bool removeTrailingNewlines) { if(!decodeText()) //this is not a text content !! return; TQString unicode; bool ok=true; TQTextCodec *codec=TDEGlobal::charsets()->codecForName(contentType()->charset(),ok); unicode=codec->toUnicode(b_ody.data(), b_ody.length()); if (trimText && removeTrailingNewlines) { int i; for (i=unicode.length()-1; i>=0; i--) if (!unicode[i].isSpace()) break; unicode.truncate(i+1); } else { if (unicode.right(1)=="\n") unicode.truncate(unicode.length()-1); // remove trailing new-line } l=TQStringList::split('\n', unicode, true); //split the string at linebreaks } void Content::fromUnicodeString(const TQString &s) { bool ok=true; TQTextCodec *codec=TDEGlobal::charsets()->codecForName(contentType()->charset(),ok); if(!ok) { // no suitable codec found => try local settings and hope the best ;-) codec=TDEGlobal::locale()->codecForEncoding(); TQCString chset=TDEGlobal::locale()->encoding(); contentType()->setCharset(chset); } b_ody=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 if(c_ontents) for(Content *c=c_ontents->first(); c; c=c_ontents->next()) if( (ret=c->textContent())!=0 ) break; return ret; } void Content::attachments(Content::List *dst, bool incAlternatives) { dst->setAutoDelete(false); //don't delete the contents if(!c_ontents) dst->append(this); else { for(Content *c=c_ontents->first(); c; c=c_ontents->next()) { if( !incAlternatives && c->contentType()->category()==Headers::CCalternativePart) continue; else c->attachments(dst, incAlternatives); } } if(type()!=ATmimeContent) { // this is the toplevel article Content *text=textContent(); if(text) dst->removeRef(text); } } void Content::addContent(Content *c, bool prepend) { if(!c_ontents) { // this message is not multipart yet c_ontents=new List(); c_ontents->setAutoDelete(true); // first we convert the body to a content Content *main=new Content(); //the Mime-Headers are needed, so we move them to the new content if(h_eaders) { main->h_eaders=new Headers::Base::List(); main->h_eaders->setAutoDelete(true); Headers::Base::List srcHdrs=(*h_eaders); srcHdrs.setAutoDelete(false); int idx=0; for(Headers::Base *h=srcHdrs.first(); h; h=srcHdrs.next()) { if(h->isMimeHeader()) { //remove from this content idx=h_eaders->findRef(h); h_eaders->take(idx); //append to new content main->h_eaders->append(h); } } } //"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->b_ody=b_ody.copy(); c_ontents->append(main); b_ody.resize(0); //not 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) c_ontents->insert(0, c); else c_ontents->append(c); } void Content::removeContent(Content *c, bool del) { if(!c_ontents) // what the .. return; int idx=0; if(del) c_ontents->removeRef(c); else { idx=c_ontents->findRef(c); c_ontents->take(idx); } //only one content left => turn this message in a single-part if(c_ontents->count()==1) { Content *main=c_ontents->first(); //first we have to move the mime-headers if(main->h_eaders) { if(!h_eaders) { h_eaders=new Headers::Base::List(); h_eaders->setAutoDelete(true); } Headers::Base::List mainHdrs=(*(main->h_eaders)); mainHdrs.setAutoDelete(false); for(Headers::Base *h=mainHdrs.first(); h; h=mainHdrs.next()) { if(h->isMimeHeader()) { removeHeader(h->type()); //remove the old header first h_eaders->append(h); //now append the new one idx=main->h_eaders->findRef(h); main->h_eaders->take(idx); //remove from the old content kdDebug(5003) << "Content::removeContent(Content *c, bool del) : mime-header moved: " << h->as7BitString() << endl; } } } //now we can copy the body b_ody=main->b_ody.copy(); //finally we can delete the content list delete c_ontents; c_ontents=0; } } void Content::changeEncoding(Headers::contentEncoding e) { Headers::CTEncoding *enc=contentTransferEncoding(); if(enc->cte()==e) //nothing to do return; if(decodeText()) enc->setCte(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) { //kdWarning(5003) << "Content::changeEncoding() : non textual data and encoding != base64 - this should not happen\n => forcing base64" << endl; e=Headers::CEbase64; } if(enc->cte()!=e) { // ok, we reencode the content using base64 b_ody = KCodecs::base64Encode(decodedContent(), true); b_ody.append("\n"); enc->setCte(e); //set encoding enc->setDecoded(false); } } } void Content::toStream(TQTextStream &ts, bool scrambleFromLines) { TQCString ret=encodedContent(false); if (scrambleFromLines) ret.replace(TQRegExp("\\n\\nFrom "), "\n\n>From "); ts << ret; } Headers::Generic* Content::getNextHeader(TQCString &head) { int pos1=-1, pos2=0, len=head.length()-1; bool folded(false); Headers::Generic *header=0; pos1 = head.find(": "); 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.find("\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 header = new Headers::Generic(head.left(pos1-2), this, head.mid(pos1, pos2-pos1).replace(TQRegExp("\\s*\\n\\s*")," ")); head.remove(0,pos2+1); } else { head = ""; } return header; } Headers::Base* Content::getHeaderByType(const char *type) { if(!type) return 0; Headers::Base *h=0; //first we check if the requested header is already cached if(h_eaders) for(h=h_eaders->first(); h; h=h_eaders->next()) if(h->is(type)) return h; //found //now we look for it in the article head TQCString 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::CTEncoding(this, raw); else if(strcasecmp("Content-Disposition", type)==0) h=new Headers::CDisposition(this, raw); else if(strcasecmp("Content-Description", type)==0) h=new Headers::CDescription(this, raw); else h=new Headers::Generic(type, this, raw); if(!h_eaders) { h_eaders=new Headers::Base::List(); h_eaders->setAutoDelete(true); } h_eaders->append(h); //add to cache return h; } else return 0; //header not found } void Content::setHeader(Headers::Base *h) { if(!h) return; removeHeader(h->type()); if(!h_eaders) { h_eaders=new Headers::Base::List(); h_eaders->setAutoDelete(true); } h_eaders->append(h); } bool Content::removeHeader(const char *type) { if(h_eaders) for(Headers::Base *h=h_eaders->first(); h; h=h_eaders->next()) if(h->is(type)) return h_eaders->remove(); return false; } int Content::size() { int ret=b_ody.length(); if(contentTransferEncoding()->cte()==Headers::CEbase64) return (ret*3/4); //base64 => 6 bit per byte return ret; } int Content::storageSize() { int s=h_ead.size(); if(!c_ontents) s+=b_ody.size(); else { for(Content *c=c_ontents->first(); c; c=c_ontents->next()) s+=c->storageSize(); } return s; } int Content::lineCount() { int ret=0; if(type()==ATmimeContent) ret+=h_ead.contains('\n'); ret+=b_ody.contains('\n'); if(c_ontents && !c_ontents->isEmpty()) for(Content *c=c_ontents->first(); c; c=c_ontents->next()) ret+=c->lineCount(); return ret; } TQCString Content::rawHeader(const char *name) { return extractHeader(h_ead, name); } bool Content::decodeText() { Headers::CTEncoding *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->cte()) { case Headers::CEbase64 : b_ody=KCodecs::base64Decode(b_ody); b_ody.append("\n"); break; case Headers::CEquPr : b_ody=KCodecs::quotedPrintableDecode(b_ody); break; case Headers::CEuuenc : b_ody=KCodecs::uudecode(b_ody); b_ody.append("\n"); break; case Headers::CEbinary : b_ody=TQCString(b_ody.data(), b_ody.size()+1); b_ody.append("\n"); default : break; } enc->setDecoded(true); return true; } void Content::setDefaultCharset(const TQCString &cs) { d_efaultCS = KMime::cachedCharset(cs); if(c_ontents && !c_ontents->isEmpty()) for(Content *c=c_ontents->first(); c; c=c_ontents->next()) c->setDefaultCharset(cs); // reparse the part and its sub-parts in order // to clear cached header values parse(); } void Content::setForceDefaultCS(bool b) { f_orceDefaultCS=b; if(c_ontents && !c_ontents->isEmpty()) for(Content *c=c_ontents->first(); c; c=c_ontents->next()) c->setForceDefaultCS(b); // reparse the part and its sub-parts in order // to clear cached header values parse(); } } // namespace KMime