blob: 354ec89eaebc04a2b58255d72d8addda1d3b2f31 [file] [log] [blame]
/*
* 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 javax.mail.internet;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.mail.Address;
import javax.mail.Session;
/**
* A representation of an Internet email address as specified by RFC822 in
* conjunction with a human-readable personal name that can be encoded as
* specified by RFC2047.
* A typical address is "user@host.domain" and personal name "Joe User"
*
* @version $Rev$ $Date$
*/
public class InternetAddress extends Address implements Cloneable {
private static final long serialVersionUID = -7507595530758302903L;
/**
* The address in RFC822 format.
*/
protected String address;
/**
* The personal name in RFC2047 format.
* Subclasses must ensure that this field is updated if the personal field
* is updated; alternatively, it can be invalidated by setting to null
* which will cause it to be recomputed.
*/
protected String encodedPersonal;
/**
* The personal name as a Java String.
* Subclasses must ensure that this field is updated if the encodedPersonal field
* is updated; alternatively, it can be invalidated by setting to null
* which will cause it to be recomputed.
*/
protected String personal;
public InternetAddress() {
}
public InternetAddress(final String address) throws AddressException {
this(address, true);
}
public InternetAddress(final String address, final boolean strict) throws AddressException {
// use the parse method to process the address. This has the wierd side effect of creating a new
// InternetAddress instance to create an InternetAddress, but these are lightweight objects and
// we need access to multiple pieces of data from the parsing process.
final AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
final InternetAddress parsedAddress = parser.parseAddress();
// copy the important information, which right now is just the address and
// personal info.
this.address = parsedAddress.address;
this.personal = parsedAddress.personal;
this.encodedPersonal = parsedAddress.encodedPersonal;
}
public InternetAddress(final String address, final String personal) throws UnsupportedEncodingException {
this(address, personal, null);
}
public InternetAddress(final String address, final String personal, final String charset) throws UnsupportedEncodingException {
this.address = address;
setPersonal(personal, charset);
}
/**
* Clone this object.
*
* @return a copy of this object as created by Object.clone()
*/
@Override
public Object clone() {
try {
return super.clone();
} catch (final CloneNotSupportedException e) {
throw new Error();
}
}
/**
* Return the type of this address.
*
* @return the type of this address; always "rfc822"
*/
@Override
public String getType() {
return "rfc822";
}
/**
* Set the address.
* No validation is performed; validate() can be used to check if it is valid.
*
* @param address the address to set
*/
public void setAddress(final String address) {
this.address = address;
}
/**
* Set the personal name.
* The name is first checked to see if it can be encoded; if this fails then an
* UnsupportedEncodingException is thrown and no fields are modified.
*
* @param name the new personal name
* @param charset the charset to use; see {@link MimeUtility#encodeWord(String, String, String) MimeUtilityencodeWord}
* @throws UnsupportedEncodingException if the name cannot be encoded
*/
public void setPersonal(final String name, final String charset) throws UnsupportedEncodingException {
personal = name;
if (name != null) {
encodedPersonal = MimeUtility.encodeWord(name, charset, null);
}
else {
encodedPersonal = null;
}
}
/**
* Set the personal name.
* The name is first checked to see if it can be encoded using {@link MimeUtility#encodeWord(String)}; if this fails then an
* UnsupportedEncodingException is thrown and no fields are modified.
*
* @param name the new personal name
* @throws UnsupportedEncodingException if the name cannot be encoded
*/
public void setPersonal(final String name) throws UnsupportedEncodingException {
personal = name;
if (name != null) {
encodedPersonal = MimeUtility.encodeWord(name);
}
else {
encodedPersonal = null;
}
}
/**
* Return the address.
*
* @return the address
*/
public String getAddress() {
return address;
}
/**
* Return the personal name.
* If the personal field is null, then an attempt is made to decode the encodedPersonal
* field using {@link MimeUtility#decodeWord(String)}; if this is sucessful, then
* the personal field is updated with that value and returned; if there is a problem
* decoding the text then the raw value from encodedPersonal is returned.
*
* @return the personal name
*/
public String getPersonal() {
if (personal == null && encodedPersonal != null) {
try {
personal = MimeUtility.decodeWord(encodedPersonal);
} catch (final ParseException e) {
return encodedPersonal;
} catch (final UnsupportedEncodingException e) {
return encodedPersonal;
}
}
return personal;
}
/**
* Return the encoded form of the personal name.
* If the encodedPersonal field is null, then an attempt is made to encode the
* personal field using {@link MimeUtility#encodeWord(String)}; if this is
* successful then the encodedPersonal field is updated with that value and returned;
* if there is a problem encoding the text then null is returned.
*
* @return the encoded form of the personal name
*/
private String getEncodedPersonal() {
if (encodedPersonal == null && personal != null) {
try {
encodedPersonal = MimeUtility.encodeWord(personal);
} catch (final UnsupportedEncodingException e) {
// as we could not encode this, return null
return null;
}
}
return encodedPersonal;
}
/**
* Return a string representation of this address using only US-ASCII characters.
*
* @return a string representation of this address
*/
@Override
public String toString() {
// group addresses are always returned without modification.
if (isGroup()) {
return address;
}
// if we have personal information, then we need to return this in the route-addr form:
// "personal <address>". If there is no personal information, then we typically return
// the address without the angle brackets. However, if the address contains anything other
// than atoms, '@', and '.' (e.g., uses domain literals, has specified routes, or uses
// quoted strings in the local-part), we bracket the address.
final String p = getEncodedPersonal();
if (p == null) {
return formatAddress(address);
}
else {
final StringBuffer buf = new StringBuffer(p.length() + 8 + address.length() + 3);
buf.append(AddressParser.quoteString(p));
buf.append(" <").append(address).append(">");
return buf.toString();
}
}
/**
* Check the form of an address, and enclose it within brackets
* if they are required for this address form.
*
* @param a The source address.
*
* @return A formatted address, which can be the original address string.
*/
private String formatAddress(final String a)
{
// this could be a group address....we don't muck with those.
if (address.endsWith(";") && address.indexOf(":") > 0) {
return address;
}
if (AddressParser.containsCharacters(a, "()<>,;:\"[]")) {
final StringBuffer buf = new StringBuffer(address.length() + 3);
buf.append("<").append(address).append(">");
return buf.toString();
}
return address;
}
/**
* Return a string representation of this address using Unicode characters.
*
* @return a string representation of this address
*/
public String toUnicodeString() {
// group addresses are always returned without modification.
if (isGroup()) {
return address;
}
// if we have personal information, then we need to return this in the route-addr form:
// "personal <address>". If there is no personal information, then we typically return
// the address without the angle brackets. However, if the address contains anything other
// than atoms, '@', and '.' (e.g., uses domain literals, has specified routes, or uses
// quoted strings in the local-part), we bracket the address.
// NB: The difference between toString() and toUnicodeString() is the use of getPersonal()
// vs. getEncodedPersonal() for the personal portion. If the personal information contains only
// ASCII-7 characters, these are the same.
final String p = getPersonal();
if (p == null) {
return formatAddress(address);
}
else {
final StringBuffer buf = new StringBuffer(p.length() + 8 + address.length() + 3);
buf.append(AddressParser.quoteString(p));
buf.append(" <").append(address).append(">");
return buf.toString();
}
}
/**
* Compares two addresses for equality.
* We define this as true if the other object is an InternetAddress
* and the two values returned by getAddress() are equal in a
* case-insensitive comparison.
*
* @param o the other object
* @return true if the addresses are the same
*/
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof InternetAddress)) {
return false;
}
final InternetAddress other = (InternetAddress) o;
final String myAddress = getAddress();
return myAddress == null ? (other.getAddress() == null) : myAddress.equalsIgnoreCase(other.getAddress());
}
/**
* Return the hashCode for this address.
* We define this to be the hashCode of the address after conversion to lowercase.
*
* @return a hashCode for this address
*/
@Override
public int hashCode() {
return (address == null) ? 0 : address.toLowerCase().hashCode();
}
/**
* Return true is this address is an RFC822 group address in the format
* <code>phrase ":" [#mailbox] ";"</code>.
* We check this by using the presense of a ':' character in the address, and a
* ';' as the very last character.
*
* @return true is this address represents a group
*/
public boolean isGroup() {
if (address == null) {
return false;
}
return address.endsWith(";") && address.indexOf(":") > 0;
}
/**
* Return the members of a group address.
*
* If strict is true and the address does not contain an initial phrase then an AddressException is thrown.
* Otherwise the phrase is skipped and the remainder of the address is checked to see if it is a group.
* If it is, the content and strict flag are passed to parseHeader to extract the list of addresses;
* if it is not a group then null is returned.
*
* @param strict whether strict RFC822 checking should be performed
* @return an array of InternetAddress objects for the group members, or null if this address is not a group
* @throws AddressException if there was a problem parsing the header
*/
public InternetAddress[] getGroup(final boolean strict) throws AddressException {
if (address == null) {
return null;
}
// create an address parser and use it to extract the group information.
final AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
return parser.extractGroupList();
}
/**
* Return an InternetAddress representing the current user.
* <P/>
* If session is not null, we first look for an address specified in its
* "mail.from" property; if this is not set, we look at its "mail.user"
* and "mail.host" properties and if both are not null then an address of
* the form "${mail.user}@${mail.host}" is created.
* If this fails to give an address, then an attempt is made to create
* an address by combining the value of the "user.name" System property
* with the value returned from InetAddress.getLocalHost().getHostName().
* Any SecurityException raised accessing the system property or any
* UnknownHostException raised getting the hostname are ignored.
* <P/>
* Finally, an attempt is made to convert the value obtained above to
* an InternetAddress. If this fails, then null is returned.
*
* @param session used to obtain mail properties
* @return an InternetAddress for the current user, or null if it cannot be determined
*/
public static InternetAddress getLocalAddress(final Session session) {
String host = null;
String user = null;
// ok, we have several steps for resolving this. To start with, we could have a from address
// configured already, which will be a full InternetAddress string. If we don't have that, then
// we need to resolve a user and host to compose an address from.
if (session != null) {
final String address = session.getProperty("mail.from");
// if we got this, we can skip out now
if (address != null) {
try {
return new InternetAddress(address);
} catch (final AddressException e) {
// invalid address on the from...treat this as an error and return null.
return null;
}
}
// now try for user and host information. We have both session and system properties to check here.
// we'll just handle the session ones here, and check the system ones below if we're missing information.
user = session.getProperty("mail.user");
host = session.getProperty("mail.host");
}
try {
// if either user or host is null, then we check non-session sources for the information.
if (user == null) {
user = System.getProperty("user.name");
}
if (host == null) {
host = InetAddress.getLocalHost().getHostName();
}
if (user != null && host != null) {
// if we have both a user and host, we can create a local address
return new InternetAddress(user + '@' + host);
}
} catch (final AddressException e) {
// ignore
} catch (final UnknownHostException e) {
// ignore
} catch (final SecurityException e) {
// ignore
}
return null;
}
/**
* Convert the supplied addresses into a single String of comma-separated text as
* produced by {@link InternetAddress#toString() toString()}.
* No line-break detection is performed.
*
* @param addresses the array of addresses to convert
* @return a one-line String of comma-separated addresses
*/
public static String toString(final Address[] addresses) {
if (addresses == null || addresses.length == 0) {
return null;
}
if (addresses.length == 1) {
return addresses[0].toString();
} else {
final StringBuffer buf = new StringBuffer(addresses.length * 32);
buf.append(addresses[0].toString());
for (int i = 1; i < addresses.length; i++) {
buf.append(", ");
buf.append(addresses[i].toString());
}
return buf.toString();
}
}
/**
* Convert the supplies addresses into a String of comma-separated text,
* inserting line-breaks between addresses as needed to restrict the line
* length to 72 characters. Splits will only be introduced between addresses
* so an address longer than 71 characters will still be placed on a single
* line.
*
* @param addresses the array of addresses to convert
* @param used the starting column
* @return a String of comma-separated addresses with optional line breaks
*/
public static String toString(final Address[] addresses, int used) {
if (addresses == null || addresses.length == 0) {
return null;
}
if (addresses.length == 1) {
String s = addresses[0].toString();
if (used + s.length() > 72) {
s = "\r\n " + s;
}
return s;
} else {
final StringBuffer buf = new StringBuffer(addresses.length * 32);
for (int i = 0; i < addresses.length; i++) {
final String s = addresses[1].toString();
if (i == 0) {
if (used + s.length() + 1 > 72) {
buf.append("\r\n ");
used = 2;
}
} else {
if (used + s.length() + 1 > 72) {
buf.append(",\r\n ");
used = 2;
} else {
buf.append(", ");
used += 2;
}
}
buf.append(s);
used += s.length();
}
return buf.toString();
}
}
/**
* Parse addresses out of the string with basic checking.
*
* @param addresses the addresses to parse
* @return an array of InternetAddresses parsed from the string
* @throws AddressException if addresses checking fails
*/
public static InternetAddress[] parse(final String addresses) throws AddressException {
return parse(addresses, true);
}
/**
* Parse addresses out of the string.
*
* @param addresses the addresses to parse
* @param strict if true perform detailed checking, if false just perform basic checking
* @return an array of InternetAddresses parsed from the string
* @throws AddressException if address checking fails
*/
public static InternetAddress[] parse(final String addresses, final boolean strict) throws AddressException {
return parse(addresses, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
}
/**
* Parse addresses out of the string.
*
* @param addresses the addresses to parse
* @param strict if true perform detailed checking, if false perform little checking
* @return an array of InternetAddresses parsed from the string
* @throws AddressException if address checking fails
*/
public static InternetAddress[] parseHeader(final String addresses, final boolean strict) throws AddressException {
return parse(addresses, strict ? AddressParser.STRICT : AddressParser.PARSE_HEADER);
}
/**
* Parse addresses with increasing degrees of RFC822 compliance checking.
*
* @param addresses the string to parse
* @param level The required strictness level.
*
* @return an array of InternetAddresses parsed from the string
* @throws AddressException
* if address checking fails
*/
private static InternetAddress[] parse(final String addresses, final int level) throws AddressException {
// create a parser and have it extract the list using the requested strictness leve.
final AddressParser parser = new AddressParser(addresses, level);
return parser.parseAddressList();
}
/**
* Validate the address portion of an internet address to ensure
* validity. Throws an AddressException if any validity
* problems are encountered.
*
* @exception AddressException
*/
public void validate() throws AddressException {
// create a parser using the strictest validation level.
final AddressParser parser = new AddressParser(formatAddress(address), AddressParser.STRICT);
parser.validateAddress();
}
}