cutelyst 5.0.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
validatoremail.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2017-2025 Matthias Fehring <mf@huessenbergnetz.de>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5
6#include "validatoremail_p.h"
7
8#include <algorithm>
9#include <functional>
10
11#include <QDnsLookup>
12#include <QEventLoop>
13#include <QTimer>
14#include <QUrl>
15
16using namespace Cutelyst;
17using namespace Qt::Literals::StringLiterals;
18
19const QRegularExpression ValidatorEmailPrivate::ipv4Regex{
20 u"\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25["
21 "0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"_s};
22const QRegularExpression ValidatorEmailPrivate::ipv6PartRegex{u"^[0-9A-Fa-f]{0,4}$"_s};
23const QString ValidatorEmailPrivate::stringSpecials{u"()<>[]:;@\\,.\""_s};
24
26 Category threshold,
27 Options options,
28 const Cutelyst::ValidatorMessages &messages,
29 const QString &defValKey)
30 : ValidatorRule(*new ValidatorEmailPrivate(field, threshold, options, messages, defValKey))
31{
32}
33
35
37{
39
40 const QString v = value(params);
41
42 Q_D(const ValidatorEmail);
43
44 if (d->options.testFlag(CheckDNS)) {
45 qCWarning(C_VALIDATOR) << "ValidatorEmail: using the CheckDNS option on validate() is"
46 << "not supported anymore. Use validateCb().";
47 }
48
49 if (!v.isEmpty()) {
50
51 ValidatorEmailDiagnoseStruct diag;
52
53 if (ValidatorEmailPrivate::checkEmail(v, d->options, d->threshold, &diag)) {
54 if (!diag.literal.isEmpty()) {
55 result.value.setValue<QString>(diag.localpart + QLatin1Char('@') + diag.literal);
56 } else {
57 result.value.setValue<QString>(diag.localpart + QLatin1Char('@') + diag.domain);
58 }
59 } else {
60 result.errorMessage =
61 validationError(c, QVariant::fromValue<Diagnose>(diag.finalStatus));
62 }
63
64 result.extra = QVariant::fromValue<QList<Diagnose>>(diag.returnStatus);
65
66 } else {
67 defaultValue(c, &result);
68 }
69
70 return result;
71}
72
74{
75 const QString v = value(params);
76
77 Q_D(const ValidatorEmail);
78
79 if (!v.isEmpty()) {
81 d->threshold,
82 d->options,
83 [c, cb, this, v](bool isValid,
84 const QString &cleanedEmail,
85 const QList<Diagnose> &diagnoses) {
86 ValidatorReturnType rt;
87 rt.extra = QVariant::fromValue<QList<Diagnose>>(diagnoses);
88 if (isValid) {
89 rt.value.setValue(cleanedEmail);
90 } else {
91 qCDebug(C_VALIDATOR).noquote() << debugString(c) << diagnoses;
92 rt.errorMessage =
93 validationError(c, QVariant::fromValue<Diagnose>(diagnoses.at(0)));
94 }
95 cb(std::move(rt));
96 });
97 } else {
98 defaultValue(c, cb);
99 }
100}
101
103{
104 QString error;
105
106 error = ValidatorEmail::diagnoseString(c, errorData.value<Diagnose>(), label(c));
107
108 return error;
109}
110
111bool ValidatorEmailPrivate::checkEmail(const QString &address,
112 ValidatorEmail::Options options,
113 ValidatorEmail::Category threshold,
114 ValidatorEmailDiagnoseStruct *diagnoseStruct)
115{
117
118 EmailPart context = ComponentLocalpart;
119 QList<EmailPart> contextStack{context};
120 EmailPart contextPrior = ComponentLocalpart;
121
122 QChar token;
123 QChar tokenPrior;
124
125 QString parseLocalPart;
126 QString parseDomain;
127 QString parseLiteral;
128 QMap<int, QString> atomListLocalPart;
129 QMap<int, QString> atomListDomain;
130 int elementCount = 0;
131 int elementLen = 0;
132 bool hyphenFlag = false;
133 bool endOrDie = false;
134 int crlf_count = 0;
135
136 const bool checkDns = options.testFlag(ValidatorEmail::CheckDNS);
137 const bool allowUtf8Local = options.testFlag(ValidatorEmail::UTF8Local);
138 const bool allowIdn = options.testFlag(ValidatorEmail::AllowIDN);
139
140 QString email;
141 const qsizetype atPos = address.lastIndexOf(QLatin1Char('@'));
142 if (allowIdn) {
143 if (atPos > 0) {
144 const QString local = address.left(atPos);
145 const QString domain = address.mid(atPos + 1);
146 bool asciiDomain = true;
147 for (const QChar &ch : domain) {
148 const ushort &uc = ch.unicode();
149 if (uc > ValidatorEmailPrivate::asciiEnd) {
150 asciiDomain = false;
151 break;
152 }
153 }
154
155 if (asciiDomain) {
156 email = address;
157 } else {
158 email = local + QLatin1Char('@') + QString::fromLatin1(QUrl::toAce(domain));
159 }
160 } else {
161 email = address;
162 }
163 } else {
164 email = address;
165 }
166
167 const qsizetype rawLength = email.length();
168
169 for (int i = 0; i < rawLength; i++) {
170 token = email[i];
171
172 switch (context) {
173 //-------------------------------------------------------------
174 // local-part
175 //-------------------------------------------------------------
176 case ComponentLocalpart:
177 {
178 // https://tools.ietf.org/html/rfc5322#section-3.4.1
179 // local-part = dot-atom / quoted-string / obs-local-part
180 //
181 // dot-atom = [CFWS] dot-atom-text [CFWS]
182 //
183 // dot-atom-text = 1*atext *("." 1*atext)
184 //
185 // quoted-string = [CFWS]
186 // DQUOTE *([FWS] qcontent) [FWS] DQUOTE
187 // [CFWS]
188 //
189 // obs-local-part = word *("." word)
190 //
191 // word = atom / quoted-string
192 //
193 // atom = [CFWS] 1*atext [CFWS]
194
195 if (token == QLatin1Char('(')) { // comment
196 if (elementLen == 0) {
197 // Comments are OK at the beginning of an element
198 returnStatus.push_back((elementCount == 0) ? ValidatorEmail::CFWSComment
199 : ValidatorEmail::DeprecatedComment);
200 } else {
201 returnStatus.push_back(ValidatorEmail::CFWSComment);
202 endOrDie = true; // We can't start a comment in the middle of an element, so
203 // this better be the end
204 }
205
206 contextStack.push_back(context);
207 context = ContextComment;
208 } else if (token == QLatin1Char('.')) { // Next dot-atom element
209 if (elementLen == 0) {
210 // Another dot, already?
211 returnStatus.push_back((elementCount == 0)
213 : ValidatorEmail::ErrorConsecutiveDots);
214 } else {
215 // The entire local part can be a quoted string for RFC 5321
216 // If it's just one atom that is quoten then it's an RFC 5322 obsolete form
217 if (endOrDie) {
218 returnStatus.push_back(ValidatorEmail::DeprecatedLocalpart);
219 }
220 }
221
222 endOrDie = false; // CFWS & quoted strings are OK again now we're at the beginning
223 // of an element (although they are obsolete forms)
224 elementLen = 0;
225 elementCount++;
226 parseLocalPart += token;
227 atomListLocalPart[elementCount].clear();
228 } else if (token == QLatin1Char('"')) {
229 if (elementLen == 0) {
230 // The entire local-part can be a quoted string for RFC 5321
231 // If it's just one atom that is quoted then it's an RFC 5322 obsolete form
232 returnStatus.push_back((elementCount == 0)
234 : ValidatorEmail::DeprecatedLocalpart);
235
236 parseLocalPart += token;
237 atomListLocalPart[elementCount] += token;
238 elementLen++;
239 endOrDie = true; // quoted string must be the entire element
240 contextStack.push_back(context);
241 context = ContextQuotedString;
242 } else {
243 returnStatus.push_back(ValidatorEmail::ErrorExpectingAText); // Fatal error
244 }
245 } else if ((token == QChar(QChar::CarriageReturn)) || (token == QChar(QChar::Space)) ||
246 (token == QChar(QChar::Tabulation))) { // Folding White Space
247 if ((token == QChar(QChar::CarriageReturn)) &&
248 ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed)))) {
249 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF);
250 break;
251 }
252
253 if (elementLen == 0) {
254 returnStatus.push_back((elementCount == 0) ? ValidatorEmail::CFWSFWS
255 : ValidatorEmail::DeprecatedFWS);
256 } else {
257 endOrDie = true; // We can't start FWS in the middle of an element, so this
258 // better be the end
259 }
260
261 contextStack.push_back(context);
262 context = ContextFWS;
263 tokenPrior = token;
264 } else if (token == QLatin1Char('@')) {
265 // At this point we should have a valid local part
266 if (contextStack.size() != 1) {
267 returnStatus.push_back(ValidatorEmail::ErrorFatal);
268 qCCritical(C_VALIDATOR) << "ValidatorEmail: Unexpected item on context stack";
269 break;
270 }
271
272 if (parseLocalPart.isEmpty()) {
273 returnStatus.push_back(ValidatorEmail::ErrorNoLocalPart); // Fatal error
274 } else if (elementLen == 0) {
275 returnStatus.push_back(ValidatorEmail::ErrorDotEnd); // Fatal Error
276 } else if (parseLocalPart.size() > ValidatorEmailPrivate::maxLocalPartLength) {
277 // https://tools.ietf.org/html/rfc5321#section-4.5.3.1.1
278 // The maximum total length of a user name or other local-part is 64
279 // octets.
280 returnStatus.push_back(ValidatorEmail::RFC5322LocalTooLong);
281 } else if ((contextPrior == ContextComment) || (contextPrior == ContextFWS)) {
282 // https://tools.ietf.org/html/rfc5322#section-3.4.1
283 // Comments and folding white space
284 // SHOULD NOT be used around the "@" in the addr-spec.
285 //
286 // https://tools.ietf.org/html/rfc2119
287 // 4. SHOULD NOT This phrase, or the phrase "NOT RECOMMENDED" mean that
288 // there may exist valid reasons in particular circumstances when the
289 // particular behavior is acceptable or even useful, but the full
290 // implications should be understood and the case carefully weighed
291 // before implementing any behavior described with this label.
292 returnStatus.push_back(ValidatorEmail::DeprecatedCFWSNearAt);
293 }
294
295 context = ComponentDomain;
296 contextStack.clear();
297 contextStack.push_back(context);
298 elementCount = 0;
299 elementLen = 0;
300 endOrDie = false;
301
302 } else { // atext
303 // https://tools.ietf.org/html/rfc5322#section-3.2.3
304 // atext = ALPHA / DIGIT / ; Printable US-ASCII
305 // "!" / "#" / ; characters not including
306 // "$" / "%" / ; specials. Used for atoms.
307 // "&" / "'" /
308 // "*" / "+" /
309 // "-" / "/" /
310 // "=" / "?" /
311 // "^" / "_" /
312 // "`" / "{" /
313 // "|" / "}" /
314 //
315 if (endOrDie) {
316 switch (contextPrior) {
317 case ContextComment:
318 case ContextFWS:
319 returnStatus.push_back(ValidatorEmail::ErrorATextAfterCFWS);
320 break;
321 case ContextQuotedString:
322 returnStatus.push_back(ValidatorEmail::ErrorATextAfterQS);
323 break;
324 default:
325 returnStatus.push_back(ValidatorEmail::ErrorFatal);
326 qCCritical(C_VALIDATOR)
327 << "ValidatorEmail: More atext found where none is allowed, "
328 "but unrecognizes prior context";
329 break;
330 }
331 } else {
332 contextPrior = context;
333 const char16_t uni = token.unicode();
334
335 if (!allowUtf8Local) {
336 if ((uni < ValidatorEmailPrivate::asciiExclamationMark) ||
337 (uni > ValidatorEmailPrivate::asciiTilde) ||
338 ValidatorEmailPrivate::stringSpecials.contains(token)) {
339 returnStatus.push_back(
341 }
342 } else {
343 if (!token.isLetterOrNumber()) {
344 if ((uni < ValidatorEmailPrivate::asciiExclamationMark) ||
345 (uni > ValidatorEmailPrivate::asciiTilde) ||
346 ValidatorEmailPrivate::stringSpecials.contains(token)) {
347 returnStatus.push_back(
349 }
350 }
351 }
352
353 parseLocalPart += token;
354 atomListLocalPart[elementCount] += token;
355 elementLen++;
356 }
357 }
358 } break;
359 //-----------------------------------------
360 // Domain
361 //-----------------------------------------
362 case ComponentDomain:
363 {
364 // https://tools.ietf.org/html/rfc5322#section-3.4.1
365 // domain = dot-atom / domain-literal / obs-domain
366 //
367 // dot-atom = [CFWS] dot-atom-text [CFWS]
368 //
369 // dot-atom-text = 1*atext *("." 1*atext)
370 //
371 // domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
372 //
373 // dtext = %d33-90 / ; Printable US-ASCII
374 // %d94-126 / ; characters not including
375 // obs-dtext ; "[", "]", or "\"
376 //
377 // obs-domain = atom *("." atom)
378 //
379 // atom = [CFWS] 1*atext [CFWS]
380 // https://tools.ietf.org/html/rfc5321#section-4.1.2
381 // Mailbox = Local-part "@" ( Domain / address-literal )
382 //
383 // Domain = sub-domain *("." sub-domain)
384 //
385 // address-literal = "[" ( IPv4-address-literal /
386 // IPv6-address-literal /
387 // General-address-literal ) "]"
388 // ; See Section 4.1.3
389 // https://tools.ietf.org/html/rfc5322#section-3.4.1
390 // Note: A liberal syntax for the domain portion of addr-spec is
391 // given here. However, the domain portion contains addressing
392 // information specified by and used in other protocols (e.g.,
393 // [RFC1034], [RFC1035], [RFC1123], [RFC5321]). It is therefore
394 // incumbent upon implementations to conform to the syntax of
395 // addresses for the context in which they are used.
396 // is_email() author's note: it's not clear how to interpret this in
397 // the context of a general email address validator. The conclusion I
398 // have reached is this: "addressing information" must comply with
399 // RFC 5321 (and in turn RFC 1035), anything that is "semantically
400 // invisible" must comply only with RFC 5322.
401
402 if (token == QLatin1Char('(')) { // comment
403 if (elementLen == 0) {
404 // Comments at the start of the domain are deprecated in the text
405 // Comments at the start of a subdomain are obs-domain
406 // (https://tools.ietf.org/html/rfc5322#section-3.4.1)
407 returnStatus.push_back((elementCount == 0)
409 : ValidatorEmail::DeprecatedComment);
410 } else {
411 returnStatus.push_back(ValidatorEmail::CFWSComment);
412 endOrDie = true; // We can't start a comment in the middle of an element, so
413 // this better be the end
414 }
415
416 contextStack.push_back(context);
417 context = ContextComment;
418 } else if (token == QLatin1Char('.')) { // next dot-atom element
419 if (elementLen == 0) {
420 // another dot, already?
421 returnStatus.push_back((elementCount == 0)
423 : ValidatorEmail::ErrorConsecutiveDots);
424 } else if (hyphenFlag) {
425 // Previous subdomain ended in a hyphen
426 returnStatus.push_back(ValidatorEmail::ErrorDomainHyphenEnd); // fatal error
427 } else {
428 // Nowhere in RFC 5321 does it say explicitly that the
429 // domain part of a Mailbox must be a valid domain according
430 // to the DNS standards set out in RFC 1035, but this *is*
431 // implied in several places. For instance, wherever the idea
432 // of host routing is discussed the RFC says that the domain
433 // must be looked up in the DNS. This would be nonsense unless
434 // the domain was designed to be a valid DNS domain. Hence we
435 // must conclude that the RFC 1035 restriction on label length
436 // also applies to RFC 5321 domains.
437 //
438 // https://tools.ietf.org/html/rfc1035#section-2.3.4
439 // labels 63 octets or less
440 if (elementLen > ValidatorEmailPrivate::maxDnsLabelLength) {
441 returnStatus.push_back(ValidatorEmail::RFC5322LabelTooLong);
442 }
443 }
444
445 endOrDie = false; // CFWS is OK again now we're at the beginning of an element
446 // (although it may be obsolete CFWS)
447 elementLen = 0;
448 elementCount++;
449 atomListDomain[elementCount].clear();
450 parseDomain += token;
451
452 } else if (token == QLatin1Char('[')) { // Domain literal
453 if (parseDomain.isEmpty()) {
454 endOrDie = true; // domain literal must be the only component
455 elementLen++;
456 contextStack.push_back(context);
457 context = ComponentLiteral;
458 parseDomain += token;
459 atomListDomain[elementCount] += token;
460 parseLiteral.clear();
461 } else {
462 returnStatus.push_back(ValidatorEmail::ErrorExpectingAText); // Fatal error
463 }
464 } else if ((token == QChar(QChar::CarriageReturn)) || (token == QChar(QChar::Space)) ||
465 (token == QChar(QChar::Tabulation))) { // Folding White Space
466 if ((token == QChar(QChar::CarriageReturn)) &&
467 ((++i == rawLength) || email[i] != QChar(QChar::LineFeed))) {
468 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF); // Fatal error
469 break;
470 }
471
472 if (elementLen == 0) {
473 returnStatus.push_back((elementCount == 0)
475 : ValidatorEmail::DeprecatedFWS);
476 } else {
477 returnStatus.push_back(ValidatorEmail::CFWSFWS);
478 endOrDie = true; // We can't start FWS in the middle of an element, so this
479 // better be the end
480 }
481
482 contextStack.push_back(context);
483 context = ContextFWS;
484 tokenPrior = token;
485
486 } else { // atext
487 // RFC 5322 allows any atext...
488 // https://tools.ietf.org/html/rfc5322#section-3.2.3
489 // atext = ALPHA / DIGIT / ; Printable US-ASCII
490 // "!" / "#" / ; characters not including
491 // "$" / "%" / ; specials. Used for atoms.
492 // "&" / "'" /
493 // "*" / "+" /
494 // "-" / "/" /
495 // "=" / "?" /
496 // "^" / "_" /
497 // "`" / "{" /
498 // "|" / "}" /
499 // "~"
500 // But RFC 5321 only allows letter-digit-hyphen to comply with DNS rules (RFCs 1034
501 // & 1123) https://tools.ietf.org/html/rfc5321#section-4.1.2
502 // sub-domain = Let-dig [Ldh-str]
503 //
504 // Let-dig = ALPHA / DIGIT
505 //
506 // Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
507 //
508
509 if (endOrDie) {
510 // We have encountered atext where it is no longer valid
511 switch (contextPrior) {
512 case ContextComment:
513 case ContextFWS:
514 returnStatus.push_back(ValidatorEmail::ErrorATextAfterCFWS);
515 break;
516 case ComponentLiteral:
517 returnStatus.push_back(ValidatorEmail::ErrorATextAfterDomLit);
518 break;
519 default:
520 returnStatus.push_back(ValidatorEmail::ErrorFatal);
521 qCCritical(C_VALIDATOR)
522 << "ValidatorEmail: More atext found where none is allowed, but"
523 << "unrecognised prior context.";
524 break;
525 }
526 }
527
528 const char16_t uni = token.unicode();
529 hyphenFlag = false; // Assume this token isn't a hyphen unless we discover it is
530
531 if ((uni < ValidatorEmailPrivate::asciiExclamationMark) ||
532 (uni > ValidatorEmailPrivate::asciiTilde) ||
533 ValidatorEmailPrivate::stringSpecials.contains(token)) {
534 returnStatus.push_back(ValidatorEmail::ErrorExpectingAText); // Fatal error
535 } else if (token == QLatin1Char('-')) {
536 if (elementLen == 0) {
537 // Hyphens can't be at the beginning of a subdomain
538 returnStatus.push_back(
540 }
541 hyphenFlag = true;
542 } else if (!(((uni >= ValidatorRulePrivate::ascii_0) &&
543 (uni <= ValidatorRulePrivate::ascii_9)) ||
544 ((uni >= ValidatorRulePrivate::ascii_A) &&
545 (uni <= ValidatorRulePrivate::ascii_Z)) ||
546 ((uni >= ValidatorRulePrivate::ascii_a) &&
547 (uni <= ValidatorRulePrivate::ascii_z)))) {
548 // NOt an RFC 5321 subdomain, but still ok by RFC 5322
549 returnStatus.push_back(ValidatorEmail::RFC5322Domain);
550 }
551
552 parseDomain += token;
553 atomListDomain[elementCount] += token;
554 elementLen++;
555 }
556 } break;
557 //-------------------------------------------------------------
558 // Domain literal
559 //-------------------------------------------------------------
560 case ComponentLiteral:
561 {
562 // https://tools.ietf.org/html/rfc5322#section-3.4.1
563 // domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
564 //
565 // dtext = %d33-90 / ; Printable US-ASCII
566 // %d94-126 / ; characters not including
567 // obs-dtext ; "[", "]", or "\"
568 //
569 // obs-dtext = obs-NO-WS-CTL / quoted-pair
570 if (token == QLatin1Char(']')) { // End of domain literal
571 if (static_cast<int>(
572 *std::max_element(returnStatus.constBegin(), returnStatus.constEnd())) <
573 static_cast<int>(ValidatorEmail::Deprecated)) {
574 // Could be a valid RFC 5321 address literal, so let's check
575
576 // https://tools.ietf.org/html/rfc5321#section-4.1.2
577 // address-literal = "[" ( IPv4-address-literal /
578 // IPv6-address-literal /
579 // General-address-literal ) "]"
580 // ; See Section 4.1.3
581 //
582 // https://tools.ietf.org/html/rfc5321#section-4.1.3
583 // IPv4-address-literal = Snum 3("." Snum)
584 //
585 // IPv6-address-literal = "IPv6:" IPv6-addr
586 //
587 // General-address-literal = Standardized-tag ":" 1*dcontent
588 //
589 // Standardized-tag = Ldh-str
590 // ; Standardized-tag MUST be specified in a
591 // ; Standards-Track RFC and registered with IANA
592 //
593 // dcontent = %d33-90 / ; Printable US-ASCII
594 // %d94-126 ; excl. "[", "\", "]"
595 //
596 // Snum = 1*3DIGIT
597 // ; representing a decimal integer
598 // ; value in the range 0 through 255
599 //
600 // IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
601 //
602 // IPv6-hex = 1*4HEXDIG
603 //
604 // IPv6-full = IPv6-hex 7(":" IPv6-hex)
605 //
606 // IPv6-comp = [IPv6-hex *5(":" IPv6-hex)] "::"
607 // [IPv6-hex *5(":" IPv6-hex)]
608 // ; The "::" represents at least 2 16-bit groups of
609 // ; zeros. No more than 6 groups in addition to the
610 // ; "::" may be present.
611 //
612 // IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
613 //
614 // IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::"
615 // [IPv6-hex *3(":" IPv6-hex) ":"]
616 // IPv4-address-literal
617 // ; The "::" represents at least 2 16-bit groups of
618 // ; zeros. No more than 4 groups in addition to the
619 // ; "::" and IPv4-address-literal may be present.
620 //
621 // is_email() author's note: We can't use ip2long() to validate
622 // IPv4 addresses because it accepts abbreviated addresses
623 // (xxx.xxx.xxx), expanding the last group to complete the address.
624 // filter_var() validates IPv6 address inconsistently (up to PHP 5.3.3
625 // at least) -- see https://bugs.php.net/bug.php?id=53236 for example
626
627 int maxGroups = 8; // NOLINT(cppcoreguidelines-avoid-magic-numbers)
628 qsizetype index = -1;
629 QString addressLiteral = parseLiteral;
630
631 const QRegularExpressionMatch ipv4Match =
632 ValidatorEmailPrivate::ipv4Regex.match(addressLiteral);
633 if (ipv4Match.hasMatch()) {
634 index = addressLiteral.lastIndexOf(ipv4Match.captured());
635 if (index != 0) {
636 addressLiteral =
637 addressLiteral.mid(0, index) +
639 "0:0"); // Convert IPv4 part to IPv6 format for further testing
640 }
641 }
642
643 if (index == 0) {
644 // Nothing there except a valid IPv4 address, so...
645 returnStatus.push_back(ValidatorEmail::RFC5321AddressLiteral);
646 } else if (QString::compare(
647 addressLiteral.left(5),
649 "IPv6:")) != // NOLINT(cppcoreguidelines-avoid-magic-numbers)
650 0) {
651 returnStatus.push_back(ValidatorEmail::RFC5322DomainLiteral);
652 } else {
653 const QString ipv6 = addressLiteral.mid(5);
654 const QStringList matchesIP = ipv6.split(QLatin1Char(':'));
655 qsizetype groupCount = matchesIP.size();
656 index = ipv6.indexOf(QLatin1String("::"));
657
658 if (index < 0) {
659 // We need exactly the right number of groups
660 if (groupCount != maxGroups) {
661 returnStatus.push_back(ValidatorEmail::RFC5322IPv6GroupCount);
662 }
663 } else {
664 if (index != ipv6.lastIndexOf(QLatin1String("::"))) {
665 returnStatus.push_back(ValidatorEmail::RFC5322IPv62x2xColon);
666 } else {
667 if ((index == 0) || (index == (ipv6.length() - 2))) {
668 maxGroups++;
669 }
670
671 if (groupCount > maxGroups) {
672 returnStatus.push_back(ValidatorEmail::RFC5322IPv6MaxGroups);
673 } else if (groupCount == maxGroups) {
674 returnStatus.push_back(
675 ValidatorEmail::RFC5321IPv6Deprecated); // Eliding a single
676 // "::"
677 }
678 }
679 }
680
681 if ((ipv6.size() == 1 && ipv6[0] == QLatin1Char(':')) ||
682 (ipv6[0] == QLatin1Char(':') && ipv6[1] != QLatin1Char(':'))) {
683 returnStatus.push_back(
684 ValidatorEmail::RFC5322IPv6ColonStart); // Address starts with a
685 // single colon
686 } else if (ipv6.right(2).at(1) == QLatin1Char(':') &&
687 ipv6.right(2).at(0) != QLatin1Char(':')) {
688 returnStatus.push_back(
689 ValidatorEmail::RFC5322IPv6ColonEnd); // Address ends with a single
690 // colon
691 } else {
692 int unmatchedChars = 0;
693 for (const QString &ip : matchesIP) {
694 if (!ip.contains(ValidatorEmailPrivate::ipv6PartRegex)) {
695 unmatchedChars++;
696 }
697 }
698 if (unmatchedChars != 0) {
699 returnStatus.push_back(ValidatorEmail::RFC5322IPv6BadChar);
700 } else {
701 returnStatus.push_back(ValidatorEmail::RFC5321AddressLiteral);
702 }
703 }
704 }
705
706 } else {
707 returnStatus.push_back(ValidatorEmail::RFC5322DomainLiteral);
708 }
709
710 parseDomain += token;
711 atomListDomain[elementCount] += token;
712 elementLen++;
713 contextPrior = context;
714 context = contextStack.takeLast();
715 } else if (token == QLatin1Char('\\')) {
716 returnStatus.push_back(ValidatorEmail::RFC5322DomLitOBSDText);
717 contextStack.push_back(context);
718 context = ContextQuotedPair;
719 } else if ((token == QChar(QChar::CarriageReturn)) || (token == QChar(QChar::Space)) ||
720 (token == QChar(QChar::Tabulation))) { // Folding White Space
721 if ((token == QChar(QChar::CarriageReturn)) &&
722 ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed)))) {
723 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF); // Fatal error
724 break;
725 }
726
727 returnStatus.push_back(ValidatorEmail::CFWSFWS);
728 contextStack.push_back(context);
729 context = ContextFWS;
730 tokenPrior = token;
731
732 } else { // dtext
733 // https://tools.ietf.org/html/rfc5322#section-3.4.1
734 // dtext = %d33-90 / ; Printable US-ASCII
735 // %d94-126 / ; characters not including
736 // obs-dtext ; "[", "]", or "\"
737 //
738 // obs-dtext = obs-NO-WS-CTL / quoted-pair
739 //
740 // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
741 // %d11 / ; characters that do not
742 // %d12 / ; include the carriage
743 // %d14-31 / ; return, line feed, and
744 // %d127 ; white space characters
745 const char16_t uni = token.unicode();
746
747 // CR, LF, SP & HTAB have already been parsed above
748 if ((uni > ValidatorEmailPrivate::asciiEnd) || (uni == 0) ||
749 (uni == QLatin1Char('[').unicode())) {
750 returnStatus.push_back(ValidatorEmail::ErrorExpectingDText); // Fatal error
751 break;
752 } else if ((uni < ValidatorEmailPrivate::asciiExclamationMark) ||
753 (uni == ValidatorEmailPrivate::asciiEnd)) {
754 returnStatus.push_back(ValidatorEmail::RFC5322DomLitOBSDText);
755 }
756
757 parseLiteral += token;
758 parseDomain += token;
759 atomListDomain[elementCount] += token;
760 elementLen++;
761 }
762 } break;
763 //-------------------------------------------------------------
764 // Quoted string
765 //-------------------------------------------------------------
766 case ContextQuotedString:
767 {
768 // https://tools.ietf.org/html/rfc5322#section-3.2.4
769 // quoted-string = [CFWS]
770 // DQUOTE *([FWS] qcontent) [FWS] DQUOTE
771 // [CFWS]
772 //
773 // qcontent = qtext / quoted-pair
774 if (token == QLatin1Char('\\')) { // Quoted pair
775 contextStack.push_back(context);
776 context = ContextQuotedPair;
777 } else if ((token == QChar(QChar::CarriageReturn)) ||
778 (token == QChar(QChar::Tabulation))) { // Folding White Space
779 // Inside a quoted string, spaces are allowed as regular characters.
780 // It's only FWS if we include HTAB or CRLF
781 if ((token == QChar(QChar::CarriageReturn)) &&
782 ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed)))) {
783 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF);
784 break;
785 }
786
787 // https://tools.ietf.org/html/rfc5322#section-3.2.2
788 // Runs of FWS, comment, or CFWS that occur between lexical tokens in a
789 // structured header field are semantically interpreted as a single
790 // space character.
791
792 // https://tools.ietf.org/html/rfc5322#section-3.2.4
793 // the CRLF in any FWS/CFWS that appears within the quoted-string [is]
794 // semantically "invisible" and therefore not part of the quoted-string
795
796 parseLocalPart += QChar(QChar::Space);
797 atomListLocalPart[elementCount] += QChar(QChar::Space);
798 elementLen++;
799
800 returnStatus.push_back(ValidatorEmail::CFWSFWS);
801 contextStack.push_back(context);
802 context = ContextFWS;
803 tokenPrior = token;
804 } else if (token == QLatin1Char('"')) { // end of quoted string
805 parseLocalPart += token;
806 atomListLocalPart[elementCount] += token;
807 elementLen++;
808 contextPrior = context;
809 context = contextStack.takeLast();
810 } else { // qtext
811 // https://tools.ietf.org/html/rfc5322#section-3.2.4
812 // qtext = %d33 / ; Printable US-ASCII
813 // %d35-91 / ; characters not including
814 // %d93-126 / ; "\" or the quote character
815 // obs-qtext
816 //
817 // obs-qtext = obs-NO-WS-CTL
818 //
819 // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
820 // %d11 / ; characters that do not
821 // %d12 / ; include the carriage
822 // %d14-31 / ; return, line feed, and
823 // %d127 ; white space characters
824 const char16_t uni = token.unicode();
825
826 if (!allowUtf8Local) {
827 if ((uni > ValidatorEmailPrivate::asciiEnd) || (uni == 0) ||
828 (uni == ValidatorEmailPrivate::asciiLF)) {
829 returnStatus.push_back(ValidatorEmail::ErrorExpectingQText); // Fatal error
830 } else if ((uni < ValidatorRulePrivate::asciiSpace) ||
831 (uni == ValidatorEmailPrivate::asciiEnd)) {
832 returnStatus.push_back(ValidatorEmail::DeprecatedQText);
833 }
834 } else {
835 if (!token.isLetterOrNumber()) {
836 if ((uni > ValidatorEmailPrivate::asciiEnd) || (uni == 0) ||
837 (uni == ValidatorEmailPrivate::asciiLF)) {
838 returnStatus.push_back(
840 } else if ((uni < ValidatorRulePrivate::asciiSpace) ||
841 (uni == ValidatorEmailPrivate::asciiEnd)) {
842 returnStatus.push_back(ValidatorEmail::DeprecatedQText);
843 }
844 }
845 }
846
847 parseLocalPart += token;
848 atomListLocalPart[elementCount] += token;
849 elementLen++;
850 }
851
852 // https://tools.ietf.org/html/rfc5322#section-3.4.1
853 // If the
854 // string can be represented as a dot-atom (that is, it contains no
855 // characters other than atext characters or "." surrounded by atext
856 // characters), then the dot-atom form SHOULD be used and the quoted-
857 // string form SHOULD NOT be used.
858 // To do
859 } break;
860 //-------------------------------------------------------------
861 // Quoted pair
862 //-------------------------------------------------------------
863 case ContextQuotedPair:
864 {
865 // https://tools.ietf.org/html/rfc5322#section-3.2.1
866 // quoted-pair = ("\" (VCHAR / WSP)) / obs-qp
867 //
868 // VCHAR = %d33-126 ; visible (printing) characters
869 // WSP = SP / HTAB ; white space
870 //
871 // obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR)
872 //
873 // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
874 // %d11 / ; characters that do not
875 // %d12 / ; include the carriage
876 // %d14-31 / ; return, line feed, and
877 // %d127 ; white space characters
878 //
879 // i.e. obs-qp = "\" (%d0-8, %d10-31 / %d127)
880
881 const char16_t uni = token.unicode();
882
883 if (uni > ValidatorEmailPrivate::asciiEnd) {
884 returnStatus.push_back(ValidatorEmail::ErrorExpectingQpair); // Fatal error
885 } else if (((uni < ValidatorEmailPrivate::asciiUS) &&
886 (uni != ValidatorRulePrivate::asciiTab)) ||
887 (uni == ValidatorEmailPrivate::asciiEnd)) {
888 returnStatus.push_back(ValidatorEmail::DeprecatedQP);
889 }
890
891 // At this point we know where this qpair occurred so
892 // we could check to see if the character actually
893 // needed to be quoted at all.
894 // https://tools.ietf.org/html/rfc5321#section-4.1.2
895 // the sending system SHOULD transmit the
896 // form that uses the minimum quoting possible.
897
898 contextPrior = context;
899 context = contextStack.takeLast();
900
901 switch (context) {
902 case ContextComment:
903 break;
904 case ContextQuotedString:
905 parseLocalPart += QLatin1Char('\\');
906 parseLocalPart += token;
907 atomListLocalPart[elementCount] += QLatin1Char('\\');
908 atomListLocalPart[elementCount] += token;
909 elementLen += 2; // The maximum sizes specified by RFC 5321 are octet counts, so we
910 // must include the backslash
911 break;
912 case ComponentLiteral:
913 parseDomain += QLatin1Char('\\');
914 parseDomain += token;
915 atomListDomain[elementCount] += QLatin1Char('\\');
916 atomListDomain[elementCount] += token;
917 elementLen += 2; // The maximum sizes specified by RFC 5321 are octet counts, so we
918 // must include the backslash
919 break;
920 default:
921 returnStatus.push_back(ValidatorEmail::ErrorFatal);
922 qCCritical(C_VALIDATOR)
923 << "ValidatorEmail: Quoted pair logic invoked in an invalid context.";
924 break;
925 }
926 } break;
927 //-------------------------------------------------------------
928 // Comment
929 //-------------------------------------------------------------
930 case ContextComment:
931 {
932 // https://tools.ietf.org/html/rfc5322#section-3.2.2
933 // comment = "(" *([FWS] ccontent) [FWS] ")"
934 //
935 // ccontent = ctext / quoted-pair / comment
936 if (token == QLatin1Char('(')) { // netsted comment
937 // nested comments are OK
938 contextStack.push_back(context);
939 context = ContextComment;
940 } else if (token == QLatin1Char(')')) {
941 contextPrior = context;
942 context = contextStack.takeLast();
943
944 // https://tools.ietf.org/html/rfc5322#section-3.2.2
945 // Runs of FWS, comment, or CFWS that occur between lexical tokens in a
946 // structured header field are semantically interpreted as a single
947 // space character.
948 //
949 // is_email() author's note: This *cannot* mean that we must add a
950 // space to the address wherever CFWS appears. This would result in
951 // any addr-spec that had CFWS outside a quoted string being invalid
952 // for RFC 5321.
953 // if (($context === ISEMAIL_COMPONENT_LOCALPART) ||
954 //($context === ISEMAIL_COMPONENT_DOMAIN)) {
955 // $parsedata[$context] .=
956 // ISEMAIL_STRING_SP;
957 // $atomlist[$context][$element_count]
958 // .= ISEMAIL_STRING_SP; $element_len++;
959 // }
960 } else if (token == QLatin1Char('\\')) { // Quoted pair
961 contextStack.push_back(context);
962 context = ContextQuotedPair;
963 } else if ((token == QChar(QChar::CarriageReturn)) || (token == QChar(QChar::Space)) ||
964 (token == QChar(QChar::Tabulation))) { // Folding White Space
965 if ((token == QChar(QChar::CarriageReturn)) &&
966 ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed)))) {
967 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF);
968 break;
969 }
970
971 returnStatus.push_back(ValidatorEmail::CFWSFWS);
972 contextStack.push_back(context);
973 context = ContextFWS;
974 tokenPrior = token;
975 } else { // ctext
976 // https://tools.ietf.org/html/rfc5322#section-3.2.3
977 // ctext = %d33-39 / ; Printable US-ASCII
978 // %d42-91 / ; characters not including
979 // %d93-126 / ; "(", ")", or "\"
980 // obs-ctext
981 //
982 // obs-ctext = obs-NO-WS-CTL
983 //
984 // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
985 // %d11 / ; characters that do not
986 // %d12 / ; include the carriage
987 // %d14-31 / ; return, line feed, and
988 // %d127 ; white space characters
989
990 const ushort uni = token.unicode();
991
992 if ((uni > ValidatorEmailPrivate::asciiEnd) || (uni == 0) ||
993 (uni == ValidatorEmailPrivate::asciiLF)) {
994 returnStatus.push_back(ValidatorEmail::ErrorExpectingCText); // Fatal error
995 break;
996 } else if ((uni < ValidatorRulePrivate::asciiSpace) ||
997 (uni == ValidatorEmailPrivate::asciiEnd)) {
998 returnStatus.push_back(ValidatorEmail::DeprecatedCText);
999 }
1000 }
1001 } break;
1002 //-------------------------------------------------------------
1003 // Folding White Space
1004 //-------------------------------------------------------------
1005 case ContextFWS:
1006 {
1007 // https://tools.ietf.org/html/rfc5322#section-3.2.2
1008 // FWS = ([*WSP CRLF] 1*WSP) / obs-FWS
1009 // ; Folding white space
1010 // But note the erratum:
1011 // https://www.rfc-editor.org/errata_search.php?rfc=5322&eid=1908:
1012 // In the obsolete syntax, any amount of folding white space MAY be
1013 // inserted where the obs-FWS rule is allowed. This creates the
1014 // possibility of having two consecutive "folds" in a line, and
1015 // therefore the possibility that a line which makes up a folded header
1016 // field could be composed entirely of white space.
1017 //
1018 // obs-FWS = 1*([CRLF] WSP)
1019 if (tokenPrior == QChar(QChar::CarriageReturn)) {
1020 if (token == QChar(QChar::CarriageReturn)) {
1021 returnStatus.push_back(ValidatorEmail::ErrorFWSCRLFx2); // Fatal error
1022 break;
1023 }
1024
1025 if (crlf_count > 0) {
1026 if (++crlf_count > 1) {
1027 returnStatus.push_back(
1028 ValidatorEmail::DeprecatedFWS); // Multiple folds = obsolete FWS
1029 }
1030 } else {
1031 crlf_count = 1;
1032 }
1033 }
1034
1035 if (token == QChar(QChar::CarriageReturn)) {
1036 if ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed))) {
1037 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF);
1038 break;
1039 }
1040 } else if ((token != QChar(QChar::Space)) && (token != QChar(QChar::Tabulation))) {
1041 if (tokenPrior == QChar(QChar::CarriageReturn)) {
1042 returnStatus.push_back(ValidatorEmail::ErrorFWSCRLFEnd); // Fatal error
1043 break;
1044 }
1045
1046 crlf_count = std::min(crlf_count, 0);
1047
1048 contextPrior = context;
1049 context = contextStack.takeLast(); // End of FWS
1050
1051 // https://tools.ietf.org/html/rfc5322#section-3.2.2
1052 // Runs of FWS, comment, or CFWS that occur between lexical tokens in a
1053 // structured header field are semantically interpreted as a single
1054 // space character.
1055 //
1056 // is_email() author's note: This *cannot* mean that we must add a
1057 // space to the address wherever CFWS appears. This would result in
1058 // any addr-spec that had CFWS outside a quoted string being invalid
1059 // for RFC 5321.
1060 // if (($context === ISEMAIL_COMPONENT_LOCALPART) ||
1061 //($context === ISEMAIL_COMPONENT_DOMAIN)) {
1062 // $parsedata[$context] .=
1063 // ISEMAIL_STRING_SP;
1064 // $atomlist[$context][$element_count]
1065 // .= ISEMAIL_STRING_SP; $element_len++;
1066 // }
1067
1068 i--; // Look at this token again in the parent context
1069 }
1070
1071 tokenPrior = token;
1072 } break;
1073 default:
1074 returnStatus.push_back(ValidatorEmail::ErrorFatal);
1075 qCCritical(C_VALIDATOR) << "ValidatorEmail: Unknown context";
1076 break;
1077 }
1078
1079 if (static_cast<int>(
1080 *std::max_element(returnStatus.constBegin(), returnStatus.constEnd())) >
1081 static_cast<int>(ValidatorEmail::RFC5322)) {
1082 break;
1083 }
1084 }
1085
1086 // Some simple final tests
1087 if (static_cast<int>(*std::max_element(returnStatus.constBegin(), returnStatus.constEnd())) <
1088 static_cast<int>(ValidatorEmail::RFC5322)) {
1089 if (context == ContextQuotedString) {
1090 returnStatus.push_back(ValidatorEmail::ErrorUnclosedQuotedStr);
1091 } else if (context == ContextQuotedPair) {
1092 returnStatus.push_back(ValidatorEmail::ErrorBackslashEnd);
1093 } else if (context == ContextComment) {
1094 returnStatus.push_back(ValidatorEmail::ErrorUnclosedComment);
1095 } else if (context == ComponentLiteral) {
1096 returnStatus.push_back(ValidatorEmail::ErrorUnclosedDomLiteral);
1097 } else if (token == QChar(QChar::CarriageReturn)) {
1098 returnStatus.push_back(ValidatorEmail::ErrorFWSCRLFEnd);
1099 } else if (parseDomain.isEmpty()) {
1100 returnStatus.push_back(ValidatorEmail::ErrorNoDomain);
1101 } else if (elementLen == 0) {
1102 returnStatus.push_back(ValidatorEmail::ErrorDotEnd);
1103 } else if (hyphenFlag) {
1104 returnStatus.push_back(ValidatorEmail::ErrorDomainHyphenEnd);
1105 } else if (parseDomain.size() > ValidatorEmailPrivate::maxDomainLength) {
1106 // https://tools.ietf.org/html/rfc5321#section-4.5.3.1.2
1107 // The maximum total length of a domain name or number is 255 octets.
1108 returnStatus.push_back(ValidatorEmail::RFC5322DomainTooLong);
1109 } else if ((parseLocalPart.size() + 1 + parseDomain.size()) >
1110 ValidatorEmailPrivate::maxMailboxLength) {
1111 // https://tools.ietf.org/html/rfc5321#section-4.1.2
1112 // Forward-path = Path
1113 //
1114 // Path = "<" [ A-d-l ":" ] Mailbox ">"
1115 //
1116 // https://tools.ietf.org/html/rfc5321#section-4.5.3.1.3
1117 // The maximum total length of a reverse-path or forward-path is 256
1118 // octets (including the punctuation and element separators).
1119 //
1120 // Thus, even without (obsolete) routing information, the Mailbox can
1121 // only be 254 characters long. This is confirmed by this verified
1122 // erratum to RFC 3696:
1123 //
1124 // https://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690
1125 // However, there is a restriction in RFC 2821 on the length of an
1126 // address in MAIL and RCPT commands of 254 characters. Since addresses
1127 // that do not fit in those fields are not normally useful, the upper
1128 // limit on address lengths should normally be considered to be 254.
1129 returnStatus.push_back(ValidatorEmail::RFC5322TooLong);
1130 } else if (elementLen > ValidatorEmailPrivate::maxDnsLabelLength) {
1131 returnStatus.push_back(ValidatorEmail::RFC5322LabelTooLong);
1132 }
1133 }
1134
1135 // Check DNS?
1136 bool dnsChecked = false;
1137
1138 // if (checkDns &&
1139 // (static_cast<int>(*std::max_element(returnStatus.constBegin(), returnStatus.constEnd()))
1140 // <
1141 // static_cast<int>(threshold))) {
1142 // // https://tools.ietf.org/html/rfc5321#section-2.3.5
1143 // // Names that can
1144 // // be resolved to MX RRs or address (i.e., A or AAAA) RRs (as discussed
1145 // // in Section 5) are permitted, as are CNAME RRs whose targets can be
1146 // // resolved, in turn, to MX or address RRs.
1147 // //
1148 // // https://tools.ietf.org/html/rfc5321#section-5.1
1149 // // The lookup first attempts to locate an MX record associated with the
1150 // // name. If a CNAME record is found, the resulting name is processed as
1151 // // if it were the initial name. ... If an empty list of MXs is returned,
1152 // // the address is treated as if it was associated with an implicit MX
1153 // // RR, with a preference of 0, pointing to that host.
1154
1155 // if (elementCount == 0) {
1156 // parseDomain += QLatin1Char('.');
1157 // }
1158
1159 // QDnsLookup mxLookup(QDnsLookup::MX, parseDomain);
1160 // QEventLoop mxLoop;
1161 // QObject::connect(&mxLookup, &QDnsLookup::finished, &mxLoop, &QEventLoop::quit);
1162 // QTimer::singleShot(ValidatorEmailPrivate::dnsLookupTimeout, &mxLookup,
1163 // &QDnsLookup::abort); mxLookup.lookup(); mxLoop.exec();
1164
1165 // if ((mxLookup.error() == QDnsLookup::NoError) && !mxLookup.mailExchangeRecords().empty())
1166 // {
1167 // dnsChecked = true;
1168 // } else {
1169 // returnStatus.push_back(ValidatorEmail::DnsWarnNoMxRecord);
1170 // QDnsLookup aLookup(QDnsLookup::A, parseDomain);
1171 // QEventLoop aLoop;
1172 // QObject::connect(&aLookup, &QDnsLookup::finished, &aLoop, &QEventLoop::quit);
1173 // QTimer::singleShot(
1174 // ValidatorEmailPrivate::dnsLookupTimeout, &aLookup, &QDnsLookup::abort);
1175 // aLookup.lookup();
1176 // aLoop.exec();
1177
1178 // if ((aLookup.error() == QDnsLookup::NoError) &&
1179 // !aLookup.hostAddressRecords().empty()) {
1180 // dnsChecked = true;
1181 // } else {
1182 // returnStatus.push_back(ValidatorEmail::DnsNoRecordFound);
1183 // }
1184 // }
1185 // }
1186
1187 // Check for TLD addresses
1188 // -----------------------
1189 // TLD addresses are specifically allowed in RFC 5321 but they are
1190 // unusual to say the least. We will allocate a separate
1191 // status to these addresses on the basis that they are more likely
1192 // to be typos than genuine addresses (unless we've already
1193 // established that the domain does have an MX record)
1194 //
1195 // https://tools.ietf.org/html/rfc5321#section-2.3.5
1196 // In the case
1197 // of a top-level domain used by itself in an email address, a single
1198 // string is used without any dots. This makes the requirement,
1199 // described in more detail below, that only fully-qualified domain
1200 // names appear in SMTP transactions on the public Internet,
1201 // particularly important where top-level domains are involved.
1202 //
1203 // TLD format
1204 // ----------
1205 // The format of TLDs has changed a number of times. The standards
1206 // used by IANA have been largely ignored by ICANN, leading to
1207 // confusion over the standards being followed. These are not defined
1208 // anywhere, except as a general component of a DNS host name (a label).
1209 // However, this could potentially lead to 123.123.123.123 being a
1210 // valid DNS name (rather than an IP address) and thereby creating
1211 // an ambiguity. The most authoritative statement on TLD formats that
1212 // the author can find is in a (rejected!) erratum to RFC 1123
1213 // submitted by John Klensin, the author of RFC 5321:
1214 //
1215 // https://www.rfc-editor.org/errata_search.php?rfc=1123&eid=1353
1216 // However, a valid host name can never have the dotted-decimal
1217 // form #.#.#.#, since this change does not permit the highest-level
1218 // component label to start with a digit even if it is not all-numeric.
1219 if (!dnsChecked &&
1220 (static_cast<int>(*std::max_element(returnStatus.constBegin(), returnStatus.constEnd())) <
1221 static_cast<int>(ValidatorEmail::DNSFailed))) {
1222 if (elementCount == 0) {
1223 returnStatus.push_back(ValidatorEmail::RFC5321TLD);
1224 }
1225
1226 if (QStringLiteral("0123456789").contains(atomListDomain[elementCount][0])) {
1227 returnStatus.push_back(ValidatorEmail::RFC5321TLDNumeric);
1228 }
1229 }
1230
1231 if (returnStatus.size() != 1) {
1233 for (const ValidatorEmail::Diagnose dia : std::as_const(returnStatus)) {
1234 if (!_rs.contains(dia) && (dia != ValidatorEmail::ValidAddress)) {
1235 _rs.append(dia); // clazy:exclude=reserve-candidates
1236 }
1237 }
1238 returnStatus = _rs;
1239
1240 std::ranges::sort(returnStatus, std::greater<>());
1241 }
1242
1243 const ValidatorEmail::Diagnose finalStatus = returnStatus.at(0);
1244
1245 if (diagnoseStruct) {
1246 diagnoseStruct->finalStatus = finalStatus;
1247 diagnoseStruct->returnStatus = returnStatus;
1248 diagnoseStruct->localpart = parseLocalPart;
1249 diagnoseStruct->domain = parseDomain;
1250 diagnoseStruct->literal = parseLiteral;
1251 if (!parseLiteral.isEmpty()) {
1252 diagnoseStruct->cleanedEmail = parseLocalPart + QLatin1Char('@') + parseLiteral;
1253 } else {
1254 diagnoseStruct->cleanedEmail = parseLocalPart + QLatin1Char('@') + parseDomain;
1255 }
1256 }
1257
1258 return static_cast<int>(finalStatus) < static_cast<int>(threshold);
1259}
1260
1262{
1263 if (label.isEmpty()) {
1264 switch (diagnose) {
1265 case ValidAddress:
1266 //% "Address is valid. Please note that this does not mean that both the "
1267 //% "address and the domain actually exist. This address could be issued "
1268 //% "by the domain owner without breaking the rules of any RFCs."
1269 return c->qtTrId("cutelyst-valemail-diag-valid");
1270 case DnsWarnNoMxRecord:
1271 //% "Could not find an MX record for this address’ domain but an A record exists."
1272 return c->qtTrId("cutelyst-valemail-diag-nomx");
1273 case DnsMxDisabled:
1274 //% "MX for this address’ domain is explicitely disabled."
1275 return c->qtTrId("cutelyst-valemail-diag-mxdisabled");
1276 case DnsNoRecordFound:
1277 //% "Could neither find an MX record nor an A record for this address’ domain."
1278 return c->qtTrId("cutelyst-valemail-diag-noarec");
1279 case DnsErrorTimeout:
1280 //% "Timeout while performing DNS check for address’ domain."
1281 return c->qtTrId("cutelyst-valemail-diag-dnstimeout");
1282 case DnsError:
1283 //% "Error while performing DNS check for address’ domain."
1284 return c->qtTrId("cutelyst-valemail-diag-dnserror");
1285 case RFC5321TLD:
1286 //% "Address is valid but at a Top Level Domain."
1287 return c->qtTrId("cutelyst-valemail-diag-rfc5321tld");
1288 case RFC5321TLDNumeric:
1289 //% "Address is valid but the Top Level Domain begins with a number."
1290 return c->qtTrId("cutelyst-valemail-diag-rfc5321tldnumeric");
1292 //% "Address is valid but contains a quoted string."
1293 return c->qtTrId("cutelyst-valemail-diag-rfc5321quotedstring");
1295 //% "Address is valid but uses an IP address instead of a domain name."
1296 return c->qtTrId("cutelyst-valemail-diag-rfc5321addressliteral");
1298 //% "Address is valid but uses an IP address that contains a :: only "
1299 //% "eliding one zero group. All implementations must accept and be "
1300 //% "able to handle any legitimate RFC 4291 format."
1301 return c->qtTrId("cutelyst-valemail-diag-rfc5321ipv6deprecated");
1302 case CFWSComment:
1303 //% "Address contains comments."
1304 return c->qtTrId("cutelyst-valemail-diag-cfwscomment");
1305 case CFWSFWS:
1306 //% "Address contains folding white spaces like line breaks."
1307 return c->qtTrId("cutelyst-valemail-diag-cfwsfws");
1309 //% "The local part is in a deprecated form."
1310 return c->qtTrId("cutelyst-valemail-diag-deprecatedlocalpart");
1311 case DeprecatedFWS:
1312 //% "Address contains an obsolete form of folding white spaces."
1313 return c->qtTrId("cutelyst-valemail-diag-deprecatedfws");
1314 case DeprecatedQText:
1315 //% "A quoted string contains a deprecated character."
1316 return c->qtTrId("cutelyst-valemail-diag-deprecatedqtext");
1317 case DeprecatedQP:
1318 //% "A quoted pair contains a deprecated character."
1319 return c->qtTrId("cutelyst-valemail-diag-deprecatedqp");
1320 case DeprecatedComment:
1321 //% "Address contains a comment in a position that is deprecated."
1322 return c->qtTrId("cutelyst-valemail-diag-deprecatedcomment");
1323 case DeprecatedCText:
1324 //% "A comment contains a deprecated character."
1325 return c->qtTrId("cutelyst-valemail-diag-deprecatedctext");
1327 //% "Address contains a comment or folding white space around the @ sign."
1328 return c->qtTrId("cutelyst-valemail-diag-cfwsnearat");
1329 case RFC5322Domain:
1330 //% "Address is RFC 5322 compliant but contains domain characters that "
1331 //% "are not allowed by DNS."
1332 return c->qtTrId("cutelyst-valemail-diag-rfc5322domain");
1333 case RFC5322TooLong:
1334 //% "The address exceeds the maximum allowed length of %1 characters."
1335 return c->qtTrId("cutelyst-valemail-diag-rfc5322toolong")
1336 .arg(c->locale().toString(ValidatorEmailPrivate::maxMailboxLength));
1338 //% "The local part of the address exceeds the maximum allowed length "
1339 //% "of %1 characters."
1340 return c->qtTrId("cutelyst-valemail-diag-rfc5322localtoolong")
1341 .arg(c->locale().toString(ValidatorEmailPrivate::maxLocalPartLength));
1343 //% "The domain part exceeds the maximum allowed length of %1 characters."
1344 return c->qtTrId("cutelyst-valemail-diag-rfc5322domaintoolong")
1345 .arg(c->locale().toString(ValidatorEmailPrivate::maxDomainLength));
1347 //% "One of the labels/sections in the domain part exceeds the maximum allowed "
1348 //% "length of %1 characters."
1349 return c->qtTrId("cutelyst-valemail-diag-rfc5322labeltoolong")
1350 .arg(c->locale().toString(ValidatorEmailPrivate::maxDnsLabelLength));
1352 //% "The domain literal is not a valid RFC 5321 address literal."
1353 return c->qtTrId("cutelyst-valemail-diag-rfc5322domainliteral");
1355 //% "The domain literal is not a valid RFC 5321 domain literal and it "
1356 //% "contains obsolete characters."
1357 return c->qtTrId("cutelyst-valemail-diag-rfc5322domlitobsdtext");
1359 //% "The IPv6 literal address contains the wrong number of groups."
1360 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6groupcount");
1362 //% "The IPv6 literal address contains too many :: sequences."
1363 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv62x2xcolon");
1364 case RFC5322IPv6BadChar:
1365 //% "The IPv6 address contains an illegal group of characters."
1366 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6badchar");
1368 //% "The IPv6 address has too many groups."
1369 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6maxgroups");
1371 //% "The IPv6 address starts with a single colon."
1372 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonstart");
1374 //% "The IPv6 address ends with a single colon."
1375 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonend");
1377 //% "A domain literal contains a character that is not allowed."
1378 return c->qtTrId("cutelyst-valemail-diag-errexpectingdtext");
1379 case ErrorNoLocalPart:
1380 //% "Address has no local part."
1381 return c->qtTrId("cutelyst-valemail-diag-errnolocalpart");
1382 case ErrorNoDomain:
1383 //% "Address has no domain part."
1384 return c->qtTrId("cutelyst-valemail-diag-errnodomain");
1386 //% "The address must not contain consecutive dots."
1387 return c->qtTrId("cutelyst-valemail-diag-errconsecutivedots");
1389 //% "Address contains text after a comment or folding white space."
1390 return c->qtTrId("cutelyst-valemail-diag-erratextaftercfws");
1391 case ErrorATextAfterQS:
1392 //% "Address contains text after a quoted string."
1393 return c->qtTrId("cutelyst-valemail-diag-erratextafterqs");
1395 //% "Extra characters were found after the end of the domain literal."
1396 return c->qtTrId("cutelyst-valemail-diag-erratextafterdomlit");
1398 //% "The Address contains a character that is not allowed in a quoted pair."
1399 return c->qtTrId("cutelyst-valemail-diag-errexpectingqpair");
1401 //% "Address contains a character that is not allowed."
1402 return c->qtTrId("cutelyst-valemail-diag-errexpectingatext");
1404 //% "A quoted string contains a character that is not allowed."
1405 return c->qtTrId("cutelyst-valemail-diag-errexpectingqtext");
1407 //% "A comment contains a character that is not allowed."
1408 return c->qtTrId("cutelyst-valemail-diag-errexpectingctext");
1409 case ErrorBackslashEnd:
1410 //% "The address can not end with a backslash."
1411 return c->qtTrId("cutelyst-valemail-diag-errbackslashend");
1412 case ErrorDotStart:
1413 //% "Neither part of the address may begin with a dot."
1414 return c->qtTrId("cutelyst-valemail-diag-errdotstart");
1415 case ErrorDotEnd:
1416 //% "Neither part of the address may end with a dot."
1417 return c->qtTrId("cutelyst-valemail-diag-errdotend");
1419 //% "A domain or subdomain can not begin with a hyphen."
1420 return c->qtTrId("cutelyst-valemail-diag-errdomainhyphenstart");
1422 //% "A domain or subdomain can not end with a hyphen."
1423 return c->qtTrId("cutelyst-valemail-diag-errdomainhyphenend");
1425 //% "Unclosed quoted string. (Missing double quotation mark)"
1426 return c->qtTrId("cutelyst-valemail-diag-errunclosedquotedstr");
1428 //% "Unclosed comment. (Missing closing parentheses)"
1429 return c->qtTrId("cutelyst-valemail-diag-errunclosedcomment");
1431 //% "Domain literal is missing its closing bracket."
1432 return c->qtTrId("cutelyst-valemail-diag-erruncloseddomliteral");
1433 case ErrorFWSCRLFx2:
1434 //% "Folding white space contains consecutive line break sequences (CRLF)."
1435 return c->qtTrId("cutelyst-valemail-diag-errfwscrlfx2");
1436 case ErrorFWSCRLFEnd:
1437 //% "Folding white space ends with a line break sequence (CRLF)."
1438 return c->qtTrId("cutelyst-valemail-diag-errfwscrlfend");
1439 case ErrorCRnoLF:
1440 //% "Address contains a carriage return (CR) that is not followed by a "
1441 //% "line feed (LF)."
1442 return c->qtTrId("cutelyst-valemail-diag-errcrnolf");
1443 case ErrorFatal:
1444 //% "A fatal error occurred while parsing the address."
1445 return c->qtTrId("cutelyst-valemail-diag-errfatal");
1446 default:
1447 return {};
1448 }
1449
1450 } else {
1451
1452 switch (diagnose) {
1453 case ValidAddress:
1454 //% "The address in the “%1” field is valid. Please note that this does not mean "
1455 //% "that both the address and the domain actually exist. This address could be "
1456 //% "issued by the domain owner without breaking the rules of any RFCs."
1457 return c->qtTrId("cutelyst-valemail-diag-valid-label").arg(label);
1458 case DnsWarnNoMxRecord:
1459 //% "Could not find an MX record for the address’ domain in the “%1” "
1460 //% "field but an A record exists."
1461 return c->qtTrId("cutelyst-valemail-diag-nomx-label").arg(label);
1462 case DnsMxDisabled:
1463 //% "MX for the address’ domain in the “%1” field is explicitely disabled."
1464 return c->qtTrId("cutelyst-valemail-diag-mxdisabled-label").arg(label);
1465 case DnsNoRecordFound:
1466 //% "Could neither find an MX record nor an A record for the address’ "
1467 //% "domain in the “%1” field."
1468 return c->qtTrId("cutelyst-valemail-diag-noarec-label").arg(label);
1469 case DnsErrorTimeout:
1470 //% "Timeout while performing DNS check for address’ domain in the “%1” field."
1471 return c->qtTrId("cutelyst-valemail-diag-dnstimeout-label").arg(label);
1472 case DnsError:
1473 //% "Error while performing DNS check for address’ domain in the “%1” field."
1474 return c->qtTrId("cutelyst-valemail-diag-dnserror-label").arg(label);
1475 case RFC5321TLD:
1476 //% "The address in the “%1” field is valid but at a Top Level Domain."
1477 return c->qtTrId("cutelyst-valemail-diag-rfc5321tld-label").arg(label);
1478 case RFC5321TLDNumeric:
1479 //% "The address in the “%1” field is valid but the Top Level Domain "
1480 //% "begins with a number."
1481 return c->qtTrId("cutelyst-valemail-diag-rfc5321tldnumeric-label").arg(label);
1483 //% "The address in the “%1” field is valid but contains a quoted string."
1484 return c->qtTrId("cutelyst-valemail-diag-rfc5321quotedstring-label").arg(label);
1486 //% "The address in the “%1” field is valid but uses an IP address "
1487 //% "instead of a domain name."
1488 return c->qtTrId("cutelyst-valemail-diag-rfc5321addressliteral-label").arg(label);
1490 //% "The address in the “%1” field is valid but uses an IP address that "
1491 //% "contains a :: only eliding one zero group. All implementations "
1492 //% "must accept and be able to handle any legitimate RFC 4291 format."
1493 return c->qtTrId("cutelyst-valemail-diag-rfc5321ipv6deprecated-label").arg(label);
1494 case CFWSComment:
1495 //% "The address in the “%1” field contains comments."
1496 return c->qtTrId("cutelyst-valemail-diag-cfwscomment-label").arg(label);
1497 case CFWSFWS:
1498 //% "The address in the “%1” field contains folding white spaces like "
1499 //% "line breaks."
1500 return c->qtTrId("cutelyst-valemail-diag-cfwsfws-label").arg(label);
1502 //% "The local part of the address in the “%1” field is in a deprecated form."
1503 return c->qtTrId("cutelyst-valemail-diag-deprecatedlocalpart-label").arg(label);
1504 case DeprecatedFWS:
1505 //% "The address in the “%1” field contains an obsolete form of folding "
1506 //% "white spaces."
1507 return c->qtTrId("cutelyst-valemail-diag-deprecatedfws-label").arg(label);
1508 case DeprecatedQText:
1509 //% "A quoted string in the address in the “%1” field contains a "
1510 //% "deprecated character."
1511 return c->qtTrId("cutelyst-valemail-diag-deprecatedqtext-label").arg(label);
1512 case DeprecatedQP:
1513 //% "A quoted pair in the address in the “%1” field contains a "
1514 //% "deprecate character."
1515 return c->qtTrId("cutelyst-valemail-diag-deprecatedqp-label").arg(label);
1516 case DeprecatedComment:
1517 //% "The address in the “%1” field contains a comment in a position "
1518 //% "that is deprecated."
1519 return c->qtTrId("cutelyst-valemail-diag-deprecatedcomment-label").arg(label);
1520 case DeprecatedCText:
1521 //% "A comment in the address in the “%1” field contains a deprecated character."
1522 return c->qtTrId("cutelyst-valemail-diag-deprecatedctext-label").arg(label);
1524 //% "The address in the “%1” field contains a comment or folding white "
1525 //% "space around the @ sign."
1526 return c->qtTrId("cutelyst-valemail-diag-cfwsnearat-label").arg(label);
1527 case RFC5322Domain:
1528 //% "The address in the “%1” field is RFC 5322 compliant but contains "
1529 //% "domain characters that are not allowed by DNS."
1530 return c->qtTrId("cutelyst-valemail-diag-rfc5322domain-label").arg(label);
1531 case RFC5322TooLong:
1532 //% "The address in the “%1” field exceeds the maximum allowed length "
1533 //% "of %2 characters."
1534 return c->qtTrId("cutelyst-valemail-diag-rfc5322toolong-label")
1535 .arg(label, c->locale().toString(ValidatorEmailPrivate::maxMailboxLength));
1537 //% "The local part of the address in the “%1” field exceeds the maximum allowed "
1538 //% "length of %2 characters."
1539 return c->qtTrId("cutelyst-valemail-diag-rfc5322localtoolong-label")
1540 .arg(label, c->locale().toString(ValidatorEmailPrivate::maxLocalPartLength));
1542 //% "The domain part of the address in the “%1” field exceeds the maximum "
1543 //% "allowed length of %2 characters."
1544 return c->qtTrId("cutelyst-valemail-diag-rfc5322domaintoolong-label")
1545 .arg(label, c->locale().toString(ValidatorEmailPrivate::maxDomainLength));
1547 //% "The domain part of the address in the “%1” field contains an element/section "
1548 //% "that exceeds the maximum allowed lenght of %2 characters."
1549 return c->qtTrId("cutelyst-valemail-diag-rfc5322labeltoolong-label")
1550 .arg(label, c->locale().toString(ValidatorEmailPrivate::maxDnsLabelLength));
1552 //% "The domain literal of the address in the “%1” field is not a valid "
1553 //% "RFC 5321 address literal."
1554 return c->qtTrId("cutelyst-valemail-diag-rfc5322domainliteral-label").arg(label);
1556 //% "The domain literal of the address in the “%1” field is not a valid "
1557 //% "RFC 5321 domain literal and it contains obsolete characters."
1558 return c->qtTrId("cutelyst-valemail-diag-rfc5322domlitobsdtext-label").arg(label);
1560 //% "The IPv6 literal of the address in the “%1” field contains the "
1561 //% "wrong number of groups."
1562 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6groupcount-label").arg(label);
1564 //% "The IPv6 literal of the address in the “%1” field contains too "
1565 //% "many :: sequences."
1566 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv62x2xcolon-label").arg(label);
1567 case RFC5322IPv6BadChar:
1568 //% "The IPv6 address of the email address in the “%1” field contains "
1569 //% "an illegal group of characters."
1570 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6badchar-label").arg(label);
1572 //% "The IPv6 address of the email address in the “%1” field has too many groups."
1573 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6maxgroups-label").arg(label);
1575 //% "The IPv6 address of the email address in the “%1” field starts "
1576 //% "with a single colon."
1577 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonstart-label").arg(label);
1579 //% "The IPv6 address of the email address in the “%1” field ends with "
1580 //% "a single colon."
1581 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonend-label").arg(label);
1583 //% "A domain literal of the address in the “%1” field contains a "
1584 //% "character that is not allowed."
1585 return c->qtTrId("cutelyst-valemail-diag-errexpectingdtext-label").arg(label);
1586 case ErrorNoLocalPart:
1587 //% "The address in the “%1” field has no local part."
1588 return c->qtTrId("cutelyst-valemail-diag-errnolocalpart-label").arg(label);
1589 case ErrorNoDomain:
1590 //% "The address in the “%1” field has no domain part."
1591 return c->qtTrId("cutelyst-valemail-diag-errnodomain-label").arg(label);
1593 //% "The address in the “%1” field must not contain consecutive dots."
1594 return c->qtTrId("cutelyst-valemail-diag-errconsecutivedots-label").arg(label);
1596 //% "The address in the “%1” field contains text after a comment or "
1597 //% "folding white space."
1598 return c->qtTrId("cutelyst-valemail-diag-erratextaftercfws-label").arg(label);
1599 case ErrorATextAfterQS:
1600 //% "The address in the “%1” field contains text after a quoted string."
1601 return c->qtTrId("cutelyst-valemail-diag-erratextafterqs-label").arg(label);
1603 //% "Extra characters were found after the end of the domain literal of "
1604 //% "the address in the “%1” field."
1605 return c->qtTrId("cutelyst-valemail-diag-erratextafterdomlit-label").arg(label);
1607 //% "The address in the “%1” field contains a character that is not "
1608 //% "allowed in a quoted pair."
1609 return c->qtTrId("cutelyst-valemail-diag-errexpectingqpair-label").arg(label);
1611 //% "The address in the “%1” field contains a character that is not allowed."
1612 return c->qtTrId("cutelyst-valemail-diag-errexpectingatext-label").arg(label);
1614 //% "A quoted string in the address in the “%1” field contains a "
1615 //% "character that is not allowed."
1616 return c->qtTrId("cutelyst-valemail-diag-errexpectingqtext-label").arg(label);
1618 //% "A comment in the address in the “%1” field contains a character "
1619 //% "that is not allowed."
1620 return c->qtTrId("cutelyst-valemail-diag-errexpectingctext-label").arg(label);
1621 case ErrorBackslashEnd:
1622 //% "The address in the “%1” field can't end with a backslash."
1623 return c->qtTrId("cutelyst-valemail-diag-errbackslashend-label").arg(label);
1624 case ErrorDotStart:
1625 //% "Neither part of the address in the “%1” field may begin with a dot."
1626 return c->qtTrId("cutelyst-valemail-diag-errdotstart-label").arg(label);
1627 case ErrorDotEnd:
1628 //% "Neither part of the address in the “%1” field may end with a dot."
1629 return c->qtTrId("cutelyst-valemail-diag-errdotend-label").arg(label);
1631 //% "A domain or subdomain of the address in the “%1” field can not "
1632 //% "begin with a hyphen."
1633 return c->qtTrId("cutelyst-valemail-diag-errdomainhyphenstart-label").arg(label);
1635 //% "A domain or subdomain of the address in the “%1” field can not end "
1636 //% "with a hyphen."
1637 return c->qtTrId("cutelyst-valemail-diag-errdomainhyphenend-label").arg(label);
1639 //% "Unclosed quoted string in the address in the “%1” field. (Missing "
1640 //% "double quotation mark)"
1641 return c->qtTrId("cutelyst-valemail-diag-errunclosedquotedstr-label").arg(label);
1643 //% "Unclosed comment in the address in the “%1” field. (Missing "
1644 //% "closing parentheses)"
1645 return c->qtTrId("cutelyst-valemail-diag-errunclosedcomment-label").arg(label);
1647 //% "Domain literal of the address in the “%1” field is missing its "
1648 //% "closing bracket."
1649 return c->qtTrId("cutelyst-valemail-diag-erruncloseddomliteral-label").arg(label);
1650 case ErrorFWSCRLFx2:
1651 //% "Folding white space in the address in the “%1” field contains "
1652 //% "consecutive line break sequences (CRLF)."
1653 return c->qtTrId("cutelyst-valemail-diag-errfwscrlfx2-label").arg(label);
1654 case ErrorFWSCRLFEnd:
1655 //% "Folding white space in the address in the “%1” field ends with a "
1656 //% "line break sequence (CRLF)."
1657 return c->qtTrId("cutelyst-valemail-diag-errfwscrlfend-label").arg(label);
1658 case ErrorCRnoLF:
1659 //% "The address in the “%1” field contains a carriage return (CR) that "
1660 //% "is not followed by a line feed (LF)."
1661 return c->qtTrId("cutelyst-valemail-diag-errcrnolf-label").arg(label);
1662 case ErrorFatal:
1663 //% "A fatal error occurred while parsing the address in the “%1” field."
1664 return c->qtTrId("cutelyst-valemail-diag-errfatal-label").arg(label);
1665 default:
1666 return {};
1667 }
1668 }
1669}
1670
1672{
1673 if (label.isEmpty()) {
1674 switch (category) {
1675 case Valid:
1676 //% "Address is valid."
1677 return c->qtTrId("cutelyst-valemail-cat-valid");
1678 case DNSWarn:
1679 //% "Address is valid but there is a warning about the DNS."
1680 return c->qtTrId("cutelyst-valemail-cat-dnswarn");
1681 case DNSFailed:
1682 //% "Address is valid but a DNS check was not successful."
1683 return c->qtTrId("cutelyst-valemail-cat-dnsfailed");
1684 case RFC5321:
1685 //% "Address is valid for SMTP but has unusual elements."
1686 return c->qtTrId("cutelyst-valemail-cat-rfc5321");
1687 case CFWS:
1688 //% "Address is valid within the message but can not be used unmodified "
1689 //% "for the envelope."
1690 return c->qtTrId("cutelyst-valemail-cat-cfws");
1691 case Deprecated:
1692 //% "Address contains deprecated elements but may still be valid in "
1693 //% "restricted contexts."
1694 return c->qtTrId("cutelyst-valemail-cat-deprecated");
1695 case RFC5322:
1696 //% "The address is only valid according to the broad definition of RFC "
1697 //% "5322. It is otherwise invalid."
1698 return c->qtTrId("cutelyst-valemail-cat-rfc5322");
1699 default:
1700 //% "Address is invalid for any purpose."
1701 return c->qtTrId("cutelyst-valemail-cat-invalid");
1702 }
1703 } else {
1704 switch (category) {
1705 case Valid:
1706 //% "The address in the “%1” field is valid."
1707 return c->qtTrId("cutelyst-valemail-cat-valid-label").arg(label);
1708 case DNSWarn:
1709 //% "The address in the “%1” field is valid but there are warnings about the DNS."
1710 return c->qtTrId("cutelyst-valemail-cat-dnswarn-label").arg(label);
1711 case DNSFailed:
1712 //% "The address in the “%1” field is valid but a DNS check was not successful."
1713 return c->qtTrId("cutelyst-valemail-cat-dnsfailed-label").arg(label);
1714 case RFC5321:
1715 //% "The address in the “%1” field is valid for SMTP but has unusual elements."
1716 return c->qtTrId("cutelyst-valemail-cat-rfc5321-label").arg(label);
1717 case CFWS:
1718 //% "The address in the “%1” field is valid within the message but can "
1719 //% "not be used unmodified for the envelope."
1720 return c->qtTrId("cutelyst-valemail-cat-cfws-label").arg(label);
1721 case Deprecated:
1722 //% "The address in the “%1” field contains deprecated elements but may "
1723 //% "still be valid in restricted contexts."
1724 return c->qtTrId("cutelyst-valemail-cat-deprecated-label").arg(label);
1725 case RFC5322:
1726 //% "The address in the “%1” field is only valid according to the broad "
1727 //% "definition of RFC 5322. It is otherwise invalid."
1728 return c->qtTrId("cutelyst-valemail-cat-rfc5322-label").arg(label);
1729 default:
1730 //% "The address in the “%1” field is invalid for any purpose."
1731 return c->qtTrId("cutelyst-valemail-cat-invalid-label").arg(label);
1732 }
1733 }
1734}
1735
1737{
1738 Category cat = Error;
1739
1740 const auto diag = static_cast<int>(diagnose);
1741
1742 if (diag < static_cast<int>(Valid)) {
1743 cat = Valid;
1744 } else if (diag < static_cast<int>(DNSWarn)) {
1745 cat = DNSWarn;
1746 } else if (diag < static_cast<int>(DNSFailed)) {
1747 cat = DNSFailed;
1748 } else if (diag < static_cast<int>(RFC5321)) {
1749 cat = RFC5321;
1750 } else if (diag < static_cast<int>(CFWS)) {
1751 cat = CFWS;
1752 } else if (diag < static_cast<int>(Deprecated)) {
1753 cat = Deprecated;
1754 } else if (diag < static_cast<int>(RFC5322)) {
1755 cat = RFC5322;
1756 }
1757
1758 return cat;
1759}
1760
1762{
1763 return categoryString(c, category(diagnose), label);
1764}
1765
1767 Category threshold,
1768 Options options,
1770{
1771 if (options.testFlag(CheckDNS)) {
1772 qCWarning(C_VALIDATOR) << "ValidatorEmail: using the CheckDNS option on validate() is"
1773 << "not supported anymore. Use validateCb().";
1774 }
1775
1776 ValidatorEmailDiagnoseStruct diag;
1777 bool ret = ValidatorEmailPrivate::checkEmail(email, options, threshold, &diag);
1778
1779 if (diagnoses) {
1780 *diagnoses = diag.returnStatus;
1781 }
1782
1783 return ret;
1784}
1785
1787 const QString &email,
1788 Category threshold,
1789 Options options,
1790 std::function<void(bool isValid, const QString &cleanedEmail, const QList<Diagnose> &diagnoses)>
1791 cb)
1792{
1793 ValidatorEmailDiagnoseStruct diag;
1794 const bool ret = ValidatorEmailPrivate::checkEmail(email, options, threshold, &diag);
1795
1796 if (ret && options.testFlag(ValidatorEmail::CheckDNS)) {
1797
1798 if (diag.domain.isEmpty()) {
1799
1800 diag.returnStatus.append(DnsError);
1801 diag.sortReturnStatus();
1802 cb(diag.isBelowThreshold(threshold), diag.cleanedEmail, diag.returnStatus);
1803
1804 } else {
1805
1806 auto mxLookup = new QDnsLookup{QDnsLookup::MX, diag.domain};
1808 mxLookup, &QDnsLookup::finished, [mxLookup, cb, diag, threshold]() mutable {
1809 if (mxLookup->error() == QDnsLookup::NoError &&
1810 !mxLookup->mailExchangeRecords().empty()) {
1811 const auto records = mxLookup->mailExchangeRecords();
1812 for (const auto &h : records) {
1813 if (h.preference() > 0 && !h.exchange().isEmpty()) {
1814 // these both values might have already been set, but as we found a
1815 // valid MX, they are no errors
1816 diag.returnStatus.removeAll(RFC5321TLD);
1817 diag.returnStatus.removeAll(RFC5321TLDNumeric);
1818 break;
1819 } else if (h.preference() == 0 &&
1820 (h.exchange().isEmpty() || h.exchange() == "."_L1)) {
1821 // this is a Null MX that explicitely indicates, that the domain
1822 // accepts no mail
1823 diag.returnStatus.append(DnsMxDisabled);
1824 break;
1825 }
1826 }
1827 diag.sortReturnStatus();
1828 cb(diag.isBelowThreshold(threshold), diag.cleanedEmail, diag.returnStatus);
1829 } else {
1830 if (mxLookup->error() == QDnsLookup::NoError) {
1831
1832 auto aLookup = new QDnsLookup{QDnsLookup::A, diag.domain};
1833 QObject::connect(aLookup,
1835 [aLookup, cb, diag, threshold]() mutable {
1836 if (aLookup->error() == QDnsLookup::NoError) {
1837 if (!aLookup->hostAddressRecords().empty()) {
1838 diag.returnStatus.append(DnsWarnNoMxRecord);
1839 } else {
1840 diag.returnStatus.append(DnsNoRecordFound);
1841 }
1842 diag.sortReturnStatus();
1843 cb(diag.isBelowThreshold(threshold),
1844 diag.cleanedEmail,
1845 diag.returnStatus);
1846 } else {
1847 switch (aLookup->error()) {
1849 diag.returnStatus.append(DnsNoRecordFound);
1850 break;
1852 diag.returnStatus.append(DnsErrorTimeout);
1853 break;
1854 default:
1855 diag.returnStatus.append(DnsError);
1856 break;
1857 }
1858 diag.sortReturnStatus();
1859 cb(diag.isBelowThreshold(threshold),
1860 diag.cleanedEmail,
1861 diag.returnStatus);
1862 }
1863 aLookup->deleteLater();
1864 });
1866 ValidatorEmailPrivate::dnsLookupTimeout, aLookup, &QDnsLookup::abort);
1867 aLookup->lookup();
1868
1869 } else {
1870 switch (mxLookup->error()) {
1872 diag.returnStatus.append(DnsNoRecordFound);
1873 break;
1875 diag.returnStatus.append(DnsErrorTimeout);
1876 break;
1877 default:
1878 diag.returnStatus.append(DnsError);
1879 break;
1880 }
1881 diag.sortReturnStatus();
1882 cb(diag.isBelowThreshold(threshold), diag.cleanedEmail, diag.returnStatus);
1883 }
1884 }
1885 mxLookup->deleteLater();
1886 });
1888 ValidatorEmailPrivate::dnsLookupTimeout, mxLookup, &QDnsLookup::abort);
1889 mxLookup->lookup();
1890 }
1891
1892 } else {
1893 cb(ret, diag.cleanedEmail, diag.returnStatus);
1894 }
1895}
1896
1897void ValidatorEmailDiagnoseStruct::sortReturnStatus()
1898{
1899 std::ranges::sort(returnStatus, std::greater<>());
1900 finalStatus = returnStatus.at(0);
1901}
1902
1903bool ValidatorEmailDiagnoseStruct::isBelowThreshold(ValidatorEmail::Category threshold) const
1904{
1905 return static_cast<int>(finalStatus) < static_cast<int>(threshold);
1906}
1907
1908#include "moc_validatoremail.cpp"
The Cutelyst Context.
Definition context.h:42
QLocale locale() const noexcept
Definition context.cpp:463
QString qtTrId(const char *id, int n=-1) const
Definition context.h:657
Checks if the value is a valid email address according to specific RFCs.
static Category category(Diagnose diagnose)
Category
Validation category, used as threshold to define valid addresses.
ValidatorEmail(const QString &field, Category threshold=RFC5321, Options options=NoOption, const ValidatorMessages &messages={}, const QString &defValKey={})
Diagnose
Single diagnose values that show why an address is not valid.
QString genericValidationError(Context *c, const QVariant &errorData=QVariant()) const override
static QString categoryString(Context *c, Category category, const QString &label={})
static QString diagnoseString(Context *c, Diagnose diagnose, const QString &label={})
Base class for all validator rules.
QString validationError(Context *c, const QVariant &errorData={}) const
QString label(const Context *c) const
std::function< void(ValidatorReturnType &&result)> ValidatorRtFn
Void callback function for validator rules that processes the ValidatorReturnType.
void defaultValue(Context *c, ValidatorReturnType *result) const
QString value(const ParamsMultiMap &params) const
static void validateCb(const QString &email, Category threshold, Options options, std::function< void(bool isValid, const QString &cleanedEmail, const QList< Diagnose > &diagnoses)> cb)
Checks if the email is a valid address according to the Category given in the threshold.
static bool validate(const QString &email, Category threshold=RFC5321, Options options=NoOption, QList< Diagnose > *diagnoses=nullptr)
Returns true if email is a valid address according to the Category given in the threshold.
The Cutelyst namespace holds all public Cutelyst API.
CarriageReturn
bool isLetterOrNumber(char32_t ucs4)
char16_t & unicode()
void abort()
void finished()
void append(QList::parameter_type value)
bool contains(const AT &value) const const
qsizetype size() const const
QString toString(QDate date, QLocale::FormatType format) const const
void clear()
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString captured(QStringView name) const const
bool hasMatch() const const
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
void clear()
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
QString fromLatin1(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar c, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
void push_back(QChar ch)
QString right(qsizetype n) const const
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
const QChar * unicode() const const
QByteArray toAce(const QString &domain, QUrl::AceProcessingOptions options)
void setValue(QVariant &&value)
T value() const const
Stores custom error messages and the input field label.
Contains the result of a single input parameter validation.