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