cutelyst  5.0.1
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
validatoremail.cpp
1 /*
2  * SPDX-FileCopyrightText: (C) 2017-2025 Matthias Fehring <mf@huessenbergnetz.de>
3  * SPDX-License-Identifier: BSD-3-Clause
4  */
5 
6 #include "validatoremail_p.h"
7 
8 #include <algorithm>
9 #include <functional>
10 
11 #include <QDnsLookup>
12 #include <QEventLoop>
13 #include <QTimer>
14 #include <QUrl>
15 
16 using namespace Cutelyst;
17 using namespace Qt::Literals::StringLiterals;
18 
19 const 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};
22 const QRegularExpression ValidatorEmailPrivate::ipv6PartRegex{u"^[0-9A-Fa-f]{0,4}$"_s};
23 const 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 {
38  ValidatorReturnType result;
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 
102 QString 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 
111 bool 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
200  } else {
201  returnStatus.push_back(ValidatorEmail::CFWSComment);
202  endOrDie = true; // We can't start a comment in the middle of an element, so
203  // this better be the end
204  }
205 
206  contextStack.push_back(context);
207  context = ContextComment;
208  } else if (token == u'.') { // Next dot-atom element
209  if (elementLen == 0) {
210  // Another dot, already?
211  returnStatus.push_back((elementCount == 0)
214  } else {
215  // The entire local part can be a quoted string for RFC 5321
216  // If it's just one atom that is quoten then it's an RFC 5322 obsolete form
217  if (endOrDie) {
218  returnStatus.push_back(ValidatorEmail::DeprecatedLocalpart);
219  }
220  }
221 
222  endOrDie = false; // CFWS & quoted strings are OK again now we're at the beginning
223  // of an element (although they are obsolete forms)
224  elementLen = 0;
225  elementCount++;
226  parseLocalPart += token;
227  atomListLocalPart[elementCount].clear();
228  } else if (token == u'"') {
229  if (elementLen == 0) {
230  // The entire local-part can be a quoted string for RFC 5321
231  // If it's just one atom that is quoted then it's an RFC 5322 obsolete form
232  returnStatus.push_back((elementCount == 0)
235 
236  parseLocalPart += token;
237  atomListLocalPart[elementCount] += token;
238  elementLen++;
239  endOrDie = true; // quoted string must be the entire element
240  contextStack.push_back(context);
241  context = ContextQuotedString;
242  } else {
243  returnStatus.push_back(ValidatorEmail::ErrorExpectingAText); // Fatal error
244  }
245  } else if ((token == QChar(QChar::CarriageReturn)) || (token == QChar(QChar::Space)) ||
246  (token == QChar(QChar::Tabulation))) { // Folding White Space
247  if ((token == QChar(QChar::CarriageReturn)) &&
248  ((++i == rawLength) || (email[i] != QChar(QChar::LineFeed)))) {
249  returnStatus.push_back(ValidatorEmail::ErrorCRnoLF);
250  break;
251  }
252 
253  if (elementLen == 0) {
254  returnStatus.push_back((elementCount == 0) ? ValidatorEmail::CFWSFWS
256  } else {
257  endOrDie = true; // We can't start FWS in the middle of an element, so this
258  // better be the end
259  }
260 
261  contextStack.push_back(context);
262  context = ContextFWS;
263  tokenPrior = token;
264  } else if (token == u'@') {
265  // At this point we should have a valid local part
266  if (contextStack.size() != 1) {
267  returnStatus.push_back(ValidatorEmail::ErrorFatal);
268  qCCritical(C_VALIDATOR) << "ValidatorEmail: Unexpected item on context stack";
269  break;
270  }
271 
272  if (parseLocalPart.isEmpty()) {
273  returnStatus.push_back(ValidatorEmail::ErrorNoLocalPart); // Fatal error
274  } else if (elementLen == 0) {
275  returnStatus.push_back(ValidatorEmail::ErrorDotEnd); // Fatal Error
276  } else if (parseLocalPart.size() > ValidatorEmailPrivate::maxLocalPartLength) {
277  // https://tools.ietf.org/html/rfc5321#section-4.5.3.1.1
278  // The maximum total length of a user name or other local-part is 64
279  // octets.
280  returnStatus.push_back(ValidatorEmail::RFC5322LocalTooLong);
281  } else if ((contextPrior == ContextComment) || (contextPrior == ContextFWS)) {
282  // https://tools.ietf.org/html/rfc5322#section-3.4.1
283  // Comments and folding white space
284  // SHOULD NOT be used around the "@" in the addr-spec.
285  //
286  // https://tools.ietf.org/html/rfc2119
287  // 4. SHOULD NOT This phrase, or the phrase "NOT RECOMMENDED" mean that
288  // there may exist valid reasons in particular circumstances when the
289  // particular behavior is acceptable or even useful, but the full
290  // implications should be understood and the case carefully weighed
291  // before implementing any behavior described with this label.
292  returnStatus.push_back(ValidatorEmail::DeprecatedCFWSNearAt);
293  }
294 
295  context = ComponentDomain;
296  contextStack.clear();
297  contextStack.push_back(context);
298  elementCount = 0;
299  elementLen = 0;
300  endOrDie = false;
301 
302  } else { // atext
303  // https://tools.ietf.org/html/rfc5322#section-3.2.3
304  // atext = ALPHA / DIGIT / ; Printable US-ASCII
305  // "!" / "#" / ; characters not including
306  // "$" / "%" / ; specials. Used for atoms.
307  // "&" / "'" /
308  // "*" / "+" /
309  // "-" / "/" /
310  // "=" / "?" /
311  // "^" / "_" /
312  // "`" / "{" /
313  // "|" / "}" /
314  //
315  if (endOrDie) {
316  switch (contextPrior) {
317  case ContextComment:
318  case ContextFWS:
319  returnStatus.push_back(ValidatorEmail::ErrorATextAfterCFWS);
320  break;
321  case ContextQuotedString:
322  returnStatus.push_back(ValidatorEmail::ErrorATextAfterQS);
323  break;
324  default:
325  returnStatus.push_back(ValidatorEmail::ErrorFatal);
326  qCCritical(C_VALIDATOR)
327  << "ValidatorEmail: More atext found where none is allowed, "
328  "but unrecognizes prior context";
329  break;
330  }
331  } else {
332  contextPrior = context;
333  const char16_t uni = token.unicode();
334 
335  if (!allowUtf8Local) {
336  if ((uni < ValidatorEmailPrivate::asciiExclamationMark) ||
337  (uni > ValidatorEmailPrivate::asciiTilde) ||
338  ValidatorEmailPrivate::stringSpecials.contains(token)) {
339  returnStatus.push_back(
340  ValidatorEmail::ErrorExpectingAText); // fatal error
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(
348  ValidatorEmail::ErrorExpectingAText); // fatal error
349  }
350  }
351  }
352 
353  parseLocalPart += token;
354  atomListLocalPart[elementCount] += token;
355  elementLen++;
356  }
357  }
358  } break;
359  //-----------------------------------------
360  // Domain
361  //-----------------------------------------
362  case ComponentDomain:
363  {
364  // https://tools.ietf.org/html/rfc5322#section-3.4.1
365  // domain = dot-atom / domain-literal / obs-domain
366  //
367  // dot-atom = [CFWS] dot-atom-text [CFWS]
368  //
369  // dot-atom-text = 1*atext *("." 1*atext)
370  //
371  // domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
372  //
373  // dtext = %d33-90 / ; Printable US-ASCII
374  // %d94-126 / ; characters not including
375  // obs-dtext ; "[", "]", or "\"
376  //
377  // obs-domain = atom *("." atom)
378  //
379  // atom = [CFWS] 1*atext [CFWS]
380  // https://tools.ietf.org/html/rfc5321#section-4.1.2
381  // Mailbox = Local-part "@" ( Domain / address-literal )
382  //
383  // Domain = sub-domain *("." sub-domain)
384  //
385  // address-literal = "[" ( IPv4-address-literal /
386  // IPv6-address-literal /
387  // General-address-literal ) "]"
388  // ; See Section 4.1.3
389  // https://tools.ietf.org/html/rfc5322#section-3.4.1
390  // Note: A liberal syntax for the domain portion of addr-spec is
391  // given here. However, the domain portion contains addressing
392  // information specified by and used in other protocols (e.g.,
393  // [RFC1034], [RFC1035], [RFC1123], [RFC5321]). It is therefore
394  // incumbent upon implementations to conform to the syntax of
395  // addresses for the context in which they are used.
396  // is_email() author's note: it's not clear how to interpret this in
397  // the context of a general email address validator. The conclusion I
398  // have reached is this: "addressing information" must comply with
399  // RFC 5321 (and in turn RFC 1035), anything that is "semantically
400  // invisible" must comply only with RFC 5322.
401 
402  if (token == u'(') { // comment
403  if (elementLen == 0) {
404  // Comments at the start of the domain are deprecated in the text
405  // Comments at the start of a subdomain are obs-domain
406  // (https://tools.ietf.org/html/rfc5322#section-3.4.1)
407  returnStatus.push_back((elementCount == 0)
410  } else {
411  returnStatus.push_back(ValidatorEmail::CFWSComment);
412  endOrDie = true; // We can't start a comment in the middle of an element, so
413  // this better be the end
414  }
415 
416  contextStack.push_back(context);
417  context = ContextComment;
418  } else if (token == u'.') { // next dot-atom element
419  if (elementLen == 0) {
420  // another dot, already?
421  returnStatus.push_back((elementCount == 0)
424  } else if (hyphenFlag) {
425  // Previous subdomain ended in a hyphen
426  returnStatus.push_back(ValidatorEmail::ErrorDomainHyphenEnd); // fatal error
427  } else {
428  // Nowhere in RFC 5321 does it say explicitly that the
429  // domain part of a Mailbox must be a valid domain according
430  // to the DNS standards set out in RFC 1035, but this *is*
431  // implied in several places. For instance, wherever the idea
432  // of host routing is discussed the RFC says that the domain
433  // must be looked up in the DNS. This would be nonsense unless
434  // the domain was designed to be a valid DNS domain. Hence we
435  // must conclude that the RFC 1035 restriction on label length
436  // also applies to RFC 5321 domains.
437  //
438  // https://tools.ietf.org/html/rfc1035#section-2.3.4
439  // labels 63 octets or less
440  if (elementLen > ValidatorEmailPrivate::maxDnsLabelLength) {
441  returnStatus.push_back(ValidatorEmail::RFC5322LabelTooLong);
442  }
443  }
444 
445  endOrDie = false; // CFWS is OK again now we're at the beginning of an element
446  // (although it may be obsolete CFWS)
447  elementLen = 0;
448  elementCount++;
449  atomListDomain[elementCount].clear();
450  parseDomain += token;
451 
452  } else if (token == u'[') { // Domain literal
453  if (parseDomain.isEmpty()) {
454  endOrDie = true; // domain literal must be the only component
455  elementLen++;
456  contextStack.push_back(context);
457  context = ComponentLiteral;
458  parseDomain += token;
459  atomListDomain[elementCount] += token;
460  parseLiteral.clear();
461  } else {
462  returnStatus.push_back(ValidatorEmail::ErrorExpectingAText); // Fatal error
463  }
464  } else if ((token == QChar(QChar::CarriageReturn)) || (token == QChar(QChar::Space)) ||
465  (token == QChar(QChar::Tabulation))) { // Folding White Space
466  if ((token == QChar(QChar::CarriageReturn)) &&
467  ((++i == rawLength) || email[i] != QChar(QChar::LineFeed))) {
468  returnStatus.push_back(ValidatorEmail::ErrorCRnoLF); // Fatal error
469  break;
470  }
471 
472  if (elementLen == 0) {
473  returnStatus.push_back((elementCount == 0)
476  } else {
477  returnStatus.push_back(ValidatorEmail::CFWSFWS);
478  endOrDie = true; // We can't start FWS in the middle of an element, so this
479  // better be the end
480  }
481 
482  contextStack.push_back(context);
483  context = ContextFWS;
484  tokenPrior = token;
485 
486  } else { // atext
487  // RFC 5322 allows any atext...
488  // https://tools.ietf.org/html/rfc5322#section-3.2.3
489  // atext = ALPHA / DIGIT / ; Printable US-ASCII
490  // "!" / "#" / ; characters not including
491  // "$" / "%" / ; specials. Used for atoms.
492  // "&" / "'" /
493  // "*" / "+" /
494  // "-" / "/" /
495  // "=" / "?" /
496  // "^" / "_" /
497  // "`" / "{" /
498  // "|" / "}" /
499  // "~"
500  // But RFC 5321 only allows letter-digit-hyphen to comply with DNS rules (RFCs 1034
501  // & 1123) https://tools.ietf.org/html/rfc5321#section-4.1.2
502  // sub-domain = Let-dig [Ldh-str]
503  //
504  // Let-dig = ALPHA / DIGIT
505  //
506  // Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
507  //
508 
509  if (endOrDie) {
510  // We have encountered atext where it is no longer valid
511  switch (contextPrior) {
512  case ContextComment:
513  case ContextFWS:
514  returnStatus.push_back(ValidatorEmail::ErrorATextAfterCFWS);
515  break;
516  case ComponentLiteral:
517  returnStatus.push_back(ValidatorEmail::ErrorATextAfterDomLit);
518  break;
519  default:
520  returnStatus.push_back(ValidatorEmail::ErrorFatal);
521  qCCritical(C_VALIDATOR)
522  << "ValidatorEmail: More atext found where none is allowed, but"
523  << "unrecognised prior context.";
524  break;
525  }
526  }
527 
528  const char16_t uni = token.unicode();
529  hyphenFlag = false; // Assume this token isn't a hyphen unless we discover it is
530 
531  if ((uni < ValidatorEmailPrivate::asciiExclamationMark) ||
532  (uni > ValidatorEmailPrivate::asciiTilde) ||
533  ValidatorEmailPrivate::stringSpecials.contains(token)) {
534  returnStatus.push_back(ValidatorEmail::ErrorExpectingAText); // Fatal error
535  } else if (token == u'-') {
536  if (elementLen == 0) {
537  // Hyphens can't be at the beginning of a subdomain
538  returnStatus.push_back(
540  }
541  hyphenFlag = true;
542  } else if (!(((uni >= ValidatorRulePrivate::ascii_0) &&
543  (uni <= ValidatorRulePrivate::ascii_9)) ||
544  ((uni >= ValidatorRulePrivate::ascii_A) &&
545  (uni <= ValidatorRulePrivate::ascii_Z)) ||
546  ((uni >= ValidatorRulePrivate::ascii_a) &&
547  (uni <= ValidatorRulePrivate::ascii_z)))) {
548  // NOt an RFC 5321 subdomain, but still ok by RFC 5322
549  returnStatus.push_back(ValidatorEmail::RFC5322Domain);
550  }
551 
552  parseDomain += token;
553  atomListDomain[elementCount] += token;
554  elementLen++;
555  }
556  } break;
557  //-------------------------------------------------------------
558  // Domain literal
559  //-------------------------------------------------------------
560  case ComponentLiteral:
561  {
562  // https://tools.ietf.org/html/rfc5322#section-3.4.1
563  // domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
564  //
565  // dtext = %d33-90 / ; Printable US-ASCII
566  // %d94-126 / ; characters not including
567  // obs-dtext ; "[", "]", or "\"
568  //
569  // obs-dtext = obs-NO-WS-CTL / quoted-pair
570  if (token == u']') { // End of domain literal
571  if (static_cast<int>(*std::ranges::max_element(returnStatus.constBegin(),
572  returnStatus.constEnd())) <
573  static_cast<int>(ValidatorEmail::Deprecated)) {
574  // Could be a valid RFC 5321 address literal, so let's check
575 
576  // https://tools.ietf.org/html/rfc5321#section-4.1.2
577  // address-literal = "[" ( IPv4-address-literal /
578  // IPv6-address-literal /
579  // General-address-literal ) "]"
580  // ; See Section 4.1.3
581  //
582  // https://tools.ietf.org/html/rfc5321#section-4.1.3
583  // IPv4-address-literal = Snum 3("." Snum)
584  //
585  // IPv6-address-literal = "IPv6:" IPv6-addr
586  //
587  // General-address-literal = Standardized-tag ":" 1*dcontent
588  //
589  // Standardized-tag = Ldh-str
590  // ; Standardized-tag MUST be specified in a
591  // ; Standards-Track RFC and registered with IANA
592  //
593  // dcontent = %d33-90 / ; Printable US-ASCII
594  // %d94-126 ; excl. "[", "\", "]"
595  //
596  // Snum = 1*3DIGIT
597  // ; representing a decimal integer
598  // ; value in the range 0 through 255
599  //
600  // IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
601  //
602  // IPv6-hex = 1*4HEXDIG
603  //
604  // IPv6-full = IPv6-hex 7(":" IPv6-hex)
605  //
606  // IPv6-comp = [IPv6-hex *5(":" IPv6-hex)] "::"
607  // [IPv6-hex *5(":" IPv6-hex)]
608  // ; The "::" represents at least 2 16-bit groups of
609  // ; zeros. No more than 6 groups in addition to the
610  // ; "::" may be present.
611  //
612  // IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
613  //
614  // IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::"
615  // [IPv6-hex *3(":" IPv6-hex) ":"]
616  // IPv4-address-literal
617  // ; The "::" represents at least 2 16-bit groups of
618  // ; zeros. No more than 4 groups in addition to the
619  // ; "::" and IPv4-address-literal may be present.
620  //
621  // is_email() author's note: We can't use ip2long() to validate
622  // IPv4 addresses because it accepts abbreviated addresses
623  // (xxx.xxx.xxx), expanding the last group to complete the address.
624  // filter_var() validates IPv6 address inconsistently (up to PHP 5.3.3
625  // at least) -- see https://bugs.php.net/bug.php?id=53236 for example
626 
627  int maxGroups = 8; // NOLINT(cppcoreguidelines-avoid-magic-numbers)
628  qsizetype index = -1;
629  QString addressLiteral = parseLiteral;
630 
631  const QRegularExpressionMatch ipv4Match =
632  ValidatorEmailPrivate::ipv4Regex.match(addressLiteral);
633  if (ipv4Match.hasMatch()) {
634  index = addressLiteral.lastIndexOf(ipv4Match.captured());
635  if (index != 0) {
636  addressLiteral =
637  addressLiteral.mid(0, index) +
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(
836  ValidatorEmail::ErrorExpectingQText); // Fatal error
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");
1294  case RFC5321QuotedString:
1295  //% "Address is valid but contains a quoted string."
1296  return c->qtTrId("cutelyst-valemail-diag-rfc5321quotedstring");
1297  case RFC5321AddressLiteral:
1298  //% "Address is valid but uses an IP address instead of a domain name."
1299  return c->qtTrId("cutelyst-valemail-diag-rfc5321addressliteral");
1300  case RFC5321IPv6Deprecated:
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");
1311  case DeprecatedLocalpart:
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");
1329  case DeprecatedCFWSNearAt:
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));
1340  case RFC5322LocalTooLong:
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));
1345  case RFC5322DomainTooLong:
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));
1349  case RFC5322LabelTooLong:
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));
1354  case RFC5322DomainLiteral:
1355  //% "The domain literal is not a valid RFC 5321 address literal."
1356  return c->qtTrId("cutelyst-valemail-diag-rfc5322domainliteral");
1357  case RFC5322DomLitOBSDText:
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");
1361  case RFC5322IPv6GroupCount:
1362  //% "The IPv6 literal address contains the wrong number of groups."
1363  return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6groupcount");
1364  case RFC5322IPv62x2xColon:
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");
1370  case RFC5322IPv6MaxGroups:
1371  //% "The IPv6 address has too many groups."
1372  return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6maxgroups");
1373  case RFC5322IPv6ColonStart:
1374  //% "The IPv6 address starts with a single colon."
1375  return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonstart");
1376  case RFC5322IPv6ColonEnd:
1377  //% "The IPv6 address ends with a single colon."
1378  return c->qtTrId("cutelyst-valemail-diag-rfc5322ipv6colonend");
1379  case ErrorExpectingDText:
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");
1388  case ErrorConsecutiveDots:
1389  //% "The address must not contain consecutive dots."
1390  return c->qtTrId("cutelyst-valemail-diag-errconsecutivedots");
1391  case ErrorATextAfterCFWS:
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");
1397  case ErrorATextAfterDomLit:
1398  //% "Extra characters were found after the end of the domain literal."
1399  return c->qtTrId("cutelyst-valemail-diag-erratextafterdomlit");
1400  case ErrorExpectingQpair:
1401  //% "The Address contains a character that is not allowed in a quoted pair."
1402  return c->qtTrId("cutelyst-valemail-diag-errexpectingqpair");
1403  case ErrorExpectingAText:
1404  //% "Address contains a character that is not allowed."
1405  return c->qtTrId("cutelyst-valemail-diag-errexpectingatext");
1406  case ErrorExpectingQText:
1407  //% "A quoted string contains a character that is not allowed."
1408  return c->qtTrId("cutelyst-valemail-diag-errexpectingqtext");
1409  case ErrorExpectingCText:
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");
1424  case ErrorDomainHyphenEnd:
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");
1430  case ErrorUnclosedComment:
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);
1485  case RFC5321QuotedString:
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);
1488  case RFC5321AddressLiteral:
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);
1492  case RFC5321IPv6Deprecated:
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);
1504  case DeprecatedLocalpart:
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);
1526  case DeprecatedCFWSNearAt:
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));
1539  case RFC5322LocalTooLong:
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));
1544  case RFC5322DomainTooLong:
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));
1549  case RFC5322LabelTooLong:
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));
1554  case RFC5322DomainLiteral:
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);
1558  case RFC5322DomLitOBSDText:
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);
1562  case RFC5322IPv6GroupCount:
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);
1566  case RFC5322IPv62x2xColon:
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);
1574  case RFC5322IPv6MaxGroups:
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);
1577  case RFC5322IPv6ColonStart:
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);
1581  case RFC5322IPv6ColonEnd:
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);
1585  case ErrorExpectingDText:
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);
1595  case ErrorConsecutiveDots:
1596  //% "The address in the “%1” field must not contain consecutive dots."
1597  return c->qtTrId("cutelyst-valemail-diag-errconsecutivedots-label").arg(label);
1598  case ErrorATextAfterCFWS:
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);
1605  case ErrorATextAfterDomLit:
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);
1609  case ErrorExpectingQpair:
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);
1613  case ErrorExpectingAText:
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);
1616  case ErrorExpectingQText:
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);
1620  case ErrorExpectingCText:
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);
1637  case ErrorDomainHyphenEnd:
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);
1645  case ErrorUnclosedComment:
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 
1900 void ValidatorEmailDiagnoseStruct::sortReturnStatus()
1901 {
1902  std::ranges::sort(returnStatus, std::greater<>());
1903  finalStatus = returnStatus.at(0);
1904 }
1905 
1906 bool ValidatorEmailDiagnoseStruct::isBelowThreshold(ValidatorEmail::Category threshold) const
1907 {
1908  return static_cast<int>(finalStatus) < static_cast<int>(threshold);
1909 }
1910 
1911 #include "moc_validatoremail.cpp"
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
Stores custom error messages and the input field label.
qsizetype size() const const
QString captured(QStringView name) const const
T value() const const
void clear()
static QString categoryString(const Context *c, Category category, const QString &label={})
qsizetype size() const const
void finished()
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString toString(QDate date, FormatType format) const const
void clear()
The Cutelyst Context.
Definition: context.h:42
void defaultValue(Context *c, ValidatorReturnType *result) const
void abort()
QByteArray toAce(const QString &domain, AceProcessingOptions options)
Checks if the value is a valid email address according to specific RFCs.
bool isEmpty() const const
ValidatorEmail(const QString &field, Category threshold=RFC5321, Options options=NoOption, const ValidatorMessages &messages={}, const QString &defValKey={})
bool hasMatch() const const
static Category category(Diagnose diagnose)
The Cutelyst namespace holds all public Cutelyst API.
Base class for all validator rules.
char16_t & unicode()
QLocale locale() const noexcept
Definition: context.cpp:461
QString right(qsizetype n) const const
void push_back(QChar ch)
bool isLetterOrNumber(char32_t ucs4)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool contains(const AT &value) const const
QString value(const ParamsMultiMap &params) const
QString label(const Context *c) const
std::function< void(ValidatorReturnType &&result)> ValidatorRtFn
Void callback function for validator rules that processes the ValidatorReturnType.
Definition: validatorrule.h:82
const QChar * unicode() const const
QString fromLatin1(QByteArrayView str)
QString validationError(Context *c, const QVariant &errorData={}) const
QString mid(qsizetype position, qsizetype n) const const
QString qtTrId(const char *id, int n=-1) const
Definition: context.h:658
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.
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
void append(QList< T > &&value)
const QChar at(qsizetype position) const const
Category
Validation category, used as threshold to define valid addresses.
static QString diagnoseString(const Context *c, Diagnose diagnose, const QString &label={})
qsizetype length() const const
QString left(qsizetype n) const const
CarriageReturn
Contains the result of a single input parameter validation.
Definition: validatorrule.h:52
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
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.
QString arg(Args &&... args) const const
Diagnose
Single diagnose values that show why an address is not valid.
void setValue(QVariant &&value)