/* * Copyright (C) The Apache Software Foundation. All rights reserved. * * This software is published under the terms of the Apache Software License * version 1.1, a copy of which has been included with this distribution in * the LICENSE file. */ package org.apache.mailet; import java.util.Locale; import javax.mail.internet.InternetAddress; import javax.mail.internet.ParseException; /** * A representation of an email address. *

This class encapsulates functionalities to access to different * parts of an email address without dealing with its parsing.

* *

A MailAddress is an address specified in the MAIL FROM and * RCPT TO commands in SMTP sessions. These are either passed by * an external server to the mailet-compliant SMTP server, or they * are created programmatically by the mailet-compliant server to * send to another (external) SMTP server. Mailets and matchers * use the MailAddress for the purpose of evaluating the sender * and recipient(s) of a message.

* *

MailAddress parses an email address as defined in RFC 821 * (SMTP) p. 30 and 31 where addresses are defined in BNF convention. * As the mailet API does not support the aged "SMTP-relayed mail" * addressing protocol, this leaves all addresses to be a , * as per the spec. The MailAddress's "user" is the of * the and "host" is the of the mailbox.

* *

This class is a good way to validate email addresses as there are * some valid addresses which would fail with a simpler approach * to parsing address. It also removes parsing burden from * mailets and matchers that might not realize the flexibility of an * SMTP address. For instance, "serge@home"@lokitech.com is a valid * SMTP address (the quoted text serge@home is the user and * lokitech.com is the host). This means all current parsing to date * is incorrect as we just find the first @ and use that to separate * user from host.

* *

This parses an address as per the BNF specification for * from RFC 821 on page 30 and 31, section 4.1.2. COMMAND SYNTAX. * http://www.freesoft.org/CIE/RFC/821/15.htm

* * @version 1.0 * @author Roberto Lo Giacco * @author Serge Knystautas * @author Gabriel Bucher * @author Stuart Roebuck */ public class MailAddress implements java.io.Serializable { //We hardcode the serialVersionUID so that from James 1.2 on, // MailAddress will be deserializable (so your mail doesn't get lost) public static final long serialVersionUID = 2779163542539434916L; private final static char[] SPECIAL = {'<', '>', '(', ')', '[', ']', '\\', '.', ',', ';', ':', '@', '\"'}; private String user = null; private String host = null; //Used for parsing private int pos = 0; /** *

Construct a MailAddress parsing the provided String object.

* *

The personal variable is left empty.

* * @param address the email address compliant to the RFC822 format * @throws ParseException if the parse failed */ public MailAddress(String address) throws ParseException { address = address.trim(); StringBuffer userSB = new StringBuffer(); StringBuffer hostSB = new StringBuffer(); //Begin parsing // ::= "@" try { //parse local-part // ::= | if (address.charAt(pos) == '\"') { userSB.append(parseQuotedLocalPart(address)); } else { userSB.append(parseUnquotedLocalPart(address)); } if (userSB.toString().length() == 0) { throw new ParseException("No local-part (user account) found at position " + (pos + 1)); } //find @ if (address.charAt(pos) != '@') { throw new ParseException("Did not find @ between local-part and domain at position " + (pos + 1)); } pos++; //parse domain // ::= | "." // ::= | "#" | "[" "]" while (true) { if (address.charAt(pos) == '#') { hostSB.append(parseNumber(address)); } else if (address.charAt(pos) == '[') { hostSB.append(parseDotNum(address)); } else { hostSB.append(parseDomainName(address)); } if (pos >= address.length()) { break; } if (address.charAt(pos) == '.') { hostSB.append('.'); pos++; continue; } break; } if (hostSB.toString().length() == 0) { throw new ParseException("No domain found at position " + (pos + 1)); } } catch (IndexOutOfBoundsException ioobe) { throw new ParseException("Out of data at position " + (pos + 1)); } user = userSB.toString(); host = hostSB.toString(); } /** * Construct a MailAddress with the provided personal name and email * address. * * @param user the username or account name on the mail server * @param host the server that should accept messages for this user * @throws ParseException if the parse failed */ public MailAddress(String newUser, String newHost) throws ParseException { /* NEEDS TO BE REWORKED TO VALIDATE EACH CHAR */ user = newUser; host = newHost; } /** * Constructs a MailAddress from a JavaMail InternetAddress, using only the * email address portion, discarding the personal name. */ public MailAddress(InternetAddress address) throws ParseException { this(address.getAddress()); } /** * Return the host part. * * @return a String object representing the host part * of this email address. If the host is of the dotNum form * (e.g. [yyy.yyy.yyy.yyy]) then strip the braces first. */ public String getHost() { if (!(host.startsWith("[") && host.endsWith("]"))) { return host; } else { return host.substring(1, host.length() -1); } } /** * Return the user part. * * @return a String object representing the user part * of this email address. * @throws AddressException if the parse failed */ public String getUser() { return user; } public String toString() { StringBuffer addressBuffer = new StringBuffer(128) .append(user) .append("@") .append(host); return addressBuffer.toString(); } public InternetAddress toInternetAddress() { try { return new InternetAddress(toString()); } catch (javax.mail.internet.AddressException ae) { //impossible really return null; } } public boolean equals(Object obj) { if (obj == null) { return false; } else if (obj instanceof String) { String theString = (String)obj; return toString().equalsIgnoreCase(theString); } else if (obj instanceof MailAddress) { MailAddress addr = (MailAddress)obj; return getUser().equalsIgnoreCase(addr.getUser()) && getHost().equalsIgnoreCase(addr.getHost()); } return false; } /** * Return a hashCode for this object which should be identical for addresses * which are equivalent. This is implemented by obtaining the default * hashcode of the String representation of the MailAddress. Without this * explicit definition, the default hashCode will create different hashcodes * for separate object instances. * * @return the hashcode. */ public int hashCode() { return toString().toLowerCase(Locale.US).hashCode(); } private String parseQuotedLocalPart(String address) throws ParseException { StringBuffer resultSB = new StringBuffer(); resultSB.append('\"'); pos++; // ::= """ """ // ::= "\" | "\" | | while (true) { if (address.charAt(pos) == '\"') { resultSB.append('\"'); //end of quoted string... move forward pos++; break; } if (address.charAt(pos) == '\\') { resultSB.append('\\'); pos++; // ::= any one of the 128 ASCII characters (no exceptions) char x = address.charAt(pos); if (x < 0 || x > 128) { throw new ParseException("Invalid \\ syntaxed character at position " + (pos + 1)); } resultSB.append(x); pos++; } else { // ::= any one of the 128 ASCII characters except , //, quote ("), or backslash (\) char q = address.charAt(pos); if (q <= 0 || q == '\n' || q == '\r' || q == '\"' || q == '\\') { throw new ParseException("Unquoted local-part (user account) must be one of the 128 ASCI characters exception , , quote (\"), or backslash (\\) at position " + (pos + 1)); } resultSB.append(q); pos++; } } return resultSB.toString(); } private String parseUnquotedLocalPart(String address) throws ParseException { StringBuffer resultSB = new StringBuffer(); // ::= | "." boolean lastCharDot = false; while (true) { // ::= | // ::= | "\" if (address.charAt(pos) == '\\') { resultSB.append('\\'); pos++; // ::= any one of the 128 ASCII characters (no exceptions) char x = address.charAt(pos); if (x < 0 || x > 128) { throw new ParseException("Invalid \\ syntaxed character at position " + (pos + 1)); } resultSB.append(x); pos++; lastCharDot = false; } else if (address.charAt(pos) == '.') { resultSB.append('.'); pos++; lastCharDot = true; } else if (address.charAt(pos) == '@') { //End of local-part break; } else { // ::= any one of the 128 ASCII characters, but not any // or // ::= "<" | ">" | "(" | ")" | "[" | "]" | "\" | "." // | "," | ";" | ":" | "@" """ | the control // characters (ASCII codes 0 through 31 inclusive and // 127) // ::= the space character (ASCII code 32) char c = address.charAt(pos); if (c <= 31 || c == 127 || c == ' ') { throw new ParseException("Invalid character in local-part (user account) at position " + (pos + 1)); } for (int i = 0; i < SPECIAL.length; i++) { if (c == SPECIAL[i]) { throw new ParseException("Invalid character in local-part (user account) at position " + (pos + 1)); } } resultSB.append(c); pos++; lastCharDot = false; } } if (lastCharDot) { throw new ParseException("local-part (user account) ended with a \".\", which is invalid."); } return resultSB.toString(); } private String parseNumber(String address) throws ParseException { // ::= | StringBuffer resultSB = new StringBuffer(); //We keep the position from the class level pos field while (true) { if (pos >= address.length()) { break; } // ::= any one of the ten digits 0 through 9 char d = address.charAt(pos); if (d == '.') { break; } if (d < '0' || d > '9') { throw new ParseException("In domain, did not find a number in # address at position " + (pos + 1)); } resultSB.append(d); pos++; } return resultSB.toString(); } private String parseDotNum(String address) throws ParseException { //throw away all irrelevant '\' they're not necessary for escaping of '.' or digits, and are illegal as part of the domain-literal while(address.indexOf("\\")>-1){ address= address.substring(0,address.indexOf("\\")) + address.substring(address.indexOf("\\")+1); } StringBuffer resultSB = new StringBuffer(); //we were passed the string with pos pointing the the [ char. // take the first char ([), put it in the result buffer and increment pos resultSB.append(address.charAt(pos)); pos++; // ::= "." "." "." for (int octet = 0; octet < 4; octet++) { // ::= one, two, or three digits representing a decimal // integer value in the range 0 through 255 // ::= any one of the ten digits 0 through 9 StringBuffer snumSB = new StringBuffer(); for (int digits = 0; digits < 3; digits++) { char d = address.charAt(pos); if (d == '.') { break; } if (d == ']') { break; } if (d < '0' || d > '9') { throw new ParseException("Invalid number at position " + (pos + 1)); } snumSB.append(d); pos++; } if (snumSB.toString().length() == 0) { throw new ParseException("Number not found at position " + (pos + 1)); } try { int snum = Integer.parseInt(snumSB.toString()); if (snum > 255) { throw new ParseException("Invalid number at position " + (pos + 1)); } } catch (NumberFormatException nfe) { throw new ParseException("Invalid number at position " + (pos + 1)); } resultSB.append(snumSB.toString()); if (address.charAt(pos) == ']') { if (octet < 3) { throw new ParseException("End of number reached too quickly at " + (pos + 1)); } else { break; } } if (address.charAt(pos) == '.') { resultSB.append('.'); pos++; } } if (address.charAt(pos) != ']') { throw new ParseException("Did not find closing bracket \"]\" in domain at position " + (pos + 1)); } resultSB.append(']'); pos++; return resultSB.toString(); } private String parseDomainName(String address) throws ParseException { StringBuffer resultSB = new StringBuffer(); // ::= // ::= | // ::= | // ::= | | "-" // ::= any one of the 52 alphabetic characters A through Z // in upper case and a through z in lower case // ::= any one of the ten digits 0 through 9 // basically, this is a series of letters, digits, and hyphens, // but it can't start with a digit or hypthen // and can't end with a hyphen // in practice though, we should relax this as domain names can start // with digits as well as letters. So only check that doesn't start // or end with hyphen. while (true) { if (pos >= address.length()) { break; } char ch = address.charAt(pos); if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch == '-')) { resultSB.append(ch); pos++; continue; } if (ch == '.') { break; } throw new ParseException("Invalid character at " + pos); } String result = resultSB.toString(); if (result.startsWith("-") || result.endsWith("-")) { throw new ParseException("Domain name cannot begin or end with a hyphen \"-\" at position " + (pos + 1)); } return result; } }