package com.netifera.platform.util.patternmatching; import java.util.EnumMap; import java.util.Locale; import java.util.Map; // FIXME 'mailto:' scheme? // TODO jdoc public final class EmailMatcher implements ITextMatcher { /** * RFC 2822 section 3.4.1: * The "local-part" of an e-mail address can be up to 64 characters. */ private static final int MAX_LOCALPART_CHARS = 64; /** * RFC 2822 section 3.4.1: * The domain name a maximum of 255 characters. */ private static final int MAX_DOMAIN_CHARS = 255; private enum KEY { ACCOUNT, DOMAIN, ADDRESS, NORMALIZED } /** Boolean indicating whether or not the input matched. */ private final boolean matched; private final Map<KEY, String> map; /** * @param text * the string to be matched */ public EmailMatcher(final String text) { boolean addressMatched = false, domainMatched = false; if (text == null) { matched = false; map = null; return; } int atIndex = text.lastIndexOf('@'); if (atIndex <= 0) { matched = false; map = null; return; } String accountname = text.substring(0, atIndex); String destination = text.substring(atIndex + 1); if (accountname.length() == 0 || destination.length() == 0) { matched = false; map = null; return; } // first check the destination if (destination.charAt(0) == '[' && destination.endsWith("]")) { destination = destination.substring(1, destination.length() - 1); if (!InternetAddressMatcher.matches(destination)) { matched = false; map = null; return; } addressMatched = true; } else if (destination.length() <= MAX_DOMAIN_CHARS && HostnameMatcher.matches(destination)) { // TODO normalize domain destination = destination.toLowerCase(Locale.ENGLISH); domainMatched = true; } else { matched = false; map = null; return; } // then check the username part if (accountname.startsWith("\"") && accountname.endsWith("\"")) { accountname = accountname.substring(1, accountname.length() - 1); // FIXME allow without parsing? } else if (!accountname.matches("[\\w-\\.\\+=/]*[\\w-]")) { // TODO doc matched = false; map = null; return; } if (accountname.length() > MAX_LOCALPART_CHARS) { matched = false; map = null; return; } matched = true; map = new EnumMap<KEY, String>(KEY.class); map.put(KEY.ACCOUNT, accountname); if (domainMatched) { map.put(KEY.DOMAIN, destination); map.put(KEY.NORMALIZED, accountname + "@" + destination); } if (addressMatched) { map.put(KEY.ADDRESS, destination); map.put(KEY.NORMALIZED, accountname + "@[" + destination + "]"); } } @Override public String toString() { return matched ? map.toString() : "no email matched"; } /** * Test if this object matched an RFC 2822 email address format. * * @return <tt>true</tt> if, and only if, this string matches an email * address. */ public boolean matches() { return matched; } private String get(final KEY key) { if (matched && map.containsKey(key)) { return map.get(key); } return null; } public String getNormalizedEmail() { return get(KEY.NORMALIZED); } public String getAccountName() { return get(KEY.ACCOUNT); } public String getDomain() { return get(KEY.DOMAIN); } public String getInternetAddress() { return get(KEY.ADDRESS); } // TODO normalized() ? /** * Test if the input text matches an RFC 2822 email address format. * * @param text * The string to match. * @return <tt>true</tt> if, and only if, this string matches an email * address. */ public static boolean matches(final String text) { return new EmailMatcher(text).matches(); } }