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.
1160 lines
33 KiB
1160 lines
33 KiB
/* Copyright 2009 Thomas McGuire <mcguire@kde.org> |
|
|
|
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) version 3 or any later version |
|
accepted by the membership of KDE e.V. (or its successor approved |
|
by the membership of KDE e.V.), which shall act as a proxy |
|
defined in Section 14 of version 3 of the license. |
|
|
|
This program 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 General Public License for more details. |
|
|
|
You should have received a copy of the GNU General Public License |
|
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
*/ |
|
#include "stringutil.h" |
|
|
|
#ifndef KMAIL_UNITTESTS |
|
|
|
#include "kmaddrbook.h" |
|
#include "kmkernel.h" |
|
|
|
#include <libkdepim/kaddrbookexternal.h> |
|
#include <mimelib/enum.h> |
|
|
|
#include <kmime/kmime_charfreq.h> |
|
#include <kmime/kmime_header_parsing.h> |
|
#include <KPIMUtils/Email> |
|
#include <KPIMIdentities/IdentityManager> |
|
|
|
#include <kascii.h> |
|
#include <KConfigGroup> |
|
#include <KDebug> |
|
#include <KUrl> |
|
#include <kuser.h> |
|
|
|
#include <QHostInfo> |
|
#include <QRegExp> |
|
#endif |
|
#include <QStringList> |
|
|
|
#ifndef KMAIL_UNITTESTS |
|
|
|
using namespace KMime; |
|
using namespace KMime::Types; |
|
using namespace KMime::HeaderParsing; |
|
|
|
#endif |
|
|
|
namespace KMail |
|
{ |
|
|
|
namespace StringUtil |
|
{ |
|
|
|
// Removes trailing spaces and tabs at the end of the line |
|
static void removeTrailingSpace( QString &line ) |
|
{ |
|
int i = line.length() - 1 ; |
|
while( ( i >= 0 ) && ( ( line[i] == ' ' ) || ( line[i] == '\t' ) ) ) |
|
i--; |
|
line.truncate( i + 1 ); |
|
} |
|
|
|
// Spilts the line off in two parts: The quote prefixes and the actual text of the line. |
|
// For example, for the string "> > > Hello", it would be split up in "> > > " as the quote |
|
// prefix, and "Hello" as the actual text. |
|
// The actual text is written back to the "line" parameter, and the quote prefix is returned. |
|
static QString splitLine( QString &line) |
|
{ |
|
removeTrailingSpace( line ); |
|
int i = 0; |
|
int startOfActualText = -1; |
|
|
|
// TODO: Replace tabs with spaces first. |
|
|
|
// Loop through the chars in the line to find the place where the quote prefix stops |
|
while( i < line.length() ) |
|
{ |
|
const QChar c = line[i]; |
|
const bool isAllowedQuoteChar = (c == '>') || (c == ':') || (c == '|') || |
|
(c == ' ') || (c == '\t'); |
|
if ( isAllowedQuoteChar ) |
|
startOfActualText = i + 1; |
|
else |
|
break; |
|
i++; |
|
} |
|
|
|
// If the quote prefix only consists of whitespace, don't consider it as a quote prefix at all |
|
if ( line.left( startOfActualText ).trimmed().isEmpty() ) |
|
startOfActualText = 0; |
|
|
|
// No quote prefix there -> nothing to do |
|
if ( startOfActualText <= 0 ) |
|
{ |
|
return QString(); |
|
} |
|
|
|
// Entire line consists of only the quote prefix |
|
if ( i == line.length() ) |
|
{ |
|
const QString quotePrefix = line.left( startOfActualText ); |
|
line.clear(); |
|
return quotePrefix; |
|
} |
|
|
|
// Line contains both the quote prefix and the actual text, really split it up now |
|
const QString quotePrefix = line.left( startOfActualText ); |
|
line = line.mid( startOfActualText ); |
|
return quotePrefix; |
|
} |
|
|
|
// Changes the given text so that each line of it fits into the given maximal length. |
|
// At each line, the "indent" string is prepended, which is usually the quote prefix. |
|
// The text parameter will be empty afterwards. |
|
// Example: |
|
// text = "Hello World, this is a test." |
|
// indent = "> " |
|
// maxLength = 16 |
|
// Result: "> Hello World,\n" |
|
// "> this is a test." |
|
static QString flowText( QString &text, const QString& indent, int maxLength ) |
|
{ |
|
maxLength--; |
|
if ( text.isEmpty() ) { |
|
return indent + "\n"; |
|
} |
|
|
|
QString result; |
|
while ( !text.isEmpty() ) |
|
{ |
|
// Find the next point in the text where we have to do a line break. Start searching |
|
// at maxLength position and then walk backwards looking for a space |
|
int breakPosition; |
|
if ( text.length() > maxLength ) |
|
{ |
|
breakPosition = maxLength; |
|
while( ( breakPosition >= 0 ) && ( text[breakPosition] != ' ' ) ) |
|
breakPosition--; |
|
if ( breakPosition <= 0 ) { |
|
// Couldn't break before maxLength. |
|
breakPosition = maxLength; |
|
} |
|
} |
|
else { |
|
breakPosition = text.length(); |
|
} |
|
|
|
QString line = text.left( breakPosition ); |
|
if ( breakPosition < text.length() ) |
|
text = text.mid( breakPosition ); |
|
else |
|
text.clear(); |
|
|
|
// Strip leading whitespace of new lines, since that looks strange |
|
if ( !result.isEmpty() && line.startsWith( ' ' ) ) |
|
line = line.mid( 1 ); |
|
|
|
result += indent + line + '\n'; |
|
} |
|
|
|
return result; |
|
} |
|
|
|
// Writes all lines/text parts contained in the "textParts" list to the output text, "msg". |
|
// Quote characters are added in front of each line, and no line is longer than |
|
// maxLength. |
|
// |
|
// Although the lines in textParts are considered seperate lines, they can actually be run |
|
// together into a single line in some cases. This is basically the main difference to flowText(). |
|
// |
|
// Example: |
|
// textParts = "Hello World, this is a test.", "Really" |
|
// indent = ">" |
|
// maxLength = 20 |
|
// Result: "> Hello World, this\n |
|
// > is a test. Really" |
|
// Notice how in this example, the text line "Really" is no longer a seperate line, it was run |
|
// together with a previously broken line. |
|
// |
|
// "textParts" is cleared upon return. |
|
static bool flushPart( QString &msg, QStringList &textParts, |
|
const QString &indent, int maxLength ) |
|
{ |
|
maxLength -= indent.length(); |
|
if ( maxLength < 20 ) |
|
maxLength = 20; |
|
|
|
// Remove empty lines at end of quote |
|
while ( !textParts.isEmpty() && textParts.last().isEmpty() ) { |
|
textParts.removeLast(); |
|
} |
|
|
|
QString text; |
|
foreach( const QString line, textParts ) { |
|
|
|
// An empty line in the input means that an empty line should be in the output as well. |
|
// Therefore, we write all of our text so far to the msg. |
|
if ( line.isEmpty() ) { |
|
if ( !text.isEmpty() ) |
|
msg += flowText( text, indent, maxLength ); |
|
msg += indent + '\n'; |
|
} |
|
|
|
else { |
|
if ( text.isEmpty() ) |
|
text = line; |
|
else |
|
text += ' ' + line.trimmed(); |
|
|
|
// If the line doesn't need to be wrapped at all, just write it out as-is. |
|
// When a line exceeds the maximum length and therefore needs to be broken, this statement |
|
// if false, and therefore we keep adding lines to our text, so they get ran together in the |
|
// next flowText call, as "text" contains several text parts/lines then. |
|
if ( ( text.length() < maxLength ) || ( line.length() < ( maxLength - 10 ) ) ) |
|
msg += flowText( text, indent, maxLength ); |
|
} |
|
} |
|
|
|
// Write out pending text to the msg |
|
if ( !text.isEmpty() ) |
|
msg += flowText( text, indent, maxLength ); |
|
|
|
const bool appendEmptyLine = !textParts.isEmpty(); |
|
textParts.clear(); |
|
return appendEmptyLine; |
|
} |
|
|
|
QString stripSignature ( const QString & msg, bool clearSigned ) |
|
{ |
|
// Following RFC 3676, only > before -- |
|
// I prefer to not delete a SB instead of delete good mail content. |
|
const QRegExp sbDelimiterSearch = clearSigned ? |
|
QRegExp( "(^|\n)[> ]*--\\s?\n" ) : QRegExp( "(^|\n)[> ]*-- \n" ); |
|
// The regular expression to look for prefix change |
|
const QRegExp commonReplySearch = QRegExp( "^[ ]*>" ); |
|
|
|
QString res = msg; |
|
int posDeletingStart = 1; // to start looking at 0 |
|
|
|
// While there are SB delimiters (start looking just before the deleted SB) |
|
while ( ( posDeletingStart = res.indexOf( sbDelimiterSearch , posDeletingStart -1 ) ) >= 0 ) |
|
{ |
|
QString prefix; // the current prefix |
|
QString line; // the line to check if is part of the SB |
|
int posNewLine = -1; |
|
int posSignatureBlock = -1; |
|
// Look for the SB beginning |
|
posSignatureBlock = res.indexOf( '-', posDeletingStart ); |
|
// The prefix before "-- "$ |
|
if ( res[posDeletingStart] == '\n' ) ++posDeletingStart; |
|
prefix = res.mid( posDeletingStart, posSignatureBlock - posDeletingStart ); |
|
posNewLine = res.indexOf( '\n', posSignatureBlock ) + 1; |
|
|
|
// now go to the end of the SB |
|
while ( posNewLine < res.size() && posNewLine > 0 ) |
|
{ |
|
// handle the undefined case for mid ( x , -n ) where n>1 |
|
int nextPosNewLine = res.indexOf( '\n', posNewLine ); |
|
if ( nextPosNewLine < 0 ) nextPosNewLine = posNewLine - 1; |
|
line = res.mid( posNewLine, nextPosNewLine - posNewLine ); |
|
|
|
// check when the SB ends: |
|
// * does not starts with prefix or |
|
// * starts with prefix+(any substring of prefix) |
|
if ( ( prefix.isEmpty() && line.indexOf( commonReplySearch ) < 0 ) || |
|
( !prefix.isEmpty() && line.startsWith( prefix ) && |
|
line.mid( prefix.size() ).indexOf( commonReplySearch ) < 0 ) ) |
|
{ |
|
posNewLine = res.indexOf( '\n', posNewLine ) + 1; |
|
} |
|
else |
|
break; // end of the SB |
|
} |
|
// remove the SB or truncate when is the last SB |
|
if ( posNewLine > 0 ) |
|
res.remove( posDeletingStart, posNewLine - posDeletingStart ); |
|
else |
|
res.truncate( posDeletingStart ); |
|
} |
|
return res; |
|
} |
|
|
|
#ifndef KMAIL_UNITTESTS |
|
//----------------------------------------------------------------------------- |
|
QList<int> determineAllowedCtes( const CharFreq& cf, |
|
bool allow8Bit, |
|
bool willBeSigned ) |
|
{ |
|
QList<int> allowedCtes; |
|
|
|
switch ( cf.type() ) { |
|
case CharFreq::SevenBitText: |
|
allowedCtes << DwMime::kCte7bit; |
|
case CharFreq::EightBitText: |
|
if ( allow8Bit ) |
|
allowedCtes << DwMime::kCte8bit; |
|
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. |
|
allowedCtes << DwMime::kCteQp; |
|
allowedCtes << DwMime::kCteBase64; |
|
} else { |
|
allowedCtes << DwMime::kCteBase64; |
|
allowedCtes << DwMime::kCteQp; |
|
} |
|
break; |
|
case CharFreq::EightBitData: |
|
allowedCtes << DwMime::kCteBase64; |
|
break; |
|
case CharFreq::None: |
|
default: |
|
// just nothing (avoid compiler warning) |
|
; |
|
} |
|
|
|
// In the following cases only QP and Base64 are allowed: |
|
// - the buffer will be OpenPGP/MIME signed and it contains trailing |
|
// whitespace (cf. RFC 3156) |
|
// - a line starts with "From " |
|
if ( ( willBeSigned && cf.hasTrailingWhitespace() ) || |
|
cf.hasLeadingFrom() ) { |
|
allowedCtes.removeAll( DwMime::kCte8bit ); |
|
allowedCtes.removeAll( DwMime::kCte7bit ); |
|
} |
|
|
|
return allowedCtes; |
|
} |
|
|
|
AddressList splitAddrField( const QByteArray & str ) |
|
{ |
|
AddressList result; |
|
const char * scursor = str.begin(); |
|
if ( !scursor ) |
|
return AddressList(); |
|
const char * const send = str.begin() + str.length(); |
|
if ( !parseAddressList( scursor, send, result ) ) { |
|
kDebug() << "Error in address splitting: parseAddressList returned false!"; |
|
} |
|
return result; |
|
} |
|
|
|
QString generateMessageId( const QString& addr ) |
|
{ |
|
QDateTime datetime = QDateTime::currentDateTime(); |
|
QString msgIdStr; |
|
|
|
msgIdStr = '<' + datetime.toString( "yyyyMMddhhmm.sszzz" ); |
|
|
|
QString msgIdSuffix; |
|
KConfigGroup general( KMKernel::config(), "General" ); |
|
|
|
if( general.readEntry( "useCustomMessageIdSuffix", false ) ) |
|
msgIdSuffix = general.readEntry( "myMessageIdSuffix" ); |
|
|
|
if( !msgIdSuffix.isEmpty() ) |
|
msgIdStr += '@' + msgIdSuffix; |
|
else |
|
msgIdStr += '.' + KPIMUtils::toIdn( addr ); |
|
|
|
msgIdStr += '>'; |
|
|
|
return msgIdStr; |
|
} |
|
#endif |
|
|
|
QByteArray html2source( const QByteArray & src ) |
|
{ |
|
QByteArray result( 1 + 6*src.length(), '\0' ); // maximal possible length |
|
|
|
QByteArray::ConstIterator s = src.begin(); |
|
QByteArray::Iterator d = result.begin(); |
|
while ( *s ) { |
|
switch ( *s ) { |
|
case '<': { |
|
*d++ = '&'; |
|
*d++ = 'l'; |
|
*d++ = 't'; |
|
*d++ = ';'; |
|
++s; |
|
} |
|
break; |
|
case '\r': { |
|
++s; |
|
} |
|
break; |
|
case '\n': { |
|
*d++ = '<'; |
|
*d++ = 'b'; |
|
*d++ = 'r'; |
|
*d++ = '>'; |
|
++s; |
|
} |
|
break; |
|
case '>': { |
|
*d++ = '&'; |
|
*d++ = 'g'; |
|
*d++ = 't'; |
|
*d++ = ';'; |
|
++s; |
|
} |
|
break; |
|
case '&': { |
|
*d++ = '&'; |
|
*d++ = 'a'; |
|
*d++ = 'm'; |
|
*d++ = 'p'; |
|
*d++ = ';'; |
|
++s; |
|
} |
|
break; |
|
case '"': { |
|
*d++ = '&'; |
|
*d++ = 'q'; |
|
*d++ = 'u'; |
|
*d++ = 'o'; |
|
*d++ = 't'; |
|
*d++ = ';'; |
|
++s; |
|
} |
|
break; |
|
case '\'': { |
|
*d++ = '&'; |
|
*d++ = 'a'; |
|
*d++ = 'p'; |
|
*d++ = 's'; |
|
*d++ = ';'; |
|
++s; |
|
} |
|
break; |
|
default: |
|
*d++ = *s++; |
|
} |
|
} |
|
result.truncate( d - result.begin() ); |
|
return result; |
|
} |
|
|
|
#ifndef KMAIL_UNITTESTS |
|
QString encodeMailtoUrl( const QString& str ) |
|
{ |
|
QString result; |
|
result = QString::fromLatin1( KMMsgBase::encodeRFC2047String( str, |
|
"utf-8" ) ); |
|
result = KUrl::toPercentEncoding( result ); |
|
return result; |
|
} |
|
|
|
QString decodeMailtoUrl( const QString& url ) |
|
{ |
|
QString result; |
|
result = KUrl::fromPercentEncoding( url.toLatin1() ); |
|
result = KMMsgBase::decodeRFC2047String( result.toLatin1() ); |
|
return result; |
|
} |
|
#endif |
|
|
|
QByteArray stripEmailAddr( const QByteArray& aStr ) |
|
{ |
|
//kDebug() << "(" << aStr << ")"; |
|
|
|
if ( aStr.isEmpty() ) |
|
return QByteArray(); |
|
|
|
QByteArray result; |
|
|
|
// The following is a primitive parser for a mailbox-list (cf. RFC 2822). |
|
// The purpose is to extract a displayable string from the mailboxes. |
|
// Comments in the addr-spec are not handled. No error checking is done. |
|
|
|
QByteArray name; |
|
QByteArray comment; |
|
QByteArray angleAddress; |
|
enum { TopLevel, InComment, InAngleAddress } context = TopLevel; |
|
bool inQuotedString = false; |
|
int commentLevel = 0; |
|
|
|
for ( const char* p = aStr.data(); *p; ++p ) { |
|
switch ( context ) { |
|
case TopLevel : { |
|
switch ( *p ) { |
|
case '"' : inQuotedString = !inQuotedString; |
|
break; |
|
case '(' : if ( !inQuotedString ) { |
|
context = InComment; |
|
commentLevel = 1; |
|
} |
|
else |
|
name += *p; |
|
break; |
|
case '<' : if ( !inQuotedString ) { |
|
context = InAngleAddress; |
|
} |
|
else |
|
name += *p; |
|
break; |
|
case '\\' : // quoted character |
|
++p; // skip the '\' |
|
if ( *p ) |
|
name += *p; |
|
break; |
|
case ',' : if ( !inQuotedString ) { |
|
// next email address |
|
if ( !result.isEmpty() ) |
|
result += ", "; |
|
name = name.trimmed(); |
|
comment = comment.trimmed(); |
|
angleAddress = angleAddress.trimmed(); |
|
/* |
|
kDebug() << "Name : \"" << name |
|
<< "\""; |
|
kDebug() << "Comment : \"" << comment |
|
<< "\""; |
|
kDebug() << "Address : \"" << angleAddress |
|
<< "\""; |
|
*/ |
|
if ( angleAddress.isEmpty() && !comment.isEmpty() ) { |
|
// handle Outlook-style addresses like |
|
// john.doe@invalid (John Doe) |
|
result += comment; |
|
} |
|
else if ( !name.isEmpty() ) { |
|
result += name; |
|
} |
|
else if ( !comment.isEmpty() ) { |
|
result += comment; |
|
} |
|
else if ( !angleAddress.isEmpty() ) { |
|
result += angleAddress; |
|
} |
|
name = QByteArray(); |
|
comment = QByteArray(); |
|
angleAddress = QByteArray(); |
|
} |
|
else |
|
name += *p; |
|
break; |
|
default : name += *p; |
|
} |
|
break; |
|
} |
|
case InComment : { |
|
switch ( *p ) { |
|
case '(' : ++commentLevel; |
|
comment += *p; |
|
break; |
|
case ')' : --commentLevel; |
|
if ( commentLevel == 0 ) { |
|
context = TopLevel; |
|
comment += ' '; // separate the text of several comments |
|
} |
|
else |
|
comment += *p; |
|
break; |
|
case '\\' : // quoted character |
|
++p; // skip the '\' |
|
if ( *p ) |
|
comment += *p; |
|
break; |
|
default : comment += *p; |
|
} |
|
break; |
|
} |
|
case InAngleAddress : { |
|
switch ( *p ) { |
|
case '"' : inQuotedString = !inQuotedString; |
|
angleAddress += *p; |
|
break; |
|
case '>' : if ( !inQuotedString ) { |
|
context = TopLevel; |
|
} |
|
else |
|
angleAddress += *p; |
|
break; |
|
case '\\' : // quoted character |
|
++p; // skip the '\' |
|
if ( *p ) |
|
angleAddress += *p; |
|
break; |
|
default : angleAddress += *p; |
|
} |
|
break; |
|
} |
|
} // switch ( context ) |
|
} |
|
if ( !result.isEmpty() ) |
|
result += ", "; |
|
name = name.trimmed(); |
|
comment = comment.trimmed(); |
|
angleAddress = angleAddress.trimmed(); |
|
/* |
|
kDebug() << "Name : \"" << name <<"\""; |
|
kDebug() << "Comment : \"" << comment <<"\""; |
|
kDebug() << "Address : \"" << angleAddress <<"\""; |
|
*/ |
|
if ( angleAddress.isEmpty() && !comment.isEmpty() ) { |
|
// handle Outlook-style addresses like |
|
// john.doe@invalid (John Doe) |
|
result += comment; |
|
} |
|
else if ( !name.isEmpty() ) { |
|
result += name; |
|
} |
|
else if ( !comment.isEmpty() ) { |
|
result += comment; |
|
} |
|
else if ( !angleAddress.isEmpty() ) { |
|
result += angleAddress; |
|
} |
|
|
|
//kDebug() << "Returns \"" << result << "\""; |
|
return result; |
|
} |
|
|
|
QString stripEmailAddr( const QString& aStr ) |
|
{ |
|
//kDebug() << "(" << aStr << ")"; |
|
|
|
if ( aStr.isEmpty() ) |
|
return QString(); |
|
|
|
QString result; |
|
|
|
// The following is a primitive parser for a mailbox-list (cf. RFC 2822). |
|
// The purpose is to extract a displayable string from the mailboxes. |
|
// Comments in the addr-spec are not handled. No error checking is done. |
|
|
|
QString name; |
|
QString comment; |
|
QString angleAddress; |
|
enum { TopLevel, InComment, InAngleAddress } context = TopLevel; |
|
bool inQuotedString = false; |
|
int commentLevel = 0; |
|
|
|
QChar ch; |
|
int strLength(aStr.length()); |
|
for ( int index = 0; index < strLength; ++index ) { |
|
ch = aStr[index]; |
|
switch ( context ) { |
|
case TopLevel : { |
|
switch ( ch.toLatin1() ) { |
|
case '"' : inQuotedString = !inQuotedString; |
|
break; |
|
case '(' : if ( !inQuotedString ) { |
|
context = InComment; |
|
commentLevel = 1; |
|
} |
|
else |
|
name += ch; |
|
break; |
|
case '<' : if ( !inQuotedString ) { |
|
context = InAngleAddress; |
|
} |
|
else |
|
name += ch; |
|
break; |
|
case '\\' : // quoted character |
|
++index; // skip the '\' |
|
if ( index < aStr.length() ) |
|
name += aStr[index]; |
|
break; |
|
case ',' : if ( !inQuotedString ) { |
|
// next email address |
|
if ( !result.isEmpty() ) |
|
result += ", "; |
|
name = name.trimmed(); |
|
comment = comment.trimmed(); |
|
angleAddress = angleAddress.trimmed(); |
|
/* |
|
kDebug() << "Name : \"" << name |
|
<< "\""; |
|
kDebug() << "Comment : \"" << comment |
|
<< "\""; |
|
kDebug() << "Address : \"" << angleAddress |
|
<< "\""; |
|
*/ |
|
if ( angleAddress.isEmpty() && !comment.isEmpty() ) { |
|
// handle Outlook-style addresses like |
|
// john.doe@invalid (John Doe) |
|
result += comment; |
|
} |
|
else if ( !name.isEmpty() ) { |
|
result += name; |
|
} |
|
else if ( !comment.isEmpty() ) { |
|
result += comment; |
|
} |
|
else if ( !angleAddress.isEmpty() ) { |
|
result += angleAddress; |
|
} |
|
name.clear(); |
|
comment.clear(); |
|
angleAddress.clear(); |
|
} |
|
else |
|
name += ch; |
|
break; |
|
default : name += ch; |
|
} |
|
break; |
|
} |
|
case InComment : { |
|
switch ( ch.toLatin1() ) { |
|
case '(' : ++commentLevel; |
|
comment += ch; |
|
break; |
|
case ')' : --commentLevel; |
|
if ( commentLevel == 0 ) { |
|
context = TopLevel; |
|
comment += ' '; // separate the text of several comments |
|
} |
|
else |
|
comment += ch; |
|
break; |
|
case '\\' : // quoted character |
|
++index; // skip the '\' |
|
if ( index < aStr.length() ) |
|
comment += aStr[index]; |
|
break; |
|
default : comment += ch; |
|
} |
|
break; |
|
} |
|
case InAngleAddress : { |
|
switch ( ch.toLatin1() ) { |
|
case '"' : inQuotedString = !inQuotedString; |
|
angleAddress += ch; |
|
break; |
|
case '>' : if ( !inQuotedString ) { |
|
context = TopLevel; |
|
} |
|
else |
|
angleAddress += ch; |
|
break; |
|
case '\\' : // quoted character |
|
++index; // skip the '\' |
|
if ( index < aStr.length() ) |
|
angleAddress += aStr[index]; |
|
break; |
|
default : angleAddress += ch; |
|
} |
|
break; |
|
} |
|
} // switch ( context ) |
|
} |
|
if ( !result.isEmpty() ) |
|
result += ", "; |
|
name = name.trimmed(); |
|
comment = comment.trimmed(); |
|
angleAddress = angleAddress.trimmed(); |
|
/* |
|
kDebug() << "Name : \"" << name <<"\""; |
|
kDebug() << "Comment : \"" << comment <<"\""; |
|
kDebug() << "Address : \"" << angleAddress <<"\""; |
|
*/ |
|
if ( angleAddress.isEmpty() && !comment.isEmpty() ) { |
|
// handle Outlook-style addresses like |
|
// john.doe@invalid (John Doe) |
|
result += comment; |
|
} |
|
else if ( !name.isEmpty() ) { |
|
result += name; |
|
} |
|
else if ( !comment.isEmpty() ) { |
|
result += comment; |
|
} |
|
else if ( !angleAddress.isEmpty() ) { |
|
result += angleAddress; |
|
} |
|
|
|
//kDebug() << "Returns \"" << result << "\""; |
|
return result; |
|
} |
|
|
|
QString quoteHtmlChars( const QString& str, bool removeLineBreaks ) |
|
{ |
|
QString result; |
|
|
|
unsigned int strLength(str.length()); |
|
result.reserve( 6*strLength ); // maximal possible length |
|
for( unsigned int i = 0; i < strLength; ++i ) { |
|
switch ( str[i].toLatin1() ) { |
|
case '<': |
|
result += "<"; |
|
break; |
|
case '>': |
|
result += ">"; |
|
break; |
|
case '&': |
|
result += "&"; |
|
break; |
|
case '"': |
|
result += """; |
|
break; |
|
case '\n': |
|
if ( !removeLineBreaks ) |
|
result += "<br>"; |
|
break; |
|
case '\r': |
|
// ignore CR |
|
break; |
|
default: |
|
result += str[i]; |
|
} |
|
} |
|
|
|
result.squeeze(); |
|
return result; |
|
} |
|
|
|
#ifndef KMAIL_UNITTESTS |
|
QString emailAddrAsAnchor( const QString& aEmail, Display display, const QString& cssStyle, |
|
Link link, AddressMode expandable, const QString& fieldName ) |
|
{ |
|
if( aEmail.isEmpty() ) |
|
return aEmail; |
|
|
|
const QStringList addressList = KPIMUtils::splitAddressList( aEmail ); |
|
|
|
QString result; |
|
int numberAddresses = 0; |
|
bool expandableInserted = false; |
|
|
|
for( QStringList::ConstIterator it = addressList.constBegin(); |
|
( it != addressList.constEnd() ); |
|
++it ) { |
|
if( !(*it).isEmpty() ) { |
|
numberAddresses++; |
|
|
|
QString address = *it; |
|
if( expandable == ExpandableAddresses && |
|
!expandableInserted && numberAddresses > GlobalSettings::self()->numberOfAddressesToShow() ) { |
|
Q_ASSERT( !fieldName.isEmpty() ); |
|
result = "<span id=\"icon" + fieldName + "\"></span>" + result; |
|
result += "<span id=\"dots" + fieldName + "\">...</span><span id=\"hidden" + fieldName +"\">"; |
|
expandableInserted = true; |
|
} |
|
if( link == ShowLink ) { |
|
result += "<a href=\"mailto:" |
|
+ encodeMailtoUrl( address ) |
|
+ "\" "+cssStyle+">"; |
|
} |
|
if( display == DisplayNameOnly ) |
|
address = stripEmailAddr( address ); |
|
result += quoteHtmlChars( address, true ); |
|
if( link == ShowLink ) { |
|
result += "</a>, "; |
|
} |
|
} |
|
} |
|
// cut of the trailing ", " |
|
if( link == ShowLink ) { |
|
result.truncate( result.length() - 2 ); |
|
} |
|
if( expandableInserted ) { |
|
result += "</span>"; |
|
} |
|
|
|
//kDebug() << "('" << aEmail << "') returns:\n-->" << result << "<--"; |
|
return result; |
|
} |
|
|
|
QStringList stripAddressFromAddressList( const QString& address, |
|
const QStringList& list ) |
|
{ |
|
QStringList addresses( list ); |
|
QString addrSpec( KPIMUtils::extractEmailAddress( address ) ); |
|
for ( QStringList::Iterator it = addresses.begin(); |
|
it != addresses.end(); ) { |
|
if ( kasciistricmp( addrSpec.toUtf8().data(), |
|
KPIMUtils::extractEmailAddress( *it ).toUtf8().data() ) == 0 ) { |
|
kDebug() << "Removing" << *it << "from the address list"; |
|
it = addresses.erase( it ); |
|
} |
|
else |
|
++it; |
|
} |
|
return addresses; |
|
} |
|
|
|
QStringList stripMyAddressesFromAddressList( const QStringList& list ) |
|
{ |
|
QStringList addresses = list; |
|
for( QStringList::Iterator it = addresses.begin(); |
|
it != addresses.end(); ) { |
|
kDebug() << "Check whether" << *it <<"is one of my addresses"; |
|
if( kmkernel->identityManager()->thatIsMe( KPIMUtils::extractEmailAddress( *it ) ) ) { |
|
kDebug() << "Removing" << *it <<"from the address list"; |
|
it = addresses.erase( it ); |
|
} |
|
else |
|
++it; |
|
} |
|
return addresses; |
|
} |
|
|
|
bool addressIsInAddressList( const QString& address, |
|
const QStringList& addresses ) |
|
{ |
|
QString addrSpec = KPIMUtils::extractEmailAddress( address ); |
|
for( QStringList::ConstIterator it = addresses.begin(); |
|
it != addresses.end(); ++it ) { |
|
if ( kasciistricmp( addrSpec.toUtf8().data(), |
|
KPIMUtils::extractEmailAddress( *it ).toUtf8().data() ) == 0 ) |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
QString expandAliases( const QString& recipients, QStringList &distributionListEmpty ) |
|
{ |
|
if ( recipients.isEmpty() ) |
|
return QString(); |
|
|
|
QStringList recipientList = KPIMUtils::splitAddressList( recipients ); |
|
QString expandedRecipients; |
|
for ( QStringList::Iterator it = recipientList.begin(); |
|
it != recipientList.end(); ++it ) { |
|
if ( !expandedRecipients.isEmpty() ) |
|
expandedRecipients += ", "; |
|
QString receiver = (*it).trimmed(); |
|
|
|
// try to expand distribution list |
|
bool distributionListIsEmpty = false; |
|
QString expandedList = KPIM::KAddrBookExternal::expandDistributionList( receiver, distributionListIsEmpty ); |
|
if ( distributionListIsEmpty ) { |
|
expandedRecipients += receiver; |
|
distributionListEmpty << receiver; |
|
continue; |
|
} |
|
|
|
if ( !expandedList.isEmpty()) { |
|
expandedRecipients += expandedList; |
|
continue; |
|
} |
|
|
|
// try to expand nick name |
|
QString expandedNickName = KabcBridge::expandNickName( receiver ); |
|
if ( !expandedNickName.isEmpty() ) { |
|
expandedRecipients += expandedNickName; |
|
continue; |
|
} |
|
|
|
// check whether the address is missing the domain part |
|
QByteArray displayName, addrSpec, comment; |
|
KPIMUtils::splitAddress( receiver.toLatin1(), displayName, addrSpec, comment ); |
|
if ( !addrSpec.contains('@') ) { |
|
KConfigGroup general( KMKernel::config(), "General" ); |
|
QString defaultdomain = general.readEntry( "Default domain" ); |
|
if ( !defaultdomain.isEmpty() ) { |
|
expandedRecipients += KPIMUtils::normalizedAddress( displayName, addrSpec + '@' + defaultdomain, comment ); |
|
} |
|
else { |
|
expandedRecipients += guessEmailAddressFromLoginName( addrSpec ); |
|
} |
|
} |
|
else |
|
expandedRecipients += receiver; |
|
} |
|
|
|
return expandedRecipients; |
|
} |
|
|
|
QString guessEmailAddressFromLoginName( const QString& loginName ) |
|
{ |
|
if ( loginName.isEmpty() ) |
|
return QString(); |
|
|
|
QString address = loginName; |
|
address += '@'; |
|
address += QHostInfo::localHostName(); |
|
|
|
// try to determine the real name |
|
const KUser user( loginName ); |
|
if ( user.isValid() ) { |
|
QString fullName = user.property( KUser::FullName ).toString(); |
|
if ( fullName.contains( QRegExp( "[^ 0-9A-Za-z\\x0080-\\xFFFF]" ) ) ) |
|
address = '"' + fullName.replace( '\\', "\\" ).replace( '"', "\\" ) |
|
+ "\" <" + address + '>'; |
|
else |
|
address = fullName + " <" + address + '>'; |
|
} |
|
|
|
return address; |
|
} |
|
#endif |
|
|
|
QString smartQuote( const QString &msg, int maxLineLength ) |
|
{ |
|
// The algorithm here is as follows: |
|
// We split up the incomming msg into lines, and then iterate over each line. |
|
// We keep adding lines with the same indent ( = quote prefix, e.g. "> " ) to a |
|
// "textParts" list. So the textParts list contains only lines with the same quote |
|
// prefix. |
|
// |
|
// When all lines with the same indent are collected in "textParts", we write those out |
|
// to the result by calling flushPart(), which does all the nice formatting for us. |
|
|
|
QStringList textParts; |
|
QString oldIndent; |
|
bool firstPart = true; |
|
QString result; |
|
foreach ( QString line, msg.split( '\n' ) ) { |
|
|
|
// Split off the indent from the line |
|
const QString indent = splitLine( line ); |
|
|
|
if ( line.isEmpty() ) { |
|
if ( !firstPart ) |
|
textParts.append( QString() ); |
|
continue; |
|
}; |
|
|
|
if ( firstPart ) { |
|
oldIndent = indent; |
|
firstPart = false; |
|
} |
|
|
|
// The indent changed, that means we have to write everything contained in textParts to the |
|
// result, which we do by calling flushPart(). |
|
if ( oldIndent != indent ) { |
|
|
|
// Check if the last non-blank line is a "From" line. A from line is the line containing the |
|
// attribution to a quote, e.g. "Yesterday, you wrote:". We'll just check for the last colon |
|
// here, to simply things. |
|
// If there is a From line, remove it from the textParts to that flushPart won't break it. |
|
// We'll manually add it to the result afterwards. |
|
QString fromLine; |
|
if ( !textParts.isEmpty() ) { |
|
for ( int i = textParts.count() - 1; i >= 0; i-- ) { |
|
|
|
// Check if we have found the From line |
|
if ( textParts[i].endsWith( ':' ) ) { |
|
fromLine = oldIndent + textParts[i] + '\n'; |
|
textParts.removeAt( i ); |
|
break; |
|
} |
|
|
|
// Abort on first non-empty line |
|
if ( !textParts[i].trimmed().isEmpty() ) |
|
break; |
|
} |
|
} |
|
|
|
// Write out all lines with the same indent using flushPart(). The textParts list |
|
// is cleared for us. |
|
if ( flushPart( result, textParts, oldIndent, maxLineLength ) ) { |
|
if ( oldIndent.length() > indent.length() ) |
|
result += indent + '\n'; |
|
else |
|
result += oldIndent + '\n'; |
|
} |
|
|
|
if ( !fromLine.isEmpty() ) { |
|
result += fromLine; |
|
} |
|
|
|
oldIndent = indent; |
|
} |
|
|
|
textParts.append( line ); |
|
} |
|
|
|
// Write out anything still pending |
|
flushPart( result, textParts, oldIndent, maxLineLength ); |
|
|
|
// Remove superflous newline which was appended in flowText |
|
if ( !result.isEmpty() && result.endsWith( '\n' ) ) |
|
result.chop( 1 ); |
|
|
|
return result; |
|
} |
|
|
|
QString formatString( const QString &wildString, const QString &fromAddr ) |
|
{ |
|
QString result; |
|
|
|
if ( wildString.isEmpty() ) { |
|
return wildString; |
|
} |
|
|
|
unsigned int strLength( wildString.length() ); |
|
for ( uint i=0; i<strLength; ) { |
|
QChar ch = wildString[i++]; |
|
if ( ch == '%' && i<strLength ) { |
|
ch = wildString[i++]; |
|
switch ( ch.toLatin1() ) { |
|
case 'f': // sender's initals |
|
{ |
|
QString str = stripEmailAddr( fromAddr ); |
|
|
|
uint j = 0; |
|
for ( ; str[j]>' '; j++ ) |
|
; |
|
unsigned int strLength( str.length() ); |
|
for ( ; j < strLength && str[j] <= ' '; j++ ) |
|
; |
|
result += str[0]; |
|
if ( str[j] > ' ' ) { |
|
result += str[j]; |
|
} else { |
|
if ( str[1] > ' ' ) { |
|
result += str[1]; |
|
} |
|
} |
|
} |
|
break; |
|
case '_': |
|
result += ' '; |
|
break; |
|
case '%': |
|
result += '%'; |
|
break; |
|
default: |
|
result += '%'; |
|
result += ch; |
|
break; |
|
} |
|
} else { |
|
result += ch; |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
#ifndef KMAIL_UNITTESTS |
|
|
|
QMap<QString, QString> parseMailtoUrl ( const KUrl& url ) |
|
{ |
|
kDebug() << url.pathOrUrl(); |
|
QMap<QString, QString> values = url.queryItems( KUrl::CaseInsensitiveKeys ); |
|
QString to = decodeMailtoUrl( url.path() ); |
|
to = to.isEmpty() ? values.value( "to" ) : to + QString( ", " ) + values.value( "to" ); |
|
values.insert( "to", to ); |
|
return values; |
|
} |
|
|
|
#endif |
|
|
|
bool isCryptoPart( const QString &type, const QString &subType, const QString &fileName ) |
|
{ |
|
return ( type.toLower() == "application" && |
|
( subType.toLower() == "pgp-encrypted" || |
|
subType.toLower() == "pgp-signature" || |
|
subType.toLower() == "pkcs7-mime" || |
|
subType.toLower() == "pkcs7-signature" || |
|
subType.toLower() == "x-pkcs7-signature" || |
|
( subType.toLower() == "octet-stream" && |
|
fileName.toLower() == "msg.asc" ) ) ); |
|
} |
|
|
|
} |
|
|
|
}
|
|
|