You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdepim/libkmime/kmime_content.cpp

898 lines
24 KiB

/*
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 <kcharsets.h>
#include <kmdcodec.h>
#include <kglobal.h>
#include <klocale.h>
#include <kdebug.h>
#include <tqtextcodec.h>
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;i<uup.binaryParts().count();i++) {
c=new Content();
//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(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;i<yenc.binaryParts().count();i++) {
c=new Content();
//generate content with mime-compliant headers
tmp="Content-Type: ";
tmp += yenc.mimeTypes().at(i);
tmp += "; name=\"";
tmp += yenc.filenames().at(i);
tmp += "\"\nContent-Transfer-Encoding: binary\nContent-Disposition: attachment; filename=\"";
tmp += yenc.filenames().at(i);
tmp += "\"\n\n";
c->setContent(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