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: 467553 $ $Date: 2006-10-25 00:01:51 -0400 (Wed, 25 Oct 2006) $
042     */
043    public class InternetAddress extends Address implements Cloneable {
044        /**
045         * The address in RFC822 format.
046         */
047        protected String address;
048    
049        /**
050         * The personal name in RFC2047 format.
051         * Subclasses must ensure that this field is updated if the personal field
052         * is updated; alternatively, it can be invalidated by setting to null
053         * which will cause it to be recomputed.
054         */
055        protected String encodedPersonal;
056    
057        /**
058         * The personal name as a Java String.
059         * Subclasses must ensure that this field is updated if the encodedPersonal field
060         * is updated; alternatively, it can be invalidated by setting to null
061         * which will cause it to be recomputed.
062         */
063        protected String personal;
064    
065        public InternetAddress() {
066        }
067    
068        public InternetAddress(String address) throws AddressException {
069            this(address, true);
070        }
071    
072        public InternetAddress(String address, boolean strict) throws AddressException {
073            // use the parse method to process the address.  This has the wierd side effect of creating a new
074            // InternetAddress instance to create an InternetAddress, but these are lightweight objects and
075            // we need access to multiple pieces of data from the parsing process.
076            AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
077    
078            InternetAddress parsedAddress = parser.parseAddress();
079            // copy the important information, which right now is just the address and
080            // personal info.
081            this.address = parsedAddress.address;
082            this.personal = parsedAddress.personal;
083            this.encodedPersonal = parsedAddress.encodedPersonal;
084        }
085    
086        public InternetAddress(String address, String personal) throws UnsupportedEncodingException {
087            this(address, personal, null);
088        }
089    
090        public InternetAddress(String address, String personal, String charset) throws UnsupportedEncodingException {
091            this.address = address;
092            setPersonal(personal, charset);
093        }
094    
095        /**
096         * Clone this object.
097         *
098         * @return a copy of this object as created by Object.clone()
099         */
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    }