001 /** 002 * 003 * Copyright 2003-2004 The Apache Software Foundation 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package javax.mail.internet; 018 019 import java.io.UnsupportedEncodingException; 020 import java.lang.reflect.Array; 021 import java.net.InetAddress; 022 import java.net.UnknownHostException; 023 import java.util.ArrayList; 024 import java.util.List; 025 import java.util.StringTokenizer; 026 027 import javax.mail.Address; 028 import javax.mail.Session; 029 030 import org.apache.geronimo.mail.util.SessionUtil; 031 032 /** 033 * A representation of an Internet email address as specified by RFC822 in 034 * conjunction with a human-readable personal name that can be encoded as 035 * specified by RFC2047. 036 * A typical address is "user@host.domain" and personal name "Joe User" 037 * 038 * @version $Rev: 382387 $ $Date: 2006-03-02 06:18:24 -0800 (Thu, 02 Mar 2006) $ 039 */ 040 public class InternetAddress extends Address implements Cloneable { 041 /** 042 * The address in RFC822 format. 043 */ 044 protected String address; 045 046 /** 047 * The personal name in RFC2047 format. 048 * Subclasses must ensure that this field is updated if the personal field 049 * is updated; alternatively, it can be invalidated by setting to null 050 * which will cause it to be recomputed. 051 */ 052 protected String encodedPersonal; 053 054 /** 055 * The personal name as a Java String. 056 * Subclasses must ensure that this field is updated if the encodedPersonal field 057 * is updated; alternatively, it can be invalidated by setting to null 058 * which will cause it to be recomputed. 059 */ 060 protected String personal; 061 062 public InternetAddress() { 063 } 064 065 public InternetAddress(String address) throws AddressException { 066 this(address, true); 067 } 068 069 public InternetAddress(String address, boolean strict) throws AddressException { 070 // use the parse method to process the address. This has the wierd side effect of creating a new 071 // InternetAddress instance to create an InternetAddress, but these are lightweight objects and 072 // we need access to multiple pieces of data from the parsing process. 073 AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT); 074 075 InternetAddress parsedAddress = parser.parseAddress(); 076 // copy the important information, which right now is just the address and 077 // personal info. 078 this.address = parsedAddress.address; 079 this.personal = parsedAddress.personal; 080 this.encodedPersonal = parsedAddress.encodedPersonal; 081 } 082 083 public InternetAddress(String address, String personal) throws UnsupportedEncodingException { 084 this(address, personal, null); 085 } 086 087 public InternetAddress(String address, String personal, String charset) throws UnsupportedEncodingException { 088 this.address = address; 089 setPersonal(personal, charset); 090 } 091 092 /** 093 * Clone this object. 094 * 095 * @return a copy of this object as created by Object.clone() 096 */ 097 public Object clone() { 098 try { 099 return super.clone(); 100 } catch (CloneNotSupportedException e) { 101 throw new Error(); 102 } 103 } 104 105 /** 106 * Return the type of this address. 107 * 108 * @return the type of this address; always "rfc822" 109 */ 110 public String getType() { 111 return "rfc822"; 112 } 113 114 /** 115 * Set the address. 116 * No validation is performed; validate() can be used to check if it is valid. 117 * 118 * @param address the address to set 119 */ 120 public void setAddress(String address) { 121 this.address = address; 122 } 123 124 /** 125 * Set the personal name. 126 * The name is first checked to see if it can be encoded; if this fails then an 127 * UnsupportedEncodingException is thrown and no fields are modified. 128 * 129 * @param name the new personal name 130 * @param charset the charset to use; see {@link MimeUtility#encodeWord(String, String, String) MimeUtilityencodeWord} 131 * @throws UnsupportedEncodingException if the name cannot be encoded 132 */ 133 public void setPersonal(String name, String charset) throws UnsupportedEncodingException { 134 personal = name; 135 if (name != null) { 136 encodedPersonal = MimeUtility.encodeWord(name, charset, null); 137 } 138 else { 139 encodedPersonal = null; 140 } 141 } 142 143 /** 144 * Set the personal name. 145 * The name is first checked to see if it can be encoded using {@link MimeUtility#encodeWord(String)}; if this fails then an 146 * UnsupportedEncodingException is thrown and no fields are modified. 147 * 148 * @param name the new personal name 149 * @throws UnsupportedEncodingException if the name cannot be encoded 150 */ 151 public void setPersonal(String name) throws UnsupportedEncodingException { 152 personal = name; 153 if (name != null) { 154 encodedPersonal = MimeUtility.encodeWord(name); 155 } 156 else { 157 encodedPersonal = null; 158 } 159 } 160 161 /** 162 * Return the address. 163 * 164 * @return the address 165 */ 166 public String getAddress() { 167 return address; 168 } 169 170 /** 171 * Return the personal name. 172 * If the personal field is null, then an attempt is made to decode the encodedPersonal 173 * field using {@link MimeUtility#decodeWord(String)}; if this is sucessful, then 174 * the personal field is updated with that value and returned; if there is a problem 175 * decoding the text then the raw value from encodedPersonal is returned. 176 * 177 * @return the personal name 178 */ 179 public String getPersonal() { 180 if (personal == null && encodedPersonal != null) { 181 try { 182 personal = MimeUtility.decodeWord(encodedPersonal); 183 } catch (ParseException e) { 184 return encodedPersonal; 185 } catch (UnsupportedEncodingException e) { 186 return encodedPersonal; 187 } 188 } 189 return personal; 190 } 191 192 /** 193 * Return the encoded form of the personal name. 194 * If the encodedPersonal field is null, then an attempt is made to encode the 195 * personal field using {@link MimeUtility#encodeWord(String)}; if this is 196 * successful then the encodedPersonal field is updated with that value and returned; 197 * if there is a problem encoding the text then null is returned. 198 * 199 * @return the encoded form of the personal name 200 */ 201 private String getEncodedPersonal() { 202 if (encodedPersonal == null && personal != null) { 203 try { 204 encodedPersonal = MimeUtility.encodeWord(personal); 205 } catch (UnsupportedEncodingException e) { 206 // as we could not encode this, return null 207 return null; 208 } 209 } 210 return encodedPersonal; 211 } 212 213 214 /** 215 * Return a string representation of this address using only US-ASCII characters. 216 * 217 * @return a string representation of this address 218 */ 219 public String toString() { 220 // group addresses are always returned without modification. 221 if (isGroup()) { 222 return address; 223 } 224 225 // if we have personal information, then we need to return this in the route-addr form: 226 // "personal <address>". If there is no personal information, then we typically return 227 // the address without the angle brackets. However, if the address contains anything other 228 // than atoms, '@', and '.' (e.g., uses domain literals, has specified routes, or uses 229 // quoted strings in the local-part), we bracket the address. 230 String p = getEncodedPersonal(); 231 if (p == null) { 232 return formatAddress(address); 233 } 234 else { 235 StringBuffer buf = new StringBuffer(p.length() + 8 + address.length() + 3); 236 buf.append(AddressParser.quoteString(p)); 237 buf.append(" <").append(address).append(">"); 238 return buf.toString(); 239 } 240 } 241 242 /** 243 * Check the form of an address, and enclose it within brackets 244 * if they are required for this address form. 245 * 246 * @param a The source address. 247 * 248 * @return A formatted address, which can be the original address string. 249 */ 250 private String formatAddress(String a) 251 { 252 // this could be a group address....we don't muck with those. 253 if (address.endsWith(";") && address.indexOf(":") > 0) { 254 return address; 255 } 256 257 if (AddressParser.containsCharacters(a, "()<>,;:\"[]")) { 258 StringBuffer buf = new StringBuffer(address.length() + 3); 259 buf.append("<").append(address).append(">"); 260 return buf.toString(); 261 } 262 return address; 263 } 264 265 /** 266 * Return a string representation of this address using Unicode characters. 267 * 268 * @return a string representation of this address 269 */ 270 public String toUnicodeString() { 271 // group addresses are always returned without modification. 272 if (isGroup()) { 273 return address; 274 } 275 276 // if we have personal information, then we need to return this in the route-addr form: 277 // "personal <address>". If there is no personal information, then we typically return 278 // the address without the angle brackets. However, if the address contains anything other 279 // than atoms, '@', and '.' (e.g., uses domain literals, has specified routes, or uses 280 // quoted strings in the local-part), we bracket the address. 281 282 // NB: The difference between toString() and toUnicodeString() is the use of getPersonal() 283 // vs. getEncodedPersonal() for the personal portion. If the personal information contains only 284 // ASCII-7 characters, these are the same. 285 String p = getPersonal(); 286 if (p == null) { 287 return formatAddress(address); 288 } 289 else { 290 StringBuffer buf = new StringBuffer(p.length() + 8 + address.length() + 3); 291 buf.append(AddressParser.quoteString(p)); 292 buf.append(" <").append(address).append(">"); 293 return buf.toString(); 294 } 295 } 296 297 /** 298 * Compares two addresses for equality. 299 * We define this as true if the other object is an InternetAddress 300 * and the two values returned by getAddress() are equal in a 301 * case-insensitive comparison. 302 * 303 * @param o the other object 304 * @return true if the addresses are the same 305 */ 306 public boolean equals(Object o) { 307 if (this == o) return true; 308 if (!(o instanceof InternetAddress)) return false; 309 310 InternetAddress other = (InternetAddress) o; 311 String myAddress = getAddress(); 312 return myAddress == null ? (other.getAddress() == null) : myAddress.equalsIgnoreCase(other.getAddress()); 313 } 314 315 /** 316 * Return the hashCode for this address. 317 * We define this to be the hashCode of the address after conversion to lowercase. 318 * 319 * @return a hashCode for this address 320 */ 321 public int hashCode() { 322 return (address == null) ? 0 : address.toLowerCase().hashCode(); 323 } 324 325 /** 326 * Return true is this address is an RFC822 group address in the format 327 * <code>phrase ":" [#mailbox] ";"</code>. 328 * We check this by using the presense of a ':' character in the address, and a 329 * ';' as the very last character. 330 * 331 * @return true is this address represents a group 332 */ 333 public boolean isGroup() { 334 if (address == null) { 335 return false; 336 } 337 338 return address.endsWith(";") && address.indexOf(":") > 0; 339 } 340 341 /** 342 * Return the members of a group address. 343 * 344 * If strict is true and the address does not contain an initial phrase then an AddressException is thrown. 345 * Otherwise the phrase is skipped and the remainder of the address is checked to see if it is a group. 346 * If it is, the content and strict flag are passed to parseHeader to extract the list of addresses; 347 * if it is not a group then null is returned. 348 * 349 * @param strict whether strict RFC822 checking should be performed 350 * @return an array of InternetAddress objects for the group members, or null if this address is not a group 351 * @throws AddressException if there was a problem parsing the header 352 */ 353 public InternetAddress[] getGroup(boolean strict) throws AddressException { 354 if (address == null) { 355 return null; 356 } 357 358 // create an address parser and use it to extract the group information. 359 AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT); 360 return parser.extractGroupList(); 361 } 362 363 /** 364 * Return an InternetAddress representing the current user. 365 * <P/> 366 * If session is not null, we first look for an address specified in its 367 * "mail.from" property; if this is not set, we look at its "mail.user" 368 * and "mail.host" properties and if both are not null then an address of 369 * the form "${mail.user}@${mail.host}" is created. 370 * If this fails to give an address, then an attempt is made to create 371 * an address by combining the value of the "user.name" System property 372 * with the value returned from InetAddress.getLocalHost().getHostName(). 373 * Any SecurityException raised accessing the system property or any 374 * UnknownHostException raised getting the hostname are ignored. 375 * <P/> 376 * Finally, an attempt is made to convert the value obtained above to 377 * an InternetAddress. If this fails, then null is returned. 378 * 379 * @param session used to obtain mail properties 380 * @return an InternetAddress for the current user, or null if it cannot be determined 381 */ 382 public static InternetAddress getLocalAddress(Session session) { 383 String host = null; 384 String user = null; 385 386 // ok, we have several steps for resolving this. To start with, we could have a from address 387 // configured already, which will be a full InternetAddress string. If we don't have that, then 388 // we need to resolve a user and host to compose an address from. 389 if (session != null) { 390 String address = session.getProperty("mail.from"); 391 // if we got this, we can skip out now 392 if (address != null) { 393 try { 394 return new InternetAddress(address); 395 } catch (AddressException e) { 396 // invalid address on the from...treat this as an error and return null. 397 return null; 398 } 399 } 400 401 // now try for user and host information. We have both session and system properties to check here. 402 // we'll just handle the session ones here, and check the system ones below if we're missing information. 403 user = session.getProperty("mail.user"); 404 host = session.getProperty("mail.host"); 405 } 406 407 try { 408 409 // if either user or host is null, then we check non-session sources for the information. 410 if (user == null) { 411 user = System.getProperty("user.name"); 412 } 413 414 if (host == null) { 415 host = InetAddress.getLocalHost().getHostName(); 416 } 417 418 if (user != null && host != null) { 419 // if we have both a user and host, we can create a local address 420 return new InternetAddress(user + '@' + host); 421 } 422 } catch (AddressException e) { 423 // ignore 424 } catch (UnknownHostException e) { 425 // ignore 426 } catch (SecurityException e) { 427 // ignore 428 } 429 return null; 430 } 431 432 /** 433 * Convert the supplied addresses into a single String of comma-separated text as 434 * produced by {@link InternetAddress#toString() toString()}. 435 * No line-break detection is performed. 436 * 437 * @param addresses the array of addresses to convert 438 * @return a one-line String of comma-separated addresses 439 */ 440 public static String toString(Address[] addresses) { 441 if (addresses == null || addresses.length == 0) { 442 return null; 443 } 444 if (addresses.length == 1) { 445 return addresses[0].toString(); 446 } else { 447 StringBuffer buf = new StringBuffer(addresses.length * 32); 448 buf.append(addresses[0].toString()); 449 for (int i = 1; i < addresses.length; i++) { 450 buf.append(", "); 451 buf.append(addresses[i].toString()); 452 } 453 return buf.toString(); 454 } 455 } 456 457 /** 458 * Convert the supplies addresses into a String of comma-separated text, 459 * inserting line-breaks between addresses as needed to restrict the line 460 * length to 72 characters. Splits will only be introduced between addresses 461 * so an address longer than 71 characters will still be placed on a single 462 * line. 463 * 464 * @param addresses the array of addresses to convert 465 * @param used the starting column 466 * @return a String of comma-separated addresses with optional line breaks 467 */ 468 public static String toString(Address[] addresses, int used) { 469 if (addresses == null || addresses.length == 0) { 470 return null; 471 } 472 if (addresses.length == 1) { 473 String s = addresses[0].toString(); 474 if (used + s.length() > 72) { 475 s = "\r\n " + s; 476 } 477 return s; 478 } else { 479 StringBuffer buf = new StringBuffer(addresses.length * 32); 480 for (int i = 0; i < addresses.length; i++) { 481 String s = addresses[1].toString(); 482 if (i == 0) { 483 if (used + s.length() + 1 > 72) { 484 buf.append("\r\n "); 485 used = 2; 486 } 487 } else { 488 if (used + s.length() + 1 > 72) { 489 buf.append(",\r\n "); 490 used = 2; 491 } else { 492 buf.append(", "); 493 used += 2; 494 } 495 } 496 buf.append(s); 497 used += s.length(); 498 } 499 return buf.toString(); 500 } 501 } 502 503 /** 504 * Parse addresses out of the string with basic checking. 505 * 506 * @param addresses the addresses to parse 507 * @return an array of InternetAddresses parsed from the string 508 * @throws AddressException if addresses checking fails 509 */ 510 public static InternetAddress[] parse(String addresses) throws AddressException { 511 return parse(addresses, true); 512 } 513 514 /** 515 * Parse addresses out of the string. 516 * 517 * @param addresses the addresses to parse 518 * @param strict if true perform detailed checking, if false just perform basic checking 519 * @return an array of InternetAddresses parsed from the string 520 * @throws AddressException if address checking fails 521 */ 522 public static InternetAddress[] parse(String addresses, boolean strict) throws AddressException { 523 return parse(addresses, strict ? AddressParser.STRICT : AddressParser.NONSTRICT); 524 } 525 526 /** 527 * Parse addresses out of the string. 528 * 529 * @param addresses the addresses to parse 530 * @param strict if true perform detailed checking, if false perform little checking 531 * @return an array of InternetAddresses parsed from the string 532 * @throws AddressException if address checking fails 533 */ 534 public static InternetAddress[] parseHeader(String addresses, boolean strict) throws AddressException { 535 return parse(addresses, strict ? AddressParser.STRICT : AddressParser.PARSE_HEADER); 536 } 537 538 /** 539 * Parse addresses with increasing degrees of RFC822 compliance checking. 540 * 541 * @param addresses the string to parse 542 * @param level The required strictness level. 543 * 544 * @return an array of InternetAddresses parsed from the string 545 * @throws AddressException 546 * if address checking fails 547 */ 548 private static InternetAddress[] parse(String addresses, int level) throws AddressException { 549 // create a parser and have it extract the list using the requested strictness leve. 550 AddressParser parser = new AddressParser(addresses, level); 551 return parser.parseAddressList(); 552 } 553 554 /** 555 * Validate the address portion of an internet address to ensure 556 * validity. Throws an AddressException if any validity 557 * problems are encountered. 558 * 559 * @exception AddressException 560 */ 561 public void validate() throws AddressException { 562 563 // create a parser using the strictest validation level. 564 AddressParser parser = new AddressParser(formatAddress(address), AddressParser.STRICT); 565 parser.validateAddress(); 566 } 567 }