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