Subversion Repositories javautils

Rev

Blame | Last modification | View Log | RSS feed

  1. package com.iamcal.rfc3696;
  2.  
  3. import java.util.regex.MatchResult;
  4. import java.util.regex.Matcher;
  5. import java.util.regex.Pattern;
  6.  
  7. //
  8. // RFC3696 Email Parser
  9. //
  10. // By Cal Henderson <cal@iamcal.com>
  11. //
  12. // This code is dual licensed:
  13. // CC Attribution-ShareAlike 2.5 - http://creativecommons.org/licenses/by-sa/2.5/
  14. // GPLv3 - http://www.gnu.org/copyleft/gpl.html
  15. //
  16. // $Revision: 5039 $
  17. //
  18. // Translated from PHP to Java and slightly improved by Daniel Marschall
  19. // Source: http://code.iamcal.com/php/rfc822/rfc3696.phps
  20. // Current version: 2010-06-10
  21. //
  22.  
  23. public class RFC3696EmailParser {
  24.  
  25.         public static boolean isValidEmailAddress(String email) { // was isValidRFC3696EmailAddress(String)
  26.                
  27.                 if (email == null) email = "";
  28.                
  29. //              ####################################################################################
  30. //              #
  31. //              # NO-WS-CTL       =       %d1-8 /         ; US-ASCII control characters
  32. //              #                         %d11 /          ;  that do not include the
  33. //              #                         %d12 /          ;  carriage return, line feed,
  34. //              #                         %d14-31 /       ;  and white space characters
  35. //              #                         %d127
  36. //              # ALPHA          =  %x41-5A / %x61-7A   ; A-Z / a-z
  37. //              # DIGIT          =  %x30-39
  38.  
  39.                 final String no_ws_ctl  = "[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]";
  40.                 final String alpha              = "[\\x41-\\x5a\\x61-\\x7a]";
  41.                 final String digit              = "[\\x30-\\x39]";
  42.                 final String cr                 = "\\x0d";
  43.                 final String lf                 = "\\x0a";
  44.                 final String crlf               = "(?:"+cr+lf+")";
  45.  
  46. //              ####################################################################################
  47. //              #
  48. //              # obs-char        =       %d0-9 / %d11 /          ; %d0-127 except CR and
  49. //              #                         %d12 / %d14-127         ;  LF
  50. //              # obs-text        =       *LF *CR *(obs-char *LF *CR)
  51. //              # text            =       %d1-9 /         ; Characters excluding CR and LF
  52. //              #                         %d11 /
  53. //              #                         %d12 /
  54. //              #                         %d14-127 /
  55. //              #                         obs-text
  56. //              # obs-qp          =       "\" (%d0-127)
  57. //              # quoted-pair     =       ("\" text) / obs-qp
  58.  
  59.                 final String obs_char   = "[\\x00-\\x09\\x0b\\x0c\\x0e-\\x7f]";
  60. //              final String obs_text   = "(?:"+lf+"*"+cr+"*(?:"+obs_char+lf+"*"+cr+"*)*)";
  61. //              final String text               = "(?:[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f]|"+obs_text+")";
  62.  
  63. //              #
  64. //              # there's an issue with the definition of 'text', since 'obs_text' can
  65. //              # be blank and that allows qp's with no character after the slash. we're
  66. //              # treating that as bad, so this just checks we have at least one
  67. //              # (non-CRLF) character
  68. //              #
  69.  
  70.                 final String text                       = "(?:"+lf+"*"+cr+"*"+obs_char+lf+"*"+cr+"*)";
  71.                 final String obs_qp                     = "(?:\\x5c[\\x00-\\x7f])";
  72.                 final String quoted_pair        = "(?:\\x5c"+text+"|"+obs_qp+")";
  73.  
  74. //              ####################################################################################
  75. //              #
  76. //              # obs-FWS         =       1*WSP *(CRLF 1*WSP)
  77. //              # FWS             =       ([*WSP CRLF] 1*WSP) /   ; Folding white space
  78. //              #                         obs-FWS
  79. //              # ctext           =       NO-WS-CTL /     ; Non white space controls
  80. //              #                         %d33-39 /       ; The rest of the US-ASCII
  81. //              #                         %d42-91 /       ;  characters not including "(",
  82. //              #                         %d93-126        ;  ")", or "\"
  83. //              # ccontent        =       ctext / quoted-pair / comment
  84. //              # comment         =       "(" *([FWS] ccontent) [FWS] ")"
  85. //              # CFWS            =       *([FWS] comment) (([FWS] comment) / FWS)
  86. //
  87. //              #
  88. //              # note: we translate ccontent only partially to avoid an infinite loop
  89. //              # instead, we'll recursively strip *nested* comments before processing
  90. //              # the input. that will leave 'plain old comments' to be matched during
  91. //              # the main parse.
  92. //              #
  93.  
  94.                 final String wsp                = "[\\x20\\x09]";
  95.                 final String obs_fws    = "(?:"+wsp+"+(?:"+crlf+wsp+"+)*)";
  96.                 final String fws                = "(?:(?:(?:"+wsp+"*"+crlf+")?"+wsp+"+)|"+obs_fws+")";
  97.                 final String ctext              = "(?:"+no_ws_ctl+"|[\\x21-\\x27\\x2A-\\x5b\\x5d-\\x7e])";
  98.                 final String ccontent   = "(?:"+ctext+"|"+quoted_pair+")";
  99.                 final String comment    = "(?:\\x28(?:"+fws+"?"+ccontent+")*"+fws+"?\\x29)";
  100.                 final String cfws               = "(?:(?:"+fws+"?"+comment+")*(?:"+fws+"?"+comment+"|"+fws+"))";
  101.  
  102. //              #
  103. //              # these are the rules for removing *nested* comments. we'll just detect
  104. //              # outer comment and replace it with an empty comment, and recurse until
  105. //              # we stop.
  106. //              #
  107.  
  108.                 final String outer_ccontent_dull        = "(?:"+fws+"?"+ctext+"|"+quoted_pair+")";
  109.                 final String outer_ccontent_nest        = "(?:"+fws+"?"+comment+")";
  110.                 final String outer_comment                      = "(?:\\x28"+outer_ccontent_dull+"*(?:"+outer_ccontent_nest+outer_ccontent_dull+"*)+"+fws+"?\\x29)";
  111.  
  112. //              ####################################################################################
  113. //              #
  114. //              # atext           =       ALPHA / DIGIT / ; Any character except controls,
  115. //              #                         "!" / "#" /     ;  SP, and specials.
  116. //              #                         "$" / "%" /     ;  Used for atoms
  117. //              #                         "&" / "'" /
  118. //              #                         "*" / "+" /
  119. //              #                         "-" / "/" /
  120. //              #                         "=" / "?" /
  121. //              #                         "^" / "_" /
  122. //              #                         "`" / "{" /
  123. //              #                         "|" / "}" /
  124. //              #                         "~"
  125. //              # atom            =       [CFWS] 1*atext [CFWS]
  126.  
  127.                 final String atext      = "(?:"+alpha+"|"+digit+"|[\\x21\\x23-\\x27\\x2a\\x2b\\x2d\\x2f\\x3d\\x3f\\x5e\\x5f\\x60\\x7b-\\x7e])";
  128.                 final String atom       = "(?:"+cfws+"?(?:"+atext+")+"+cfws+"?)";
  129.  
  130. //              ####################################################################################
  131. //              #
  132. //              # qtext           =       NO-WS-CTL /     ; Non white space controls
  133. //              #                         %d33 /          ; The rest of the US-ASCII
  134. //              #                         %d35-91 /       ;  characters not including "\"
  135. //              #                         %d93-126        ;  or the quote character
  136. //              # qcontent        =       qtext / quoted-pair
  137. //              # quoted-string   =       [CFWS]
  138. //              #                         DQUOTE *([FWS] qcontent) [FWS] DQUOTE
  139. //              #                         [CFWS]
  140. //              # word            =       atom / quoted-string
  141.  
  142.                 final String qtext                      = "(?:"+no_ws_ctl+"|[\\x21\\x23-\\x5b\\x5d-\\x7e])";
  143.                 final String qcontent           = "(?:"+qtext+"|"+quoted_pair+")";
  144. //              final String quoted_string      = "(?:"+cfws+"?\\x22(?:"+fws+"?"+qcontent+")*"+fws+"?\\x22"+cfws+"?)";
  145.  
  146. //              #
  147. //              # changed the '*' to a '+' to require that quoted strings are not empty
  148. //              #
  149.  
  150.                 final String quoted_string      = "(?:"+cfws+"?\\x22(?:"+fws+"?"+qcontent+")+"+fws+"?\\x22"+cfws+"?)";
  151.                 final String word                       = "(?:"+atom+"|"+quoted_string+")";
  152.  
  153. //              ####################################################################################
  154. //              #
  155. //              # obs-local-part  =       word *("." word)
  156. //              # obs-domain      =       atom *("." atom)
  157.  
  158.                 final String obs_local_part     = "(?:"+word+"(?:\\x2e"+word+")*)";
  159.                 final String obs_domain         = "(?:"+atom+"(?:\\x2e"+atom+")*)";
  160.  
  161. //              ####################################################################################
  162. //              #
  163. //              # dot-atom-text   =       1*atext *("." 1*atext)
  164. //              # dot-atom        =       [CFWS] dot-atom-text [CFWS]
  165.  
  166.                 final String dot_atom_text      = "(?:"+atext+"+(?:\\x2e"+atext+"+)*)";
  167.                 final String dot_atom           = "(?:"+cfws+"?"+dot_atom_text+""+cfws+"?)";
  168.  
  169. //              ####################################################################################
  170. //              #
  171. //              # domain-literal  =       [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
  172. //              # dcontent        =       dtext / quoted-pair
  173. //              # dtext           =       NO-WS-CTL /     ; Non white space controls
  174. //              #
  175. //              #                         %d33-90 /       ; The rest of the US-ASCII
  176. //              #                         %d94-126        ;  characters not including "[",
  177. //              #                                         ;  "]", or "\"
  178.  
  179.                 final String dtext                      = "(?:"+no_ws_ctl+"|[\\x21-\\x5a\\x5e-\\x7e])";
  180.                 final String dcontent           = "(?:"+dtext+"|"+quoted_pair+")";
  181.                 final String domain_literal     = "(?:"+cfws+"?\\x5b(?:"+fws+"?"+dcontent+")*"+fws+"?\\x5d"+cfws+"?)";
  182.  
  183. //              ####################################################################################
  184. //              #
  185. //              # local-part      =       dot-atom / quoted-string / obs-local-part
  186. //              # domain          =       dot-atom / domain-literal / obs-domain
  187. //              # addr-spec       =       local-part "@" domain
  188.  
  189.                 final String local_part = "(("+dot_atom+")|("+quoted_string+")|("+obs_local_part+"))";
  190.                 final String domain             = "(("+dot_atom+")|("+domain_literal+")|("+obs_domain+"))";
  191.                 final String addr_spec  = local_part+"\\x40"+domain;
  192.  
  193. //              #
  194. //              # see http://www.dominicsayers.com/isemail/ for details, but this should probably be 254
  195. //              #
  196.  
  197.                 // TODO: Change to 254.
  198.                 // According to Errata ID 1690 of RFC 3696 (submitted by Dominik Sayers)
  199.                 // the length accepted by the IETF is 254 and not 256.
  200.                 // http://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690
  201.                 if (email.length() > 256) return false;
  202.  
  203. //              #
  204. //              # we need to strip nested comments first - we replace them with a simple comment
  205. //              #
  206.  
  207.                 email = RFC3696StripComments(outer_comment, email, "(x)");
  208.  
  209. //              #
  210. //              # now match what's left
  211. //              #
  212.                
  213.                 Matcher matcher = Pattern.compile("^"+addr_spec+"$").matcher(email);
  214.                 matcher.find();
  215.                
  216.                 if (!matcher.matches()) {
  217.                         return false;
  218.                 }
  219.                
  220.                 MatchResult m = matcher.toMatchResult();
  221.                
  222.                 Bits bits = new Bits();
  223.                 bits.setLocal((m.group(1) == null) ? "" : m.group(1));
  224.                 bits.setLocalAtom((m.group(2) == null) ? "" : m.group(2));
  225.                 bits.setLocalQuoted((m.group(3) == null) ? "" : m.group(3));
  226.                 bits.setLocalObs((m.group(4) == null) ? "" : m.group(4));
  227.                 bits.setDomain((m.group(5) == null) ? "" : m.group(5));
  228.                 bits.setDomainAtom((m.group(6) == null) ? "" : m.group(6));
  229.                 bits.setDomainLiteral((m.group(7) == null) ? "" : m.group(7));
  230.                 bits.setDomainObs((m.group(8) == null) ? "" : m.group(8));
  231.  
  232. //              #
  233. //              # we need to now strip comments from bits.getLocal() and bits.getDomain(),
  234. //              # since we know they're i the right place and we want them out of the
  235. //              # way for checking IPs, label sizes, etc
  236. //              #
  237.  
  238.                 bits.setLocal(RFC3696StripComments(comment, bits.getLocal()));
  239.                 bits.setDomain(RFC3696StripComments(comment, bits.getDomain()));
  240.  
  241. //              #
  242. //              # length limits on segments
  243. //              #
  244.  
  245.                 if (bits.getLocal().length() > 64) return false;
  246.                 if (bits.getDomain().length() > 255) return false;
  247.  
  248. //              #
  249. //              # restrictions on domain-literals from RFC2821 section 4.1.3
  250. //              #
  251.  
  252.                 if (bits.getDomainLiteral().length() > 0) {
  253.  
  254.                         final String Snum                                       = "(\\d{1,3})";
  255.                         final String IPv4_address_literal       = Snum+"\\."+Snum+"\\."+Snum+"\\."+Snum;
  256.  
  257.                         final String IPv6_hex                           = "(?:[0-9a-fA-F]{1,4})";
  258.  
  259.                         final String IPv6_full                          = "IPv6\\:"+IPv6_hex+"(:?\\:"+IPv6_hex+"){7}";
  260.  
  261.                         final String IPv6_comp_part                     = "(?:"+IPv6_hex+"(?:\\:"+IPv6_hex+"){0,5})?";
  262.                         final String IPv6_comp                          = "IPv6\\:("+IPv6_comp_part+"\\:\\:"+IPv6_comp_part+")";
  263.  
  264.                         final String IPv6v4_full                        = "IPv6\\:"+IPv6_hex+"(?:\\:"+IPv6_hex+"){5}\\:"+IPv4_address_literal;
  265.  
  266.                         final String IPv6v4_comp_part           = IPv6_hex+"(?:\\:"+IPv6_hex+"){0,3}";
  267.                         final String IPv6v4_comp                        = "IPv6\\:((?:"+IPv6v4_comp_part+")?\\:\\:(?:"+IPv6v4_comp_part+"\\:)?)"+IPv4_address_literal;
  268.  
  269. //                      #
  270. //                      # IPv4 is simple
  271. //                      #
  272.                        
  273.                         matcher = Pattern.compile("^\\["+IPv4_address_literal+"\\]$").matcher(bits.getDomain());
  274.                         matcher.find();
  275.                         m = matcher.toMatchResult();
  276.  
  277.                         if (matcher.matches()) {
  278.                                 if (Integer.parseInt(m.group(1)) > 255) return false;
  279.                                 if (Integer.parseInt(m.group(2)) > 255) return false;
  280.                                 if (Integer.parseInt(m.group(3)) > 255) return false;
  281.                                 if (Integer.parseInt(m.group(4)) > 255) return false;
  282.                         } else {
  283.  
  284. //                              #
  285. //                              # this should be IPv6 - a bunch of tests are needed here :)
  286. //                              #
  287.  
  288.                                 while (true) {
  289.  
  290.                                         matcher = Pattern.compile("^\\["+IPv6_full+"\\]$").matcher(bits.getDomain());
  291.                                         matcher.find();
  292.                                        
  293.                                         if (matcher.matches()){
  294.                                                 break;
  295.                                         }
  296.  
  297.                                         matcher = Pattern.compile("^\\["+IPv6_comp+"\\]$").matcher(bits.getDomain());
  298.                                         matcher.find();
  299.                                         m = matcher.toMatchResult();
  300.  
  301.                                         if (matcher.matches()) {
  302.                                                 String m1 = m.group(1);
  303.                                                 String[] explode = m1.split("::");
  304.                                                 String a = "";
  305.                                                 String b = "";
  306.                                                 if (explode.length >= 2) {
  307.                                                         if (explode[0] != null) a = explode[0];
  308.                                                         if (explode[1] != null) b = explode[1];
  309.                                                 }
  310.  
  311.                                                 String folded = ((a.length() > 0) && (b.length() > 0)) ? a+":"+b : a+b;
  312.                                                 String[] groups = folded.split(":");
  313.                                                 if (groups.length > 6) return false;
  314.                                                 break;
  315.                                         }
  316.  
  317.                                         matcher = Pattern.compile("^\\["+IPv6v4_full+"\\]$").matcher(bits.getDomain());
  318.                                         matcher.find();
  319.                                         m = matcher.toMatchResult();
  320.  
  321.                                         if (matcher.matches()) {
  322.                                                 if (Integer.parseInt(m.group(1)) > 255) return false;
  323.                                                 if (Integer.parseInt(m.group(2)) > 255) return false;
  324.                                                 if (Integer.parseInt(m.group(3)) > 255) return false;
  325.                                                 if (Integer.parseInt(m.group(4)) > 255) return false;
  326.                                                 break;
  327.                                         }
  328.  
  329.                                         matcher = Pattern.compile("^\\["+IPv6v4_comp+"\\]$").matcher(bits.getDomain());
  330.                                         matcher.find();
  331.                                         m = matcher.toMatchResult();
  332.  
  333.                                         if (matcher.matches()) {
  334.                                                 String m1 = m.group(1);
  335.                                                 String[] explode = m1.split("::");
  336.                                                 String a = "";
  337.                                                 String b = "";
  338.                                                 if (explode.length >= 2) {
  339.                                                         if (explode[0] != null) a = explode[0];
  340.                                                         if (explode[1] != null) b = explode[1];
  341.                                                 }
  342.                                                
  343.                                                 if (b.length() > 0) /* Added by Daniel Marschall due to translation process */ {
  344.                                                         b = b.substring(0, b.length()-1); // remove the trailing colon before the IPv4 address
  345.                                                 }
  346.                                                 String folded = ((a.length() > 0) && (b.length() > 0)) ? a+":"+b : a+b;
  347.                                                 String[] groups = folded.split(":");
  348.                                                 if (groups.length > 4) return false;
  349.                                                 break;
  350.                                         }
  351.  
  352.                                         return false;
  353.                                 }
  354.                         }                      
  355.                 }else{
  356.  
  357. //                      #
  358. //                      # the domain is either dot-atom or obs-domain - either way, it's
  359. //                      # made up of simple labels and we split on dots
  360. //                      #
  361.  
  362.                         String[] labels = bits.getDomain().split("\\.");
  363.  
  364. //                      #
  365. //                      # this is allowed by both dot-atom and obs-domain, but is un-routeable on the
  366. //                      # public internet, so we'll fail it (e.g. user@localhost)
  367. //                      #
  368.  
  369.                         if (labels.length == 1) return false;
  370.  
  371. //                      #
  372. //                      # checks on each label
  373. //                      #
  374.  
  375.                         for (String label : labels) {
  376.                                 if (label.length() > 63) return false;
  377.                                 if (label.charAt(0) == '-') return false;
  378.                                 if (label.charAt(label.length()-1) == '-') return false;
  379.                         }
  380.                        
  381. //                      #
  382. //                      # last label can't be all numeric
  383. //                      #
  384.                        
  385.                         String arrayPopResult;
  386.                         if (labels.length > 0) {
  387.                                 arrayPopResult = labels[labels.length - 1];
  388.                                 String[] tmpLabels = new String[labels.length - 1];
  389.                                 for (int i = 0; i < labels.length - 1; i++) {
  390.                                         tmpLabels[i] = labels[i];
  391.                                 }
  392.                         } else {
  393.                                 arrayPopResult = "";
  394.                         }
  395.  
  396.                         matcher = Pattern.compile("^[0-9]+$").matcher(arrayPopResult);
  397.                         matcher.find();
  398.                         if (matcher.matches()) return false;
  399.                 }
  400.  
  401.                 return true;
  402.         }
  403.  
  404.         private static String RFC3696StripComments(String comment, String email,
  405.                         String replace) {
  406.                 while (true) {
  407.                         String newEmail = email.replaceAll(comment, replace);
  408.                         if (newEmail.length() == email.length()) {
  409.                                 return email;
  410.                         }
  411.                         email = newEmail;
  412.                 }
  413.         }
  414.  
  415.         private static String RFC3696StripComments(String comment, String email) {
  416.                 return RFC3696StripComments(comment, email, "");
  417.         }
  418.  
  419.         private RFC3696EmailParser() {
  420.         }
  421.  
  422. }