1/*
2 This file is part of the kpimutils library.
3 Copyright (c) 2004 Matt Douhan <matt@fruitsalad.org>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public
7 License as published by the Free Software Foundation; either
8 version 2 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to
17 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 Boston, MA 02110-1301, USA.
19*/
20/**
21 @file
22 This file is part of the KDEPIM Utilities library and provides
23 static methods for email address validation.
24
25 @author Matt Douhan \<matt@fruitsalad.org\>
26*/
27#include "email.h"
28
29#include <kmime/kmime_util.h>
30
31#include <KDebug>
32#include <KLocalizedString>
33#include <KUrl>
34
35#include <QtCore/QRegExp>
36#include <QtCore/QByteArray>
37
38#include <kglobal.h>
39
40static const KCatalogLoader loader( QLatin1String("libkpimutils") );
41
42using namespace KPIMUtils;
43
44//-----------------------------------------------------------------------------
45QStringList KPIMUtils::splitAddressList( const QString &aStr )
46{
47 // Features:
48 // - always ignores quoted characters
49 // - ignores everything (including parentheses and commas)
50 // inside quoted strings
51 // - supports nested comments
52 // - ignores everything (including double quotes and commas)
53 // inside comments
54
55 QStringList list;
56
57 if ( aStr.isEmpty() ) {
58 return list;
59 }
60
61 QString addr;
62 uint addrstart = 0;
63 int commentlevel = 0;
64 bool insidequote = false;
65
66 for ( int index = 0; index<aStr.length(); index++ ) {
67 // the following conversion to latin1 is o.k. because
68 // we can safely ignore all non-latin1 characters
69 switch ( aStr[index].toLatin1() ) {
70 case '"' : // start or end of quoted string
71 if ( commentlevel == 0 ) {
72 insidequote = !insidequote;
73 }
74 break;
75 case '(' : // start of comment
76 if ( !insidequote ) {
77 commentlevel++;
78 }
79 break;
80 case ')' : // end of comment
81 if ( !insidequote ) {
82 if ( commentlevel > 0 ) {
83 commentlevel--;
84 } else {
85 return list;
86 }
87 }
88 break;
89 case '\\' : // quoted character
90 index++; // ignore the quoted character
91 break;
92 case ',' :
93 case ';' :
94 if ( !insidequote && ( commentlevel == 0 ) ) {
95 addr = aStr.mid( addrstart, index - addrstart );
96 if ( !addr.isEmpty() ) {
97 list += addr.simplified();
98 }
99 addrstart = index + 1;
100 }
101 break;
102 }
103 }
104 // append the last address to the list
105 if ( !insidequote && ( commentlevel == 0 ) ) {
106 addr = aStr.mid( addrstart, aStr.length() - addrstart );
107 if ( !addr.isEmpty() ) {
108 list += addr.simplified();
109 }
110 }
111
112 return list;
113}
114
115//-----------------------------------------------------------------------------
116// Used by KPIMUtils::splitAddress(...) and KPIMUtils::firstEmailAddress(...).
117KPIMUtils::EmailParseResult splitAddressInternal( const QByteArray address,
118 QByteArray &displayName,
119 QByteArray &addrSpec,
120 QByteArray &comment,
121 bool allowMultipleAddresses )
122{
123 // kDebug() << "address";
124 displayName = "";
125 addrSpec = "";
126 comment = "";
127
128 if ( address.isEmpty() ) {
129 return AddressEmpty;
130 }
131
132 // The following is a primitive parser for a mailbox-list (cf. RFC 2822).
133 // The purpose is to extract a displayable string from the mailboxes.
134 // Comments in the addr-spec are not handled. No error checking is done.
135
136 enum {
137 TopLevel,
138 InComment,
139 InAngleAddress
140 } context = TopLevel;
141 bool inQuotedString = false;
142 int commentLevel = 0;
143 bool stop = false;
144
145 for ( const char *p = address.data(); *p && !stop; ++p ) {
146 switch ( context ) {
147 case TopLevel :
148 {
149 switch ( *p ) {
150 case '"' :
151 inQuotedString = !inQuotedString;
152 displayName += *p;
153 break;
154 case '(' :
155 if ( !inQuotedString ) {
156 context = InComment;
157 commentLevel = 1;
158 } else {
159 displayName += *p;
160 }
161 break;
162 case '<' :
163 if ( !inQuotedString ) {
164 context = InAngleAddress;
165 } else {
166 displayName += *p;
167 }
168 break;
169 case '\\' : // quoted character
170 displayName += *p;
171 ++p; // skip the '\'
172 if ( *p ) {
173 displayName += *p;
174 } else {
175 return UnexpectedEnd;
176 }
177 break;
178 case ',' :
179 if ( !inQuotedString ) {
180 if ( allowMultipleAddresses ) {
181 stop = true;
182 } else {
183 return UnexpectedComma;
184 }
185 } else {
186 displayName += *p;
187 }
188 break;
189 default :
190 displayName += *p;
191 }
192 break;
193 }
194 case InComment :
195 {
196 switch ( *p ) {
197 case '(' :
198 ++commentLevel;
199 comment += *p;
200 break;
201 case ')' :
202 --commentLevel;
203 if ( commentLevel == 0 ) {
204 context = TopLevel;
205 comment += ' '; // separate the text of several comments
206 } else {
207 comment += *p;
208 }
209 break;
210 case '\\' : // quoted character
211 comment += *p;
212 ++p; // skip the '\'
213 if ( *p ) {
214 comment += *p;
215 } else {
216 return UnexpectedEnd;
217 }
218 break;
219 default :
220 comment += *p;
221 }
222 break;
223 }
224 case InAngleAddress :
225 {
226 switch ( *p ) {
227 case '"' :
228 inQuotedString = !inQuotedString;
229 addrSpec += *p;
230 break;
231 case '>' :
232 if ( !inQuotedString ) {
233 context = TopLevel;
234 } else {
235 addrSpec += *p;
236 }
237 break;
238 case '\\' : // quoted character
239 addrSpec += *p;
240 ++p; // skip the '\'
241 if ( *p ) {
242 addrSpec += *p;
243 } else {
244 return UnexpectedEnd;
245 }
246 break;
247 default :
248 addrSpec += *p;
249 }
250 break;
251 }
252 } // switch ( context )
253 }
254 // check for errors
255 if ( inQuotedString ) {
256 return UnbalancedQuote;
257 }
258 if ( context == InComment ) {
259 return UnbalancedParens;
260 }
261 if ( context == InAngleAddress ) {
262 return UnclosedAngleAddr;
263 }
264
265 displayName = displayName.trimmed();
266 comment = comment.trimmed();
267 addrSpec = addrSpec.trimmed();
268
269 if ( addrSpec.isEmpty() ) {
270 if ( displayName.isEmpty() ) {
271 return NoAddressSpec;
272 } else {
273 addrSpec = displayName;
274 displayName.truncate( 0 );
275 }
276 }
277 /*
278 kDebug() << "display-name : \"" << displayName << "\"";
279 kDebug() << "comment : \"" << comment << "\"";
280 kDebug() << "addr-spec : \"" << addrSpec << "\"";
281 */
282 return AddressOk;
283}
284
285//-----------------------------------------------------------------------------
286EmailParseResult KPIMUtils::splitAddress( const QByteArray &address,
287 QByteArray &displayName,
288 QByteArray &addrSpec,
289 QByteArray &comment )
290{
291 return splitAddressInternal( address, displayName, addrSpec, comment,
292 false/* don't allow multiple addresses */ );
293}
294
295//-----------------------------------------------------------------------------
296EmailParseResult KPIMUtils::splitAddress( const QString &address,
297 QString &displayName,
298 QString &addrSpec,
299 QString &comment )
300{
301 QByteArray d, a, c;
302 // FIXME: toUtf8() is probably not safe here, what if the second byte of a multi-byte character
303 // has the same code as one of the ASCII characters that splitAddress uses as delimiters?
304 EmailParseResult result = splitAddress( address.toUtf8(), d, a, c );
305
306 if ( result == AddressOk ) {
307 displayName = QString::fromUtf8( d );
308 addrSpec = QString::fromUtf8( a );
309 comment = QString::fromUtf8( c );
310 }
311 return result;
312}
313
314//-----------------------------------------------------------------------------
315EmailParseResult KPIMUtils::isValidAddress( const QString &aStr )
316{
317 // If we are passed an empty string bail right away no need to process
318 // further and waste resources
319 if ( aStr.isEmpty() ) {
320 return AddressEmpty;
321 }
322
323 // count how many @'s are in the string that is passed to us
324 // if 0 or > 1 take action
325 // at this point to many @'s cannot bail out right away since
326 // @ is allowed in qoutes, so we use a bool to keep track
327 // and then make a judgment further down in the parser
328
329 bool tooManyAtsFlag = false;
330
331 int atCount = aStr.count( QLatin1Char('@') );
332 if ( atCount > 1 ) {
333 tooManyAtsFlag = true;
334 } else if ( atCount == 0 ) {
335 return TooFewAts;
336 }
337
338 int dotCount = aStr.count( QLatin1Char('.'));
339
340 // The main parser, try and catch all weird and wonderful
341 // mistakes users and/or machines can create
342
343 enum {
344 TopLevel,
345 InComment,
346 InAngleAddress
347 } context = TopLevel;
348 bool inQuotedString = false;
349 int commentLevel = 0;
350
351 unsigned int strlen = aStr.length();
352
353 for ( unsigned int index = 0; index < strlen; index++ ) {
354 switch ( context ) {
355 case TopLevel :
356 {
357 switch ( aStr[index].toLatin1() ) {
358 case '"' :
359 inQuotedString = !inQuotedString;
360 break;
361 case '(' :
362 if ( !inQuotedString ) {
363 context = InComment;
364 commentLevel = 1;
365 }
366 break;
367 case '[' :
368 if ( !inQuotedString ) {
369 return InvalidDisplayName;
370 }
371 break;
372 case ']' :
373 if ( !inQuotedString ) {
374 return InvalidDisplayName;
375 }
376 break;
377 case ':' :
378 if ( !inQuotedString ) {
379 return DisallowedChar;
380 }
381 break;
382 case '<' :
383 if ( !inQuotedString ) {
384 context = InAngleAddress;
385 }
386 break;
387 case '\\' : // quoted character
388 ++index; // skip the '\'
389 if ( ( index + 1 ) > strlen ) {
390 return UnexpectedEnd;
391 }
392 break;
393 case ',' :
394 if ( !inQuotedString ) {
395 return UnexpectedComma;
396 }
397 break;
398 case ')' :
399 if ( !inQuotedString ) {
400 return UnbalancedParens;
401 }
402 break;
403 case '>' :
404 if ( !inQuotedString ) {
405 return UnopenedAngleAddr;
406 }
407 break;
408 case '@' :
409 if ( !inQuotedString ) {
410 if ( index == 0 ) { // Missing local part
411 return MissingLocalPart;
412 } else if ( index == strlen-1 ) {
413 return MissingDomainPart;
414 }
415 } else if ( inQuotedString ) {
416 --atCount;
417 if ( atCount == 1 ) {
418 tooManyAtsFlag = false;
419 }
420 }
421 break;
422 case '.' :
423 if ( inQuotedString ) {
424 --dotCount;
425 }
426 break;
427 }
428 break;
429 }
430 case InComment :
431 {
432 switch ( aStr[index].toLatin1() ) {
433 case '(' :
434 ++commentLevel;
435 break;
436 case ')' :
437 --commentLevel;
438 if ( commentLevel == 0 ) {
439 context = TopLevel;
440 }
441 break;
442 case '\\' : // quoted character
443 ++index; // skip the '\'
444 if ( ( index + 1 ) > strlen ) {
445 return UnexpectedEnd;
446 }
447 break;
448 }
449 break;
450 }
451
452 case InAngleAddress :
453 {
454 switch ( aStr[index].toLatin1() ) {
455 case ',' :
456 if ( !inQuotedString ) {
457 return UnexpectedComma;
458 }
459 break;
460 case '"' :
461 inQuotedString = !inQuotedString;
462 break;
463 case '@' :
464 if ( inQuotedString ) {
465 --atCount;
466 }
467 if ( atCount == 1 ) {
468 tooManyAtsFlag = false;
469 }
470 break;
471 case '.' :
472 if ( inQuotedString ) {
473 --dotCount;
474 }
475 break;
476 case '>' :
477 if ( !inQuotedString ) {
478 context = TopLevel;
479 break;
480 }
481 break;
482 case '\\' : // quoted character
483 ++index; // skip the '\'
484 if ( ( index + 1 ) > strlen ) {
485 return UnexpectedEnd;
486 }
487 break;
488 }
489 break;
490 }
491 }
492 }
493
494 if ( dotCount == 0 && !inQuotedString ) {
495 return TooFewDots;
496 }
497
498 if ( atCount == 0 && !inQuotedString ) {
499 return TooFewAts;
500 }
501
502 if ( inQuotedString ) {
503 return UnbalancedQuote;
504 }
505
506 if ( context == InComment ) {
507 return UnbalancedParens;
508 }
509
510 if ( context == InAngleAddress ) {
511 return UnclosedAngleAddr;
512 }
513
514 if ( tooManyAtsFlag ) {
515 return TooManyAts;
516 }
517
518 return AddressOk;
519}
520
521//-----------------------------------------------------------------------------
522KPIMUtils::EmailParseResult KPIMUtils::isValidAddressList( const QString &aStr,
523 QString &badAddr )
524{
525 if ( aStr.isEmpty() ) {
526 return AddressEmpty;
527 }
528
529 const QStringList list = splitAddressList( aStr );
530
531 QStringList::const_iterator it = list.begin();
532 EmailParseResult errorCode = AddressOk;
533 for ( it = list.begin(); it != list.end(); ++it ) {
534 qDebug()<<" *it"<<(*it);
535 errorCode = isValidAddress( *it );
536 if ( errorCode != AddressOk ) {
537 badAddr = ( *it );
538 break;
539 }
540 }
541 return errorCode;
542}
543
544//-----------------------------------------------------------------------------
545QString KPIMUtils::emailParseResultToString( EmailParseResult errorCode )
546{
547 switch ( errorCode ) {
548 case TooManyAts :
549 return i18n( "The email address you entered is not valid because it "
550 "contains more than one @. "
551 "You will not create valid messages if you do not "
552 "change your address." );
553 case TooFewAts :
554 return i18n( "The email address you entered is not valid because it "
555 "does not contain a @. "
556 "You will not create valid messages if you do not "
557 "change your address." );
558 case AddressEmpty :
559 return i18n( "You have to enter something in the email address field." );
560 case MissingLocalPart :
561 return i18n( "The email address you entered is not valid because it "
562 "does not contain a local part." );
563 case MissingDomainPart :
564 return i18n( "The email address you entered is not valid because it "
565 "does not contain a domain part." );
566 case UnbalancedParens :
567 return i18n( "The email address you entered is not valid because it "
568 "contains unclosed comments/brackets." );
569 case AddressOk :
570 return i18n( "The email address you entered is valid." );
571 case UnclosedAngleAddr :
572 return i18n( "The email address you entered is not valid because it "
573 "contains an unclosed angle bracket." );
574 case UnopenedAngleAddr :
575 return i18n( "The email address you entered is not valid because it "
576 "contains too many closing angle brackets." );
577 case UnexpectedComma :
578 return i18n( "The email address you have entered is not valid because it "
579 "contains an unexpected comma." );
580 case UnexpectedEnd :
581 return i18n( "The email address you entered is not valid because it ended "
582 "unexpectedly. This probably means you have used an escaping "
583 "type character like a '\\' as the last character in your "
584 "email address." );
585 case UnbalancedQuote :
586 return i18n( "The email address you entered is not valid because it "
587 "contains quoted text which does not end." );
588 case NoAddressSpec :
589 return i18n( "The email address you entered is not valid because it "
590 "does not seem to contain an actual email address, i.e. "
591 "something of the form joe@example.org." );
592 case DisallowedChar :
593 return i18n( "The email address you entered is not valid because it "
594 "contains an illegal character." );
595 case InvalidDisplayName :
596 return i18n( "The email address you have entered is not valid because it "
597 "contains an invalid display name." );
598 case TooFewDots :
599 return i18n( "The email address you entered is not valid because it "
600 "does not contain a \'.\'. "
601 "You will not create valid messages if you do not "
602 "change your address." );
603
604 }
605 return i18n( "Unknown problem with email address" );
606}
607
608//-----------------------------------------------------------------------------
609bool KPIMUtils::isValidSimpleAddress( const QString &aStr )
610{
611 // If we are passed an empty string bail right away no need to process further
612 // and waste resources
613 if ( aStr.isEmpty() ) {
614 return false;
615 }
616
617 int atChar = aStr.lastIndexOf( QLatin1Char('@') );
618 QString domainPart = aStr.mid( atChar + 1 );
619 QString localPart = aStr.left( atChar );
620
621 // Both of these parts must be non empty
622 // after all we cannot have emails like:
623 // @kde.org, or foo@
624 if ( localPart.isEmpty() || domainPart.isEmpty() ) {
625 return false;
626 }
627
628 bool tooManyAtsFlag = false;
629 bool inQuotedString = false;
630 int atCount = localPart.count( QLatin1Char('@') );
631
632 unsigned int strlen = localPart.length();
633 for ( unsigned int index = 0; index < strlen; index++ ) {
634 switch ( localPart[ index ].toLatin1() ) {
635 case '"' :
636 inQuotedString = !inQuotedString;
637 break;
638 case '@' :
639 if ( inQuotedString ) {
640 --atCount;
641 if ( atCount == 0 ) {
642 tooManyAtsFlag = false;
643 }
644 }
645 break;
646 }
647 }
648
649 QString addrRx;
650
651 if ( localPart[ 0 ] == QLatin1Char('\"') || localPart[ localPart.length()-1 ] == QLatin1Char('\"') ) {
652 addrRx = QLatin1String("\"[a-zA-Z@]*[\\w.@-]*[a-zA-Z0-9@]\"@");
653 } else {
654 addrRx = QLatin1String("[a-zA-Z]*[~|{}`\\^?=/+*'&%$#!_\\w.-]*[~|{}`\\^?=/+*'&%$#!_a-zA-Z0-9-]@");
655 }
656 if ( domainPart[ 0 ] == QLatin1Char('[') || domainPart[ domainPart.length()-1 ] == QLatin1Char(']') ) {
657 addrRx += QLatin1String("\\[[0-9]{,3}(\\.[0-9]{,3}){3}\\]");
658 } else {
659 addrRx += QLatin1String("[\\w-#]+(\\.[\\w-#]+)*");
660 }
661 QRegExp rx( addrRx );
662 return rx.exactMatch( aStr ) && !tooManyAtsFlag;
663}
664
665//-----------------------------------------------------------------------------
666QString KPIMUtils::simpleEmailAddressErrorMsg()
667{
668 return i18n( "The email address you entered is not valid because it "
669 "does not seem to contain an actual email address, i.e. "
670 "something of the form joe@example.org." );
671}
672
673//-----------------------------------------------------------------------------
674QByteArray KPIMUtils::extractEmailAddress( const QByteArray &address )
675{
676 QByteArray dummy1, dummy2, addrSpec;
677 EmailParseResult result =
678 splitAddressInternal( address, dummy1, addrSpec, dummy2,
679 false/* don't allow multiple addresses */ );
680 if ( result != AddressOk ) {
681 addrSpec = QByteArray();
682 if ( result != AddressEmpty ) {
683 kDebug()
684 << "Input:" << address << "\nError:"
685 << emailParseResultToString( result );
686 }
687 }
688
689 return addrSpec;
690}
691
692//-----------------------------------------------------------------------------
693QString KPIMUtils::extractEmailAddress( const QString &address )
694{
695 return QString::fromUtf8( extractEmailAddress( address.toUtf8() ) );
696}
697
698//-----------------------------------------------------------------------------
699QByteArray KPIMUtils::firstEmailAddress( const QByteArray &addresses )
700{
701 QByteArray dummy1, dummy2, addrSpec;
702 EmailParseResult result =
703 splitAddressInternal( addresses, dummy1, addrSpec, dummy2,
704 true/* allow multiple addresses */ );
705 if ( result != AddressOk ) {
706 addrSpec = QByteArray();
707 if ( result != AddressEmpty ) {
708 kDebug()
709 << "Input: aStr\nError:"
710 << emailParseResultToString( result );
711 }
712 }
713
714 return addrSpec;
715}
716
717//-----------------------------------------------------------------------------
718QString KPIMUtils::firstEmailAddress( const QString &addresses )
719{
720 return QString::fromUtf8( firstEmailAddress( addresses.toUtf8() ) );
721}
722
723//-----------------------------------------------------------------------------
724bool KPIMUtils::extractEmailAddressAndName( const QString &aStr,
725 QString &mail, QString &name )
726{
727 name.clear();
728 mail.clear();
729
730 const int len = aStr.length();
731 const char cQuotes = '"';
732
733 bool bInComment = false;
734 bool bInQuotesOutsideOfEmail = false;
735 int i = 0, iAd = 0, iMailStart = 0, iMailEnd = 0;
736 QChar c;
737 unsigned int commentstack = 0;
738
739 // Find the '@' of the email address
740 // skipping all '@' inside "(...)" comments:
741 while ( i < len ) {
742 c = aStr[i];
743 if ( QLatin1Char('(') == c ) {
744 commentstack++;
745 }
746 if ( QLatin1Char(')') == c ) {
747 commentstack--;
748 }
749 bInComment = commentstack != 0;
750 if ( QLatin1Char('"') == c && !bInComment ) {
751 bInQuotesOutsideOfEmail = !bInQuotesOutsideOfEmail;
752 }
753
754 if ( !bInComment && !bInQuotesOutsideOfEmail ) {
755 if ( QLatin1Char('@') == c ) {
756 iAd = i;
757 break; // found it
758 }
759 }
760 ++i;
761 }
762
763 if ( !iAd ) {
764 // We suppose the user is typing the string manually and just
765 // has not finished typing the mail address part.
766 // So we take everything that's left of the '<' as name and the rest as mail
767 for ( i = 0; len > i; ++i ) {
768 c = aStr[i];
769 if ( QLatin1Char('<') != c ) {
770 name.append( c );
771 } else {
772 break;
773 }
774 }
775 mail = aStr.mid( i + 1 );
776 if ( mail.endsWith( QLatin1Char('>') ) ) {
777 mail.truncate( mail.length() - 1 );
778 }
779
780 } else {
781 // Loop backwards until we find the start of the string
782 // or a ',' that is outside of a comment
783 // and outside of quoted text before the leading '<'.
784 bInComment = false;
785 bInQuotesOutsideOfEmail = false;
786 for ( i = iAd-1; 0 <= i; --i ) {
787 c = aStr[i];
788 if ( bInComment ) {
789 if ( QLatin1Char('(') == c ) {
790 if ( !name.isEmpty() ) {
791 name.prepend( QLatin1Char(' ') );
792 }
793 bInComment = false;
794 } else {
795 name.prepend( c ); // all comment stuff is part of the name
796 }
797 } else if ( bInQuotesOutsideOfEmail ) {
798 if ( QLatin1Char(cQuotes) == c ) {
799 bInQuotesOutsideOfEmail = false;
800 } else if ( c != QLatin1Char('\\') ) {
801 name.prepend( c );
802 }
803 } else {
804 // found the start of this addressee ?
805 if ( QLatin1Char(',') == c ) {
806 break;
807 }
808 // stuff is before the leading '<' ?
809 if ( iMailStart ) {
810 if ( QLatin1Char(cQuotes) == c ) {
811 bInQuotesOutsideOfEmail = true; // end of quoted text found
812 } else {
813 name.prepend( c );
814 }
815 } else {
816 switch ( c.toLatin1() ) {
817 case '<':
818 iMailStart = i;
819 break;
820 case ')':
821 if ( !name.isEmpty() ) {
822 name.prepend( QLatin1Char(' ') );
823 }
824 bInComment = true;
825 break;
826 default:
827 if ( QLatin1Char(' ') != c ) {
828 mail.prepend( c );
829 }
830 }
831 }
832 }
833 }
834
835 name = name.simplified();
836 mail = mail.simplified();
837
838 if ( mail.isEmpty() ) {
839 return false;
840 }
841
842 mail.append( QLatin1Char('@') );
843
844 // Loop forward until we find the end of the string
845 // or a ',' that is outside of a comment
846 // and outside of quoted text behind the trailing '>'.
847 bInComment = false;
848 bInQuotesOutsideOfEmail = false;
849 int parenthesesNesting = 0;
850 for ( i = iAd+1; len > i; ++i ) {
851 c = aStr[i];
852 if ( bInComment ) {
853 if ( QLatin1Char(')') == c ) {
854 if ( --parenthesesNesting == 0 ) {
855 bInComment = false;
856 if ( !name.isEmpty() ) {
857 name.append( QLatin1Char(' ') );
858 }
859 } else {
860 // nested ")", add it
861 name.append( QLatin1Char(')') ); // name can't be empty here
862 }
863 } else {
864 if ( QLatin1Char('(') == c ) {
865 // nested "("
866 ++parenthesesNesting;
867 }
868 name.append( c ); // all comment stuff is part of the name
869 }
870 } else if ( bInQuotesOutsideOfEmail ) {
871 if ( QLatin1Char(cQuotes) == c ) {
872 bInQuotesOutsideOfEmail = false;
873 } else if ( c != QLatin1Char('\\') ) {
874 name.append( c );
875 }
876 } else {
877 // found the end of this addressee ?
878 if ( QLatin1Char(',') == c ) {
879 break;
880 }
881 // stuff is behind the trailing '>' ?
882 if ( iMailEnd ) {
883 if ( QLatin1Char(cQuotes) == c ) {
884 bInQuotesOutsideOfEmail = true; // start of quoted text found
885 } else {
886 name.append( c );
887 }
888 } else {
889 switch ( c.toLatin1() ) {
890 case '>':
891 iMailEnd = i;
892 break;
893 case '(':
894 if ( !name.isEmpty() ) {
895 name.append( QLatin1Char(' ') );
896 }
897 if ( ++parenthesesNesting > 0 ) {
898 bInComment = true;
899 }
900 break;
901 default:
902 if ( QLatin1Char(' ') != c ) {
903 mail.append( c );
904 }
905 }
906 }
907 }
908 }
909 }
910
911 name = name.simplified();
912 mail = mail.simplified();
913
914 return ! ( name.isEmpty() || mail.isEmpty() );
915}
916
917//-----------------------------------------------------------------------------
918bool KPIMUtils::compareEmail( const QString &email1, const QString &email2,
919 bool matchName )
920{
921 QString e1Name, e1Email, e2Name, e2Email;
922
923 extractEmailAddressAndName( email1, e1Email, e1Name );
924 extractEmailAddressAndName( email2, e2Email, e2Name );
925
926 return e1Email == e2Email &&
927 ( !matchName || ( e1Name == e2Name ) );
928}
929
930//-----------------------------------------------------------------------------
931QString KPIMUtils::normalizedAddress( const QString &displayName,
932 const QString &addrSpec,
933 const QString &comment )
934{
935 const QString realDisplayName = KMime::removeBidiControlChars( displayName );
936 if ( realDisplayName.isEmpty() && comment.isEmpty() ) {
937 return addrSpec;
938 } else if ( comment.isEmpty() ) {
939 if ( !realDisplayName.startsWith( QLatin1Char('\"') ) ) {
940 return quoteNameIfNecessary( realDisplayName ) + QLatin1String(" <") + addrSpec + QLatin1Char('>');
941 } else {
942 return realDisplayName + QLatin1String(" <") + addrSpec + QLatin1Char('>');
943 }
944 } else if ( realDisplayName.isEmpty() ) {
945 QString commentStr = comment;
946 return quoteNameIfNecessary( commentStr ) + QLatin1String(" <") + addrSpec + QLatin1Char('>');
947 } else {
948 return realDisplayName + QLatin1String(" (") + comment +QLatin1String( ") <") + addrSpec + QLatin1Char('>');
949 }
950}
951
952//-----------------------------------------------------------------------------
953QString KPIMUtils::fromIdn( const QString &addrSpec )
954{
955 const int atPos = addrSpec.lastIndexOf( QLatin1Char('@') );
956 if ( atPos == -1 ) {
957 return addrSpec;
958 }
959
960 QString idn = KUrl::fromAce( addrSpec.mid( atPos + 1 ).toLatin1() );
961 if ( idn.isEmpty() ) {
962 return QString();
963 }
964
965 return addrSpec.left( atPos + 1 ) + idn;
966}
967
968//-----------------------------------------------------------------------------
969QString KPIMUtils::toIdn( const QString &addrSpec )
970{
971 const int atPos = addrSpec.lastIndexOf( QLatin1Char('@') );
972 if ( atPos == -1 ) {
973 return addrSpec;
974 }
975
976 QString idn = QLatin1String(KUrl::toAce( addrSpec.mid( atPos + 1 )) );
977 if ( idn.isEmpty() ) {
978 return addrSpec;
979 }
980
981 return addrSpec.left( atPos + 1 ) + idn;
982}
983
984//-----------------------------------------------------------------------------
985QString KPIMUtils::normalizeAddressesAndDecodeIdn( const QString &str )
986{
987 // kDebug() << str;
988 if ( str.isEmpty() ) {
989 return str;
990 }
991
992 const QStringList addressList = splitAddressList( str );
993 QStringList normalizedAddressList;
994
995 QByteArray displayName, addrSpec, comment;
996
997 for ( QStringList::ConstIterator it = addressList.begin();
998 ( it != addressList.end() );
999 ++it ) {
1000 if ( !( *it ).isEmpty() ) {
1001 if ( splitAddress( ( *it ).toUtf8(),
1002 displayName, addrSpec, comment ) == AddressOk ) {
1003
1004 displayName = KMime::decodeRFC2047String( displayName ).toUtf8();
1005 comment = KMime::decodeRFC2047String( comment ).toUtf8();
1006
1007 normalizedAddressList
1008 << normalizedAddress( QString::fromUtf8( displayName ),
1009 fromIdn( QString::fromUtf8( addrSpec ) ),
1010 QString::fromUtf8( comment ) );
1011 }
1012 }
1013 }
1014 /*
1015 kDebug() << "normalizedAddressList: \""
1016 << normalizedAddressList.join( ", " )
1017 << "\"";
1018 */
1019 return normalizedAddressList.join( QLatin1String(", ") );
1020}
1021
1022//-----------------------------------------------------------------------------
1023QString KPIMUtils::normalizeAddressesAndEncodeIdn( const QString &str )
1024{
1025 //kDebug() << str;
1026 if ( str.isEmpty() ) {
1027 return str;
1028 }
1029
1030 const QStringList addressList = splitAddressList( str );
1031 QStringList normalizedAddressList;
1032
1033 QByteArray displayName, addrSpec, comment;
1034
1035 for ( QStringList::ConstIterator it = addressList.begin();
1036 ( it != addressList.end() );
1037 ++it ) {
1038 if ( !( *it ).isEmpty() ) {
1039 if ( splitAddress( ( *it ).toUtf8(),
1040 displayName, addrSpec, comment ) == AddressOk ) {
1041
1042 normalizedAddressList << normalizedAddress( QString::fromUtf8( displayName ),
1043 toIdn( QString::fromUtf8( addrSpec ) ),
1044 QString::fromUtf8( comment ) );
1045 }
1046 }
1047 }
1048
1049 /*
1050 kDebug() << "normalizedAddressList: \""
1051 << normalizedAddressList.join( ", " )
1052 << "\"";
1053 */
1054 return normalizedAddressList.join( QLatin1String(", ") );
1055}
1056
1057//-----------------------------------------------------------------------------
1058// Escapes unescaped doublequotes in str.
1059static QString escapeQuotes( const QString &str )
1060{
1061 if ( str.isEmpty() ) {
1062 return QString();
1063 }
1064
1065 QString escaped;
1066 // reserve enough memory for the worst case ( """..."" -> \"\"\"...\"\" )
1067 escaped.reserve( 2 * str.length() );
1068 unsigned int len = 0;
1069 for ( int i = 0; i < str.length(); ++i, ++len ) {
1070 if ( str[i] == QLatin1Char('"') ) { // unescaped doublequote
1071 escaped[len] = QLatin1Char('\\');
1072 ++len;
1073 } else if ( str[i] == QLatin1Char('\\') ) { // escaped character
1074 escaped[len] = QLatin1Char('\\');
1075 ++len;
1076 ++i;
1077 if ( i >= str.length() ) { // handle trailing '\' gracefully
1078 break;
1079 }
1080 }
1081 escaped[len] = str[i];
1082 }
1083 escaped.truncate( len );
1084 return escaped;
1085}
1086
1087//-----------------------------------------------------------------------------
1088QString KPIMUtils::quoteNameIfNecessary( const QString &str )
1089{
1090 QString quoted = str;
1091
1092 QRegExp needQuotes( QLatin1String("[^ 0-9A-Za-z\\x0080-\\xFFFF]") );
1093 // avoid double quoting
1094 if ( ( quoted[0] == QLatin1Char('"') ) && ( quoted[quoted.length() - 1] ==QLatin1Char( '"') ) ) {
1095 quoted = QLatin1String("\"") + escapeQuotes( quoted.mid( 1, quoted.length() - 2 ) ) + QLatin1String("\"");
1096 } else if ( quoted.indexOf( needQuotes ) != -1 ) {
1097 quoted = QLatin1String("\"") + escapeQuotes( quoted ) + QLatin1String("\"");
1098 }
1099
1100 return quoted;
1101}
1102
1103KUrl KPIMUtils::encodeMailtoUrl( const QString &mailbox )
1104{
1105 const QByteArray encodedPath = KMime::encodeRFC2047String( mailbox, "utf-8" );
1106 KUrl mailtoUrl;
1107 mailtoUrl.setProtocol( QLatin1String("mailto") );
1108 mailtoUrl.setPath( QLatin1String(encodedPath) );
1109 return mailtoUrl;
1110}
1111
1112QString KPIMUtils::decodeMailtoUrl( const KUrl &mailtoUrl )
1113{
1114 Q_ASSERT( mailtoUrl.protocol().toLower() == QLatin1String("mailto") );
1115 return KMime::decodeRFC2047String( mailtoUrl.path().toUtf8() );
1116}
1117