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