
org.apache.mailet.MailAddress Maven / Gradle / Ivy
Show all versions of apache-mailet-api Show documentation
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/
package org.apache.mailet;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import java.util.Locale;
/**
* A representation of an email address.
*
* This class encapsulates functionality to access 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 {@code },
* as per the spec.
*
* 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 the 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 local-part and
* lokitech.com is the domain). This means all current parsing to date
* is incorrect as we just find the first '@' and use that to separate
* local-part from domain.
*
* 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
*/
public class MailAddress implements java.io.Serializable {
/**
* We hardcode the serialVersionUID
* This version (2779163542539434916L) retains compatibility back to
* Mailet version 1.2 (James 1.2) so that MailAddress will be
* deserializable and mail doesn't get lost after an upgrade.
*/
public static final long serialVersionUID = 2779163542539434916L;
private final static char[] SPECIAL =
{'<', '>', '(', ')', '[', ']', '\\', '.', ',', ';', ':', '@', '\"'};
private String localPart = null;
private String domain = null;
/**
* Strips source routing. According to RFC-2821 it is a valid approach
* to handle mails containing RFC-821 source-route information.
*
* @param address the address to strip
* @param pos current position
* @return new pos
*/
private int stripSourceRoute(String address, int pos) {
if (pos < address.length()) {
if (address.charAt(pos) == '@') {
int i = address.indexOf(':');
if (i != -1) {
pos = i + 1;
}
}
}
return pos;
}
/**
* Constructs a MailAddress by parsing the provided address.
*
* @param address the email address, compliant to the RFC2822 3.4.1. addr-spec specification
* @throws AddressException if the parse failed
*/
public MailAddress(String address) throws AddressException {
address = address.trim();
int pos = 0;
// Test if mail address has source routing information (RFC-821) and get rid of it!!
//must be called first!! (or at least prior to updating pos)
stripSourceRoute(address, pos);
StringBuffer localPartSB = new StringBuffer();
StringBuffer domainSB = new StringBuffer();
//Begin parsing
// ::= "@"
try {
//parse local-part
// ::= |
if (address.charAt(pos) == '\"') {
pos = parseQuotedLocalPartOrThrowException(localPartSB, address, pos);
} else {
pos = parseUnquotedLocalPartOrThrowException(localPartSB, address, pos);
}
//find @
if (pos >= address.length() || address.charAt(pos) != '@') {
throw new AddressException("Did not find @ between local-part and domain at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
pos++;
//parse domain
// ::= | "."
// ::= | "#" | "[" "]"
while (true) {
if (address.charAt(pos) == '#') {
pos = parseNumber(domainSB, address, pos);
} else if (address.charAt(pos) == '[') {
pos = parseDomainLiteral(domainSB, address, pos);
} else {
pos = parseDomain(domainSB, address, pos);
}
if (pos >= address.length()) {
break;
}
if (address.charAt(pos) == '.') {
char lastChar = address.charAt(pos - 1);
if (lastChar == '@' || lastChar == '.') {
throw new AddressException("Subdomain expected before '.' or duplicate '.' in " + address);
}
domainSB.append('.');
pos++;
continue;
}
break;
}
if (domainSB.toString().length() == 0) {
throw new AddressException("No domain found at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
} catch (IndexOutOfBoundsException ioobe) {
throw new AddressException("Out of data at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
localPart = localPartSB.toString();
domain = domainSB.toString();
}
private int parseUnquotedLocalPartOrThrowException(StringBuffer localPartSB, String address, int pos)
throws AddressException {
pos = parseUnquotedLocalPart(localPartSB, address, pos);
if (localPartSB.toString().length() == 0) {
throw new AddressException("No local-part (user account) found at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
return pos;
}
private int parseQuotedLocalPartOrThrowException(StringBuffer localPartSB, String address, int pos)
throws AddressException {
pos = parseQuotedLocalPart(localPartSB, address, pos);
if (localPartSB.toString().length() == 2) {
throw new AddressException("No quoted local-part (user account) found at position " +
(pos + 2) + " in '" + address + "'", address, pos + 2);
}
return pos;
}
/**
* Constructs a MailAddress with the provided local part and domain.
*
* @param localPart the local-part portion. This is a domain dependent string.
* In addresses, it is simply interpreted on the particular host as a
* name of a particular mailbox. per RFC2822 3.4.1. addr-spec specification
* @param domain the domain portion. This identifies the point to which the mail
* is delivered per RFC2822 3.4.1. addr-spec specification
* @throws AddressException if the parse failed
*/
public MailAddress(String localPart, String domain) throws AddressException {
this(new InternetAddress(localPart + "@" + domain));
}
/**
* Constructs a MailAddress from an InternetAddress, using only the
* email address portion (an "addr-spec", not "name-addr", as
* defined in the RFC2822 3.4. Address Specification)
*
* @param address the address
* @throws AddressException if the parse failed
*/
public MailAddress(InternetAddress address) throws AddressException {
this(address.getAddress());
}
/**
* Returns the host part.
*
* @return 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.
* @deprecated use {@link #getDomain()}, whose name was changed to
* align with RFC2822 3.4.1. addr-spec specification
*/
@Deprecated
public String getHost() {
return getDomain();
}
/**
* Returns the domain part per RFC2822 3.4.1. addr-spec specification.
*
* @return the domain part of this email address. If the domain is of
* the domain-literal form (e.g. [yyy.yyy.yyy.yyy]), the braces will
* have been stripped returning the raw IP address.
* @since Mailet API 2.4
*/
public String getDomain() {
if (!(domain.startsWith("[") && domain.endsWith("]"))) {
return domain;
}
return domain.substring(1, domain.length() - 1);
}
/**
* Returns the user part.
*
* @return the user part of this email address
* @deprecated use {@link #getLocalPart()}, whose name was changed to
* align with the RFC2822 3.4.1. addr-spec specification
*/
@Deprecated
public String getUser() {
return getLocalPart();
}
/**
* Returns the local-part per RFC2822 3.4.1. addr-spec specification.
*
* @return the local-part of this email address as defined by the
* RFC2822 3.4.1. addr-spec specification.
* The local-part portion is a domain dependent string.
* In addresses, it is simply interpreted on the particular
* host as a name of a particular mailbox
* (the part before the "@" character)
* @since Mailet API 2.4
*/
public String getLocalPart() {
return localPart;
}
@Override
public String toString() {
return localPart + "@" + domain;
}
/**
* Returns an InternetAddress representing the same address
* as this MailAddress.
*
* @return the address
*/
public InternetAddress toInternetAddress() {
try {
return new InternetAddress(toString());
} catch (javax.mail.internet.AddressException ae) {
//impossible really
return null;
}
}
/**
* Indicates whether some other object is "equal to" this one.
*
* Note that this implementation breaks the general contract of the
* equals
method by allowing an instance to equal to a
* String
. It is recommended that implementations avoid
* relying on this design which may be removed in a future release.
*
* @returns true if the given object is equal to this one, false otherwise
*/
@Override
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 getLocalPart().equalsIgnoreCase(addr.getLocalPart()) && getDomain().equalsIgnoreCase(addr.getDomain());
}
return false;
}
/**
* Returns a hash code value for this object.
*
* This method is implemented by returning the hash code of the canonical
* string representation of this address, so that all instances representing
* the same address will return an identical hash code.
*
* @return the hashcode.
*/
@Override
public int hashCode() {
return toString().toLowerCase(Locale.US).hashCode();
}
private int parseQuotedLocalPart(StringBuffer lpSB, String address, int pos) throws AddressException {
StringBuilder resultSB = new StringBuilder();
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 > 127) {
throw new AddressException("Invalid \\ syntaxed character at position " +
(pos + 1) + " in '" + address + "'", address, 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 AddressException("Unquoted local-part (user account) must be one of the 128 ASCI " +
"characters exception , , quote (\"), or backslash (\\) at position " +
(pos + 1) + " in '" + address + "'");
}
resultSB.append(q);
pos++;
}
}
lpSB.append(resultSB);
return pos;
}
private int parseUnquotedLocalPart(StringBuffer lpSB, String address, int pos) throws AddressException {
StringBuilder resultSB = new StringBuilder();
// ::= | "."
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 > 127) {
throw new AddressException("Invalid \\ syntaxed character at position " + (pos + 1) +
" in '" + address + "'", address, pos + 1);
}
resultSB.append(x);
pos++;
lastCharDot = false;
} else if (address.charAt(pos) == '.') {
if (pos == 0) {
throw new AddressException("Local part must not start with a '.'");
}
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 AddressException("Invalid character in local-part (user account) at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
int i = 0;
while (i < SPECIAL.length) {
if (c == SPECIAL[i]) {
throw new AddressException("Invalid character in local-part (user account) at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
i++;
}
resultSB.append(c);
pos++;
lastCharDot = false;
}
}
if (lastCharDot) {
throw new AddressException("local-part (user account) ended with a \".\", which is invalid in address '" +
address + "'", address, pos);
}
lpSB.append(resultSB);
return pos;
}
private int parseNumber(StringBuffer dSB, String address, int pos) throws AddressException {
// ::= |
StringBuilder resultSB = new StringBuilder();
// 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++;
//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 AddressException("In domain, did not find a number in # address at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
resultSB.append(d);
pos++;
}
if (resultSB.length() < 2) {
throw new AddressException("In domain, did not find a number in # address at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
dSB.append(resultSB);
return pos;
}
private int parseDomainLiteral(StringBuffer dSB, String address, int pos) throws AddressException {
StringBuilder resultSB = new StringBuilder();
//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
StringBuilder snumSB = new StringBuilder();
for (int digits = 0; digits < 3; digits++) {
char currentChar = address.charAt(pos);
if (currentChar == '.' || currentChar == ']') {
break;
} else if (currentChar < '0' || currentChar > '9') {
throw new AddressException("Invalid number at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
snumSB.append(currentChar);
pos++;
}
if (snumSB.toString().length() == 0) {
throw new AddressException("Number not found at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
try {
int snum = Integer.parseInt(snumSB.toString());
if (snum > 255) {
throw new AddressException("Invalid number at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
} catch (NumberFormatException nfe) {
throw new AddressException("Invalid number at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
resultSB.append(snumSB.toString());
if (address.charAt(pos) == ']') {
if (octet < 3) {
throw new AddressException("End of number reached too quickly at " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
break;
}
if (address.charAt(pos) == '.') {
resultSB.append('.');
pos++;
}
}
if (address.charAt(pos) != ']') {
throw new AddressException("Did not find closing bracket \"]\" in domain at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
resultSB.append(']');
pos++;
dSB.append(resultSB);
return pos;
}
private int parseDomain(StringBuffer dSB, String address, int pos) throws AddressException {
StringBuilder resultSB = new StringBuilder();
// ::=
// ::= |
// ::= |
// ::= | | "-"
// ::= 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 AddressException("Invalid character at " + pos + " in '" + address + "'", address, pos);
}
String result = resultSB.toString();
if (result.startsWith("-") || result.endsWith("-")) {
throw new AddressException("Domain name cannot begin or end with a hyphen \"-\" at position " +
(pos + 1) + " in '" + address + "'", address, pos + 1);
}
dSB.append(result);
return pos;
}
}