mirror of
https://bitbucket.org/smil3y/kde-playground.git
synced 2025-02-23 18:32:51 +00:00
1029 lines
27 KiB
C++
1029 lines
27 KiB
C++
/*
|
|
kmime_util.cpp
|
|
|
|
KMime, the KDE Internet mail/usenet news message library.
|
|
Copyright (c) 2001 the KMime authors.
|
|
See file AUTHORS for details
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Library General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Library General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
along with this library; see the file COPYING.LIB. If not, write to
|
|
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "kmime_util.h"
|
|
#include "kmime_util_p.h"
|
|
|
|
#include "kmime_charfreq.h"
|
|
#include "kmime_codecs.h"
|
|
#include "kmime_header_parsing.h"
|
|
#include "kmime_message.h"
|
|
#include "kmime_warning.h"
|
|
|
|
#include <config-kmime.h>
|
|
#include <kdefakes.h> // for strcasestr
|
|
#include <kglobal.h>
|
|
#include <klocale.h>
|
|
#include <klocalizedstring.h>
|
|
#include <kcharsets.h>
|
|
#include <kcodecs.h>
|
|
#include <kdebug.h>
|
|
|
|
#include <QtCore/QList>
|
|
#include <QtCore/QString>
|
|
#include <QtCore/QTextCodec>
|
|
|
|
#include <ctype.h>
|
|
#include <time.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
|
|
using namespace KMime;
|
|
|
|
namespace KMime {
|
|
|
|
QList<QByteArray> c_harsetCache;
|
|
QList<QByteArray> l_anguageCache;
|
|
QString f_allbackCharEnc;
|
|
bool u_seOutlookEncoding = false;
|
|
|
|
QByteArray cachedCharset( const QByteArray &name )
|
|
{
|
|
foreach ( const QByteArray& charset, c_harsetCache ) {
|
|
if ( qstricmp( name.data(), charset.data() ) == 0 ) {
|
|
return charset;
|
|
}
|
|
}
|
|
|
|
c_harsetCache.append( name.toUpper() );
|
|
//kDebug() << "KNMimeBase::cachedCharset() number of cs" << c_harsetCache.count();
|
|
return c_harsetCache.last();
|
|
}
|
|
|
|
QByteArray cachedLanguage( const QByteArray &name )
|
|
{
|
|
foreach ( const QByteArray& language, l_anguageCache ) {
|
|
if ( qstricmp( name.data(), language.data() ) == 0 ) {
|
|
return language;
|
|
}
|
|
}
|
|
|
|
l_anguageCache.append( name.toUpper() );
|
|
//kDebug() << "KNMimeBase::cachedCharset() number of cs" << c_harsetCache.count();
|
|
return l_anguageCache.last();
|
|
}
|
|
|
|
bool isUsAscii( const QString &s )
|
|
{
|
|
uint sLength = s.length();
|
|
for ( uint i=0; i<sLength; i++ ) {
|
|
if ( s.at( i ).toLatin1() <= 0 ) { // c==0: non-latin1, c<0: non-us-ascii
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QString nameForEncoding( Headers::contentEncoding enc )
|
|
{
|
|
switch ( enc ) {
|
|
case Headers::CE7Bit: return QString::fromLatin1( "7bit" );
|
|
case Headers::CE8Bit: return QString::fromLatin1( "8bit" );
|
|
case Headers::CEquPr: return QString::fromLatin1( "quoted-printable" );
|
|
case Headers::CEbase64: return QString::fromLatin1( "base64" );
|
|
case Headers::CEuuenc: return QString::fromLatin1( "uuencode" );
|
|
case Headers::CEbinary: return QString::fromLatin1( "binary" );
|
|
default: return QString::fromLatin1( "unknown" );
|
|
}
|
|
}
|
|
|
|
QList<Headers::contentEncoding> encodingsForData( const QByteArray &data )
|
|
{
|
|
QList<Headers::contentEncoding> allowed;
|
|
CharFreq cf( data );
|
|
|
|
switch ( cf.type() ) {
|
|
case CharFreq::SevenBitText:
|
|
allowed << Headers::CE7Bit;
|
|
case CharFreq::EightBitText:
|
|
allowed << Headers::CE8Bit;
|
|
case CharFreq::SevenBitData:
|
|
if ( cf.printableRatio() > 5.0/6.0 ) {
|
|
// let n the length of data and p the number of printable chars.
|
|
// Then base64 \approx 4n/3; qp \approx p + 3(n-p)
|
|
// => qp < base64 iff p > 5n/6.
|
|
allowed << Headers::CEquPr;
|
|
allowed << Headers::CEbase64;
|
|
} else {
|
|
allowed << Headers::CEbase64;
|
|
allowed << Headers::CEquPr;
|
|
}
|
|
break;
|
|
case CharFreq::EightBitData:
|
|
allowed << Headers::CEbase64;
|
|
break;
|
|
case CharFreq::None:
|
|
default:
|
|
Q_ASSERT( false );
|
|
}
|
|
|
|
return allowed;
|
|
}
|
|
|
|
// "(),.:;<>@[\]
|
|
const uchar specialsMap[16] = {
|
|
0x00, 0x00, 0x00, 0x00, // CTLs
|
|
0x20, 0xCA, 0x00, 0x3A, // SPACE ... '?'
|
|
0x80, 0x00, 0x00, 0x1C, // '@' ... '_'
|
|
0x00, 0x00, 0x00, 0x00 // '`' ... DEL
|
|
};
|
|
|
|
// "(),:;<>@[\]/=?
|
|
const uchar tSpecialsMap[16] = {
|
|
0x00, 0x00, 0x00, 0x00, // CTLs
|
|
0x20, 0xC9, 0x00, 0x3F, // SPACE ... '?'
|
|
0x80, 0x00, 0x00, 0x1C, // '@' ... '_'
|
|
0x00, 0x00, 0x00, 0x00 // '`' ... DEL
|
|
};
|
|
|
|
// all except specials, CTLs, SPACE.
|
|
const uchar aTextMap[16] = {
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x5F, 0x35, 0xFF, 0xC5,
|
|
0x7F, 0xFF, 0xFF, 0xE3,
|
|
0xFF, 0xFF, 0xFF, 0xFE
|
|
};
|
|
|
|
// all except tspecials, CTLs, SPACE.
|
|
const uchar tTextMap[16] = {
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x5F, 0x36, 0xFF, 0xC0,
|
|
0x7F, 0xFF, 0xFF, 0xE3,
|
|
0xFF, 0xFF, 0xFF, 0xFE
|
|
};
|
|
|
|
// none except a-zA-Z0-9!*+-/
|
|
const uchar eTextMap[16] = {
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x40, 0x35, 0xFF, 0xC0,
|
|
0x7F, 0xFF, 0xFF, 0xE0,
|
|
0x7F, 0xFF, 0xFF, 0xE0
|
|
};
|
|
|
|
void setFallbackCharEncoding(const QString& fallbackCharEnc)
|
|
{
|
|
f_allbackCharEnc = fallbackCharEnc;
|
|
}
|
|
|
|
QString fallbackCharEncoding()
|
|
{
|
|
return f_allbackCharEnc;
|
|
}
|
|
|
|
void setUseOutlookAttachmentEncoding( bool violateStandard )
|
|
{
|
|
u_seOutlookEncoding = violateStandard;
|
|
}
|
|
|
|
bool useOutlookAttachmentEncoding()
|
|
{
|
|
return u_seOutlookEncoding;
|
|
}
|
|
|
|
|
|
QString decodeRFC2047String( const QByteArray &src, QByteArray &usedCS,
|
|
const QByteArray &defaultCS, bool forceCS )
|
|
{
|
|
QByteArray result;
|
|
QByteArray spaceBuffer;
|
|
const char *scursor = src.constData();
|
|
const char *send = scursor + src.length();
|
|
bool onlySpacesSinceLastWord = false;
|
|
|
|
while ( scursor != send ) {
|
|
// space
|
|
if ( isspace( *scursor ) && onlySpacesSinceLastWord ) {
|
|
spaceBuffer += *scursor++;
|
|
continue;
|
|
}
|
|
|
|
// possible start of an encoded word
|
|
if ( *scursor == '=' ) {
|
|
QByteArray language;
|
|
QString decoded;
|
|
++scursor;
|
|
const char *start = scursor;
|
|
if ( HeaderParsing::parseEncodedWord( scursor, send, decoded, language, usedCS, defaultCS, forceCS ) ) {
|
|
result += decoded.toUtf8();
|
|
onlySpacesSinceLastWord = true;
|
|
spaceBuffer.clear();
|
|
} else {
|
|
if ( onlySpacesSinceLastWord ) {
|
|
result += spaceBuffer;
|
|
onlySpacesSinceLastWord = false;
|
|
}
|
|
result += '=';
|
|
scursor = start; // reset cursor after parsing failure
|
|
}
|
|
continue;
|
|
} else {
|
|
// unencoded data
|
|
if ( onlySpacesSinceLastWord ) {
|
|
result += spaceBuffer;
|
|
onlySpacesSinceLastWord = false;
|
|
}
|
|
result += *scursor;
|
|
++scursor;
|
|
}
|
|
}
|
|
// If there are any chars that couldn't be decoded in UTF-8,
|
|
// use the fallback charset if it exists
|
|
const QString tryUtf8 = QString::fromUtf8( result );
|
|
if ( tryUtf8.contains( 0xFFFD ) && !f_allbackCharEnc.isEmpty() ) {
|
|
QTextCodec* codec = KGlobal::charsets()->codecForName( f_allbackCharEnc );
|
|
return codec->toUnicode( result );
|
|
} else {
|
|
return tryUtf8;
|
|
}
|
|
}
|
|
|
|
QString decodeRFC2047String( const QByteArray &src )
|
|
{
|
|
QByteArray usedCS;
|
|
return decodeRFC2047String( src, usedCS, "utf-8", false );
|
|
}
|
|
|
|
static const char *reservedCharacters = "\"()<>@,.;:\\[]=";
|
|
|
|
QByteArray encodeRFC2047String( const QString &src, const QByteArray &charset,
|
|
bool addressHeader, bool allow8BitHeaders )
|
|
{
|
|
QByteArray result;
|
|
int start=0, end=0;
|
|
bool nonAscii=false, ok=true, useQEncoding=false;
|
|
|
|
// fromLatin1() is safe here, codecForName() uses toLatin1() internally
|
|
const QTextCodec *codec = KGlobal::charsets()->codecForName( QString::fromLatin1( charset ), ok );
|
|
|
|
QByteArray usedCS;
|
|
if ( !ok ) {
|
|
//no codec available => try local8Bit and hope the best ;-)
|
|
usedCS = KGlobal::locale()->encoding();
|
|
codec = KGlobal::charsets()->codecForName( QString::fromLatin1( usedCS ), ok );
|
|
} else {
|
|
Q_ASSERT( codec );
|
|
if ( charset.isEmpty() ) {
|
|
usedCS = codec->name();
|
|
} else {
|
|
usedCS = charset;
|
|
}
|
|
}
|
|
|
|
QTextCodec::ConverterState converterState( QTextCodec::IgnoreHeader );
|
|
QByteArray encoded8Bit = codec->fromUnicode( src.constData(), src.length(), &converterState );
|
|
if ( converterState.invalidChars > 0 ) {
|
|
usedCS = "utf-8";
|
|
codec = QTextCodec::codecForName( usedCS );
|
|
encoded8Bit = codec->fromUnicode( src );
|
|
}
|
|
|
|
if ( usedCS.contains( "8859-" ) ) { // use "B"-Encoding for non iso-8859-x charsets
|
|
useQEncoding = true;
|
|
}
|
|
|
|
if ( allow8BitHeaders ) {
|
|
return encoded8Bit;
|
|
}
|
|
|
|
uint encoded8BitLength = encoded8Bit.length();
|
|
for ( unsigned int i=0; i<encoded8BitLength; i++ ) {
|
|
if ( encoded8Bit[i] == ' ' ) { // encoding starts at word boundaries
|
|
start = i + 1;
|
|
}
|
|
|
|
// encode escape character, for japanese encodings...
|
|
if ( ( (signed char)encoded8Bit[i] < 0 ) || ( encoded8Bit[i] == '\033' ) ||
|
|
( addressHeader && ( strchr( "\"()<>@,.;:\\[]=", encoded8Bit[i] ) != 0 ) ) ) {
|
|
end = start; // non us-ascii char found, now we determine where to stop encoding
|
|
nonAscii = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( nonAscii ) {
|
|
while ( ( end < encoded8Bit.length() ) && ( encoded8Bit[end] != ' ' ) ) {
|
|
// we encode complete words
|
|
end++;
|
|
}
|
|
|
|
for ( int x=end; x<encoded8Bit.length(); x++ ) {
|
|
if ( ( (signed char)encoded8Bit[x] < 0 ) || ( encoded8Bit[x] == '\033' ) ||
|
|
( addressHeader && ( strchr( reservedCharacters, encoded8Bit[x] ) != 0 ) ) ) {
|
|
end = x; // we found another non-ascii word
|
|
|
|
while ( ( end < encoded8Bit.length() ) && ( encoded8Bit[end] != ' ' ) ) {
|
|
// we encode complete words
|
|
end++;
|
|
}
|
|
}
|
|
}
|
|
|
|
result = encoded8Bit.left( start ) + "=?" + usedCS;
|
|
|
|
if ( useQEncoding ) {
|
|
result += "?Q?";
|
|
|
|
char c, hexcode;// "Q"-encoding implementation described in RFC 2047
|
|
for ( int i=start; i<end; i++ ) {
|
|
c = encoded8Bit[i];
|
|
if ( c == ' ' ) { // make the result readable with not MIME-capable readers
|
|
result += '_';
|
|
} else {
|
|
if ( ( ( c >= 'a' ) && ( c <= 'z' ) ) || // paranoid mode, encode *all* special chars to avoid problems
|
|
( ( c >= 'A' ) && ( c <= 'Z' ) ) || // with "From" & "To" headers
|
|
( ( c >= '0' ) && ( c <= '9' ) ) ) {
|
|
result += c;
|
|
} else {
|
|
result += '='; // "stolen" from KMail ;-)
|
|
hexcode = ( ( c & 0xF0 ) >> 4 ) + 48;
|
|
if ( hexcode >= 58 ) {
|
|
hexcode += 7;
|
|
}
|
|
result += hexcode;
|
|
hexcode = ( c & 0x0F ) + 48;
|
|
if ( hexcode >= 58 ) {
|
|
hexcode += 7;
|
|
}
|
|
result += hexcode;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
result += "?B?" + encoded8Bit.mid( start, end - start ).toBase64();
|
|
}
|
|
|
|
result +="?=";
|
|
result += encoded8Bit.right( encoded8Bit.length() - end );
|
|
} else {
|
|
result = encoded8Bit;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
QByteArray encodeRFC2047Sentence(const QString& src, const QByteArray& charset )
|
|
{
|
|
QByteArray result;
|
|
QList<QChar> splitChars;
|
|
splitChars << QLatin1Char( ',' ) << QLatin1Char( '\"' ) << QLatin1Char( ';' ) << QLatin1Char( '\\' );
|
|
const QChar *ch = src.constData();
|
|
const int length = src.length();
|
|
int pos = 0;
|
|
int wordStart = 0;
|
|
|
|
//qDebug() << "Input:" << src;
|
|
// Loop over all characters of the string.
|
|
// When encountering a split character, RFC-2047-encode the word before it, and add it to the result.
|
|
while ( pos < length ) {
|
|
//qDebug() << "Pos:" << pos << "Result:" << result << "Char:" << ch->toLatin1();
|
|
const bool isAscii = ch->unicode() < 127;
|
|
const bool isReserved = ( strchr( reservedCharacters, ch->toLatin1() ) != 0 );
|
|
if ( isAscii && isReserved ) {
|
|
const int wordSize = pos - wordStart;
|
|
if ( wordSize > 0 ) {
|
|
const QString word = src.mid( wordStart, wordSize );
|
|
result += encodeRFC2047String( word, charset );
|
|
}
|
|
|
|
result += ch->toLatin1();
|
|
wordStart = pos + 1;
|
|
}
|
|
ch++;
|
|
pos++;
|
|
}
|
|
|
|
// Encode the last word
|
|
const int wordSize = pos - wordStart;
|
|
if ( wordSize > 0 ) {
|
|
const QString word = src.mid( wordStart, pos - wordStart );
|
|
result += encodeRFC2047String( word, charset );
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
QByteArray encodeRFC2231String( const QString& str, const QByteArray& charset )
|
|
{
|
|
if ( str.isEmpty() ) {
|
|
return QByteArray();
|
|
}
|
|
|
|
const QTextCodec *codec = KGlobal::charsets()->codecForName( QString::fromLatin1( charset ) );
|
|
QByteArray latin;
|
|
if ( charset == "us-ascii" ) {
|
|
latin = str.toLatin1();
|
|
} else if ( codec ) {
|
|
latin = codec->fromUnicode( str );
|
|
} else {
|
|
latin = str.toLocal8Bit();
|
|
}
|
|
|
|
char *l;
|
|
for ( l = latin.data(); *l; ++l ) {
|
|
if ( ( ( *l & 0xE0 ) == 0 ) || ( *l & 0x80 ) ) {
|
|
// *l is control character or 8-bit char
|
|
break;
|
|
}
|
|
}
|
|
if ( !*l ) {
|
|
return latin;
|
|
}
|
|
|
|
QByteArray result = charset + "''";
|
|
for ( l = latin.data(); *l; ++l ) {
|
|
bool needsQuoting = ( *l & 0x80 ) || ( *l == '%' );
|
|
if ( !needsQuoting ) {
|
|
const QByteArray especials = "()<>@,;:\"/[]?.= \033";
|
|
int len = especials.length();
|
|
for ( int i = 0; i < len; i++ ) {
|
|
if ( *l == especials[i] ) {
|
|
needsQuoting = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ( needsQuoting ) {
|
|
result += '%';
|
|
unsigned char hexcode;
|
|
hexcode = ( ( *l & 0xF0 ) >> 4 ) + 48;
|
|
if ( hexcode >= 58 ) {
|
|
hexcode += 7;
|
|
}
|
|
result += hexcode;
|
|
hexcode = ( *l & 0x0F ) + 48;
|
|
if ( hexcode >= 58 ) {
|
|
hexcode += 7;
|
|
}
|
|
result += hexcode;
|
|
} else {
|
|
result += *l;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
QString decodeRFC2231String( const QByteArray &str, QByteArray &usedCS, const QByteArray &defaultCS,
|
|
bool forceCS )
|
|
{
|
|
int p = str.indexOf( '\'' );
|
|
if ( p < 0 ) {
|
|
return KGlobal::charsets()->codecForName( QString::fromLatin1( defaultCS ) )->toUnicode( str );
|
|
}
|
|
|
|
|
|
QByteArray charset = str.left( p );
|
|
|
|
QByteArray st = str.mid( str.lastIndexOf( '\'' ) + 1 );
|
|
|
|
char ch, ch2;
|
|
p = 0;
|
|
while ( p < (int)st.length() ) {
|
|
if ( st.at( p ) == 37 ) {
|
|
// Only try to decode the percent-encoded character if the percent sign
|
|
// is really followed by two other characters, see testcase at bug 163024
|
|
if ( p + 2 < st.length() ) {
|
|
ch = st.at( p + 1 ) - 48;
|
|
if ( ch > 16 ) {
|
|
ch -= 7;
|
|
}
|
|
ch2 = st.at( p + 2 ) - 48;
|
|
if ( ch2 > 16 ) {
|
|
ch2 -= 7;
|
|
}
|
|
st[p] = ch * 16 + ch2;
|
|
st.remove( p + 1, 2 );
|
|
}
|
|
}
|
|
p++;
|
|
}
|
|
kDebug() << "Got pre-decoded:" << st;
|
|
QString result;
|
|
const QTextCodec * charsetcodec = KGlobal::charsets()->codecForName( QString::fromLatin1( charset ) );
|
|
if ( !charsetcodec || forceCS ) {
|
|
charsetcodec = KGlobal::charsets()->codecForName( QString::fromLatin1( defaultCS ) );
|
|
}
|
|
|
|
usedCS = charsetcodec->name();
|
|
return charsetcodec->toUnicode( st );
|
|
}
|
|
|
|
QString decodeRFC2231String( const QByteArray &src )
|
|
{
|
|
QByteArray usedCS;
|
|
return decodeRFC2231String( src, usedCS, "utf-8", false );
|
|
}
|
|
|
|
QByteArray uniqueString()
|
|
{
|
|
static char chars[] = "0123456789abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
time_t now;
|
|
char p[11];
|
|
int pos, ran;
|
|
unsigned int timeval;
|
|
|
|
p[10] = '\0';
|
|
now = time( 0 );
|
|
ran = 1 + (int)( 1000.0 * rand() / ( RAND_MAX + 1.0 ) );
|
|
timeval = ( now / ran ) + getpid();
|
|
|
|
for ( int i = 0; i < 10; i++ ) {
|
|
pos = (int) ( 61.0 * rand() / ( RAND_MAX + 1.0 ) );
|
|
//kDebug() << pos;
|
|
p[i] = chars[pos];
|
|
}
|
|
|
|
QByteArray ret;
|
|
ret.setNum( timeval );
|
|
ret += '.';
|
|
ret += p;
|
|
|
|
return ret;
|
|
}
|
|
|
|
QByteArray multiPartBoundary()
|
|
{
|
|
return "nextPart" + uniqueString();
|
|
}
|
|
|
|
QByteArray unfoldHeader( const QByteArray &header )
|
|
{
|
|
QByteArray result;
|
|
if ( header.isEmpty() ) {
|
|
return result;
|
|
}
|
|
|
|
int pos = 0, foldBegin = 0, foldMid = 0, foldEnd = 0;
|
|
while ( ( foldMid = header.indexOf( '\n', pos ) ) >= 0 ) {
|
|
foldBegin = foldEnd = foldMid;
|
|
// find the first space before the line-break
|
|
while ( foldBegin > 0 ) {
|
|
if ( !QChar::fromLatin1( header[foldBegin - 1] ).isSpace() ) {
|
|
break;
|
|
}
|
|
--foldBegin;
|
|
}
|
|
// find the first non-space after the line-break
|
|
while ( foldEnd <= header.length() - 1 ) {
|
|
if ( QChar::fromLatin1( header[foldEnd] ).isSpace() ) {
|
|
++foldEnd;
|
|
} else if ( foldEnd > 0 && header[foldEnd - 1] == '\n' &&
|
|
header[foldEnd] == '=' && foldEnd + 2 < header.length() &&
|
|
( ( header[foldEnd + 1] == '0' &&
|
|
header[foldEnd + 2] == '9' ) ||
|
|
( header[foldEnd + 1] == '2' &&
|
|
header[foldEnd + 2] == '0' ) ) ) {
|
|
// bug #86302: malformed header continuation starting with =09/=20
|
|
foldEnd += 3;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
result += header.mid( pos, foldBegin - pos );
|
|
if ( foldEnd < header.length() - 1 ) {
|
|
result += ' ';
|
|
}
|
|
pos = foldEnd;
|
|
}
|
|
const int len = header.length();
|
|
if ( len > pos ) {
|
|
result += header.mid( pos, len - pos );
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int findHeaderLineEnd( const QByteArray &src, int &dataBegin, bool *folded )
|
|
{
|
|
int end = dataBegin;
|
|
int len = src.length() - 1;
|
|
|
|
if ( folded ) {
|
|
*folded = false;
|
|
}
|
|
|
|
if ( dataBegin < 0 ) {
|
|
// Not found
|
|
return -1;
|
|
}
|
|
|
|
if ( dataBegin > len ) {
|
|
// No data available
|
|
return len + 1;
|
|
}
|
|
|
|
// If the first line contains nothing, but the next line starts with a space
|
|
// or a tab, that means a stupid mail client has made the first header field line
|
|
// entirely empty, and has folded the rest to the next line(s).
|
|
if ( src.at( end ) == '\n' && end + 1 < len &&
|
|
( src[end + 1] == ' ' || src[end + 1] == '\t' ) ) {
|
|
|
|
// Skip \n and first whitespace
|
|
dataBegin += 2;
|
|
end += 2;
|
|
}
|
|
|
|
if ( src.at( end ) != '\n' ) { // check if the header is not empty
|
|
while ( true ) {
|
|
end = src.indexOf( '\n', end + 1 );
|
|
if ( end == -1 || end == len ) {
|
|
// end of string
|
|
break;
|
|
} else if ( src[end + 1] == ' ' || src[end + 1] == '\t' ||
|
|
( src[end + 1] == '=' && end + 3 <= len &&
|
|
( ( src[end + 2] == '0' && src[end + 3] == '9' ) ||
|
|
( src[end + 2] == '2' && src[end + 3] == '0' ) ) ) ) {
|
|
// next line is header continuation or starts with =09/=20 (bug #86302)
|
|
if ( folded ) {
|
|
*folded = true;
|
|
}
|
|
} else {
|
|
// end of header (no header continuation)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( end < 0 ) {
|
|
end = len + 1; //take the rest of the string
|
|
}
|
|
return end;
|
|
}
|
|
|
|
int indexOfHeader( const QByteArray &src, const QByteArray &name, int &end, int &dataBegin, bool *folded )
|
|
{
|
|
QByteArray n = name;
|
|
n.append( ':' );
|
|
int begin = -1;
|
|
|
|
if ( qstrnicmp( n.constData(), src.constData(), n.length() ) == 0 ) {
|
|
begin = 0;
|
|
} else {
|
|
n.prepend( '\n' );
|
|
const char *p = strcasestr( src.constData(), n.constData() );
|
|
if ( !p ) {
|
|
begin = -1;
|
|
} else {
|
|
begin = p - src.constData();
|
|
++begin;
|
|
}
|
|
}
|
|
|
|
if ( begin > -1 ) { //there is a header with the given name
|
|
dataBegin = begin + name.length() + 1; //skip the name
|
|
// skip the usual space after the colon
|
|
if ( src.at( dataBegin ) == ' ' ) {
|
|
++dataBegin;
|
|
}
|
|
end = findHeaderLineEnd( src, dataBegin, folded );
|
|
return begin;
|
|
|
|
} else {
|
|
end = -1;
|
|
dataBegin = -1;
|
|
return -1; //header not found
|
|
}
|
|
}
|
|
|
|
QByteArray extractHeader( const QByteArray &src, const QByteArray &name )
|
|
{
|
|
int begin, end;
|
|
bool folded;
|
|
QByteArray result;
|
|
|
|
if ( src.isEmpty() || indexOfHeader( src, name, end, begin, &folded ) < 0 ) {
|
|
return result;
|
|
}
|
|
|
|
if ( begin >= 0 ) {
|
|
if ( !folded ) {
|
|
result = src.mid( begin, end - begin );
|
|
} else {
|
|
if ( end > begin ) {
|
|
QByteArray hdrValue = src.mid( begin, end - begin );
|
|
result = unfoldHeader( hdrValue );
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QList<QByteArray> extractHeaders( const QByteArray &src, const QByteArray &name )
|
|
{
|
|
int begin, end;
|
|
bool folded;
|
|
QList<QByteArray> result;
|
|
QByteArray copySrc( src );
|
|
|
|
if ( indexOfHeader( copySrc, name, end, begin, &folded ) < 0 ) {
|
|
return result;
|
|
}
|
|
|
|
while ( begin >= 0 ) {
|
|
if ( !folded ) {
|
|
result.append( copySrc.mid( begin, end - begin ) );
|
|
} else {
|
|
QByteArray hdrValue = copySrc.mid( begin, end - begin );
|
|
result.append( unfoldHeader( hdrValue ) );
|
|
}
|
|
|
|
// get the next one, a tiny bit ugly, but we don't want the previous to be found again...
|
|
copySrc = copySrc.mid( end );
|
|
if ( indexOfHeader( copySrc, name, end, begin, &folded ) < 0 ) {
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void removeHeader( QByteArray &header, const QByteArray &name )
|
|
{
|
|
int begin, end, dummy;
|
|
begin = indexOfHeader( header, name, end, dummy );
|
|
if ( begin >= 0 ) {
|
|
header.remove( begin, end - begin + 1 );
|
|
}
|
|
}
|
|
|
|
QByteArray CRLFtoLF( const QByteArray &s )
|
|
{
|
|
QByteArray ret = s;
|
|
ret.replace( "\r\n", "\n" );
|
|
return ret;
|
|
}
|
|
|
|
QByteArray CRLFtoLF( const char *s )
|
|
{
|
|
QByteArray ret = s;
|
|
return CRLFtoLF( ret );
|
|
}
|
|
|
|
QByteArray LFtoCRLF( const QByteArray &s )
|
|
{
|
|
QByteArray ret = s;
|
|
ret.replace( '\n', "\r\n" );
|
|
return ret;
|
|
}
|
|
|
|
QByteArray LFtoCRLF( const char *s )
|
|
{
|
|
QByteArray ret = s;
|
|
return LFtoCRLF( ret );
|
|
}
|
|
|
|
namespace {
|
|
template < typename StringType, typename CharType > void removeQuotesGeneric( StringType & str )
|
|
{
|
|
bool inQuote = false;
|
|
for ( int i = 0; i < str.length(); ++i ) {
|
|
if ( str[i] == CharType( '"' ) ) {
|
|
str.remove( i, 1 );
|
|
i--;
|
|
inQuote = !inQuote;
|
|
} else {
|
|
if ( inQuote && ( str[i] == CharType( '\\' ) ) ) {
|
|
str.remove( i, 1 );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void removeQuots( QByteArray &str )
|
|
{
|
|
removeQuotesGeneric<QByteArray, char>( str );
|
|
}
|
|
|
|
void removeQuots( QString &str )
|
|
{
|
|
removeQuotesGeneric<QString, QLatin1Char>( str );
|
|
}
|
|
|
|
template<class StringType,class CharType,class CharConverterType,class StringConverterType,class ToString>
|
|
void addQuotes_impl( StringType &str, bool forceQuotes )
|
|
{
|
|
bool needsQuotes=false;
|
|
for ( int i=0; i < str.length(); i++ ) {
|
|
const CharType cur = str.at( i );
|
|
if ( QString( ToString( str ) ).contains( QRegExp( QLatin1String( "\"|\\\\|=|\\]|\\[|:|;|,|\\.|,|@|<|>|\\)|\\(" ) ) ) ) {
|
|
needsQuotes = true;
|
|
}
|
|
if ( cur == CharConverterType( '\\' ) || cur == CharConverterType( '\"' ) ) {
|
|
str.insert( i, CharConverterType( '\\' ) );
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if ( needsQuotes || forceQuotes ) {
|
|
str.insert( 0, CharConverterType( '\"' ) );
|
|
str.append( StringConverterType( "\"" ) );
|
|
}
|
|
}
|
|
|
|
void addQuotes( QByteArray &str, bool forceQuotes )
|
|
{
|
|
addQuotes_impl<QByteArray, char, char, char*, QLatin1String>( str, forceQuotes );
|
|
}
|
|
|
|
void addQuotes( QString &str, bool forceQuotes )
|
|
{
|
|
addQuotes_impl<QString, QChar, QLatin1Char, QLatin1String, QString>( str, forceQuotes );
|
|
}
|
|
|
|
KMIME_EXPORT QString balanceBidiState( const QString &input )
|
|
{
|
|
const int LRO = 0x202D;
|
|
const int RLO = 0x202E;
|
|
const int LRE = 0x202A;
|
|
const int RLE = 0x202B;
|
|
const int PDF = 0x202C;
|
|
|
|
QString result = input;
|
|
|
|
int openDirChangers = 0;
|
|
int numPDFsRemoved = 0;
|
|
for ( int i = 0; i < input.length(); i++ ) {
|
|
const ushort &code = input.at( i ).unicode();
|
|
if ( code == LRO || code == RLO || code == LRE || code == RLE ) {
|
|
openDirChangers++;
|
|
} else if ( code == PDF ) {
|
|
if ( openDirChangers > 0 ) {
|
|
openDirChangers--;
|
|
} else {
|
|
// One PDF too much, remove it
|
|
kWarning() << "Possible Unicode spoofing (unexpected PDF) detected in" << input;
|
|
result.remove( i - numPDFsRemoved, 1 );
|
|
numPDFsRemoved++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( openDirChangers > 0 ) {
|
|
kWarning() << "Possible Unicode spoofing detected in" << input;
|
|
|
|
// At PDF chars to the end until the correct state is restored.
|
|
// As a special exception, when encountering quoted strings, place the PDF before
|
|
// the last quote.
|
|
for ( int i = openDirChangers; i > 0; i-- ) {
|
|
if ( result.endsWith( QLatin1Char( '"' ) ) ) {
|
|
result.insert( result.length() - 1, QChar( PDF ) );
|
|
} else {
|
|
result += QChar( PDF );
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
QString removeBidiControlChars( const QString &input )
|
|
{
|
|
const int LRO = 0x202D;
|
|
const int RLO = 0x202E;
|
|
const int LRE = 0x202A;
|
|
const int RLE = 0x202B;
|
|
QString result = input;
|
|
result.remove( LRO );
|
|
result.remove( RLO );
|
|
result.remove( LRE );
|
|
result.remove( RLE );
|
|
return result;
|
|
}
|
|
|
|
static bool isCryptoPart( Content* content )
|
|
{
|
|
if ( !content->contentType( false ) ) {
|
|
return false;
|
|
}
|
|
|
|
if ( content->contentType()->subType().toLower() == "octet-stream" &&
|
|
!content->contentDisposition( false ) ) {
|
|
return false;
|
|
}
|
|
|
|
const Headers::ContentType *contentType = content->contentType();
|
|
const QByteArray lowerSubType = contentType->subType().toLower();
|
|
return ( contentType->mediaType().toLower() == "application" &&
|
|
( lowerSubType == "pgp-encrypted" ||
|
|
lowerSubType == "pgp-signature" ||
|
|
lowerSubType == "pkcs7-mime" ||
|
|
lowerSubType == "pkcs7-signature" ||
|
|
lowerSubType == "x-pkcs7-signature" ||
|
|
( lowerSubType == "octet-stream" &&
|
|
content->contentDisposition()->filename().toLower() == QLatin1String( "msg.asc" ) ) ) );
|
|
}
|
|
|
|
bool hasAttachment( Content* content )
|
|
{
|
|
if ( !content ) {
|
|
return false;
|
|
}
|
|
|
|
bool emptyFilename = true;
|
|
if ( content->contentDisposition( false ) &&
|
|
!content->contentDisposition()->filename().isEmpty() ) {
|
|
emptyFilename = false;
|
|
}
|
|
|
|
if ( emptyFilename &&
|
|
content->contentType( false ) &&
|
|
!content->contentType()->name().isEmpty() ) {
|
|
emptyFilename = false;
|
|
}
|
|
|
|
// ignore crypto parts
|
|
if ( !emptyFilename && !isCryptoPart( content ) ) {
|
|
return true;
|
|
}
|
|
|
|
// Ok, content itself is not an attachment. now we deal with multiparts
|
|
if ( content->contentType()->isMultipart() ) {
|
|
Q_FOREACH ( Content *child, content->contents() ) {
|
|
if ( hasAttachment( child ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isSigned( Message *message )
|
|
{
|
|
if ( !message ) {
|
|
return false;
|
|
}
|
|
|
|
const KMime::Headers::ContentType* const contentType = message->contentType();
|
|
if ( contentType->isSubtype( "signed" ) ||
|
|
contentType->isSubtype( "pgp-signature" ) ||
|
|
contentType->isSubtype( "pkcs7-signature" ) ||
|
|
contentType->isSubtype( "x-pkcs7-signature" ) ||
|
|
message->mainBodyPart( "multipart/signed" ) ||
|
|
message->mainBodyPart( "application/pgp-signature" ) ||
|
|
message->mainBodyPart( "application/pkcs7-signature" ) ||
|
|
message->mainBodyPart( "application/x-pkcs7-signature" ) ) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isEncrypted( Message *message )
|
|
{
|
|
if ( !message ) {
|
|
return false;
|
|
}
|
|
|
|
const KMime::Headers::ContentType* const contentType = message->contentType();
|
|
if ( contentType->isSubtype( "encrypted" ) ||
|
|
contentType->isSubtype( "pgp-encrypted" ) ||
|
|
contentType->isSubtype( "pkcs7-mime" ) ||
|
|
message->mainBodyPart( "multipart/encrypted" ) ||
|
|
message->mainBodyPart( "application/pgp-encrypted" ) ||
|
|
message->mainBodyPart( "application/pkcs7-mime" ) ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool isInvitation( Content *content )
|
|
{
|
|
if ( !content ) {
|
|
return false;
|
|
}
|
|
|
|
const KMime::Headers::ContentType* const contentType = content->contentType( false );
|
|
|
|
if ( contentType && contentType->isMediatype( "text" ) && contentType->isSubtype( "calendar" ) ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
} // namespace KMime
|