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 + 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
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(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
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 == u'.') { // 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 == 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)
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 == 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)
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 == u'.') { // 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 == 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)
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 == 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) +
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(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) {
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
1765{
1766 return categoryString(c, category(diagnose), label);
1767}
1768
1770 Category threshold,
1771 Options options,
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};
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,
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()) {
1852 diag.returnStatus.append(DnsNoRecordFound);
1853 break;
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 });
1869 ValidatorEmailPrivate::dnsLookupTimeout, aLookup, &QDnsLookup::abort);
1870 aLookup->lookup();
1871
1872 } else {
1873 switch (mxLookup->error()) {
1875 diag.returnStatus.append(DnsNoRecordFound);
1876 break;
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 });
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
Checks if the value is a valid email address according to specific RFCs.
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={})
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)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
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.