Build with non-standard boost locations.
[kdepim.git] / messagecore / stringutil.cpp
blobe54ebfa665ca9fa686255f911fdb2fd16277cbce
1 /* Copyright 2009 Thomas McGuire <mcguire@kde.org>
3 This program is free software; you can redistribute it and/or
4 modify it under the terms of the GNU General Public License as
5 published by the Free Software Foundation; either version 2 of
6 the License or (at your option) version 3 or any later version
7 accepted by the membership of KDE e.V. (or its successor approved
8 by the membership of KDE e.V.), which shall act as a proxy
9 defined in Section 14 of version 3 of the license.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #include "stringutil.h"
21 #include <kmime/kmime_charfreq.h>
22 #include <kmime/kmime_header_parsing.h>
23 #include <kmime/kmime_util.h>
24 #include <kmime/kmime_headers.h>
25 #include <kmime/kmime_message.h>
26 #include <KPIMUtils/Email>
27 #include <KPIMIdentities/IdentityManager>
29 #include <kascii.h>
30 #include <KConfigGroup>
31 #include <KDebug>
32 #include <KUser>
33 #include <KUrl>
34 #include <KDebug>
36 #include <QHostInfo>
37 #include <QRegExp>
38 #include <QStringList>
40 using namespace KMime;
41 using namespace KMime::Types;
42 using namespace KMime::HeaderParsing;
44 namespace MessageCore
46 namespace StringUtil
49 // Removes trailing spaces and tabs at the end of the line
50 static void removeTrailingSpace( QString &line )
52 int i = line.length() - 1 ;
53 while( ( i >= 0 ) && ( ( line[i] == ' ' ) || ( line[i] == '\t' ) ) )
54 i--;
55 line.truncate( i + 1 );
58 // Spilts the line off in two parts: The quote prefixes and the actual text of the line.
59 // For example, for the string "> > > Hello", it would be split up in "> > > " as the quote
60 // prefix, and "Hello" as the actual text.
61 // The actual text is written back to the "line" parameter, and the quote prefix is returned.
62 static QString splitLine( QString &line)
64 removeTrailingSpace( line );
65 int i = 0;
66 int startOfActualText = -1;
68 // TODO: Replace tabs with spaces first.
70 // Loop through the chars in the line to find the place where the quote prefix stops
71 while( i < line.length() )
73 const QChar c = line[i];
74 const bool isAllowedQuoteChar = (c == '>') || (c == ':') || (c == '|') ||
75 (c == ' ') || (c == '\t');
76 if ( isAllowedQuoteChar )
77 startOfActualText = i + 1;
78 else
79 break;
80 i++;
83 // If the quote prefix only consists of whitespace, don't consider it as a quote prefix at all
84 if ( line.left( startOfActualText ).trimmed().isEmpty() )
85 startOfActualText = 0;
87 // No quote prefix there -> nothing to do
88 if ( startOfActualText <= 0 )
90 return QString();
93 // Entire line consists of only the quote prefix
94 if ( i == line.length() )
96 const QString quotePrefix = line.left( startOfActualText );
97 line.clear();
98 return quotePrefix;
101 // Line contains both the quote prefix and the actual text, really split it up now
102 const QString quotePrefix = line.left( startOfActualText );
103 line = line.mid( startOfActualText );
104 return quotePrefix;
107 // Changes the given text so that each line of it fits into the given maximal length.
108 // At each line, the "indent" string is prepended, which is usually the quote prefix.
109 // The text parameter will be empty afterwards.
110 // Example:
111 // text = "Hello World, this is a test."
112 // indent = "> "
113 // maxLength = 16
114 // Result: "> Hello World,\n"
115 // "> this is a test."
116 static QString flowText( QString &text, const QString& indent, int maxLength )
118 maxLength--;
119 if ( text.isEmpty() ) {
120 return indent + "\n";
123 QString result;
124 while ( !text.isEmpty() )
126 // Find the next point in the text where we have to do a line break. Start searching
127 // at maxLength position and then walk backwards looking for a space
128 int breakPosition;
129 if ( text.length() > maxLength )
131 breakPosition = maxLength;
132 while( ( breakPosition >= 0 ) && ( text[breakPosition] != ' ' ) )
133 breakPosition--;
134 if ( breakPosition <= 0 ) {
135 // Couldn't break before maxLength.
136 breakPosition = maxLength;
139 else {
140 breakPosition = text.length();
143 QString line = text.left( breakPosition );
144 if ( breakPosition < text.length() )
145 text = text.mid( breakPosition );
146 else
147 text.clear();
149 // Strip leading whitespace of new lines, since that looks strange
150 if ( !result.isEmpty() && line.startsWith( ' ' ) )
151 line = line.mid( 1 );
153 result += indent + line + '\n';
156 return result;
159 // Writes all lines/text parts contained in the "textParts" list to the output text, "msg".
160 // Quote characters are added in front of each line, and no line is longer than
161 // maxLength.
163 // Although the lines in textParts are considered separate lines, they can actually be run
164 // together into a single line in some cases. This is basically the main difference to flowText().
166 // Example:
167 // textParts = "Hello World, this is a test.", "Really"
168 // indent = ">"
169 // maxLength = 20
170 // Result: "> Hello World, this\n
171 // > is a test. Really"
172 // Notice how in this example, the text line "Really" is no longer a separate line, it was run
173 // together with a previously broken line.
175 // "textParts" is cleared upon return.
176 static bool flushPart( QString &msg, QStringList &textParts,
177 const QString &indent, int maxLength )
179 maxLength -= indent.length();
180 if ( maxLength < 20 )
181 maxLength = 20;
183 // Remove empty lines at end of quote
184 while ( !textParts.isEmpty() && textParts.last().isEmpty() ) {
185 textParts.removeLast();
188 QString text;
189 foreach( const QString line, textParts ) {
191 // An empty line in the input means that an empty line should be in the output as well.
192 // Therefore, we write all of our text so far to the msg.
193 if ( line.isEmpty() ) {
194 if ( !text.isEmpty() )
195 msg += flowText( text, indent, maxLength );
196 msg += indent + '\n';
199 else {
200 if ( text.isEmpty() )
201 text = line;
202 else
203 text += ' ' + line.trimmed();
205 // If the line doesn't need to be wrapped at all, just write it out as-is.
206 // When a line exceeds the maximum length and therefore needs to be broken, this statement
207 // if false, and therefore we keep adding lines to our text, so they get ran together in the
208 // next flowText call, as "text" contains several text parts/lines then.
209 if ( ( text.length() < maxLength ) || ( line.length() < ( maxLength - 10 ) ) )
210 msg += flowText( text, indent, maxLength );
214 // Write out pending text to the msg
215 if ( !text.isEmpty() )
216 msg += flowText( text, indent, maxLength );
218 const bool appendEmptyLine = !textParts.isEmpty();
219 textParts.clear();
220 return appendEmptyLine;
223 QStringList stripMyAddressesFromAddressList( const QStringList& list, const KPIMIdentities::IdentityManager* identMan )
225 QStringList addresses = list;
226 for( QStringList::Iterator it = addresses.begin();
227 it != addresses.end(); ) {
228 kDebug() << "Check whether" << *it <<"is one of my addresses";
229 if( identMan->thatIsMe( KPIMUtils::extractEmailAddress( *it ) ) ) {
230 kDebug() << "Removing" << *it <<"from the address list";
231 it = addresses.erase( it );
233 else
234 ++it;
236 return addresses;
239 QMap<QString, QString> parseMailtoUrl ( const KUrl& url )
241 kDebug() << url.pathOrUrl();
242 QMap<QString, QString> values = url.queryItems( KUrl::CaseInsensitiveKeys );
243 QString to = KPIMUtils::decodeMailtoUrl( url );
244 to = to.isEmpty() ? values.value( "to" ) : to + QString( ", " ) + values.value( "to" );
245 values.insert( "to", to );
246 return values;
249 QString stripSignature ( const QString & msg, bool clearSigned )
251 // Following RFC 3676, only > before --
252 // I prefer to not delete a SB instead of delete good mail content.
253 const QRegExp sbDelimiterSearch = clearSigned ?
254 QRegExp( "(^|\n)[> ]*--\\s?\n" ) : QRegExp( "(^|\n)[> ]*-- \n" );
255 // The regular expression to look for prefix change
256 const QRegExp commonReplySearch = QRegExp( "^[ ]*>" );
258 QString res = msg;
259 int posDeletingStart = 1; // to start looking at 0
261 // While there are SB delimiters (start looking just before the deleted SB)
262 while ( ( posDeletingStart = res.indexOf( sbDelimiterSearch , posDeletingStart -1 ) ) >= 0 )
264 QString prefix; // the current prefix
265 QString line; // the line to check if is part of the SB
266 int posNewLine = -1;
267 int posSignatureBlock = -1;
268 // Look for the SB beginning
269 posSignatureBlock = res.indexOf( '-', posDeletingStart );
270 // The prefix before "-- "$
271 if ( res[posDeletingStart] == '\n' ) ++posDeletingStart;
272 prefix = res.mid( posDeletingStart, posSignatureBlock - posDeletingStart );
273 posNewLine = res.indexOf( '\n', posSignatureBlock ) + 1;
275 // now go to the end of the SB
276 while ( posNewLine < res.size() && posNewLine > 0 )
278 // handle the undefined case for mid ( x , -n ) where n>1
279 int nextPosNewLine = res.indexOf( '\n', posNewLine );
280 if ( nextPosNewLine < 0 ) nextPosNewLine = posNewLine - 1;
281 line = res.mid( posNewLine, nextPosNewLine - posNewLine );
283 // check when the SB ends:
284 // * does not starts with prefix or
285 // * starts with prefix+(any substring of prefix)
286 if ( ( prefix.isEmpty() && line.indexOf( commonReplySearch ) < 0 ) ||
287 ( !prefix.isEmpty() && line.startsWith( prefix ) &&
288 line.mid( prefix.size() ).indexOf( commonReplySearch ) < 0 ) )
290 posNewLine = res.indexOf( '\n', posNewLine ) + 1;
292 else
293 break; // end of the SB
295 // remove the SB or truncate when is the last SB
296 if ( posNewLine > 0 )
297 res.remove( posDeletingStart, posNewLine - posDeletingStart );
298 else
299 res.truncate( posDeletingStart );
301 return res;
304 AddressList splitAddrField( const QByteArray & str )
306 AddressList result;
307 const char * scursor = str.begin();
308 if ( !scursor )
309 return AddressList();
310 const char * const send = str.begin() + str.length();
311 if ( !parseAddressList( scursor, send, result ) )
312 kDebug() << "Error in address splitting: parseAddressList returned false!";
313 return result;
316 QString generateMessageId( const QString& addr, const QString &msgIdSuffix )
318 const QDateTime datetime = QDateTime::currentDateTime();
320 QString msgIdStr = '<' + datetime.toString( "yyyyMMddhhmm.sszzz" );
322 if( !msgIdSuffix.isEmpty() )
323 msgIdStr += '@' + msgIdSuffix;
324 else
325 msgIdStr += '.' + KPIMUtils::toIdn( addr );
327 msgIdStr += '>';
329 return msgIdStr;
332 QByteArray html2source( const QByteArray & src )
334 QByteArray result( 1 + 6*src.length(), '\0' ); // maximal possible length
336 QByteArray::ConstIterator s = src.begin();
337 QByteArray::Iterator d = result.begin();
338 while ( *s ) {
339 switch ( *s ) {
340 case '<': {
341 *d++ = '&';
342 *d++ = 'l';
343 *d++ = 't';
344 *d++ = ';';
345 ++s;
347 break;
348 case '\r': {
349 ++s;
351 break;
352 case '\n': {
353 *d++ = '<';
354 *d++ = 'b';
355 *d++ = 'r';
356 *d++ = '>';
357 ++s;
359 break;
360 case '>': {
361 *d++ = '&';
362 *d++ = 'g';
363 *d++ = 't';
364 *d++ = ';';
365 ++s;
367 break;
368 case '&': {
369 *d++ = '&';
370 *d++ = 'a';
371 *d++ = 'm';
372 *d++ = 'p';
373 *d++ = ';';
374 ++s;
376 break;
377 case '"': {
378 *d++ = '&';
379 *d++ = 'q';
380 *d++ = 'u';
381 *d++ = 'o';
382 *d++ = 't';
383 *d++ = ';';
384 ++s;
386 break;
387 case '\'': {
388 *d++ = '&';
389 *d++ = 'a';
390 *d++ = 'p';
391 *d++ = 's';
392 *d++ = ';';
393 ++s;
395 break;
396 default:
397 *d++ = *s++;
400 result.truncate( d - result.begin() );
401 return result;
404 QByteArray stripEmailAddr( const QByteArray& aStr )
406 if ( aStr.isEmpty() )
407 return QByteArray();
409 QByteArray result;
411 // The following is a primitive parser for a mailbox-list (cf. RFC 2822).
412 // The purpose is to extract a displayable string from the mailboxes.
413 // Comments in the addr-spec are not handled. No error checking is done.
415 QByteArray name;
416 QByteArray comment;
417 QByteArray angleAddress;
418 enum { TopLevel, InComment, InAngleAddress } context = TopLevel;
419 bool inQuotedString = false;
420 int commentLevel = 0;
422 for ( const char* p = aStr.data(); *p; ++p ) {
423 switch ( context ) {
424 case TopLevel : {
425 switch ( *p ) {
426 case '"' : inQuotedString = !inQuotedString;
427 break;
428 case '(' : if ( !inQuotedString ) {
429 context = InComment;
430 commentLevel = 1;
432 else
433 name += *p;
434 break;
435 case '<' : if ( !inQuotedString ) {
436 context = InAngleAddress;
438 else
439 name += *p;
440 break;
441 case '\\' : // quoted character
442 ++p; // skip the '\'
443 if ( *p )
444 name += *p;
445 break;
446 case ',' : if ( !inQuotedString ) {
447 // next email address
448 if ( !result.isEmpty() )
449 result += ", ";
450 name = name.trimmed();
451 comment = comment.trimmed();
452 angleAddress = angleAddress.trimmed();
453 if ( angleAddress.isEmpty() && !comment.isEmpty() ) {
454 // handle Outlook-style addresses like
455 // john.doe@invalid (John Doe)
456 result += comment;
458 else if ( !name.isEmpty() ) {
459 result += name;
461 else if ( !comment.isEmpty() ) {
462 result += comment;
464 else if ( !angleAddress.isEmpty() ) {
465 result += angleAddress;
467 name = QByteArray();
468 comment = QByteArray();
469 angleAddress = QByteArray();
471 else
472 name += *p;
473 break;
474 default : name += *p;
476 break;
478 case InComment : {
479 switch ( *p ) {
480 case '(' : ++commentLevel;
481 comment += *p;
482 break;
483 case ')' : --commentLevel;
484 if ( commentLevel == 0 ) {
485 context = TopLevel;
486 comment += ' '; // separate the text of several comments
488 else
489 comment += *p;
490 break;
491 case '\\' : // quoted character
492 ++p; // skip the '\'
493 if ( *p )
494 comment += *p;
495 break;
496 default : comment += *p;
498 break;
500 case InAngleAddress : {
501 switch ( *p ) {
502 case '"' : inQuotedString = !inQuotedString;
503 angleAddress += *p;
504 break;
505 case '>' : if ( !inQuotedString ) {
506 context = TopLevel;
508 else
509 angleAddress += *p;
510 break;
511 case '\\' : // quoted character
512 ++p; // skip the '\'
513 if ( *p )
514 angleAddress += *p;
515 break;
516 default : angleAddress += *p;
518 break;
520 } // switch ( context )
522 if ( !result.isEmpty() )
523 result += ", ";
524 name = name.trimmed();
525 comment = comment.trimmed();
526 angleAddress = angleAddress.trimmed();
527 if ( angleAddress.isEmpty() && !comment.isEmpty() ) {
528 // handle Outlook-style addresses like
529 // john.doe@invalid (John Doe)
530 result += comment;
532 else if ( !name.isEmpty() ) {
533 result += name;
535 else if ( !comment.isEmpty() ) {
536 result += comment;
538 else if ( !angleAddress.isEmpty() ) {
539 result += angleAddress;
542 //kDebug() << "Returns \"" << result << "\"";
543 return result;
546 QString stripEmailAddr( const QString& aStr )
548 //kDebug() << "(" << aStr << ")";
550 if ( aStr.isEmpty() )
551 return QString();
553 QString result;
555 // The following is a primitive parser for a mailbox-list (cf. RFC 2822).
556 // The purpose is to extract a displayable string from the mailboxes.
557 // Comments in the addr-spec are not handled. No error checking is done.
559 QString name;
560 QString comment;
561 QString angleAddress;
562 enum { TopLevel, InComment, InAngleAddress } context = TopLevel;
563 bool inQuotedString = false;
564 int commentLevel = 0;
566 QChar ch;
567 int strLength(aStr.length());
568 for ( int index = 0; index < strLength; ++index ) {
569 ch = aStr[index];
570 switch ( context ) {
571 case TopLevel : {
572 switch ( ch.toLatin1() ) {
573 case '"' : inQuotedString = !inQuotedString;
574 break;
575 case '(' : if ( !inQuotedString ) {
576 context = InComment;
577 commentLevel = 1;
579 else
580 name += ch;
581 break;
582 case '<' : if ( !inQuotedString ) {
583 context = InAngleAddress;
585 else
586 name += ch;
587 break;
588 case '\\' : // quoted character
589 ++index; // skip the '\'
590 if ( index < aStr.length() )
591 name += aStr[index];
592 break;
593 case ',' : if ( !inQuotedString ) {
594 // next email address
595 if ( !result.isEmpty() )
596 result += ", ";
597 name = name.trimmed();
598 comment = comment.trimmed();
599 angleAddress = angleAddress.trimmed();
600 if ( angleAddress.isEmpty() && !comment.isEmpty() ) {
601 // handle Outlook-style addresses like
602 // john.doe@invalid (John Doe)
603 result += comment;
605 else if ( !name.isEmpty() ) {
606 result += name;
608 else if ( !comment.isEmpty() ) {
609 result += comment;
611 else if ( !angleAddress.isEmpty() ) {
612 result += angleAddress;
614 name.clear();
615 comment.clear();
616 angleAddress.clear();
618 else
619 name += ch;
620 break;
621 default : name += ch;
623 break;
625 case InComment : {
626 switch ( ch.toLatin1() ) {
627 case '(' : ++commentLevel;
628 comment += ch;
629 break;
630 case ')' : --commentLevel;
631 if ( commentLevel == 0 ) {
632 context = TopLevel;
633 comment += ' '; // separate the text of several comments
635 else
636 comment += ch;
637 break;
638 case '\\' : // quoted character
639 ++index; // skip the '\'
640 if ( index < aStr.length() )
641 comment += aStr[index];
642 break;
643 default : comment += ch;
645 break;
647 case InAngleAddress : {
648 switch ( ch.toLatin1() ) {
649 case '"' : inQuotedString = !inQuotedString;
650 angleAddress += ch;
651 break;
652 case '>' : if ( !inQuotedString ) {
653 context = TopLevel;
655 else
656 angleAddress += ch;
657 break;
658 case '\\' : // quoted character
659 ++index; // skip the '\'
660 if ( index < aStr.length() )
661 angleAddress += aStr[index];
662 break;
663 default : angleAddress += ch;
665 break;
667 } // switch ( context )
669 if ( !result.isEmpty() )
670 result += ", ";
671 name = name.trimmed();
672 comment = comment.trimmed();
673 angleAddress = angleAddress.trimmed();
674 if ( angleAddress.isEmpty() && !comment.isEmpty() ) {
675 // handle Outlook-style addresses like
676 // john.doe@invalid (John Doe)
677 result += comment;
679 else if ( !name.isEmpty() ) {
680 result += name;
682 else if ( !comment.isEmpty() ) {
683 result += comment;
685 else if ( !angleAddress.isEmpty() ) {
686 result += angleAddress;
689 //kDebug() << "Returns \"" << result << "\"";
690 return result;
693 QString quoteHtmlChars( const QString& str, bool removeLineBreaks )
695 QString result;
697 unsigned int strLength(str.length());
698 result.reserve( 6*strLength ); // maximal possible length
699 for( unsigned int i = 0; i < strLength; ++i ) {
700 switch ( str[i].toLatin1() ) {
701 case '<':
702 result += "&lt;";
703 break;
704 case '>':
705 result += "&gt;";
706 break;
707 case '&':
708 result += "&amp;";
709 break;
710 case '"':
711 result += "&quot;";
712 break;
713 case '\n':
714 if ( !removeLineBreaks )
715 result += "<br>";
716 break;
717 case '\r':
718 // ignore CR
719 break;
720 default:
721 result += str[i];
725 result.squeeze();
726 return result;
729 void removePrivateHeaderFields( const KMime::Message::Ptr &msg ) {
730 msg->removeHeader("Status");
731 msg->removeHeader("X-Status");
732 msg->removeHeader("X-KMail-EncryptionState");
733 msg->removeHeader("X-KMail-SignatureState");
734 msg->removeHeader("X-KMail-MDN-Sent");
735 msg->removeHeader("X-KMail-Transport");
736 msg->removeHeader("X-KMail-Identity");
737 msg->removeHeader("X-KMail-Fcc");
738 msg->removeHeader("X-KMail-Redirect-From");
739 msg->removeHeader("X-KMail-Link-Message");
740 msg->removeHeader("X-KMail-Link-Type");
741 msg->removeHeader("X-KMail-QuotePrefix");
742 msg->removeHeader("X-KMail-CursorPos");
743 msg->removeHeader( "X-KMail-Templates" );
744 msg->removeHeader( "X-KMail-Drafts" );
745 msg->removeHeader( "X-KMail-Tag" );
748 QByteArray asSendableString( const KMime::Message::Ptr &msg )
750 KMime::Message message;
751 message.setContent( msg->encodedContent() );
752 removePrivateHeaderFields( KMime::Message::Ptr( &message ) );
753 message.removeHeader("Bcc");
754 return message.encodedContent();
757 QByteArray headerAsSendableString( const KMime::Message::Ptr &msg )
759 KMime::Message message;
760 message.setContent( msg->encodedContent() );
761 removePrivateHeaderFields( KMime::Message::Ptr( &message ) );
762 message.removeHeader("Bcc");
763 return message.head();
767 QString emailAddrAsAnchor( const KMime::Types::Mailbox::List &mailboxList,
768 Display display, const QString& cssStyle,
769 Link link, AddressMode expandable, const QString& fieldName,
770 int collapseNumber )
772 QString result;
773 int numberAddresses = 0;
774 bool expandableInserted = false;
777 foreach( KMime::Types::Mailbox mailbox, mailboxList ) {
778 if( !mailbox.prettyAddress().isEmpty() ) {
779 numberAddresses++;
780 if( expandable == ExpandableAddresses && !expandableInserted && numberAddresses > collapseNumber ) {
781 result = "<span id=\"icon" + fieldName + "\"></span>" + result;
782 result += "<span id=\"dots" + fieldName + "\">...</span><span id=\"hidden" + fieldName +"\">";
783 expandableInserted = true;
786 if( link == ShowLink ) {
787 result += "<a href=\"mailto:"
788 + KUrl::toPercentEncoding( KPIMUtils::encodeMailtoUrl( mailbox.prettyAddress( KMime::Types::Mailbox::QuoteWhenNecessary ) ).path() )
789 + "\" "+cssStyle+">";
791 if ( display == DisplayNameOnly ) {
792 if ( !mailbox.name().isEmpty() ) // Fallback to the email address when the name is not set.
793 result += quoteHtmlChars( mailbox.name(), true );
794 else
795 result += quoteHtmlChars( mailbox.prettyAddress(), true );
796 } else {
797 result += quoteHtmlChars( mailbox.prettyAddress( KMime::Types::Mailbox::QuoteWhenNecessary ), true );
799 if( link == ShowLink ) {
800 result += "</a>, ";
805 // cut of the trailing ", "
806 if( link == ShowLink ) {
807 result.truncate( result.length() - 2 );
810 if( expandableInserted ) {
811 result += "</span>";
813 return result;
816 QString emailAddrAsAnchor( KMime::Headers::Generics::MailboxList *mailboxList,
817 Display display, const QString& cssStyle,
818 Link link, AddressMode expandable, const QString& fieldName,
819 int collapseNumber )
821 Q_ASSERT( mailboxList );
822 return emailAddrAsAnchor( mailboxList->mailboxes(), display, cssStyle, link, expandable, fieldName, collapseNumber );
825 QString emailAddrAsAnchor( KMime::Headers::Generics::AddressList *addressList,
826 Display display, const QString& cssStyle,
827 Link link, AddressMode expandable, const QString& fieldName,
828 int collapseNumber )
830 Q_ASSERT( addressList );
831 return emailAddrAsAnchor( addressList->mailboxes(), display, cssStyle, link, expandable, fieldName, collapseNumber );
834 QStringList stripAddressFromAddressList( const QString& address,
835 const QStringList& list )
837 QStringList addresses( list );
838 QString addrSpec( KPIMUtils::extractEmailAddress( address ) );
839 for ( QStringList::Iterator it = addresses.begin();
840 it != addresses.end(); ) {
841 if ( kasciistricmp( addrSpec.toUtf8().data(),
842 KPIMUtils::extractEmailAddress( *it ).toUtf8().data() ) == 0 ) {
843 kDebug() << "Removing" << *it << "from the address list";
844 it = addresses.erase( it );
846 else
847 ++it;
849 return addresses;
852 bool addressIsInAddressList( const QString& address,
853 const QStringList& addresses )
855 QString addrSpec = KPIMUtils::extractEmailAddress( address );
856 for( QStringList::ConstIterator it = addresses.begin();
857 it != addresses.end(); ++it ) {
858 if ( kasciistricmp( addrSpec.toUtf8().data(),
859 KPIMUtils::extractEmailAddress( *it ).toUtf8().data() ) == 0 )
860 return true;
862 return false;
865 QString guessEmailAddressFromLoginName( const QString& loginName )
867 if ( loginName.isEmpty() )
868 return QString();
870 QString address = loginName;
871 address += '@';
872 address += QHostInfo::localHostName();
874 // try to determine the real name
875 const KUser user( loginName );
876 if ( user.isValid() ) {
877 QString fullName = user.property( KUser::FullName ).toString();
878 if ( fullName.contains( QRegExp( "[^ 0-9A-Za-z\\x0080-\\xFFFF]" ) ) )
879 address = '"' + fullName.replace( '\\', "\\" ).replace( '"', "\\" )
880 + "\" <" + address + '>';
881 else
882 address = fullName + " <" + address + '>';
885 return address;
888 QString smartQuote( const QString &msg, int maxLineLength )
890 // The algorithm here is as follows:
891 // We split up the incoming msg into lines, and then iterate over each line.
892 // We keep adding lines with the same indent ( = quote prefix, e.g. "> " ) to a
893 // "textParts" list. So the textParts list contains only lines with the same quote
894 // prefix.
896 // When all lines with the same indent are collected in "textParts", we write those out
897 // to the result by calling flushPart(), which does all the nice formatting for us.
899 QStringList textParts;
900 QString oldIndent;
901 bool firstPart = true;
902 QString result;
903 foreach ( QString line, msg.split( '\n' ) ) {
905 // Split off the indent from the line
906 const QString indent = splitLine( line );
908 if ( line.isEmpty() ) {
909 if ( !firstPart )
910 textParts.append( QString() );
911 continue;
914 if ( firstPart ) {
915 oldIndent = indent;
916 firstPart = false;
919 // The indent changed, that means we have to write everything contained in textParts to the
920 // result, which we do by calling flushPart().
921 if ( oldIndent != indent ) {
923 // Check if the last non-blank line is a "From" line. A from line is the line containing the
924 // attribution to a quote, e.g. "Yesterday, you wrote:". We'll just check for the last colon
925 // here, to simply things.
926 // If there is a From line, remove it from the textParts to that flushPart won't break it.
927 // We'll manually add it to the result afterwards.
928 QString fromLine;
929 if ( !textParts.isEmpty() ) {
930 for ( int i = textParts.count() - 1; i >= 0; i-- ) {
932 // Check if we have found the From line
933 if ( textParts[i].endsWith( ':' ) ) {
934 fromLine = oldIndent + textParts[i] + '\n';
935 textParts.removeAt( i );
936 break;
939 // Abort on first non-empty line
940 if ( !textParts[i].trimmed().isEmpty() )
941 break;
945 // Write out all lines with the same indent using flushPart(). The textParts list
946 // is cleared for us.
947 if ( flushPart( result, textParts, oldIndent, maxLineLength ) ) {
948 if ( oldIndent.length() > indent.length() )
949 result += indent + '\n';
950 else
951 result += oldIndent + '\n';
954 if ( !fromLine.isEmpty() ) {
955 result += fromLine;
958 oldIndent = indent;
961 textParts.append( line );
964 // Write out anything still pending
965 flushPart( result, textParts, oldIndent, maxLineLength );
967 // Remove superfluous newline which was appended in flowText
968 if ( !result.isEmpty() && result.endsWith( '\n' ) )
969 result.chop( 1 );
971 return result;
974 bool isCryptoPart( const QString &type, const QString &subType, const QString &fileName )
976 return ( type.toLower() == "application" &&
977 ( subType.toLower() == "pgp-encrypted" ||
978 subType.toLower() == "pgp-signature" ||
979 subType.toLower() == "pkcs7-mime" ||
980 subType.toLower() == "pkcs7-signature" ||
981 subType.toLower() == "x-pkcs7-signature" ||
982 ( subType.toLower() == "octet-stream" &&
983 fileName.toLower() == "msg.asc" ) ) );
986 QString formatString( const QString &wildString, const QString &fromAddr )
988 QString result;
990 if ( wildString.isEmpty() ) {
991 return wildString;
994 unsigned int strLength( wildString.length() );
995 for ( uint i=0; i<strLength; ) {
996 QChar ch = wildString[i++];
997 if ( ch == '%' && i<strLength ) {
998 ch = wildString[i++];
999 switch ( ch.toLatin1() ) {
1000 case 'f': // sender's initals
1002 QString str = stripEmailAddr( fromAddr );
1004 uint j = 0;
1005 for ( ; str[j]>' '; j++ )
1007 unsigned int strLength( str.length() );
1008 for ( ; j < strLength && str[j] <= ' '; j++ )
1010 result += str[0];
1011 if ( str[j] > ' ' ) {
1012 result += str[j];
1013 } else {
1014 if ( str[1] > ' ' ) {
1015 result += str[1];
1019 break;
1020 case '_':
1021 result += ' ';
1022 break;
1023 case '%':
1024 result += '%';
1025 break;
1026 default:
1027 result += '%';
1028 result += ch;
1029 break;
1031 } else {
1032 result += ch;
1035 return result;
1038 QString cleanFileName( const QString &name )
1040 QString fileName = name.trimmed();
1042 // We need to replace colons with underscores since those cause problems with
1043 // KFileDialog (bug in KFileDialog though) and also on Windows filesystems.
1044 // We also look at the special case of ": ", since converting that to "_ "
1045 // would look strange, simply "_" looks better.
1046 // https://issues.kolab.org/issue3805
1047 fileName.replace( ": ", "_" );
1048 // replace all ':' with '_' because ':' isn't allowed on FAT volumes
1049 fileName.replace( ':', '_' );
1050 // better not use a dir-delimiter in a filename
1051 fileName.replace( '/', '_' );
1052 fileName.replace( '\\', '_' );
1054 // replace all '.' with '_', not just at the start of the filename
1055 // but don't replace the last '.' before the file extension.
1056 int i = fileName.lastIndexOf( '.' );
1057 if( i != -1 )
1058 i = fileName.lastIndexOf( '.' , i - 1 );
1060 while( i != -1 )
1062 fileName.replace( i, 1, '_' );
1063 i = fileName.lastIndexOf( '.', i - 1 );
1066 // replace all '~' with '_', not just leading '~' either.
1067 fileName.replace( '~', '_' );
1069 return fileName;