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 }