From 30d46f59623fe43def738ce7484ce1689b0b60d7 Mon Sep 17 00:00:00 2001 From: Misir Jafarov Date: Tue, 4 Nov 2025 23:36:34 +0100 Subject: [PATCH 1/5] Change default email validation logic to be more syntactically accurate --- example/pubspec.lock | 84 +++++----- lib/src/validator_builder.dart | 58 ++++++- lib/src/validator_options.dart | 6 +- pubspec.lock | 269 ++++++++++++++++++++------------- test/validation_test.dart | 38 +++-- 5 files changed, 286 insertions(+), 169 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 6639b9c..ddf1049 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -78,34 +78,34 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -118,87 +118,87 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.6" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.2" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/src/validator_builder.dart b/lib/src/validator_builder.dart index 5611102..4062916 100644 --- a/lib/src/validator_builder.dart +++ b/lib/src/validator_builder.dart @@ -133,8 +133,62 @@ class ValidationBuilder { add((v) => regExp.hasMatch(v!) ? null : message); /// Value must be a well formatted email - ValidationBuilder email([String? message]) => add((v) => - _options.emailRegExp.hasMatch(v!) ? null : message ?? _locale.email(v)); + ValidationBuilder email([String? message]) => + add((v) => (_options.emailRegExp != null + ? _options.emailRegExp!.hasMatch(v!) + : _checkEmail(v!)) + ? null + : message ?? _locale.email(v)); + + static final RegExp _emailLocalSpecialChars = RegExp(r'["(),:;<>@\[\\\]]'); + + static bool _checkEmail(String s) { + /* + Ref 1: https://stackoverflow.com/a/48170419 + Ref 2: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html#syntactic-validation + + 1. The email address contains two parts, separated with an @ symbol. + 2. The email address does not contain dangerous characters (such as backticks, single or double quotes, or null bytes). + Exactly which characters are dangerous will depend on how the address is going to be used (echoed in page, inserted into database, etc). + 3. The domain part contains only letters, numbers, hyphens (-) and periods (.). + 4. The email address is a reasonable length: + 4.1. The local part (before the @) should be no more than 63 characters. + 4.2. The total length should be no more than 254 characters. + */ + + // 2. "dangerous characters" is backend dependent, thus we can't implement one fits all solution + + // 4.2. + if (s.length > 254 || s.length < 3) return false; + + // 1. + final atIndex = s.lastIndexOf('@'); + if (atIndex < 0) return false; + + // 4. + final local = s.substring(0, atIndex); + if (local.length > 63 || local.length == 0) return false; + + final localIsQuoted = + local.startsWith('"') && local.endsWith('"') && local.length > 2; + if (!localIsQuoted) { + /* + Ref 3: https://en.wikipedia.org/wiki/Email_address#:~:text=Space%20and%20special%20characters + + Space and special characters "(),:;<>@[\] are allowed with + restrictions (they are only allowed inside a quoted string, + as described in the paragraph below, and in that quoted string, + any backslash or double-quote must be preceded once by a backslash); + */ + if (_emailLocalSpecialChars.hasMatch(local)) return false; + } + + // 3. + final domain = s.substring(atIndex + 1); + if (!domain.contains('.')) return false; + + return true; + } // needed for short circuiting the full validation static final RegExp _anyLetter = RegExp(r'[A-Za-z]'); diff --git a/lib/src/validator_options.dart b/lib/src/validator_options.dart index f2b0fb5..c31e40b 100644 --- a/lib/src/validator_options.dart +++ b/lib/src/validator_options.dart @@ -7,19 +7,19 @@ class ValidatorOptions { RegExp? ipv4RegExp, RegExp? ipv6RegExp, RegExp? urlRegExp, - }) : this.emailRegExp = emailRegExp ?? _defaultEmailRegExp, + }) : this.emailRegExp = emailRegExp, this.phoneRegExp = phoneRegExp ?? _defaultPhoneRegExp, this.ipv4RegExp = ipv4RegExp ?? _defaultIpv4RegExp, this.ipv6RegExp = ipv6RegExp ?? _defaultIpv6RegExp, this.urlRegExp = urlRegExp ?? _defaultUrlRegExp; - RegExp emailRegExp; + RegExp? emailRegExp; RegExp phoneRegExp; RegExp ipv4RegExp; RegExp ipv6RegExp; RegExp urlRegExp; - static final RegExp _defaultEmailRegExp = RegExp( + static final RegExp LegacyEmailRegExpThatHadFalseNegatives = RegExp( r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9\-\_]+(\.[a-zA-Z]+)*$"); static final RegExp _defaultPhoneRegExp = RegExp(r'^\d{7,15}$'); diff --git a/pubspec.lock b/pubspec.lock index 96ba5d5..068f3e6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,330 +5,385 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + url: "https://pub.dev" source: hosted - version: "20.0.0" + version: "91.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "8.4.1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.7.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" source: hosted - version: "1.2.0" - cli_util: + version: "2.1.2" + cli_config: dependency: transitive description: - name: cli_util - url: "https://pub.dartlang.org" + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.19.1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - url: "https://pub.dartlang.org" + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.15.0" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.7" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.3" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.2" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.5" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.2" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" source: hosted - version: "0.12.10" + version: "0.12.17" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.17.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0" node_preamble: dependency: transitive description: name: node_preamble - url: "https://pub.dartlang.org" + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.2.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" source: hosted - version: "1.8.0" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.0" + version: "1.9.1" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.2" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.2.0" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler - url: "https://pub.dartlang.org" + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.2" shelf_static: dependency: transitive description: name: shelf_static - url: "https://pub.dartlang.org" + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "3.0.0" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace - url: "https://pub.dartlang.org" + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" source_maps: dependency: transitive description: name: source_maps - url: "https://pub.dartlang.org" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" source: hosted - version: "0.10.10" + version: "0.10.13" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" test: dependency: "direct dev" description: name: test - url: "https://pub.dartlang.org" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" source: hosted - version: "1.16.8" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - url: "https://pub.dartlang.org" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + url: "https://pub.dev" source: hosted - version: "0.3.19" + version: "0.6.12" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" vm_service: dependency: transitive description: name: vm_service - url: "https://pub.dartlang.org" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.4" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol - url: "https://pub.dartlang.org" + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.3" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=3.9.0 <4.0.0" diff --git a/test/validation_test.dart b/test/validation_test.dart index db34aee..87c26f6 100644 --- a/test/validation_test.dart +++ b/test/validation_test.dart @@ -44,20 +44,28 @@ void main() { // What characters are allowed in an email address? // ref: https://stackoverflow.com/a/2049510/7616528 - checkValidation(validate, validValues: [ - 'user@gmil.com', - 'mani@main.com', - 'email@no-domain', - 'somelonger_email@domain.co.uk', - 'santa.claus@somewhere.us.com', - 'mail?@gmail.com', - 'a@b.c', - ], invalidValues: [ - 'notanemail', - 'email@gmail@mail.com', - '@g.com', - 'username@', - ]); + checkValidation( + validate, + validValues: [ + 'user@gmil.com', + 'mani@main.com', + 'email@nope.', + 'somelonger_email@domain.co.uk', + 'santa.claus@somewhere.us.com', + 'mail?@gmail.com', + 'a@b.c', + 'a.b.c@d.e.f', + 'a.b.c@d.e.f.', + 'a.b.c@127.0.0.1', + '"@"@at.', + ], + invalidValues: [ + 'notanemail', + '@g.com', + 'username@', + '@@a', + ], + ); }); test('validate phone number', () { @@ -93,7 +101,7 @@ void main() { ], invalidValues: [ '+123 some text 56789', '+1234567890123456', - 'mail@@gmail.com', + '@@gmail.com', ]); expect(validate('nothing'), equals('wrong email'), From b4e58035e13429c025107a9674d2dc429b8aaff3 Mon Sep 17 00:00:00 2001 From: Misir Jafarov Date: Tue, 4 Nov 2025 23:40:48 +0100 Subject: [PATCH 2/5] export ValidatorOptions --- lib/form_validator.dart | 1 + lib/src/validator_options.dart | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/form_validator.dart b/lib/form_validator.dart index 4ec14b4..7a01f3d 100644 --- a/lib/form_validator.dart +++ b/lib/form_validator.dart @@ -3,3 +3,4 @@ library form_validator; export 'src/locale.dart'; export 'src/validator_builder.dart' show ValidationBuilder, StringValidationCallback; +export 'src/validator_options.dart' show ValidatorOptions; diff --git a/lib/src/validator_options.dart b/lib/src/validator_options.dart index c31e40b..d1a1208 100644 --- a/lib/src/validator_options.dart +++ b/lib/src/validator_options.dart @@ -1,5 +1,3 @@ -typedef ValidatorPredicate = bool Function(String value); - class ValidatorOptions { ValidatorOptions({ RegExp? emailRegExp, From 237fdb11df753cc544df976123564ea026900731 Mon Sep 17 00:00:00 2001 From: Misir Jafarov Date: Tue, 4 Nov 2025 23:53:44 +0100 Subject: [PATCH 3/5] more checks --- lib/src/validator_builder.dart | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/src/validator_builder.dart b/lib/src/validator_builder.dart index 4062916..d49f6c1 100644 --- a/lib/src/validator_builder.dart +++ b/lib/src/validator_builder.dart @@ -143,6 +143,13 @@ class ValidationBuilder { static final RegExp _emailLocalSpecialChars = RegExp(r'["(),:;<>@\[\\\]]'); static bool _checkEmail(String s) { + // The goal is to allow as much values as possible while eliminating obvious + // invalid values. False negatives are way more harmful than false positives + // for client side email validation. + // + // A proper server-side SMTP based validation should be used on top whenever + // the validity of the email address is a concern. + /* Ref 1: https://stackoverflow.com/a/48170419 Ref 2: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html#syntactic-validation @@ -187,6 +194,19 @@ class ValidationBuilder { final domain = s.substring(atIndex + 1); if (!domain.contains('.')) return false; + // Not practical, but syntactically correct + if (domain.length < 3) return false; + + /* + Ref 4: https://webmasters.stackexchange.com/a/119105 + + > Each node has a label, which is zero to 63 octets in length. [...] + > One label is reserved, and that is the null (i.e., zero length) label used for the root. + > + > RFC 1034 + */ + if (domain.contains('..')) return false; + return true; } From 5be823056e7a22fea03b7c3b5bae1090cb7307bd Mon Sep 17 00:00:00 2001 From: Misir Jafarov Date: Tue, 4 Nov 2025 23:55:22 +0100 Subject: [PATCH 4/5] more domain checks --- lib/src/validator_builder.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/validator_builder.dart b/lib/src/validator_builder.dart index d49f6c1..61b38c0 100644 --- a/lib/src/validator_builder.dart +++ b/lib/src/validator_builder.dart @@ -205,6 +205,7 @@ class ValidationBuilder { > > RFC 1034 */ + if (domain.startsWith('.')) return false; if (domain.contains('..')) return false; return true; From e36d2d3eb2011526ca6cc60e46c2b7b620faafe3 Mon Sep 17 00:00:00 2001 From: Misir Jafarov Date: Wed, 5 Nov 2025 00:33:13 +0100 Subject: [PATCH 5/5] more email tests --- lib/src/validator_builder.dart | 63 +++++++++++++++++++++++----------- test/validation_test.dart | 2 ++ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/lib/src/validator_builder.dart b/lib/src/validator_builder.dart index 61b38c0..0cd28b9 100644 --- a/lib/src/validator_builder.dart +++ b/lib/src/validator_builder.dart @@ -136,13 +136,13 @@ class ValidationBuilder { ValidationBuilder email([String? message]) => add((v) => (_options.emailRegExp != null ? _options.emailRegExp!.hasMatch(v!) - : _checkEmail(v!)) + : _isValidEmail(v!)) ? null : message ?? _locale.email(v)); static final RegExp _emailLocalSpecialChars = RegExp(r'["(),:;<>@\[\\\]]'); - static bool _checkEmail(String s) { + bool _isValidEmail(String s) { // The goal is to allow as much values as possible while eliminating obvious // invalid values. False negatives are way more harmful than false positives // for client side email validation. @@ -176,9 +176,8 @@ class ValidationBuilder { final local = s.substring(0, atIndex); if (local.length > 63 || local.length == 0) return false; - final localIsQuoted = - local.startsWith('"') && local.endsWith('"') && local.length > 2; - if (!localIsQuoted) { + final localIsPotentiallyQuoted = local.contains('"') && local.length > 2; + if (!localIsPotentiallyQuoted) { /* Ref 3: https://en.wikipedia.org/wiki/Email_address#:~:text=Space%20and%20special%20characters @@ -192,21 +191,35 @@ class ValidationBuilder { // 3. final domain = s.substring(atIndex + 1); - if (!domain.contains('.')) return false; - // Not practical, but syntactically correct if (domain.length < 3) return false; - /* - Ref 4: https://webmasters.stackexchange.com/a/119105 + if (domain.startsWith('[')) { + if (domain.endsWith(']') && domain.length > 3) { + // IPv4 or IPv6 + final ip = domain.substring(1, domain.length - 1); + if (!_isValidIpv4(ip) && !_isValidIpv6(ip)) { + return false; + } + } else { + // unterminated + return false; + } + } else { + // 3. + if (!domain.contains('.')) return false; - > Each node has a label, which is zero to 63 octets in length. [...] - > One label is reserved, and that is the null (i.e., zero length) label used for the root. - > - > RFC 1034 - */ - if (domain.startsWith('.')) return false; - if (domain.contains('..')) return false; + /* + Ref 4: https://webmasters.stackexchange.com/a/119105 + + > Each node has a label, which is zero to 63 octets in length. [...] + > One label is reserved, and that is the null (i.e., zero length) label used for the root. + > + > RFC 1034 + */ + if (domain.startsWith('.')) return false; + if (domain.contains('..')) return false; + } return true; } @@ -223,12 +236,22 @@ class ValidationBuilder { : message ?? _locale.phoneNumber(v)); /// Value must be a well formatted IPv4 address - ValidationBuilder ip([String? message]) => add((v) => - _options.ipv4RegExp.hasMatch(v!) ? null : message ?? _locale.ip(v)); + ValidationBuilder ip([String? message]) => + add((v) => _isValidIpv4(v!) ? null : message ?? _locale.ip(v)); + + bool _isValidIpv4(String v) { + // todo: change to something that doesn't rely on RegExp + return _options.ipv4RegExp.hasMatch(v); + } /// Value must be a well formatted IPv6 address - ValidationBuilder ipv6([String? message]) => add((v) => - _options.ipv6RegExp.hasMatch(v!) ? null : message ?? _locale.ipv6(v)); + ValidationBuilder ipv6([String? message]) => + add((v) => _isValidIpv6(v!) ? null : message ?? _locale.ipv6(v)); + + bool _isValidIpv6(String v) { + // todo: change to something that doesn't rely on RegExp + return _options.ipv6RegExp.hasMatch(v); + } /// Value must be a well formatted URL address ValidationBuilder url([String? message]) => add((v) => diff --git a/test/validation_test.dart b/test/validation_test.dart index 87c26f6..aad1256 100644 --- a/test/validation_test.dart +++ b/test/validation_test.dart @@ -57,6 +57,8 @@ void main() { 'a.b.c@d.e.f', 'a.b.c@d.e.f.', 'a.b.c@127.0.0.1', + 'a.b.c@[127.0.0.1]', + 'cool@[fe80:3::1ff:fe23:4567:890a]', '"@"@at.', ], invalidValues: [