cutelyst 5.0.1
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 + u'@' + diag.literal);
56 } else {
57 result.value.setValue<QString>(diag.localpart + u'@' + 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
102QString ValidatorEmail::genericValidationError(Context *c, const QVariant &errorData) const
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{
116 QList<ValidatorEmail::Diagnose> returnStatus{ValidatorEmail::ValidAddress};
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(u'@');
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 + u'@' + 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 == u'(') { // comment
196 if (elementLen == 0) {
197 // Comments are OK at the beginning of an element
198 returnStatus.push_back((elementCount == 0) ? ValidatorEmail::CFWSComment
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 == u'.') { // Next dot-atom element
209 if (elementLen == 0) {
210 // Another dot, already?
211 returnStatus.push_back((elementCount == 0)
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 == u'"') {
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)
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
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 == u'@') {
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 == u'(') { // 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)
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 == u'.') { // next dot-atom element
419 if (elementLen == 0) {
420 // another dot, already?
421 returnStatus.push_back((elementCount == 0)
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 == u'[') { // 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)
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 == u'-') {
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 == u']') { // End of domain literal
571 if (static_cast<int>(*std::ranges::max_element(returnStatus.constBegin(),
572 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) +
638 QLatin1String(
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),
648 QLatin1String(
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(u':');
655 qsizetype groupCount = matchesIP.size();
656 index = ipv6.indexOf(u"::");
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(u"::")) {
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] == u':') ||
682 (ipv6[0] == u':' && ipv6[1] != u':')) {
683 returnStatus.push_back(
684 ValidatorEmail::RFC5322IPv6ColonStart); // Address starts with a
685 // single colon
686 } else if (ipv6.right(2).at(1) == u':' && ipv6.right(2).at(0) != u':') {
687 returnStatus.push_back(
688 ValidatorEmail::RFC5322IPv6ColonEnd); // Address ends with a single
689 // colon
690 } else {
691 int unmatchedChars =
692 std::ranges::count_if(matchesIP, [](const QString &ip) {
693 return !ip.contains(ValidatorEmailPrivate::ipv6PartRegex);
694 });
695
696 if (unmatchedChars != 0) {
697 returnStatus.push_back(ValidatorEmail::RFC5322IPv6BadChar);
698 } else {
699 returnStatus.push_back(ValidatorEmail::RFC5321AddressLiteral);
700 }
701 }
702 }
703
704 } else {
705 returnStatus.push_back(ValidatorEmail::RFC5322DomainLiteral);
706 }
707
708 parseDomain += token;
709 atomListDomain[elementCount] += token;
710 elementLen++;
711 contextPrior = context;
712 context = contextStack.takeLast();
713 } else if (token == u'\\') {
714 returnStatus.push_back(ValidatorEmail::RFC5322DomLitOBSDText);
715 contextStack.push_back(context);
716 context = ContextQuotedPair;
717 } else if ((token == QChar(QChar::CarriageReturn)) || (token == QChar(QChar::Space)) ||
718 (token == QChar(QChar::Tabulation))) { // Folding White Space
719 if ((token == QChar(QChar::CarriageReturn)) &&
720 ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed)))) {
721 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF); // Fatal error
722 break;
723 }
724
725 returnStatus.push_back(ValidatorEmail::CFWSFWS);
726 contextStack.push_back(context);
727 context = ContextFWS;
728 tokenPrior = token;
729
730 } else { // dtext
731 // https://tools.ietf.org/html/rfc5322#section-3.4.1
732 // dtext = %d33-90 / ; Printable US-ASCII
733 // %d94-126 / ; characters not including
734 // obs-dtext ; "[", "]", or "\"
735 //
736 // obs-dtext = obs-NO-WS-CTL / quoted-pair
737 //
738 // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
739 // %d11 / ; characters that do not
740 // %d12 / ; include the carriage
741 // %d14-31 / ; return, line feed, and
742 // %d127 ; white space characters
743 const char16_t uni = token.unicode();
744
745 // CR, LF, SP & HTAB have already been parsed above
746 if ((uni > ValidatorEmailPrivate::asciiEnd) || (uni == 0) || (uni == u'[')) {
747 returnStatus.push_back(ValidatorEmail::ErrorExpectingDText); // Fatal error
748 break;
749 } else if ((uni < ValidatorEmailPrivate::asciiExclamationMark) ||
750 (uni == ValidatorEmailPrivate::asciiEnd)) {
751 returnStatus.push_back(ValidatorEmail::RFC5322DomLitOBSDText);
752 }
753
754 parseLiteral += token;
755 parseDomain += token;
756 atomListDomain[elementCount] += token;
757 elementLen++;
758 }
759 } break;
760 //-------------------------------------------------------------
761 // Quoted string
762 //-------------------------------------------------------------
763 case ContextQuotedString:
764 {
765 // https://tools.ietf.org/html/rfc5322#section-3.2.4
766 // quoted-string = [CFWS]
767 // DQUOTE *([FWS] qcontent) [FWS] DQUOTE
768 // [CFWS]
769 //
770 // qcontent = qtext / quoted-pair
771 if (token == u'\\') { // Quoted pair
772 contextStack.push_back(context);
773 context = ContextQuotedPair;
774 } else if ((token == QChar(QChar::CarriageReturn)) ||
775 (token == QChar(QChar::Tabulation))) { // Folding White Space
776 // Inside a quoted string, spaces are allowed as regular characters.
777 // It's only FWS if we include HTAB or CRLF
778 if ((token == QChar(QChar::CarriageReturn)) &&
779 ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed)))) {
780 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF);
781 break;
782 }
783
784 // https://tools.ietf.org/html/rfc5322#section-3.2.2
785 // Runs of FWS, comment, or CFWS that occur between lexical tokens in a
786 // structured header field are semantically interpreted as a single
787 // space character.
788
789 // https://tools.ietf.org/html/rfc5322#section-3.2.4
790 // the CRLF in any FWS/CFWS that appears within the quoted-string [is]
791 // semantically "invisible" and therefore not part of the quoted-string
792
793 parseLocalPart += QChar(QChar::Space);
794 atomListLocalPart[elementCount] += QChar(QChar::Space);
795 elementLen++;
796
797 returnStatus.push_back(ValidatorEmail::CFWSFWS);
798 contextStack.push_back(context);
799 context = ContextFWS;
800 tokenPrior = token;
801 } else if (token == u'"') { // end of quoted string
802 parseLocalPart += token;
803 atomListLocalPart[elementCount] += token;
804 elementLen++;
805 contextPrior = context;
806 context = contextStack.takeLast();
807 } else { // qtext
808 // https://tools.ietf.org/html/rfc5322#section-3.2.4
809 // qtext = %d33 / ; Printable US-ASCII
810 // %d35-91 / ; characters not including
811 // %d93-126 / ; "\" or the quote character
812 // obs-qtext
813 //
814 // obs-qtext = obs-NO-WS-CTL
815 //
816 // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
817 // %d11 / ; characters that do not
818 // %d12 / ; include the carriage
819 // %d14-31 / ; return, line feed, and
820 // %d127 ; white space characters
821 const char16_t uni = token.unicode();
822
823 if (!allowUtf8Local) {
824 if ((uni > ValidatorEmailPrivate::asciiEnd) || (uni == 0) ||
825 (uni == ValidatorEmailPrivate::asciiLF)) {
826 returnStatus.push_back(ValidatorEmail::ErrorExpectingQText); // Fatal error
827 } else if ((uni < ValidatorRulePrivate::asciiSpace) ||
828 (uni == ValidatorEmailPrivate::asciiEnd)) {
829 returnStatus.push_back(ValidatorEmail::DeprecatedQText);
830 }
831 } else {
832 if (!token.isLetterOrNumber()) {
833 if ((uni > ValidatorEmailPrivate::asciiEnd) || (uni == 0) ||
834 (uni == ValidatorEmailPrivate::asciiLF)) {
835 returnStatus.push_back(
837 } else if ((uni < ValidatorRulePrivate::asciiSpace) ||
838 (uni == ValidatorEmailPrivate::asciiEnd)) {
839 returnStatus.push_back(ValidatorEmail::DeprecatedQText);
840 }
841 }
842 }
843
844 parseLocalPart += token;
845 atomListLocalPart[elementCount] += token;
846 elementLen++;
847 }
848
849 // https://tools.ietf.org/html/rfc5322#section-3.4.1
850 // If the
851 // string can be represented as a dot-atom (that is, it contains no
852 // characters other than atext characters or "." surrounded by atext
853 // characters), then the dot-atom form SHOULD be used and the quoted-
854 // string form SHOULD NOT be used.
855 // To do
856 } break;
857 //-------------------------------------------------------------
858 // Quoted pair
859 //-------------------------------------------------------------
860 case ContextQuotedPair:
861 {
862 // https://tools.ietf.org/html/rfc5322#section-3.2.1
863 // quoted-pair = ("\" (VCHAR / WSP)) / obs-qp
864 //
865 // VCHAR = %d33-126 ; visible (printing) characters
866 // WSP = SP / HTAB ; white space
867 //
868 // obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR)
869 //
870 // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
871 // %d11 / ; characters that do not
872 // %d12 / ; include the carriage
873 // %d14-31 / ; return, line feed, and
874 // %d127 ; white space characters
875 //
876 // i.e. obs-qp = "\" (%d0-8, %d10-31 / %d127)
877
878 const char16_t uni = token.unicode();
879
880 if (uni > ValidatorEmailPrivate::asciiEnd) {
881 returnStatus.push_back(ValidatorEmail::ErrorExpectingQpair); // Fatal error
882 } else if (((uni < ValidatorEmailPrivate::asciiUS) &&
883 (uni != ValidatorRulePrivate::asciiTab)) ||
884 (uni == ValidatorEmailPrivate::asciiEnd)) {
885 returnStatus.push_back(ValidatorEmail::DeprecatedQP);
886 }
887
888 // At this point we know where this qpair occurred so
889 // we could check to see if the character actually
890 // needed to be quoted at all.
891 // https://tools.ietf.org/html/rfc5321#section-4.1.2
892 // the sending system SHOULD transmit the
893 // form that uses the minimum quoting possible.
894
895 contextPrior = context;
896 context = contextStack.takeLast();
897
898 switch (context) {
899 case ContextComment:
900 break;
901 case ContextQuotedString:
902 parseLocalPart += u'\\';
903 parseLocalPart += token;
904 atomListLocalPart[elementCount] += u'\\';
905 atomListLocalPart[elementCount] += token;
906 elementLen += 2; // The maximum sizes specified by RFC 5321 are octet counts, so we
907 // must include the backslash
908 break;
909 case ComponentLiteral:
910 parseDomain += u'\\';
911 parseDomain += token;
912 atomListDomain[elementCount] += u'\\';
913 atomListDomain[elementCount] += token;
914 elementLen += 2; // The maximum sizes specified by RFC 5321 are octet counts, so we
915 // must include the backslash
916 break;
917 default:
918 returnStatus.push_back(ValidatorEmail::ErrorFatal);
919 qCCritical(C_VALIDATOR)
920 << "ValidatorEmail: Quoted pair logic invoked in an invalid context.";
921 break;
922 }
923 } break;
924 //-------------------------------------------------------------
925 // Comment
926 //-------------------------------------------------------------
927 case ContextComment:
928 {
929 // https://tools.ietf.org/html/rfc5322#section-3.2.2
930 // comment = "(" *([FWS] ccontent) [FWS] ")"
931 //
932 // ccontent = ctext / quoted-pair / comment
933 if (token == u'(') { // netsted comment
934 // nested comments are OK
935 contextStack.push_back(context);
936 context = ContextComment;
937 } else if (token == u')') {
938 contextPrior = context;
939 context = contextStack.takeLast();
940
941 // https://tools.ietf.org/html/rfc5322#section-3.2.2
942 // Runs of FWS, comment, or CFWS that occur between lexical tokens in a
943 // structured header field are semantically interpreted as a single
944 // space character.
945 //
946 // is_email() author's note: This *cannot* mean that we must add a
947 // space to the address wherever CFWS appears. This would result in
948 // any addr-spec that had CFWS outside a quoted string being invalid
949 // for RFC 5321.
950 // if (($context === ISEMAIL_COMPONENT_LOCALPART) ||
951 //($context === ISEMAIL_COMPONENT_DOMAIN)) {
952 // $parsedata[$context] .=
953 // ISEMAIL_STRING_SP;
954 // $atomlist[$context][$element_count]
955 // .= ISEMAIL_STRING_SP; $element_len++;
956 // }
957 } else if (token == u'\\') { // Quoted pair
958 contextStack.push_back(context);
959 context = ContextQuotedPair;
960 } else if ((token == QChar(QChar::CarriageReturn)) || (token == QChar(QChar::Space)) ||
961 (token == QChar(QChar::Tabulation))) { // Folding White Space
962 if ((token == QChar(QChar::CarriageReturn)) &&
963 ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed)))) {
964 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF);
965 break;
966 }
967
968 returnStatus.push_back(ValidatorEmail::CFWSFWS);
969 contextStack.push_back(context);
970 context = ContextFWS;
971 tokenPrior = token;
972 } else { // ctext
973 // https://tools.ietf.org/html/rfc5322#section-3.2.3
974 // ctext = %d33-39 / ; Printable US-ASCII
975 // %d42-91 / ; characters not including
976 // %d93-126 / ; "(", ")", or "\"
977 // obs-ctext
978 //
979 // obs-ctext = obs-NO-WS-CTL
980 //
981 // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
982 // %d11 / ; characters that do not
983 // %d12 / ; include the carriage
984 // %d14-31 / ; return, line feed, and
985 // %d127 ; white space characters
986
987 const ushort uni = token.unicode();
988
989 if ((uni > ValidatorEmailPrivate::asciiEnd) || (uni == 0) ||
990 (uni == ValidatorEmailPrivate::asciiLF)) {
991 returnStatus.push_back(ValidatorEmail::ErrorExpectingCText); // Fatal error
992 break;
993 } else if ((uni < ValidatorRulePrivate::asciiSpace) ||
994 (uni == ValidatorEmailPrivate::asciiEnd)) {
995 returnStatus.push_back(ValidatorEmail::DeprecatedCText);
996 }
997 }
998 } break;
999 //-------------------------------------------------------------
1000 // Folding White Space
1001 //-------------------------------------------------------------
1002 case ContextFWS:
1003 {
1004 // https://tools.ietf.org/html/rfc5322#section-3.2.2
1005 // FWS = ([*WSP CRLF] 1*WSP) / obs-FWS
1006 // ; Folding white space
1007 // But note the erratum:
1008 // https://www.rfc-editor.org/errata_search.php?rfc=5322&eid=1908:
1009 // In the obsolete syntax, any amount of folding white space MAY be
1010 // inserted where the obs-FWS rule is allowed. This creates the
1011 // possibility of having two consecutive "folds" in a line, and
1012 // therefore the possibility that a line which makes up a folded header
1013 // field could be composed entirely of white space.
1014 //
1015 // obs-FWS = 1*([CRLF] WSP)
1016 if (tokenPrior == QChar(QChar::CarriageReturn)) {
1017 if (token == QChar(QChar::CarriageReturn)) {
1018 returnStatus.push_back(ValidatorEmail::ErrorFWSCRLFx2); // Fatal error
1019 break;
1020 }
1021
1022 // TODO FIXME
1023 if (crlf_count > 0) { // cppcheck-suppress knownConditionTrueFalse
1024 if (++crlf_count > 1) { // cppcheck-suppress knownConditionTrueFalse
1025 returnStatus.push_back(
1026 ValidatorEmail::DeprecatedFWS); // Multiple folds = obsolete FWS
1027 }
1028 } else {
1029 crlf_count = 1;
1030 }
1031 }
1032
1033 if (token == QChar(QChar::CarriageReturn)) {
1034 if ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed))) {
1035 returnStatus.push_back(ValidatorEmail::ErrorCRnoLF);
1036 break;
1037 }
1038 } else if ((token != QChar(QChar::Space)) && (token != QChar(QChar::Tabulation))) {
1039 if (tokenPrior == QChar(QChar::CarriageReturn)) {
1040 returnStatus.push_back(ValidatorEmail::ErrorFWSCRLFEnd); // Fatal error
1041 break;
1042 }
1043
1044 crlf_count = std::ranges::min(crlf_count, 0);
1045
1046 contextPrior = context;
1047 context = contextStack.takeLast(); // End of FWS
1048
1049 // https://tools.ietf.org/html/rfc5322#section-3.2.2
1050 // Runs of FWS, comment, or CFWS that occur between lexical tokens in a
1051 // structured header field are semantically interpreted as a single
1052 // space character.
1053 //
1054 // is_email() author's note: This *cannot* mean that we must add a
1055 // space to the address wherever CFWS appears. This would result in
1056 // any addr-spec that had CFWS outside a quoted string being invalid
1057 // for RFC 5321.
1058 // if (($context === ISEMAIL_COMPONENT_LOCALPART) ||
1059 //($context === ISEMAIL_COMPONENT_DOMAIN)) {
1060 // $parsedata[$context] .=
1061 // ISEMAIL_STRING_SP;
1062 // $atomlist[$context][$element_count]
1063 // .= ISEMAIL_STRING_SP; $element_len++;
1064 // }
1065
1066 i--; // Look at this token again in the parent context
1067 }
1068
1069 tokenPrior = token;
1070 } break;
1071 default:
1072 returnStatus.push_back(ValidatorEmail::ErrorFatal);
1073 qCCritical(C_VALIDATOR) << "ValidatorEmail: Unknown context";
1074 break;
1075 }
1076
1077 if (static_cast<int>(
1078 *std::ranges::max_element(returnStatus.constBegin(), returnStatus.constEnd())) >
1079 static_cast<int>(ValidatorEmail::RFC5322)) {
1080 break;
1081 }
1082 }
1083
1084 // Some simple final tests
1085 if (static_cast<int>(
1086 *std::ranges::max_element(returnStatus.constBegin(), returnStatus.constEnd())) <
1087 static_cast<int>(ValidatorEmail::RFC5322)) {
1088 if (context == ContextQuotedString) {
1089 returnStatus.push_back(ValidatorEmail::ErrorUnclosedQuotedStr);
1090 } else if (context == ContextQuotedPair) {
1091 returnStatus.push_back(ValidatorEmail::ErrorBackslashEnd);
1092 } else if (context == ContextComment) {
1093 returnStatus.push_back(ValidatorEmail::ErrorUnclosedComment);
1094 } else if (context == ComponentLiteral) {
1095 returnStatus.push_back(ValidatorEmail::ErrorUnclosedDomLiteral);
1096 } else if (token == QChar(QChar::CarriageReturn)) {
1097 returnStatus.push_back(ValidatorEmail::ErrorFWSCRLFEnd);
1098 } else if (parseDomain.isEmpty()) {
1099 returnStatus.push_back(ValidatorEmail::ErrorNoDomain);
1100 } else if (elementLen == 0) {
1101 returnStatus.push_back(ValidatorEmail::ErrorDotEnd);
1102 } else if (hyphenFlag) {
1103 returnStatus.push_back(ValidatorEmail::ErrorDomainHyphenEnd);
1104 } else if (parseDomain.size() > ValidatorEmailPrivate::maxDomainLength) {
1105 // https://tools.ietf.org/html/rfc5321#section-4.5.3.1.2
1106 // The maximum total length of a domain name or number is 255 octets.
1107 returnStatus.push_back(ValidatorEmail::RFC5322DomainTooLong);
1108 } else if ((parseLocalPart.size() + 1 + parseDomain.size()) >
1109 ValidatorEmailPrivate::maxMailboxLength) {
1110 // https://tools.ietf.org/html/rfc5321#section-4.1.2
1111 // Forward-path = Path
1112 //
1113 // Path = "<" [ A-d-l ":" ] Mailbox ">"
1114 //
1115 // https://tools.ietf.org/html/rfc5321#section-4.5.3.1.3
1116 // The maximum total length of a reverse-path or forward-path is 256
1117 // octets (including the punctuation and element separators).
1118 //
1119 // Thus, even without (obsolete) routing information, the Mailbox can
1120 // only be 254 characters long. This is confirmed by this verified
1121 // erratum to RFC 3696:
1122 //
1123 // https://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690
1124 // However, there is a restriction in RFC 2821 on the length of an
1125 // address in MAIL and RCPT commands of 254 characters. Since addresses
1126 // that do not fit in those fields are not normally useful, the upper
1127 // limit on address lengths should normally be considered to be 254.
1128 returnStatus.push_back(ValidatorEmail::RFC5322TooLong);
1129 } else if (elementLen > ValidatorEmailPrivate::maxDnsLabelLength) {
1130 returnStatus.push_back(ValidatorEmail::RFC5322LabelTooLong);
1131 }
1132 }
1133
1134 // Check DNS?
1135 bool dnsChecked = false;
1136
1137 // if (checkDns &&
1138 // (static_cast<int>(*std::ranges::max_element(returnStatus.constBegin(),
1139 // 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 += u'.';
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 && // cppcheck-suppress knownConditionTrueFalse
1220 (static_cast<int>(
1221 *std::ranges::max_element(returnStatus.constBegin(), returnStatus.constEnd())) <
1222 static_cast<int>(ValidatorEmail::DNSFailed))) {
1223 if (elementCount == 0) {
1224 returnStatus.push_back(ValidatorEmail::RFC5321TLD);
1225 }
1226
1227 if (u"0123456789"_s.contains(atomListDomain[elementCount][0])) {
1228 returnStatus.push_back(ValidatorEmail::RFC5321TLDNumeric);
1229 }
1230 }
1231
1232 if (returnStatus.size() != 1) {
1233 QList<ValidatorEmail::Diagnose> _rs;
1234 for (const ValidatorEmail::Diagnose dia : std::as_const(returnStatus)) {
1235 if (!_rs.contains(dia) && (dia != ValidatorEmail::ValidAddress)) {
1236 _rs.append(dia); // clazy:exclude=reserve-candidates
1237 }
1238 }
1239 returnStatus = _rs;
1240
1241 std::ranges::sort(returnStatus, std::greater<>());
1242 }
1243
1244 const ValidatorEmail::Diagnose finalStatus = returnStatus.at(0);
1245
1246 if (diagnoseStruct) {
1247 diagnoseStruct->finalStatus = finalStatus;
1248 diagnoseStruct->returnStatus = returnStatus;
1249 diagnoseStruct->localpart = parseLocalPart;
1250 diagnoseStruct->domain = parseDomain;
1251 diagnoseStruct->literal = parseLiteral;
1252 if (!parseLiteral.isEmpty()) {
1253 diagnoseStruct->cleanedEmail = parseLocalPart + u'@' + parseLiteral;
1254 } else {
1255 diagnoseStruct->cleanedEmail = parseLocalPart + u'@' + parseDomain;
1256 }
1257 }
1258
1259 return static_cast<int>(finalStatus) < static_cast<int>(threshold);
1260}
1261
1263 Diagnose diagnose,
1264 const QString &label)
1265{
1266 if (label.isEmpty()) {
1267 switch (diagnose) {
1268 case ValidAddress:
1269 //% "Address is valid. Please note that this does not mean that both the "
1270 //% "address and the domain actually exist. This address could be issued "
1271 //% "by the domain owner without breaking the rules of any RFCs."
1272 return c->qtTrId("cutelyst-valemail-diag-valid");
1273 case DnsWarnNoMxRecord:
1274 //% "Could not find an MX record for this address’ domain but an A record exists."
1275 return c->qtTrId("cutelyst-valemail-diag-nomx");
1276 case DnsMxDisabled:
1277 //% "MX for this address’ domain is explicitely disabled."
1278 return c->qtTrId("cutelyst-valemail-diag-mxdisabled");
1279 case DnsNoRecordFound:
1280 //% "Could neither find an MX record nor an A record for this address’ domain."
1281 return c->qtTrId("cutelyst-valemail-diag-noarec");
1282 case DnsErrorTimeout:
1283 //% "Timeout while performing DNS check for address’ domain."
1284 return c->qtTrId("cutelyst-valemail-diag-dnstimeout");
1285 case DnsError:
1286 //% "Error while performing DNS check for address’ domain."
1287 return c->qtTrId("cutelyst-valemail-diag-dnserror");
1288 case RFC5321TLD:
1289 //% "Address is valid but at a Top Level Domain."
1290 return c->qtTrId("cutelyst-valemail-diag-rfc5321tld");
1291 case RFC5321TLDNumeric:
1292 //% "Address is valid but the Top Level Domain begins with a number."
1293 return c->qtTrId("cutelyst-valemail-diag-rfc5321tldnumeric");
1295 //% "Address is valid but contains a quoted string."
1296 return c->qtTrId("cutelyst-valemail-diag-rfc5321quotedstring");
1298 //% "Address is valid but uses an IP address instead of a domain name."
1299 return c->qtTrId("cutelyst-valemail-diag-rfc5321addressliteral");
1301 //% "Address is valid but uses an IP address that contains a :: only "
1302 //% "eliding one zero group. All implementations must accept and be "
1303 //% "able to handle any legitimate RFC 4291 format."
1304 return c->qtTrId("cutelyst-valemail-diag-rfc5321ipv6deprecated");
1305 case CFWSComment:
1306 //% "Address contains comments."
1307 return c->qtTrId("cutelyst-valemail-diag-cfwscomment");
1308 case CFWSFWS:
1309 //% "Address contains folding white spaces like line breaks."
1310 return c->qtTrId("cutelyst-valemail-diag-cfwsfws");
1312 //% "The local part is in a deprecated form."
1313 return c->qtTrId("cutelyst-valemail-diag-deprecatedlocalpart");
1314 case DeprecatedFWS:
1315 //% "Address contains an obsolete form of folding white spaces."
1316 return c->qtTrId("cutelyst-valemail-diag-deprecatedfws");
1317 case DeprecatedQText:
1318 //% "A quoted string contains a deprecated character."
1319 return c->qtTrId("cutelyst-valemail-diag-deprecatedqtext");
1320 case DeprecatedQP:
1321 //% "A quoted pair contains a deprecated character."
1322 return c->qtTrId("cutelyst-valemail-diag-deprecatedqp");
1323 case DeprecatedComment:
1324 //% "Address contains a comment in a position that is deprecated."
1325 return c->qtTrId("cutelyst-valemail-diag-deprecatedcomment");
1326 case DeprecatedCText:
1327 //% "A comment contains a deprecated character."
1328 return c->qtTrId("cutelyst-valemail-diag-deprecatedctext");
1330 //% "Address contains a comment or folding white space around the @ sign."
1331 return c->qtTrId("cutelyst-valemail-diag-cfwsnearat");
1332 case RFC5322Domain:
1333 //% "Address is RFC 5322 compliant but contains domain characters that "
1334 //% "are not allowed by DNS."
1335 return c->qtTrId("cutelyst-valemail-diag-rfc5322domain");
1336 case RFC5322TooLong:
1337 //% "The address exceeds the maximum allowed length of %1 characters."
1338 return c->qtTrId("cutelyst-valemail-diag-rfc5322toolong")
1339 .arg(c->locale().toString(ValidatorEmailPrivate::maxMailboxLength));
1341 //% "The local part of the address exceeds the maximum allowed length "
1342 //% "of %1 characters."
1343 return c->qtTrId("cutelyst-valemail-diag-rfc5322localtoolong")
1344 .arg(c->locale().toString(ValidatorEmailPrivate::maxLocalPartLength));
1346 //% "The domain part exceeds the maximum allowed length of %1 characters."
1347 return c->qtTrId("cutelyst-valemail-diag-rfc5322domaintoolong")
1348 .arg(c->locale().toString(ValidatorEmailPrivate::maxDomainLength));
1350 //% "One of the labels/sections in the domain part exceeds the maximum allowed "
1351 //% "length of %1 characters."
1352 return c->qtTrId("cutelyst-valemail-diag-rfc5322labeltoolong")
1353 .arg(c->locale().toString(ValidatorEmailPrivate::maxDnsLabelLength));
1355 //% "The domain literal is not a valid RFC 5321 address literal."
1356 return c->qtTrId("cutelyst-valemail-diag-rfc5322domainliteral");
1358 //% "The domain literal is not a valid RFC 5321 domain literal and it "
1359 //% "contains obsolete characters."
1360 return c->qtTrId("cutelyst-valemail-diag-rfc5322domlitobsdtext");
1362 //% "The IPv6 literal address contains the wrong number of groups."
1363 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6groupcount");
1365 //% "The IPv6 literal address contains too many :: sequences."
1366 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv62x2xcolon");
1367 case RFC5322IPv6BadChar:
1368 //% "The IPv6 address contains an illegal group of characters."
1369 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6badchar");
1371 //% "The IPv6 address has too many groups."
1372 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6maxgroups");
1374 //% "The IPv6 address starts with a single colon."
1375 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonstart");
1377 //% "The IPv6 address ends with a single colon."
1378 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonend");
1380 //% "A domain literal contains a character that is not allowed."
1381 return c->qtTrId("cutelyst-valemail-diag-errexpectingdtext");
1382 case ErrorNoLocalPart:
1383 //% "Address has no local part."
1384 return c->qtTrId("cutelyst-valemail-diag-errnolocalpart");
1385 case ErrorNoDomain:
1386 //% "Address has no domain part."
1387 return c->qtTrId("cutelyst-valemail-diag-errnodomain");
1389 //% "The address must not contain consecutive dots."
1390 return c->qtTrId("cutelyst-valemail-diag-errconsecutivedots");
1392 //% "Address contains text after a comment or folding white space."
1393 return c->qtTrId("cutelyst-valemail-diag-erratextaftercfws");
1394 case ErrorATextAfterQS:
1395 //% "Address contains text after a quoted string."
1396 return c->qtTrId("cutelyst-valemail-diag-erratextafterqs");
1398 //% "Extra characters were found after the end of the domain literal."
1399 return c->qtTrId("cutelyst-valemail-diag-erratextafterdomlit");
1401 //% "The Address contains a character that is not allowed in a quoted pair."
1402 return c->qtTrId("cutelyst-valemail-diag-errexpectingqpair");
1404 //% "Address contains a character that is not allowed."
1405 return c->qtTrId("cutelyst-valemail-diag-errexpectingatext");
1407 //% "A quoted string contains a character that is not allowed."
1408 return c->qtTrId("cutelyst-valemail-diag-errexpectingqtext");
1410 //% "A comment contains a character that is not allowed."
1411 return c->qtTrId("cutelyst-valemail-diag-errexpectingctext");
1412 case ErrorBackslashEnd:
1413 //% "The address can not end with a backslash."
1414 return c->qtTrId("cutelyst-valemail-diag-errbackslashend");
1415 case ErrorDotStart:
1416 //% "Neither part of the address may begin with a dot."
1417 return c->qtTrId("cutelyst-valemail-diag-errdotstart");
1418 case ErrorDotEnd:
1419 //% "Neither part of the address may end with a dot."
1420 return c->qtTrId("cutelyst-valemail-diag-errdotend");
1422 //% "A domain or subdomain can not begin with a hyphen."
1423 return c->qtTrId("cutelyst-valemail-diag-errdomainhyphenstart");
1425 //% "A domain or subdomain can not end with a hyphen."
1426 return c->qtTrId("cutelyst-valemail-diag-errdomainhyphenend");
1428 //% "Unclosed quoted string. (Missing double quotation mark)"
1429 return c->qtTrId("cutelyst-valemail-diag-errunclosedquotedstr");
1431 //% "Unclosed comment. (Missing closing parentheses)"
1432 return c->qtTrId("cutelyst-valemail-diag-errunclosedcomment");
1434 //% "Domain literal is missing its closing bracket."
1435 return c->qtTrId("cutelyst-valemail-diag-erruncloseddomliteral");
1436 case ErrorFWSCRLFx2:
1437 //% "Folding white space contains consecutive line break sequences (CRLF)."
1438 return c->qtTrId("cutelyst-valemail-diag-errfwscrlfx2");
1439 case ErrorFWSCRLFEnd:
1440 //% "Folding white space ends with a line break sequence (CRLF)."
1441 return c->qtTrId("cutelyst-valemail-diag-errfwscrlfend");
1442 case ErrorCRnoLF:
1443 //% "Address contains a carriage return (CR) that is not followed by a "
1444 //% "line feed (LF)."
1445 return c->qtTrId("cutelyst-valemail-diag-errcrnolf");
1446 case ErrorFatal:
1447 //% "A fatal error occurred while parsing the address."
1448 return c->qtTrId("cutelyst-valemail-diag-errfatal");
1449 default:
1450 return {};
1451 }
1452
1453 } else {
1454
1455 switch (diagnose) {
1456 case ValidAddress:
1457 //% "The address in the “%1” field is valid. Please note that this does not mean "
1458 //% "that both the address and the domain actually exist. This address could be "
1459 //% "issued by the domain owner without breaking the rules of any RFCs."
1460 return c->qtTrId("cutelyst-valemail-diag-valid-label").arg(label);
1461 case DnsWarnNoMxRecord:
1462 //% "Could not find an MX record for the address’ domain in the “%1” "
1463 //% "field but an A record exists."
1464 return c->qtTrId("cutelyst-valemail-diag-nomx-label").arg(label);
1465 case DnsMxDisabled:
1466 //% "MX for the address’ domain in the “%1” field is explicitely disabled."
1467 return c->qtTrId("cutelyst-valemail-diag-mxdisabled-label").arg(label);
1468 case DnsNoRecordFound:
1469 //% "Could neither find an MX record nor an A record for the address’ "
1470 //% "domain in the “%1” field."
1471 return c->qtTrId("cutelyst-valemail-diag-noarec-label").arg(label);
1472 case DnsErrorTimeout:
1473 //% "Timeout while performing DNS check for address’ domain in the “%1” field."
1474 return c->qtTrId("cutelyst-valemail-diag-dnstimeout-label").arg(label);
1475 case DnsError:
1476 //% "Error while performing DNS check for address’ domain in the “%1” field."
1477 return c->qtTrId("cutelyst-valemail-diag-dnserror-label").arg(label);
1478 case RFC5321TLD:
1479 //% "The address in the “%1” field is valid but at a Top Level Domain."
1480 return c->qtTrId("cutelyst-valemail-diag-rfc5321tld-label").arg(label);
1481 case RFC5321TLDNumeric:
1482 //% "The address in the “%1” field is valid but the Top Level Domain "
1483 //% "begins with a number."
1484 return c->qtTrId("cutelyst-valemail-diag-rfc5321tldnumeric-label").arg(label);
1486 //% "The address in the “%1” field is valid but contains a quoted string."
1487 return c->qtTrId("cutelyst-valemail-diag-rfc5321quotedstring-label").arg(label);
1489 //% "The address in the “%1” field is valid but uses an IP address "
1490 //% "instead of a domain name."
1491 return c->qtTrId("cutelyst-valemail-diag-rfc5321addressliteral-label").arg(label);
1493 //% "The address in the “%1” field is valid but uses an IP address that "
1494 //% "contains a :: only eliding one zero group. All implementations "
1495 //% "must accept and be able to handle any legitimate RFC 4291 format."
1496 return c->qtTrId("cutelyst-valemail-diag-rfc5321ipv6deprecated-label").arg(label);
1497 case CFWSComment:
1498 //% "The address in the “%1” field contains comments."
1499 return c->qtTrId("cutelyst-valemail-diag-cfwscomment-label").arg(label);
1500 case CFWSFWS:
1501 //% "The address in the “%1” field contains folding white spaces like "
1502 //% "line breaks."
1503 return c->qtTrId("cutelyst-valemail-diag-cfwsfws-label").arg(label);
1505 //% "The local part of the address in the “%1” field is in a deprecated form."
1506 return c->qtTrId("cutelyst-valemail-diag-deprecatedlocalpart-label").arg(label);
1507 case DeprecatedFWS:
1508 //% "The address in the “%1” field contains an obsolete form of folding "
1509 //% "white spaces."
1510 return c->qtTrId("cutelyst-valemail-diag-deprecatedfws-label").arg(label);
1511 case DeprecatedQText:
1512 //% "A quoted string in the address in the “%1” field contains a "
1513 //% "deprecated character."
1514 return c->qtTrId("cutelyst-valemail-diag-deprecatedqtext-label").arg(label);
1515 case DeprecatedQP:
1516 //% "A quoted pair in the address in the “%1” field contains a "
1517 //% "deprecate character."
1518 return c->qtTrId("cutelyst-valemail-diag-deprecatedqp-label").arg(label);
1519 case DeprecatedComment:
1520 //% "The address in the “%1” field contains a comment in a position "
1521 //% "that is deprecated."
1522 return c->qtTrId("cutelyst-valemail-diag-deprecatedcomment-label").arg(label);
1523 case DeprecatedCText:
1524 //% "A comment in the address in the “%1” field contains a deprecated character."
1525 return c->qtTrId("cutelyst-valemail-diag-deprecatedctext-label").arg(label);
1527 //% "The address in the “%1” field contains a comment or folding white "
1528 //% "space around the @ sign."
1529 return c->qtTrId("cutelyst-valemail-diag-cfwsnearat-label").arg(label);
1530 case RFC5322Domain:
1531 //% "The address in the “%1” field is RFC 5322 compliant but contains "
1532 //% "domain characters that are not allowed by DNS."
1533 return c->qtTrId("cutelyst-valemail-diag-rfc5322domain-label").arg(label);
1534 case RFC5322TooLong:
1535 //% "The address in the “%1” field exceeds the maximum allowed length "
1536 //% "of %2 characters."
1537 return c->qtTrId("cutelyst-valemail-diag-rfc5322toolong-label")
1538 .arg(label, c->locale().toString(ValidatorEmailPrivate::maxMailboxLength));
1540 //% "The local part of the address in the “%1” field exceeds the maximum allowed "
1541 //% "length of %2 characters."
1542 return c->qtTrId("cutelyst-valemail-diag-rfc5322localtoolong-label")
1543 .arg(label, c->locale().toString(ValidatorEmailPrivate::maxLocalPartLength));
1545 //% "The domain part of the address in the “%1” field exceeds the maximum "
1546 //% "allowed length of %2 characters."
1547 return c->qtTrId("cutelyst-valemail-diag-rfc5322domaintoolong-label")
1548 .arg(label, c->locale().toString(ValidatorEmailPrivate::maxDomainLength));
1550 //% "The domain part of the address in the “%1” field contains an element/section "
1551 //% "that exceeds the maximum allowed lenght of %2 characters."
1552 return c->qtTrId("cutelyst-valemail-diag-rfc5322labeltoolong-label")
1553 .arg(label, c->locale().toString(ValidatorEmailPrivate::maxDnsLabelLength));
1555 //% "The domain literal of the address in the “%1” field is not a valid "
1556 //% "RFC 5321 address literal."
1557 return c->qtTrId("cutelyst-valemail-diag-rfc5322domainliteral-label").arg(label);
1559 //% "The domain literal of the address in the “%1” field is not a valid "
1560 //% "RFC 5321 domain literal and it contains obsolete characters."
1561 return c->qtTrId("cutelyst-valemail-diag-rfc5322domlitobsdtext-label").arg(label);
1563 //% "The IPv6 literal of the address in the “%1” field contains the "
1564 //% "wrong number of groups."
1565 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6groupcount-label").arg(label);
1567 //% "The IPv6 literal of the address in the “%1” field contains too "
1568 //% "many :: sequences."
1569 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv62x2xcolon-label").arg(label);
1570 case RFC5322IPv6BadChar:
1571 //% "The IPv6 address of the email address in the “%1” field contains "
1572 //% "an illegal group of characters."
1573 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6badchar-label").arg(label);
1575 //% "The IPv6 address of the email address in the “%1” field has too many groups."
1576 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6maxgroups-label").arg(label);
1578 //% "The IPv6 address of the email address in the “%1” field starts "
1579 //% "with a single colon."
1580 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonstart-label").arg(label);
1582 //% "The IPv6 address of the email address in the “%1” field ends with "
1583 //% "a single colon."
1584 return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonend-label").arg(label);
1586 //% "A domain literal of the address in the “%1” field contains a "
1587 //% "character that is not allowed."
1588 return c->qtTrId("cutelyst-valemail-diag-errexpectingdtext-label").arg(label);
1589 case ErrorNoLocalPart:
1590 //% "The address in the “%1” field has no local part."
1591 return c->qtTrId("cutelyst-valemail-diag-errnolocalpart-label").arg(label);
1592 case ErrorNoDomain:
1593 //% "The address in the “%1” field has no domain part."
1594 return c->qtTrId("cutelyst-valemail-diag-errnodomain-label").arg(label);
1596 //% "The address in the “%1” field must not contain consecutive dots."
1597 return c->qtTrId("cutelyst-valemail-diag-errconsecutivedots-label").arg(label);
1599 //% "The address in the “%1” field contains text after a comment or "
1600 //% "folding white space."
1601 return c->qtTrId("cutelyst-valemail-diag-erratextaftercfws-label").arg(label);
1602 case ErrorATextAfterQS:
1603 //% "The address in the “%1” field contains text after a quoted string."
1604 return c->qtTrId("cutelyst-valemail-diag-erratextafterqs-label").arg(label);
1606 //% "Extra characters were found after the end of the domain literal of "
1607 //% "the address in the “%1” field."
1608 return c->qtTrId("cutelyst-valemail-diag-erratextafterdomlit-label").arg(label);
1610 //% "The address in the “%1” field contains a character that is not "
1611 //% "allowed in a quoted pair."
1612 return c->qtTrId("cutelyst-valemail-diag-errexpectingqpair-label").arg(label);
1614 //% "The address in the “%1” field contains a character that is not allowed."
1615 return c->qtTrId("cutelyst-valemail-diag-errexpectingatext-label").arg(label);
1617 //% "A quoted string in the address in the “%1” field contains a "
1618 //% "character that is not allowed."
1619 return c->qtTrId("cutelyst-valemail-diag-errexpectingqtext-label").arg(label);
1621 //% "A comment in the address in the “%1” field contains a character "
1622 //% "that is not allowed."
1623 return c->qtTrId("cutelyst-valemail-diag-errexpectingctext-label").arg(label);
1624 case ErrorBackslashEnd:
1625 //% "The address in the “%1” field can't end with a backslash."
1626 return c->qtTrId("cutelyst-valemail-diag-errbackslashend-label").arg(label);
1627 case ErrorDotStart:
1628 //% "Neither part of the address in the “%1” field may begin with a dot."
1629 return c->qtTrId("cutelyst-valemail-diag-errdotstart-label").arg(label);
1630 case ErrorDotEnd:
1631 //% "Neither part of the address in the “%1” field may end with a dot."
1632 return c->qtTrId("cutelyst-valemail-diag-errdotend-label").arg(label);
1634 //% "A domain or subdomain of the address in the “%1” field can not "
1635 //% "begin with a hyphen."
1636 return c->qtTrId("cutelyst-valemail-diag-errdomainhyphenstart-label").arg(label);
1638 //% "A domain or subdomain of the address in the “%1” field can not end "
1639 //% "with a hyphen."
1640 return c->qtTrId("cutelyst-valemail-diag-errdomainhyphenend-label").arg(label);
1642 //% "Unclosed quoted string in the address in the “%1” field. (Missing "
1643 //% "double quotation mark)"
1644 return c->qtTrId("cutelyst-valemail-diag-errunclosedquotedstr-label").arg(label);
1646 //% "Unclosed comment in the address in the “%1” field. (Missing "
1647 //% "closing parentheses)"
1648 return c->qtTrId("cutelyst-valemail-diag-errunclosedcomment-label").arg(label);
1650 //% "Domain literal of the address in the “%1” field is missing its "
1651 //% "closing bracket."
1652 return c->qtTrId("cutelyst-valemail-diag-erruncloseddomliteral-label").arg(label);
1653 case ErrorFWSCRLFx2:
1654 //% "Folding white space in the address in the “%1” field contains "
1655 //% "consecutive line break sequences (CRLF)."
1656 return c->qtTrId("cutelyst-valemail-diag-errfwscrlfx2-label").arg(label);
1657 case ErrorFWSCRLFEnd:
1658 //% "Folding white space in the address in the “%1” field ends with a "
1659 //% "line break sequence (CRLF)."
1660 return c->qtTrId("cutelyst-valemail-diag-errfwscrlfend-label").arg(label);
1661 case ErrorCRnoLF:
1662 //% "The address in the “%1” field contains a carriage return (CR) that "
1663 //% "is not followed by a line feed (LF)."
1664 return c->qtTrId("cutelyst-valemail-diag-errcrnolf-label").arg(label);
1665 case ErrorFatal:
1666 //% "A fatal error occurred while parsing the address in the “%1” field."
1667 return c->qtTrId("cutelyst-valemail-diag-errfatal-label").arg(label);
1668 default:
1669 return {};
1670 }
1671 }
1672}
1673
1675{
1676 if (label.isEmpty()) {
1677 switch (category) {
1678 case Valid:
1679 //% "Address is valid."
1680 return c->qtTrId("cutelyst-valemail-cat-valid");
1681 case DNSWarn:
1682 //% "Address is valid but there is a warning about the DNS."
1683 return c->qtTrId("cutelyst-valemail-cat-dnswarn");
1684 case DNSFailed:
1685 //% "Address is valid but a DNS check was not successful."
1686 return c->qtTrId("cutelyst-valemail-cat-dnsfailed");
1687 case RFC5321:
1688 //% "Address is valid for SMTP but has unusual elements."
1689 return c->qtTrId("cutelyst-valemail-cat-rfc5321");
1690 case CFWS:
1691 //% "Address is valid within the message but can not be used unmodified "
1692 //% "for the envelope."
1693 return c->qtTrId("cutelyst-valemail-cat-cfws");
1694 case Deprecated:
1695 //% "Address contains deprecated elements but may still be valid in "
1696 //% "restricted contexts."
1697 return c->qtTrId("cutelyst-valemail-cat-deprecated");
1698 case RFC5322:
1699 //% "The address is only valid according to the broad definition of RFC "
1700 //% "5322. It is otherwise invalid."
1701 return c->qtTrId("cutelyst-valemail-cat-rfc5322");
1702 default:
1703 //% "Address is invalid for any purpose."
1704 return c->qtTrId("cutelyst-valemail-cat-invalid");
1705 }
1706 } else {
1707 switch (category) {
1708 case Valid:
1709 //% "The address in the “%1” field is valid."
1710 return c->qtTrId("cutelyst-valemail-cat-valid-label").arg(label);
1711 case DNSWarn:
1712 //% "The address in the “%1” field is valid but there are warnings about the DNS."
1713 return c->qtTrId("cutelyst-valemail-cat-dnswarn-label").arg(label);
1714 case DNSFailed:
1715 //% "The address in the “%1” field is valid but a DNS check was not successful."
1716 return c->qtTrId("cutelyst-valemail-cat-dnsfailed-label").arg(label);
1717 case RFC5321:
1718 //% "The address in the “%1” field is valid for SMTP but has unusual elements."
1719 return c->qtTrId("cutelyst-valemail-cat-rfc5321-label").arg(label);
1720 case CFWS:
1721 //% "The address in the “%1” field is valid within the message but can "
1722 //% "not be used unmodified for the envelope."
1723 return c->qtTrId("cutelyst-valemail-cat-cfws-label").arg(label);
1724 case Deprecated:
1725 //% "The address in the “%1” field contains deprecated elements but may "
1726 //% "still be valid in restricted contexts."
1727 return c->qtTrId("cutelyst-valemail-cat-deprecated-label").arg(label);
1728 case RFC5322:
1729 //% "The address in the “%1” field is only valid according to the broad "
1730 //% "definition of RFC 5322. It is otherwise invalid."
1731 return c->qtTrId("cutelyst-valemail-cat-rfc5322-label").arg(label);
1732 default:
1733 //% "The address in the “%1” field is invalid for any purpose."
1734 return c->qtTrId("cutelyst-valemail-cat-invalid-label").arg(label);
1735 }
1736 }
1737}
1738
1740{
1741 Category cat = Error;
1742
1743 const auto diag = static_cast<int>(diagnose);
1744
1745 if (diag < static_cast<int>(Valid)) {
1746 cat = Valid;
1747 } else if (diag < static_cast<int>(DNSWarn)) {
1748 cat = DNSWarn;
1749 } else if (diag < static_cast<int>(DNSFailed)) {
1750 cat = DNSFailed;
1751 } else if (diag < static_cast<int>(RFC5321)) {
1752 cat = RFC5321;
1753 } else if (diag < static_cast<int>(CFWS)) {
1754 cat = CFWS;
1755 } else if (diag < static_cast<int>(Deprecated)) {
1756 cat = Deprecated;
1757 } else if (diag < static_cast<int>(RFC5322)) {
1758 cat = RFC5322;
1759 }
1760
1761 return cat;
1762}
1763
1764QString ValidatorEmail::categoryString(const Context *c, Diagnose diagnose, const QString &label)
1765{
1766 return categoryString(c, category(diagnose), label);
1767}
1768
1769bool ValidatorEmail::validate(const QString &email,
1770 Category threshold,
1771 Options options,
1772 QList<Cutelyst::ValidatorEmail::Diagnose> *diagnoses)
1773{
1774 if (options.testFlag(CheckDNS)) {
1775 qCWarning(C_VALIDATOR) << "ValidatorEmail: using the CheckDNS option on validate() is"
1776 << "not supported anymore. Use validateCb().";
1777 }
1778
1779 ValidatorEmailDiagnoseStruct diag;
1780 bool ret = ValidatorEmailPrivate::checkEmail(email, options, threshold, &diag);
1781
1782 if (diagnoses) {
1783 *diagnoses = diag.returnStatus;
1784 }
1785
1786 return ret;
1787}
1788
1790 const QString &email,
1791 Category threshold,
1792 Options options,
1793 std::function<void(bool isValid, const QString &cleanedEmail, const QList<Diagnose> &diagnoses)>
1794 cb)
1795{
1796 ValidatorEmailDiagnoseStruct diag;
1797 const bool ret = ValidatorEmailPrivate::checkEmail(email, options, threshold, &diag);
1798
1799 if (ret && options.testFlag(ValidatorEmail::CheckDNS)) {
1800
1801 if (diag.domain.isEmpty()) {
1802
1803 diag.returnStatus.append(DnsError);
1804 diag.sortReturnStatus();
1805 cb(diag.isBelowThreshold(threshold), diag.cleanedEmail, diag.returnStatus);
1806
1807 } else {
1808
1809 auto mxLookup = new QDnsLookup{QDnsLookup::MX, diag.domain};
1810 QObject::connect(
1811 mxLookup, &QDnsLookup::finished, [mxLookup, cb, diag, threshold]() mutable {
1812 if (mxLookup->error() == QDnsLookup::NoError &&
1813 !mxLookup->mailExchangeRecords().empty()) {
1814 const auto records = mxLookup->mailExchangeRecords();
1815 for (const auto &h : records) {
1816 if (h.preference() > 0 && !h.exchange().isEmpty()) {
1817 // these both values might have already been set, but as we found a
1818 // valid MX, they are no errors
1819 diag.returnStatus.removeAll(RFC5321TLD);
1820 diag.returnStatus.removeAll(RFC5321TLDNumeric);
1821 break;
1822 } else if (h.preference() == 0 &&
1823 (h.exchange().isEmpty() || h.exchange() == "."_L1)) {
1824 // this is a Null MX that explicitely indicates, that the domain
1825 // accepts no mail
1826 diag.returnStatus.append(DnsMxDisabled);
1827 break;
1828 }
1829 }
1830 diag.sortReturnStatus();
1831 cb(diag.isBelowThreshold(threshold), diag.cleanedEmail, diag.returnStatus);
1832 } else {
1833 if (mxLookup->error() == QDnsLookup::NoError) {
1834
1835 auto aLookup = new QDnsLookup{QDnsLookup::A, diag.domain};
1836 QObject::connect(aLookup,
1837 &QDnsLookup::finished,
1838 [aLookup, cb, diag, threshold]() mutable {
1839 if (aLookup->error() == QDnsLookup::NoError) {
1840 if (!aLookup->hostAddressRecords().empty()) {
1841 diag.returnStatus.append(DnsWarnNoMxRecord);
1842 } else {
1843 diag.returnStatus.append(DnsNoRecordFound);
1844 }
1845 diag.sortReturnStatus();
1846 cb(diag.isBelowThreshold(threshold),
1847 diag.cleanedEmail,
1848 diag.returnStatus);
1849 } else {
1850 switch (aLookup->error()) {
1851 case QDnsLookup::NotFoundError:
1852 diag.returnStatus.append(DnsNoRecordFound);
1853 break;
1854 case QDnsLookup::OperationCancelledError:
1855 diag.returnStatus.append(DnsErrorTimeout);
1856 break;
1857 default:
1858 diag.returnStatus.append(DnsError);
1859 break;
1860 }
1861 diag.sortReturnStatus();
1862 cb(diag.isBelowThreshold(threshold),
1863 diag.cleanedEmail,
1864 diag.returnStatus);
1865 }
1866 aLookup->deleteLater();
1867 });
1868 QTimer::singleShot(
1869 ValidatorEmailPrivate::dnsLookupTimeout, aLookup, &QDnsLookup::abort);
1870 aLookup->lookup();
1871
1872 } else {
1873 switch (mxLookup->error()) {
1874 case QDnsLookup::NotFoundError:
1875 diag.returnStatus.append(DnsNoRecordFound);
1876 break;
1877 case QDnsLookup::OperationCancelledError:
1878 diag.returnStatus.append(DnsErrorTimeout);
1879 break;
1880 default:
1881 diag.returnStatus.append(DnsError);
1882 break;
1883 }
1884 diag.sortReturnStatus();
1885 cb(diag.isBelowThreshold(threshold), diag.cleanedEmail, diag.returnStatus);
1886 }
1887 }
1888 mxLookup->deleteLater();
1889 });
1890 QTimer::singleShot(
1891 ValidatorEmailPrivate::dnsLookupTimeout, mxLookup, &QDnsLookup::abort);
1892 mxLookup->lookup();
1893 }
1894
1895 } else {
1896 cb(ret, diag.cleanedEmail, diag.returnStatus);
1897 }
1898}
1899
1900void ValidatorEmailDiagnoseStruct::sortReturnStatus()
1901{
1902 std::ranges::sort(returnStatus, std::greater<>());
1903 finalStatus = returnStatus.at(0);
1904}
1905
1906bool ValidatorEmailDiagnoseStruct::isBelowThreshold(ValidatorEmail::Category threshold) const
1907{
1908 return static_cast<int>(finalStatus) < static_cast<int>(threshold);
1909}
1910
1911#include "moc_validatoremail.cpp"
The Cutelyst Context.
Definition context.h:42
QLocale locale() const noexcept
Definition context.cpp:461
QString qtTrId(const char *id, int n=-1) const
Definition context.h:657
static QString categoryString(const Context *c, Category category, const QString &label={})
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 diagnoseString(const Context *c, Diagnose diagnose, const QString &label={})
QString field() const noexcept
QString validationError(Context *c, const QVariant &errorData={}) const
QString label(const Context *c) const
ValidatorRule(const QString &field, const ValidatorMessages &messages={}, const QString &defValKey={}, QByteArrayView validatorName=nullptr)
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
QMultiMap< QString, QString > ParamsMultiMap
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.
Stores custom error messages and the input field label.
Contains the result of a single input parameter validation.