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 | |
40 | static const KCatalogLoader loader( QLatin1String("libkpimutils" ) ); |
41 | |
42 | using namespace KPIMUtils; |
43 | |
44 | //----------------------------------------------------------------------------- |
45 | QStringList 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 = 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(...). |
117 | KPIMUtils::EmailParseResult splitAddressInternal( const QByteArray address, |
118 | QByteArray &displayName, |
119 | QByteArray &addrSpec, |
120 | QByteArray &, |
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 | , |
139 | InAngleAddress |
140 | } context = TopLevel; |
141 | bool inQuotedString = false; |
142 | int = 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 | //----------------------------------------------------------------------------- |
286 | EmailParseResult KPIMUtils::splitAddress( const QByteArray &address, |
287 | QByteArray &displayName, |
288 | QByteArray &addrSpec, |
289 | QByteArray & ) |
290 | { |
291 | return splitAddressInternal( address, displayName, addrSpec, comment, |
292 | false/* don't allow multiple addresses */ ); |
293 | } |
294 | |
295 | //----------------------------------------------------------------------------- |
296 | EmailParseResult KPIMUtils::splitAddress( const QString &address, |
297 | QString &displayName, |
298 | QString &addrSpec, |
299 | QString & ) |
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 | //----------------------------------------------------------------------------- |
315 | EmailParseResult 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 | , |
346 | InAngleAddress |
347 | } context = TopLevel; |
348 | bool inQuotedString = false; |
349 | int = 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 | //----------------------------------------------------------------------------- |
522 | KPIMUtils::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 | //----------------------------------------------------------------------------- |
545 | QString 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 | //----------------------------------------------------------------------------- |
609 | bool 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 | //----------------------------------------------------------------------------- |
666 | QString 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 | //----------------------------------------------------------------------------- |
674 | QByteArray KPIMUtils::( 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 | //----------------------------------------------------------------------------- |
693 | QString KPIMUtils::( const QString &address ) |
694 | { |
695 | return QString::fromUtf8( extractEmailAddress( address.toUtf8() ) ); |
696 | } |
697 | |
698 | //----------------------------------------------------------------------------- |
699 | QByteArray 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 | //----------------------------------------------------------------------------- |
718 | QString KPIMUtils::firstEmailAddress( const QString &addresses ) |
719 | { |
720 | return QString::fromUtf8( firstEmailAddress( addresses.toUtf8() ) ); |
721 | } |
722 | |
723 | //----------------------------------------------------------------------------- |
724 | bool 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 = false; |
734 | bool bInQuotesOutsideOfEmail = false; |
735 | int i = 0, iAd = 0, iMailStart = 0, iMailEnd = 0; |
736 | QChar c; |
737 | unsigned int = 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 | //----------------------------------------------------------------------------- |
918 | bool 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 | //----------------------------------------------------------------------------- |
931 | QString KPIMUtils::normalizedAddress( const QString &displayName, |
932 | const QString &addrSpec, |
933 | const QString & ) |
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 = comment; |
946 | return quoteNameIfNecessary( commentStr ) + QLatin1String(" <" ) + addrSpec + QLatin1Char('>'); |
947 | } else { |
948 | return realDisplayName + QLatin1String(" (" ) + comment +QLatin1String( ") <" ) + addrSpec + QLatin1Char('>'); |
949 | } |
950 | } |
951 | |
952 | //----------------------------------------------------------------------------- |
953 | QString 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 | //----------------------------------------------------------------------------- |
969 | QString 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 | //----------------------------------------------------------------------------- |
985 | QString 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, ; |
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 | //----------------------------------------------------------------------------- |
1023 | QString 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, ; |
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. |
1059 | static 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 | //----------------------------------------------------------------------------- |
1088 | QString 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 | |
1103 | KUrl 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 | |
1112 | QString KPIMUtils::decodeMailtoUrl( const KUrl &mailtoUrl ) |
1113 | { |
1114 | Q_ASSERT( mailtoUrl.protocol().toLower() == QLatin1String("mailto" ) ); |
1115 | return KMime::decodeRFC2047String( mailtoUrl.path().toUtf8() ); |
1116 | } |
1117 | |