Repository: validatorjs/validator.js
Pull request #2188 (2023-02-21 12:08:24)

new validator: isMailtoURI, validate the mailto link URI format
<!--
Add a descriptive title textbox above, e.g.
feat(validatorName): brief title of what has been done
-->

<!--- briefly describe what you have done in this PR --->
Added a new validation method for validating the `mailto:` link URI. Where it'll validate the URI format and also the email provided inside the appropriate query params.

<!--- provide some (credible) references showing the structure of the data to be validated, if applicable --->
Here is the format of a mailto link URI `mailto:to(optional, comma-separated emails)?cc=(optional, comma-separated emails)&bb=(optional, comma-separated emails)&subject=(optional)&body=(optional)`

Some links:
[https://mailtrap.io/blog/mailto-links-explained/](https://mailtrap.io/blog/mailto-links-explained/)
[https://en.wikipedia.org/wiki/Mailto](https://en.wikipedia.org/wiki/Mailto)

## Checklist

- [x] PR contains only changes related; no stray files, etc.
- [x] README updated (where applicable)
- [x] Tests written (where applicable)
- [x] References provided in PR (where applicable)


codecov[bot]: # [Codecov](https://codecov.io/gh/validatorjs/validator.js/pull/2188?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs) Report
Base: **100.00**% // Head: **99.95**% // Decreases project coverage by **`-0.05%`** :warning:
> Coverage data is based on head [(`49e4e9c`)](https://codecov.io/gh/validatorjs/validator.js/pull/2188?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs) compared to base [(`43803c0`)](https://codecov.io/gh/validatorjs/validator.js/commit/43803c08d23b9cf316f2e804445b643b52920121?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs).
> Patch coverage: 97.56% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##            master    #2188      +/-   ##
===========================================
- Coverage   100.00%   99.95%   -0.05%     
===========================================
  Files          106      107       +1     
  Lines         2348     2389      +41     
  Branches       593      604      +11     
===========================================
+ Hits          2348     2388      +40     
- Partials         0        1       +1     
```


| [Impacted Files](https://codecov.io/gh/validatorjs/validator.js/pull/2188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs) | Coverage Δ | |
|---|---|---|
| [src/lib/isMailtoURI.js](https://codecov.io/gh/validatorjs/validator.js/pull/2188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs#diff-c3JjL2xpYi9pc01haWx0b1VSSS5qcw==) | `97.50% <97.50%> (ø)` | |
| [src/index.js](https://codecov.io/gh/validatorjs/validator.js/pull/2188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs#diff-c3JjL2luZGV4Lmpz) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/validatorjs/validator.js/pull/2188?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=validatorjs).

WikiRik: Could you provide some use cases for this new validator? When would an user need to input such a mailto URI? Or when would you want to check this coming from another system? I think this would be a too niche use case for this project
uksarkar: > Could you provide some use cases for this new validator? When would an user need to input such a mailto URI? Or when would you want to check this coming from another system? I think this would be a too niche use case for this project

For projects like website builder, it would be great to provide the user some feedback about what they are trying to achieve and whether it's valid or not.

Patch:
diff --git a/README.md b/README.md
index d75f15b..5a33d32 100644
--- a/README.md
+++ b/README.md
@@ -148,9 +148,10 @@ Validator                               | Description
 **isLuhnNumber(str)**                    | check if the string passes the [Luhn algorithm check](https://en.wikipedia.org/wiki/Luhn_algorithm).
 **isMACAddress(str [, options])**                   | check if the string is a MAC address.<br/><br/>`options` is an object which defaults to `{ no_separators: false }`. If `no_separators` is true, the validator will allow MAC addresses without separators. Also, it allows the use of hyphens, spaces or dots e.g. '01 02 03 04 05 ab', '01-02-03-04-05-ab' or '0102.0304.05ab'. The options also allow a `eui` property to specify if it needs to be validated against EUI-48 or EUI-64. The accepted values of `eui` are: 48, 64.
 **isMagnetURI(str)**                      | check if the string is a [Magnet URI format][Magnet URI Format].
+**isMailtoURI(str, [, options])**                      | check if the string is a [Magnet URI format][Mailto URI Format].<br/><br/>`options` is an object of validating emails inside the URI (check `isEmail`s options for details).
 **isMD5(str)**                          | check if the string is a MD5 hash.<br/><br/>Please note that you can also use the `isHash(str, 'md5')` function. Keep in mind that MD5 has some collision weaknesses compared to other algorithms (e.g., SHA).
 **isMimeType(str)**                     | check if the string matches to a valid [MIME type][MIME Type] format.
-**isMobilePhone(str [, locale [, options]])**          | check if the string is a mobile phone number,<br/><br/>`locale` is either an array of locales (e.g. `['sk-SK', 'sr-RS']`) OR one of `['am-Am', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-EH', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-PS', 'ar-SA', 'ar-SY', 'ar-TN', 'ar-YE', 'az-AZ', 'az-LB', 'az-LY', 'be-BY', 'bg-BG', 'bn-BD', 'bs-BA', 'ca-AD', 'cs-CZ', 'da-DK', 'de-AT', 'de-CH', 'de-DE', 'de-LU', 'dv-MV', 'dz-BT', 'el-CY', 'el-GR', 'en-AG', 'en-AI', 'en-AU', 'en-BM', 'en-BS', 'en-BW', 'en-CA', 'en-GB', 'en-GG', 'en-GH', 'en-GY', 'en-HK', 'en-IE', 'en-IN', 'en-JM', 'en-KE', 'en-KI', 'en-KN', 'en-LS', 'en-MO', 'en-MT', 'en-MU', 'en-NG', 'en-NZ', 'en-PG', 'en-PH', 'en-PK', 'en-RW', 'en-SG', 'en-SL', 'en-SS', 'en-TZ', 'en-UG', 'en-US', 'en-ZA', 'en-ZM', 'en-ZW', 'es-AR', 'es-BO', 'es-CL', 'es-CO', 'es-CR', 'es-CU', 'es-DO', 'es-EC', 'es-ES', 'es-HN', 'es-MX', 'es-NI', 'es-PA', 'es-PE', 'es-PY', 'es-SV', 'es-UY', 'es-VE', 'et-EE', 'fa-AF', 'fa-IR', 'fi-FI', 'fj-FJ', 'fo-FO', 'fr-BE', 'fr-BF', 'fr-BJ', 'fr-CD', 'fr-CF', 'fr-FR', 'fr-GF', 'fr-GP', 'fr-MQ', 'fr-PF', 'fr-RE', 'ga-IE', 'he-IL', 'hu-HU', 'id-ID', 'ir-IR', 'it-IT', 'it-SM', 'ja-JP', 'ka-GE', 'kk-KZ', 'kl-GL', 'ko-KR', 'ky-KG', 'lt-LT', 'mg-MG', 'mn-MN', 'ms-MY', 'my-MM', 'mz-MZ', 'nb-NO', 'ne-NP', 'nl-AW', 'nl-BE', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-AO', 'pt-BR', 'pt-PT', 'ro-Md', 'ro-RO', 'ru-RU', 'si-LK', 'sk-SK', 'sl-SI', 'so-SO', 'sq-AL', 'sr-RS', 'sv-SE', 'tg-TJ', 'th-TH', 'tk-TM', 'tr-TR', 'uk-UA', 'uz-UZ', 'vi-VN', 'zh-CN', 'zh-HK', 'zh-MO', 'zh-TW']` OR defaults to `'any'`. If 'any' or a falsey value is used, function will check if any of the locales match).<br/><br/>`options` is an optional object that can be supplied with the following keys: `strictMode`, if this is set to `true`, the mobile phone number must be supplied with the country code and therefore must start with `+`. Locale list is `validator.isMobilePhoneLocales`.
+**isMobilePhone(str [, locale [, options]])**          | check if the string is a mobile phone number,<br/><br/>`locale` is either an array of locales (e.g. `['sk-SK', 'sr-RS']`) OR one of `['am-Am', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-EH', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-PS', 'ar-SA', 'ar-SY', 'ar-TN', 'ar-YE', 'az-AZ', 'az-LB', 'az-LY', 'be-BY', 'bg-BG', 'bn-BD', 'bs-BA', 'ca-AD', 'cs-CZ', 'da-DK', 'de-AT', 'de-CH', 'de-DE', 'de-LU', 'dv-MV', 'dz-BT', 'el-CY', 'el-GR', 'en-AG', 'en-AI', 'en-AU', 'en-BM', 'en-BS', 'en-BW', 'en-CA', 'en-GB', 'en-GG', 'en-GH', 'en-GY', 'en-HK', 'en-IE', 'en-IN', 'en-JM', 'en-KE', 'en-KI', 'en-KN', 'en-LS', 'en-MO', 'en-MT', 'en-MU', 'en-NG', 'en-NZ', 'en-PG', 'en-PH', 'en-PK', 'en-RW', 'en-SG', 'en-SL', 'en-SS', 'en-TZ', 'en-UG', 'en-US', 'en-ZA', 'en-ZM', 'en-ZW', 'es-AR', 'es-BO', 'es-CL', 'es-CO', 'es-CR', 'es-CU', 'es-DO', 'es-EC', 'es-ES', 'es-HN', 'es-MX', 'es-NI', 'es-PA', 'es-PE', 'es-PY', 'es-SV', 'es-UY', 'es-VE', 'et-EE', 'fa-AF', 'fa-IR', 'fi-FI', 'fj-FJ', 'fo-FO', 'fr-BE', 'fr-BF', 'fr-BJ', 'fr-CD', 'fr-CF', 'fr-FR', 'fr-GF', 'fr-GP', 'fr-MQ', 'fr-PF', 'fr-RE', 'fr-WF', 'ga-IE', 'he-IL', 'hu-HU', 'id-ID', 'ir-IR', 'it-IT', 'it-SM', 'ja-JP', 'ka-GE', 'kk-KZ', 'kl-GL', 'ko-KR', 'ky-KG', 'lt-LT', 'mg-MG', 'mn-MN', 'ms-MY', 'my-MM', 'mz-MZ', 'nb-NO', 'ne-NP', 'nl-AW', 'nl-BE', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-AO', 'pt-BR', 'pt-PT', 'ro-Md', 'ro-RO', 'ru-RU', 'si-LK', 'sk-SK', 'sl-SI', 'so-SO', 'sq-AL', 'sr-RS', 'sv-SE', 'tg-TJ', 'th-TH', 'tk-TM', 'tr-TR', 'uk-UA', 'uz-UZ', 'vi-VN', 'zh-CN', 'zh-HK', 'zh-MO', 'zh-TW']` OR defaults to `'any'`. If 'any' or a falsey value is used, function will check if any of the locales match).<br/><br/>`options` is an optional object that can be supplied with the following keys: `strictMode`, if this is set to `true`, the mobile phone number must be supplied with the country code and therefore must start with `+`. Locale list is `validator.isMobilePhoneLocales`.
 **isMongoId(str)**                      | check if the string is a valid hex-encoded representation of a [MongoDB ObjectId][mongoid].
 **isMultibyte(str)**                    | check if the string contains one or more multibyte chars.
 **isNumeric(str [, options])**                      | check if the string contains only numbers.<br/><br/>`options` is an object which defaults to `{ no_symbols: false }` it also has `locale` as an option. If `no_symbols` is true, the validator will reject numeric strings that feature a symbol (e.g. `+`, `-`, or `.`).<br/><br/>`locale` determines the decimal separator and is one of `['ar', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-LB', 'ar-LY', 'ar-MA', 'ar-QA', 'ar-QM', 'ar-SA', 'ar-SD', 'ar-SY', 'ar-TN', 'ar-YE', 'bg-BG', 'cs-CZ', 'da-DK', 'de-DE', 'en-AU', 'en-GB', 'en-HK', 'en-IN', 'en-NZ', 'en-US', 'en-ZA', 'en-ZM', 'es-ES', 'fr-FR', 'fr-CA', 'hu-HU', 'it-IT', 'nb-NO', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-BR', 'pt-PT', 'ru-RU', 'sl-SI', 'sr-RS', 'sr-RS@latin', 'sv-SE', 'tr-TR', 'uk-UA']`.
@@ -301,6 +302,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 [ISSN]: https://en.wikipedia.org/wiki/International_Standard_Serial_Number
 [Luhn Check]: https://en.wikipedia.org/wiki/Luhn_algorithm
 [Magnet URI Format]: https://en.wikipedia.org/wiki/Magnet_URI_scheme
+[Mailto URI Format]: https://en.wikipedia.org/wiki/Mailto
 [MIME Type]: https://en.wikipedia.org/wiki/Media_type
 [mongoid]: http://docs.mongodb.org/manual/reference/object-id/
 [RFC 3339]: https://tools.ietf.org/html/rfc3339
diff --git a/src/index.js b/src/index.js
index 07cbbef..fd97dc3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -101,6 +101,7 @@ import isBase58 from './lib/isBase58';
 import isBase64 from './lib/isBase64';
 import isDataURI from './lib/isDataURI';
 import isMagnetURI from './lib/isMagnetURI';
+import isMailtoURI from './lib/isMailtoURI';
 
 import isMimeType from './lib/isMimeType';
 
@@ -213,6 +214,7 @@ const validator = {
   isBase64,
   isDataURI,
   isMagnetURI,
+  isMailtoURI,
   isMimeType,
   isLatLong,
   ltrim,
diff --git a/src/lib/isLocale.js b/src/lib/isLocale.js
index cacac8a..ec84c8f 100644
--- a/src/lib/isLocale.js
+++ b/src/lib/isLocale.js
@@ -1,11 +1,111 @@
 import assertString from './util/assertString';
 
-const localeReg = /^[A-Za-z]{2,4}([_-]([A-Za-z]{4}|[\d]{3}))?([_-]([A-Za-z]{2}|[\d]{3}))?$/;
+/*
+  = 3ALPHA              ; selected ISO 639 codes
+    *2("-" 3ALPHA)      ; permanently reserved
+ */
+const extlang = '([A-Za-z]{3}(-[A-Za-z]{3}){0,2})';
+
+/*
+  = 2*3ALPHA            ; shortest ISO 639 code
+    ["-" extlang]       ; sometimes followed by
+                        ; extended language subtags
+  / 4ALPHA              ; or reserved for future use
+  / 5*8ALPHA            ; or registered language subtag
+ */
+const language = `(([a-zA-Z]{2,3}(-${extlang})?)|([a-zA-Z]{5,8}))`;
+
+/*
+  = 4ALPHA              ; ISO 15924 code
+ */
+const script = '([A-Za-z]{4})';
+
+/*
+  = 2ALPHA              ; ISO 3166-1 code
+  / 3DIGIT              ; UN M.49 code
+ */
+const region = '([A-Za-z]{2}|\\d{3})';
+
+/*
+  = 5*8alphanum         ; registered variants
+  / (DIGIT 3alphanum)
+ */
+const variant = '([A-Za-z0-9]{5,8}|(\\d[A-Z-a-z0-9]{3}))';
+
+/*
+  = DIGIT               ; 0 - 9
+  / %x41-57             ; A - W
+  / %x59-5A             ; Y - Z
+  / %x61-77             ; a - w
+  / %x79-7A             ; y - z
+ */
+const singleton = '(\\d|[A-W]|[Y-Z]|[a-w]|[y-z])';
+
+/*
+  = singleton 1*("-" (2*8alphanum))
+                        ; Single alphanumerics
+                        ; "x" reserved for private use
+ */
+const extension = `(${singleton}(-[A-Za-z0-9]{2,8})+)`;
+
+/*
+  = "x" 1*("-" (1*8alphanum))
+ */
+const privateuse = '(x(-[A-Za-z0-9]{1,8})+)';
+
+// irregular tags do not match the 'langtag' production and would not
+// otherwise be considered 'well-formed'. These tags are all valid, but
+// most are deprecated in favor of more modern subtags or subtag combination
+
+const irregular = '((en-GB-oed)|(i-ami)|(i-bnn)|(i-default)|(i-enochian)|' +
+    '(i-hak)|(i-klingon)|(i-lux)|(i-mingo)|(i-navajo)|(i-pwn)|(i-tao)|' +
+    '(i-tay)|(i-tsu)|(sgn-BE-FR)|(sgn-BE-NL)|(sgn-CH-DE))';
+
+// regular tags match the 'langtag' production, but their subtags are not
+// extended language or variant subtags: their meaning is defined by
+// their registration and all of these are deprecated in favor of a more
+// modern subtag or sequence of subtags
+
+const regular = '((art-lojban)|(cel-gaulish)|(no-bok)|(no-nyn)|(zh-guoyu)|' +
+    '(zh-hakka)|(zh-min)|(zh-min-nan)|(zh-xiang))';
+
+/*
+  = irregular           ; non-redundant tags registered
+  / regular             ; during the RFC 3066 era
+
+ */
+const grandfathered = `(${irregular}|${regular})`;
+
+/*
+  RFC 5646 defines delimitation of subtags via a hyphen:
+
+      "Subtag" refers to a specific section of a tag, delimited by a
+      hyphen, such as the subtags 'zh', 'Hant', and 'CN' in the tag "zh-
+      Hant-CN".  Examples of subtags in this document are enclosed in
+      single quotes ('Hant')
+
+  However, we need to add "_" to maintain the existing behaviour.
+ */
+const delimiter = '(-|_)';
+
+/*
+  = language
+    ["-" script]
+    ["-" region]
+    *("-" variant)
+    *("-" extension)
+    ["-" privateuse]
+ */
+const langtag = `${language}(${delimiter}${script})?(${delimiter}${region})?(${delimiter}${variant})*(${delimiter}${extension})*(${delimiter}${privateuse})?`;
+
+/*
+  Regex implementation based on BCP RFC 5646
+  Tags for Identifying Languages
+  https://www.rfc-editor.org/rfc/rfc5646.html
+ */
+const languageTagRegex = new RegExp(`(^${privateuse}$)|(^${grandfathered}$)|(^${langtag}$)`);
 
 export default function isLocale(str) {
   assertString(str);
-  if (str === 'en_US_POSIX' || str === 'ca_ES_VALENCIA') {
-    return true;
-  }
-  return localeReg.test(str);
+  return languageTagRegex.test(str);
 }
diff --git a/src/lib/isMailtoURI.js b/src/lib/isMailtoURI.js
new file mode 100644
index 0000000..0dd95b6
--- /dev/null
+++ b/src/lib/isMailtoURI.js
@@ -0,0 +1,67 @@
+import trim from './trim';
+import isEmail from './isEmail';
+import assertString from './util/assertString';
+
+function parseMailtoQueryString(queryString) {
+  const allowedParams = new Set(['subject', 'body', 'cc', 'bcc']),
+    query = { cc: '', bcc: '' };
+  let isParseFailed = false;
+
+  const queryParams = queryString.split('&');
+
+  if (queryParams.length > 4) {
+    return false;
+  }
+
+  for (const q of queryParams) {
+    const [key, value] = q.split('=');
+
+    // checked for invalid and duplicated query params
+    if (key && !allowedParams.has(key)) {
+      isParseFailed = true;
+      break;
+    }
+
+    if (value && (key === 'cc' || key === 'bcc')) {
+      query[key] = value;
+    }
+
+    if (key) {
+      allowedParams.delete(key);
+    }
+  }
+
+  return isParseFailed ? false : query;
+}
+
+export default function isMailtoURI(url, options) {
+  assertString(url);
+
+  if (url.indexOf('mailto:') !== 0) {
+    return false;
+  }
+
+  const [to = '', queryString = ''] = url.replace('mailto:', '').split('?');
+
+  if (!to && !queryString) {
+    return true;
+  }
+
+  const query = parseMailtoQueryString(queryString);
+
+  if (!query) {
+    return false;
+  }
+
+  return `${to},${query.cc},${query.bcc}`
+    .split(',')
+    .every((email) => {
+      email = trim(email, ' ');
+
+      if (email) {
+        return isEmail(email, options);
+      }
+
+      return true;
+    });
+}
diff --git a/src/lib/isMobilePhone.js b/src/lib/isMobilePhone.js
index c6ded55..6a8f987 100644
--- a/src/lib/isMobilePhone.js
+++ b/src/lib/isMobilePhone.js
@@ -76,7 +76,7 @@ const phones = {
   'es-CO': /^(\+?57)?3(0(0|1|2|4|5)|1\d|2[0-4]|5(0|1))\d{7}$/,
   'es-CL': /^(\+?56|0)[2-9]\d{1}\d{7}$/,
   'es-CR': /^(\+506)?[2-8]\d{7}$/,
-  'es-CU': /^(\+53|0053)?5\d{7}/,
+  'es-CU': /^(\+53|0053)?5\d{7}$/,
   'es-DO': /^(\+?1)?8[024]9\d{7}$/,
   'es-HN': /^(\+?504)?[9|8|3|2]\d{7}$/,
   'es-EC': /^(\+?593|0)([2-7]|9[2-9])\d{7}$/,
@@ -104,6 +104,7 @@ const phones = {
   'fr-MQ': /^(\+?596|0|00596)[67]\d{8}$/,
   'fr-PF': /^(\+?689)?8[789]\d{6}$/,
   'fr-RE': /^(\+?262|0|00262)[67]\d{8}$/,
+  'fr-WF': /^(\+681)?\d{6}$/,
   'he-IL': /^(\+972|0)([23489]|5[012345689]|77)[1-9]\d{6}$/,
   'hu-HU': /^(\+?36|06)(20|30|31|50|70)\d{7}$/,
   'id-ID': /^(\+?62|0)8(1[123456789]|2[1238]|3[1238]|5[12356789]|7[78]|9[56789]|8[123456789])([\s?|\d]{5,11})$/,
diff --git a/src/lib/isVAT.js b/src/lib/isVAT.js
index 9559356..ece7d85 100644
--- a/src/lib/isVAT.js
+++ b/src/lib/isVAT.js
@@ -1,6 +1,21 @@
 import assertString from './util/assertString';
 import * as algorithms from './util/algorithms';
 
+const CH = (str) => {
+  // @see {@link https://www.ech.ch/de/ech/ech-0097/5.2.0}
+  const hasValidCheckNumber = (digits) => {
+    const lastDigit = digits.pop(); // used as check number
+    const weights = [5, 4, 3, 2, 7, 6, 5, 4];
+    const calculatedCheckNumber = (11 - (digits.reduce((acc, el, idx) =>
+      acc + (el * weights[idx]), 0) % 11)) % 11;
+
+    return lastDigit === calculatedCheckNumber;
+  };
+
+  // @see {@link https://www.estv.admin.ch/estv/de/home/mehrwertsteuer/uid/mwst-uid-nummer.html}
+  return /^(CHE[- ]?)?(\d{9}|(\d{3}\.\d{3}\.\d{3})|(\d{3} \d{3} \d{3})) ?(TVA|MWST|IVA)?$/.test(str) && hasValidCheckNumber((str.match(/\d/g).map(el => +el)));
+};
+
 const PT = (str) => {
   const match = str.match(/^(PT)?(\d{9})$/);
   if (!match) {
@@ -69,7 +84,7 @@ export const vatMatchers = {
   SM: str => /^(SM)?\d{5}$/.test(str),
   SA: str => /^(SA)?\d{15}$/.test(str),
   RS: str => /^(RS)?\d{9}$/.test(str),
-  CH: str => /^(CH)?(\d{6}|\d{9}|(\d{3}.\d{3})|(\d{3}.\d{3}.\d{3}))(TVA|MWST|IVA)$/.test(str),
+  CH,
   TR: str => /^(TR)?\d{10}$/.test(str),
   UA: str => /^(UA)?\d{12}$/.test(str),
   GB: str => /^GB((\d{3} \d{4} ([0-8][0-9]|9[0-6]))|(\d{9} \d{3})|(((GD[0-4])|(HA[5-9]))[0-9]{2}))$/.test(str),


Test patch:
diff --git a/test/validators.test.js b/test/validators.test.js
index 239f172..937b52a 100644
--- a/test/validators.test.js
+++ b/test/validators.test.js
@@ -4816,16 +4816,51 @@ describe('Validators', () => {
         'uz_Latn_UZ',
         'en',
         'gsw',
+        'en-US',
         'es_ES',
+        'es-419',
         'sw_KE',
         'am_ET',
+        'zh-CHS',
         'ca_ES_VALENCIA',
         'en_US_POSIX',
+        'hak-CN',
+        'zh-Hant',
+        'zh-Hans',
+        'sr-Cyrl',
+        'sr-Latn',
+        'zh-cmn-Hans-CN',
+        'cmn-Hans-CN',
+        'zh-yue-HK',
+        'yue-HK',
+        'zh-Hans-CN',
+        'sr-Latn-RS',
+        'sl-rozaj',
+        'sl-rozaj-biske',
+        'sl-nedis',
+        'de-CH-1901',
+        'sl-IT-nedis',
+        'hy-Latn-IT-arevela',
+        'i-enochian',
+        'en-scotland-fonipa',
+        'sl-IT-rozaj-biske-1994',
+        'de-CH-x-phonebk',
+        'az-Arab-x-AZE-derbend',
+        'x-whatever',
+        'qaa-Qaaa-QM-x-southern',
+        'de-Qaaa',
+        'sr-Latn-QM',
+        'sr-Qaaa-RS',
+        'en-US-u-islamcal',
+        'zh-CN-a-myext-x-private',
+        'en-a-myext-b-another',
       ],
       invalid: [
         'lo_POP',
         '12',
         '12_DD',
+        'de-419-DE',
+        'a-DE',
       ],
     });
   });
@@ -7764,6 +7799,32 @@ describe('Validators', () => {
           '6898912345',
         ],
       },
+      {
+        locale: 'fr-WF',
+        valid: [
+          '+681408500',
+          '+681499387',
+          '+681728590',
+          '+681808542',
+          '+681828540',
+          '+681832014',
+          '408500',
+          '499387',
+          '728590',
+          '808542',
+          '828540',
+          '832014',
+        ],
+        invalid: [
+          '+68189032',
+          '123456789',
+          '+681723845987',
+          '022452389',
+          '+681772345678',
+          '+681700456794',
+
+        ],
+      },
       {
         locale: 'ka-GE',
         valid: [
@@ -8296,6 +8357,7 @@ describe('Validators', () => {
           '',
           'abc',
           '+535123457',
+          '56043029304',
         ],
       },
       {
@@ -13864,18 +13926,30 @@ describe('Validators', () => {
       validator: 'isVAT',
       args: ['CH'],
       valid: [
-        'CH123456TVA',
-        '123456TVA',
-        'CH123456789MWST',
-        '123456789MWST',
-        'CH123.456IVA',
-        '123.456IVA',
-        'CH123.456.789TVA',
-        '123.456.789TVA',
-      ],
-      invalid: [
-        'CH 123456',
-        '12345',
+        // strictly valid
+        'CHE-116.281.710 MWST',
+        'CHE-116.281.710 IVA',
+        'CHE-116.281.710 TVA',
+        // loosely valid presentation variants
+        'CHE 116 281 710 IVA', // all separators are spaces
+        'CHE-191.398.369MWST', // no space before suffix
+        'CHE-116281710 MWST', // no number separators
+        'CHE-116281710MWST', // no number separators and no space before suffix
+        'CHE105854263MWST', // no separators
+        'CHE-116.285.524', // no suffix (vat abbreviation)
+        'CHE116281710', // no suffix and separators
+        '116.281.710 TVA', // no prefix (CHE, ISO-3166-1 Alpha-3)
+        '116281710MWST', // no prefix and separators
+        '100.218.485', // no prefix and suffix
+        '123456788', // no prefix, separators and suffix
+      ],
+      invalid: [
+        'CH-116.281.710 MWST', // invalid prefix (should be CHE)
+        'CHE-116.281 MWST', // invalid number of digits (should be 9)
+        'CHE-123.456.789 MWST', // invalid last digit (should match the calculated check-number 8)
+        'CHE-123.356.780 MWST', // invalid check-number (there are no swiss UIDs with the calculated check number 10)
+        'CH-116.281.710 VAT', // invalid suffix (should be MWST, IVA or TVA)
+        'CHE-116/281/710 IVA', // invalid number separators (should be all dots or all spaces)
       ],
     });
     test({
@@ -14179,4 +14253,55 @@ describe('Validators', () => {
       ],
     });
   });
+  it('should validate mailto URI', () => {
+    test({
+      validator: 'isMailtoURI',
+      valid: [
+        'mailto:?subject=something&cc=valid@mail.com',
+        'mailto:?subject=something&cc=valid@mail.com,another@mail.com,',
+        'mailto:?subject=something&bcc=valid@mail.com',
+        'mailto:?subject=something&bcc=valid@mail.com,another@mail.com',
+        'mailto:?bcc=valid@mail.com,another@mail.com',
+        'mailto:?cc=valid@mail.com,another@mail.com',
+        'mailto:?cc=valid@mail.com',
+        'mailto:?bcc=valid@mail.com',
+        'mailto:?subject=something&body=something else',
+        'mailto:?subject=something&body=something else&cc=hello@mail.com,another@mail.com',
+        'mailto:?subject=something&body=something else&bcc=hello@mail.com,another@mail.com',
+        'mailto:?subject=something&body=something else&cc=something@mail.com&bcc=hello@mail.com,another@mail.com',
+        'mailto:hello@mail.com',
+        'mailto:info@mail.com?',
+        'mailto:hey@mail.com?subject=something',
+        'mailto:info@mail.com?subject=something&cc=valid@mail.com',
+        'mailto:info@mail.com?subject=something&cc=valid@mail.com,another@mail.com,',
+        'mailto:info@mail.com?subject=something&bcc=valid@mail.com',
+        'mailto:info@mail.com?subject=something&bcc=valid@mail.com,another@mail.com',
+        'mailto:info@mail.com?bcc=valid@mail.com,another@mail.com',
+        'mailto:info@mail.com?cc=valid@mail.com,another@mail.com',
+        'mailto:info@mail.com?cc=valid@mail.com',
+        'mailto:info@mail.com?bcc=valid@mail.com&',
+        'mailto:info@mail.com?subject=something&body=something else',
+        'mailto:info@mail.com?subject=something&body=something else&cc=hello@mail.com,another@mail.com',
+        'mailto:info@mail.com?subject=something&body=something else&bcc=hello@mail.com,another@mail.com',
+        'mailto:info@mail.com?subject=something&body=something else&cc=something@mail.com&bcc=hello@mail.com,another@mail.com',
+        'mailto:',
+      ],
+      invalid: [
+        '',
+        'somthing',
+        'valid@gmail.com',
+        'mailto:?subject=okay&subject=444',
+        'mailto:?subject=something&wrong=888',
+        'mailto:somename@ｇｍａｉｌ.com',
+        'mailto:hello@world.com?cc=somename@ｇｍａｉｌ.com',
+        'mailto:hello@world.com?bcc=somename@ｇｍａｉｌ.com',
+        'mailto:hello@world.com?bcc=somename@ｇｍａｉｌ.com&bcc',
+        'mailto:valid@gmail.com?subject=anything&body=nothing&cc=&bcc=&key=',
+        'mailto:hello@world.com?cc=somename',
+        'mailto:somename',
+        'mailto:info@mail.com?subject=something&body=something else&cc=something@mail.com&bcc=hello@mail.com,another@mail.com&',
+        'mailto:?subject=something&body=something else&cc=something@mail.com&bcc=hello@mail.com,another@mail.com&',
+      ],
+    });
+  });
 });