[System.ComponentModel.DataAnnotations] Implemented EmailAddressAttribute.IsValid()
authorJeffrey Stedfast <jeff@xamarin.com>
Mon, 7 Oct 2013 21:51:17 +0000 (17:51 -0400)
committerJeffrey Stedfast <jeff@xamarin.com>
Mon, 7 Oct 2013 21:51:17 +0000 (17:51 -0400)
mcs/class/System.ComponentModel.DataAnnotations/System.ComponentModel.DataAnnotations/EmailAddressAttribute.cs
mcs/class/System.ComponentModel.DataAnnotations/Test/System.ComponentModel.DataAnnotations/EmailAddressAttributeTest.cs

index 9c7199c94b3abbf9c4452b7ad5345e58d54a47f4..5d79b926c19d1b8eed45b059188f23ba1e42b882 100644 (file)
@@ -40,6 +40,254 @@ namespace System.ComponentModel.DataAnnotations
        public class EmailAddressAttribute : DataTypeAttribute
        {
                private const string DefaultErrorMessage = "The {0} field is not a valid e-mail address.";
+               const string AtomCharacters = "!#$%&'*+-/=?^_`{|}~";
+
+               static bool IsLetterOrDigit (char c)
+               {
+                       return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
+               }
+
+               static bool IsAtom (char c)
+               {
+                       return IsLetterOrDigit (c) || AtomCharacters.IndexOf (c) != -1;
+               }
+
+               static bool IsDomain (char c)
+               {
+                       return IsLetterOrDigit (c) || c == '-';
+               }
+
+               static bool SkipAtom (string text, ref int index)
+               {
+                       int startIndex = index;
+
+                       while (index < text.Length && IsAtom (text[index]))
+                               index++;
+
+                       return index > startIndex;
+               }
+
+               static bool SkipSubDomain (string text, ref int index)
+               {
+                       if (!IsDomain (text[index]) || text[index] == '-')
+                               return false;
+
+                       index++;
+
+                       while (index < text.Length && IsDomain (text[index]))
+                               index++;
+
+                       return true;
+               }
+
+               static bool SkipDomain (string text, ref int index)
+               {
+                       if (!SkipSubDomain (text, ref index))
+                               return false;
+
+                       while (index < text.Length && text[index] == '.') {
+                               index++;
+
+                               if (index == text.Length)
+                                       return false;
+
+                               if (!SkipSubDomain (text, ref index))
+                                       return false;
+                       }
+
+                       return true;
+               }
+
+               static bool SkipQuoted (string text, ref int index)
+               {
+                       bool escaped = false;
+
+                       // skip over leading '"'
+                       index++;
+
+                       while (index < text.Length) {
+                               if (text[index] == (byte) '\\') {
+                                       escaped = !escaped;
+                               } else if (!escaped) {
+                                       if (text[index] == (byte) '"')
+                                               break;
+                               } else {
+                                       escaped = false;
+                               }
+
+                               index++;
+                       }
+
+                       if (index >= text.Length || text[index] != (byte) '"')
+                               return false;
+
+                       index++;
+
+                       return true;
+               }
+
+               static bool SkipWord (string text, ref int index)
+               {
+                       if (text[index] == (byte) '"')
+                               return SkipQuoted (text, ref index);
+
+                       return SkipAtom (text, ref index);
+               }
+
+               static bool SkipIPv4Literal (string text, ref int index)
+               {
+                       int groups = 0;
+
+                       while (index < text.Length && groups < 4) {
+                               int startIndex = index;
+                               int value = 0;
+
+                               while (index < text.Length && text[index] >= '0' && text[index] <= '9') {
+                                       value = (value * 10) + (text[index] - '0');
+                                       index++;
+                               }
+
+                               if (index == startIndex || index - startIndex > 3 || value > 255)
+                                       return false;
+
+                               groups++;
+
+                               if (groups < 4 && index < text.Length && text[index] == '.')
+                                       index++;
+                       }
+
+                       return groups == 4;
+               }
+
+               static bool IsHexDigit (char c)
+               {
+                       return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
+               }
+
+               // This needs to handle the following forms:
+               //
+               // IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
+               // IPv6-hex  = 1*4HEXDIG
+               // IPv6-full = IPv6-hex 7(":" IPv6-hex)
+               // IPv6-comp = [IPv6-hex *5(":" IPv6-hex)] "::" [IPv6-hex *5(":" IPv6-hex)]
+               //             ; The "::" represents at least 2 16-bit groups of zeros
+               //             ; No more than 6 groups in addition to the "::" may be
+               //             ; present
+               // IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
+               // IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::"
+               //               [IPv6-hex *3(":" IPv6-hex) ":"] IPv4-address-literal
+               //             ; The "::" represents at least 2 16-bit groups of zeros
+               //             ; No more than 4 groups in addition to the "::" and
+               //             ; IPv4-address-literal may be present
+               static bool SkipIPv6Literal (string text, ref int index)
+               {
+                       bool compact = false;
+                       int colons = 0;
+
+                       while (index < text.Length) {
+                               int startIndex = index;
+
+                               while (index < text.Length && IsHexDigit (text[index]))
+                                       index++;
+
+                               if (index >= text.Length)
+                                       break;
+
+                               if (index > startIndex && colons > 2 && text[index] == '.') {
+                                       // IPv6v4
+                                       index = startIndex;
+
+                                       if (!SkipIPv4Literal (text, ref index))
+                                               return false;
+
+                                       break;
+                               }
+
+                               int count = index - startIndex;
+                               if (count > 4)
+                                       return false;
+
+                               if (text[index] != ':')
+                                       break;
+
+                               startIndex = index;
+                               while (index < text.Length && text[index] == ':')
+                                       index++;
+
+                               count = index - startIndex;
+                               if (count > 2)
+                                       return false;
+
+                               if (count == 2) {
+                                       if (compact)
+                                               return false;
+
+                                       compact = true;
+                                       colons += 2;
+                               } else {
+                                       colons++;
+                               }
+                       }
+
+                       if (colons < 2)
+                               return false;
+
+                       if (compact)
+                               return colons < 6;
+
+                       return colons < 7;
+               }
+
+               static bool Validate (string email)
+               {
+                       int index = 0;
+
+                       if (email.Length == 0)
+                               return false;
+
+                       if (!SkipWord (email, ref index) || index >= email.Length)
+                               return false;
+
+                       while (index < email.Length && email[index] == '.') {
+                               index++;
+
+                               if (!SkipWord (email, ref index) || index >= email.Length)
+                                       return false;
+                       }
+
+                       if (index + 1 >= email.Length || email[index++] != '@')
+                               return false;
+
+                       if (email[index] != '[') {
+                               // domain
+                               if (!SkipDomain (email, ref index))
+                                       return false;
+
+                               return index == email.Length;
+                       }
+
+                       // address literal
+                       index++;
+
+                       // we need at least 8 more characters
+                       if (index + 8 >= email.Length)
+                               return false;
+
+                       var ipv6 = email.Substring (index, 5);
+                       if (ipv6.ToLowerInvariant () == "ipv6:") {
+                               index += "IPv6:".Length;
+                               if (!SkipIPv6Literal (email, ref index))
+                                       return false;
+                       } else {
+                               if (!SkipIPv4Literal (email, ref index))
+                                       return false;
+                       }
+
+                       if (index >= email.Length || email[index++] != ']')
+                               return false;
+
+                       return index == email.Length;
+               }
 
                public EmailAddressAttribute ()
                        : base(DataType.EmailAddress)
@@ -53,10 +301,11 @@ namespace System.ComponentModel.DataAnnotations
                        if (value == null)
                                return true;
 
-                       if (value is string)
-                               return true;
+                       string email = value as string;
+                       if (email == null)
+                               return false;
 
-                       return false;
+                       return Validate (email);
                }
        }
 }
index 6e6e86be24e17fbe58d53853123caaf02d18dc1e..09350cde8fda4bed66a18cdf15273dfb0cbce995 100644 (file)
@@ -40,20 +40,47 @@ namespace MonoTests.System.ComponentModel.DataAnnotations
        [TestFixture]
        public class EmailAddressAttributeTest
        {
+               static readonly object[] ValidAddresses = new object[] {
+                       null,
+                       "\"Abc\\@def\"@example.com",
+                       "\"Fred Bloggs\"@example.com",
+                       "\"Joe\\\\Blow\"@example.com",
+                       "\"Abc@def\"@example.com",
+                       "customer/department=shipping@example.com",
+                       "$A12345@example.com",
+                       "!def!xyz%abc@example.com",
+                       "_somename@example.com",
+                       "valid.ipv4.addr@[123.1.72.10]",
+                       "valid.ipv6.addr@[IPv6:0::1]",
+                       "valid.ipv6.addr@[IPv6:2607:f0d0:1002:51::4]",
+                       "valid.ipv6.addr@[IPv6:fe80::230:48ff:fe33:bc33]",
+                       "valid.ipv6v4.addr@[IPv6:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:127.0.0.1]",
+               };
+
+               static readonly object[] InvalidAddresses = new object[] {
+                       "",
+                       123,
+                       DateTime.Now,
+                       "invalid",
+                       "invalid@",
+                       "invalid @",
+                       "invalid@[555.666.777.888]",
+                       "invalid@[IPv6:123456]",
+                       "invalid@[127.0.0.1.]",
+                       "invalid@[127.0.0.1].",
+                       "invalid@[127.0.0.1]x",
+               };
+
                [Test]
                public void IsValid ()
                {
                        var sla = new EmailAddressAttribute ();
 
-                       Assert.IsTrue (sla.IsValid (null), "#A1-1");
-#if false
-                       Assert.IsFalse (sla.IsValid (String.Empty), "#A1-2");
-                       Assert.IsFalse (sla.IsValid ("string"), "#A1-3");
-#endif
-                       Assert.IsTrue (sla.IsValid ("addr@mail.com"), "#A1-4");
-                       Assert.IsTrue (sla.IsValid ("addr@sub.mail.com"), "#A1-5");
-                       Assert.IsFalse (sla.IsValid (123), "#A1-6");
-                       Assert.IsFalse (sla.IsValid (DateTime.Now), "#A1-7");
+                       for (int i = 0; i < ValidAddresses.Length; i++)
+                               Assert.IsTrue (sla.IsValid (ValidAddresses[i]), "#A1-{0}", i);
+
+                       for (int i = 0; i < InvalidAddresses.Length; i++)
+                               Assert.IsFalse (sla.IsValid (InvalidAddresses[i]), "#B1-{0}", i);
                }
        }
 #endif