Files
kmess/src/network/mimemessage.cpp
T
2011-02-08 19:31:37 +00:00

673 lines
16 KiB
C++

/***************************************************************************
mimemessage.cpp - description
-------------------
begin : Sat Mar 8 2003
copyright : (C) 2003 by Mike K. Bennett
email : mkb137b@hotmail.com
***************************************************************************/
/***************************************************************************
* *
* 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. *
* *
***************************************************************************/
#include "mimemessage.h"
#include "../kmessdebug.h"
#include <QByteArray>
#include <QRegExp>
#include <QTextCodec>
#include <KCharsets>
#include <KCodecs>
#include <KGlobal>
/*
The decode...() functions and getCodecByName() come from KMail
and are therefore (C) KMail developers.
*/
// The constructor
MimeMessage::MimeMessage()
{
}
// The constructor that parses a message
MimeMessage::MimeMessage(const QString &message)
{
#ifdef KMESSDEBUG_MIMEMESSAGE
kmDebug() << "Parsing the sub message";
#endif
parseMessage( message );
}
// The constructor that parses a message
MimeMessage::MimeMessage(const QByteArray &message)
{
// we get the header without the binary data that comes after
int nullPos = message.indexOf('\0');
int endMime = message.indexOf("\r\n\r\n");
QString messageHeader( QString::fromUtf8(message.data(), (nullPos == -1) ? endMime : nullPos ) );
// Regexps to parse the header
QRegExp rx1("Content-Type: ([A-Za-z0-9$!*/\\-]*)");
QRegExp rx2("Message-ID: ([^\r\n]+)");
// Get the Content-Type header
QString contentType;
if( rx1.indexIn( messageHeader ) != -1 )
{
contentType = rx1.cap(1);
}
// Some messages should be preserved as binary, so take care of that.
// - p2p messages have a 48 byte header.
// - multi-packet messages can be split between a double byte utf-8 char.
bool isP2P = ( contentType == "application/x-msnmsgrp2p" );
bool isMultiPacket = ( rx2.indexIn( messageHeader ) != -1 );
if( isP2P || isMultiPacket )
{
#ifdef KMESSDEBUG_MIMEMESSAGE
if( isP2P )
{
kmDebug() << "Parsing the message, extracting binary p2p body.";
}
else if( isMultiPacket )
{
kmDebug() << "Parsing the message, preserving multi-packet body as binary.";
}
#endif
int bodyStart = endMime + 4; // 2 newlines
int bodySize = message.size() - bodyStart;
// Extract the Mime fields from the message
QString mimeData( QString::fromUtf8( message.data(), bodyStart ) );
parseMessage( mimeData );
// Extract the binary data from the message.
// Copy to our own managed copy.
binaryBody_ = QByteArray( message.data() + bodyStart, bodySize );
}
else
{
#ifdef KMESSDEBUG_MIMEMESSAGE
kmDebug() << "Parsing the message.";
#endif
// This is a normal plain-text message.
// The fromUtf8 call is required to convert Unicode characters properly! (like Chinese)
parseMessage( QString::fromUtf8( message.data(), message.size()) );
}
}
// The copy constructor
MimeMessage::MimeMessage(const MimeMessage& other)
{
#ifdef KMESSDEBUG_MIMEMESSAGE
kmDebug() << "copy constructor";
#endif
// Get the body of the other message
fields_ = other.fields_;
values_ = other.values_;
body_ = other.body_;
binaryBody_ = other.binaryBody_;
// QValueList is implicitly shared, so the assign operation
// does not create another copy. It uses "copy on write".
// QByteArray is explicitly shared, but it's not a problem here,
// since we don't modify the array data of binaryBody_.
#ifdef KMESSTEST
// Make sure the ascii-zero characters are copied correctly:
KMESS_ASSERT( binaryBody_.size() == other.binaryBody_.size() );
KMESS_ASSERT( fields_.size() == other.fields_.size() );
#endif
}
// The destructor
MimeMessage::~MimeMessage()
{
}
// Add a field to the message
void MimeMessage::addField(const QString& field, const QString& value)
{
fields_ << field;
values_ << value;
}
// Change a field, or add it
void MimeMessage::setField(const QString& field, const QString& value)
{
int index = fields_.indexOf( field );
if(-1 == index)
{
addField( field, value );
}
else
{
values_[ index ] = value;
}
}
// Clear all fields in MimeMessage for re-use the object
void MimeMessage::clearFields()
{
body_ = "";
fields_.clear();
values_.clear();
binaryBody_ = "";
}
// decodes MIME strings like =?iso...=...?= ...
QString MimeMessage::decodeRFC2047String(const QByteArray &aStr) const
{
QString result;
QByteArray charset;
char *pos, *beg, *end, *mid=0;
QByteArray str, cstr, LWSP_buffer;
char encoding='Q', ch;
bool valid, lastWasEncodedWord=FALSE;
const int maxLen=200;
int i;
if( ! aStr.contains("=?") )
{
//QString str( messageCodec->toUnicode(aStr) );
QString str( QString::fromUtf8( aStr ) );
if (str.indexOf('\n') == -1) return str;
QString str2((QChar*)0, str.length());
int i = 0;
while (i < str.length())
{
if (str[i] == '\n')
{
str2 += ' ';
i += 2;
} else {
str2 += str[i];
i++;
}
}
return str2;
}
for (pos = const_cast<QByteArray&>( aStr ).data(); *pos; pos++)
{
// line unfolding
if ( pos[0] == '\r' && pos[1] == '\n' ) {
pos++;
continue;
}
if ( pos[0] == '\n' )
continue;
// collect LWSP after encoded-words,
// because we might need to throw it out
// (when the next word is an encoded-word)
if ( lastWasEncodedWord && ( pos[0] == ' ' || pos[0] == '\t' ) ) {
LWSP_buffer += pos[0];
continue;
}
// verbatimly copy normal text
if (pos[0]!='=' || pos[1]!='?')
{
result += LWSP_buffer + pos[0];
LWSP_buffer = 0;
lastWasEncodedWord = FALSE;
continue;
}
// found possible encoded-word
beg = pos+2;
end = beg;
valid = TRUE;
// parse charset name
charset = "";
for (i=2,pos+=2; i<maxLen && (*pos!='?'&&(*pos==' '||ispunct(*pos)||isalnum(*pos))); ++i)
{
charset += *pos;
pos++;
}
if (*pos!='?' || i<4 || i>=maxLen) valid = FALSE;
else
{
// get encoding and check delimiting question marks
encoding = (char) toupper(pos[1]);
if (pos[2]!='?' || (encoding!='Q' && encoding!='B'))
valid = FALSE;
pos+=3;
i+=3;
}
if (valid)
{
mid = pos;
// search for end of encoded part
while (i<maxLen && *pos && !(*pos=='?' && *(pos+1)=='='))
{
++i;
++pos;
}
end = pos+2;//end now points to the first char after the encoded string
if (i>=maxLen || !*pos) valid = FALSE;
}
if (valid)
{
// valid encoding: decode and throw away separating LWSP
ch = *pos;
*pos = '\0';
str = QByteArray( mid ).left((int)(mid - pos - 1));
if (encoding == 'Q')
{
// decode quoted printable text
for (i=str.length()-1; i>=0; i--)
if (str[i]=='_') str[i]=' ';
cstr = KCodecs::quotedPrintableDecode(str);
}
else
{
// decode base64 text
cstr = QByteArray::fromBase64( str );
}
QTextCodec *codec = getCodecByName(charset);
if (!codec)
{
result += QString::fromUtf8( cstr );
}
else
{
result += codec->toUnicode(cstr);
}
lastWasEncodedWord = TRUE;
*pos = ch;
pos = end -1;
}
else
{
// invalid encoding, keep separating LWSP.
//result += "=?";
//pos = beg -1; // because pos gets increased shortly afterwards
pos = beg - 2;
result += LWSP_buffer;
result += *pos++;
result += *pos;
lastWasEncodedWord = FALSE;
}
LWSP_buffer = 0;
}
return result;
}
// Return the body of the message
const QString& MimeMessage::getBody() const
{
return body_;
}
// Return the P2P data of the message
const QByteArray& MimeMessage::getBinaryBody() const
{
return binaryBody_;
}
// Finds a QTextCodec by name
QTextCodec* MimeMessage::getCodecByName(const QByteArray& codecName )
{
if ( codecName.isEmpty() )
{
return 0;
}
return KGlobal::charsets()->codecForName( codecName.toLower() );
}
// Return the field and value at the given index
void MimeMessage::getFieldAndValue(QString& field, QString& value, const int index) const
{
if ( index < fields_.count() )
{
field = fields_[index];
value = values_[index];
}
else
{
field = QString::null;
value = QString::null;
}
}
// Return the message fields as a big string
QString MimeMessage::getFields() const
{
QString message( "" );
// Get the fields and values
for( int i = 0; i < fields_.count(); i++ )
{
message += fields_[i] + ": " + values_[i] + "\r\n";
}
return message;
}
// Return the entire message as a big string
QByteArray MimeMessage::getMessage() const
{
// So much easier with Qt4!
QString message = QString( getFields() + "\r\n" + body_);
// for messages with an empty body must use two \r\n
// sequences. This fixes typing notifications not showing in WLM 2009.
if( body_.isEmpty() && binaryBody_.isEmpty() )
{
message += "\r\n";
}
QByteArray textPart = message.toUtf8();
return textPart + binaryBody_;
}
// The total number of fields
uint MimeMessage::getNoFields() const
{
return fields_.count();
}
// Get one parameter of a value that has multiple parameters
QString MimeMessage::getSubValue(const QString& field, const QString& subField) const
{
QString value, parameter;
int left, right;
// Get the value referred to by the field
value = getValue( field );
if ( !value.isNull() )
{
// If the subfield isn't specified, then get whatever is at the start of the message until the
// first semicolon or the end of the line
if ( subField.isNull() )
{
left = 0;
}
else
{
// The left of the parameter is "subField=", the right is the next semicolon or the end of the line
left = value.indexOf( subField + "=" );
if ( left >= 0 )
{
left += subField.length() + 1;
}
}
if ( left >= 0 )
{
right = value.indexOf( ";", left );
if ( right < 0 )
{
right = value.length();
}
// Get the parameter
parameter = value.mid( left, ( right - left ) );
return parameter;
}
}
return QString::null;
}
// Get a value given a field
QString MimeMessage::getValue(const QString& field ) const
{
// Search the fields for a match
for( int i = 0; i < fields_.count(); i++ )
{
if ( fields_[i] == field )
{
return values_[i];
}
}
kmWarning() << "This message contained no field \"" << field << "\".";
return QString();
}
// Test whether a given field exists in the message header
bool MimeMessage::hasField(const QString& field) const
{
// Search the fields for a match
for( int i = 0; i < fields_.count(); i++ )
{
if ( fields_[i] == field )
{
return true;
}
}
return false;
}
// Parse the message into type, body, and fields and values
void MimeMessage::parseMessage(const QString& message)
{
QString head, field, value;
QStringList lines;
// Split the message into head and body
// Split the head into its various lines
splitMessage( head, body_, message );
splitHead( lines, head );
// Split all the lines into field and value
for( int i = 0; i < lines.count(); i++ )
{
if( lines[i].startsWith("\t") )
{
#ifdef KMESSTEST
KMESS_ASSERT( fields_.last() == "Received" );
#endif
// Addition to parse email headers, with Received: headers that continue at
// the next line
values_[ values_.count() - 1 ] += "\r\n" + lines[i].mid(2);
}
else
{
splitLine( field, value, lines[i] );
if ( !field.isEmpty() )
{
// Add the fields and values to the lists
addField( field, value );
}
}
}
#ifdef KMESSTEST
KMESS_ASSERT( fields_.count() == values_.count() );
#endif
}
// Print the contents of the message to kDebug (for debugging purposes)
void MimeMessage::print() const
{
kmDebug() << "Printing MIME message, " << fields_.count() << " fields.";
// Get the fields and values
for( int i = 0; i < fields_.count(); i++ )
{
kmDebug() << "Field: \"" << fields_[i] << "\" value: \"" << values_[i] << "\"";
}
if( binaryBody_.isEmpty() )
{
kmDebug() << "Body:" << endl << body_;
}
else
{
kmDebug() << "Body: (" << binaryBody_.size() << " bytes of binary data)";
}
}
// Set the message body
void MimeMessage::setBody(const QString& body)
{
body_ = body;
}
// Set the P2P data of the message
void MimeMessage::setBinaryBody(const QByteArray &header, const QByteArray &body, const char footer[4])
{
int headerSize = header.size();
int bodySize = body.size();
int footerPos = headerSize + bodySize;
// Footer needs to be wrapped in a QByteArray
// or replace( start, end, char* ) will reduce the size of the footer (Qt 4.3.3)
QByteArray footerWrapper( footer, 4 );
// So much easier with Qt4!
binaryBody_.resize( footerPos + 4 );
binaryBody_.replace( 0, headerSize, header );
binaryBody_.replace( headerSize, bodySize, body );
binaryBody_.replace( footerPos, 4, footerWrapper );
#ifdef KMESSTEST
KMESS_ASSERT( binaryBody_.size() >= 52 ); // 48 bytes header, 4 bytes footer.
#endif
}
// Set the P2P data of the message
void MimeMessage::setBinaryBody(const QByteArray &data)
{
binaryBody_ = data;
}
// Split a line between field and value
void MimeMessage::splitLine(QString& field, QString& value, const QString& line) const
{
if ( line.isEmpty() )
{
return;
}
int index;
index = line.indexOf(":");
if ( index >= 0 )
{
field = line.left( index );
if ( ( index + 1 ) < (int)line.length() )
{
value = line.right( line.length() - index - 2 );
}
else
{
value = QString::null;
}
}
else
{
kmWarning() << "Couldn't split line '" << line << "'";
}
}
// Split the message head into components and store it in the string list
void MimeMessage::splitHead(QStringList& stringList, const QString& head) const
{
// adam (16 Oct 10) - split on \n only
// the OfflineIM service is doing something funky and only
// terminating lines with \n. If we split on \n, then trim() the string,
// we'll strip out any \r's as well.
stringList = head.split("\n");
for( int i = 0; i < stringList.count(); i++ )
{
// special case for email headers (apparently?)
if ( stringList[i].startsWith( "\t" ) ) continue;
stringList[i] = stringList[i].trimmed();
}
}
// Split a message into head and body
void MimeMessage::splitMessage(QString& head, QString& body, const QString& message) const
{
int index;
int count;
// The message is split into head and body at "\r\n\r\n"
index = message.indexOf( "\r\n\r\n" );
count = 4;
// Offline IM MIME messages don't use \r\n anymore?
// wtf? (adam - 16 Oct 10)
if ( index < 0 )
{
index = message.indexOf("\n\n");
count = 2;
}
if ( index < 0 )
{
head = message;
body = "";
}
else
{
head = message.left( index + ( count / 2 ) ); // Keep a "\r\n" at the end
body = message.right( message.length() - index - count );
}
}