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 }