001    /**
002     *
003     * Copyright 2003-2006 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    package javax.mail.internet;
018    
019    import java.io.UnsupportedEncodingException;
020    import java.lang.reflect.Array;
021    import java.net.InetAddress;
022    import java.net.UnknownHostException;
023    import java.util.ArrayList;
024    import java.util.List;
025    import java.util.StringTokenizer;
026    
027    import javax.mail.Address;
028    import javax.mail.Session;
029    
030    import org.apache.geronimo.mail.util.SessionUtil;
031    
032    /**
033     * A representation of an Internet email address as specified by RFC822 in
034     * conjunction with a human-readable personal name that can be encoded as
035     * specified by RFC2047.
036     * A typical address is "user@host.domain" and personal name "Joe User"
037     *
038     * @version $Rev: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 2006) $
039     */
040    public class InternetAddress extends Address implements Cloneable {
041        /**
042         * The address in RFC822 format.
043         */
044        protected String address;
045    
046        /**
047         * The personal name in RFC2047 format.
048         * Subclasses must ensure that this field is updated if the personal field
049         * is updated; alternatively, it can be invalidated by setting to null
050         * which will cause it to be recomputed.
051         */
052        protected String encodedPersonal;
053    
054        /**
055         * The personal name as a Java String.
056         * Subclasses must ensure that this field is updated if the encodedPersonal field
057         * is updated; alternatively, it can be invalidated by setting to null
058         * which will cause it to be recomputed.
059         */
060        protected String personal;
061    
062        public InternetAddress() {
063        }
064    
065        public InternetAddress(String address) throws AddressException {
066            this(address, true);
067        }
068    
069        public InternetAddress(String address, boolean strict) throws AddressException {
070            // use the parse method to process the address.  This has the wierd side effect of creating a new
071            // InternetAddress instance to create an InternetAddress, but these are lightweight objects and
072            // we need access to multiple pieces of data from the parsing process.
073            AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
074    
075            InternetAddress parsedAddress = parser.parseAddress();
076            // copy the important information, which right now is just the address and
077            // personal info.
078            this.address = parsedAddress.address;
079            this.personal = parsedAddress.personal;
080            this.encodedPersonal = parsedAddress.encodedPersonal;
081        }
082    
083        public InternetAddress(String address, String personal) throws UnsupportedEncodingException {
084            this(address, personal, null);
085        }
086    
087        public InternetAddress(String address, String personal, String charset) throws UnsupportedEncodingException {
088            this.address = address;
089            setPersonal(personal, charset);
090        }
091    
092        /**
093         * Clone this object.
094         *
095         * @return a copy of this object as created by Object.clone()
096         */
097        public Object clone() {
098            try {
099                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    }