View Javadoc

1   /**
2    *
3    * Copyright 2003-2006 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  package javax.mail.internet;
18  
19  import java.io.UnsupportedEncodingException;
20  import java.lang.reflect.Array;
21  import java.net.InetAddress;
22  import java.net.UnknownHostException;
23  import java.util.ArrayList;
24  import java.util.List;
25  import java.util.StringTokenizer;
26  
27  import javax.mail.Address;
28  import javax.mail.Session;
29  
30  import org.apache.geronimo.mail.util.SessionUtil;
31  
32  /**
33   * A representation of an Internet email address as specified by RFC822 in
34   * conjunction with a human-readable personal name that can be encoded as
35   * specified by RFC2047.
36   * A typical address is "user@host.domain" and personal name "Joe User"
37   *
38   * @version $Rev: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 2006) $
39   */
40  public class InternetAddress extends Address implements Cloneable {
41      /**
42       * The address in RFC822 format.
43       */
44      protected String address;
45  
46      /**
47       * The personal name in RFC2047 format.
48       * Subclasses must ensure that this field is updated if the personal field
49       * is updated; alternatively, it can be invalidated by setting to null
50       * which will cause it to be recomputed.
51       */
52      protected String encodedPersonal;
53  
54      /**
55       * The personal name as a Java String.
56       * Subclasses must ensure that this field is updated if the encodedPersonal field
57       * is updated; alternatively, it can be invalidated by setting to null
58       * which will cause it to be recomputed.
59       */
60      protected String personal;
61  
62      public InternetAddress() {
63      }
64  
65      public InternetAddress(String address) throws AddressException {
66          this(address, true);
67      }
68  
69      public InternetAddress(String address, boolean strict) throws AddressException {
70          // use the parse method to process the address.  This has the wierd side effect of creating a new
71          // InternetAddress instance to create an InternetAddress, but these are lightweight objects and
72          // we need access to multiple pieces of data from the parsing process.
73          AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
74  
75          InternetAddress parsedAddress = parser.parseAddress();
76          // copy the important information, which right now is just the address and
77          // personal info.
78          this.address = parsedAddress.address;
79          this.personal = parsedAddress.personal;
80          this.encodedPersonal = parsedAddress.encodedPersonal;
81      }
82  
83      public InternetAddress(String address, String personal) throws UnsupportedEncodingException {
84          this(address, personal, null);
85      }
86  
87      public InternetAddress(String address, String personal, String charset) throws UnsupportedEncodingException {
88          this.address = address;
89          setPersonal(personal, charset);
90      }
91  
92      /**
93       * Clone this object.
94       *
95       * @return a copy of this object as created by Object.clone()
96       */
97      public Object clone() {
98          try {
99              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 }