View Javadoc

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