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: 920714 $ $Date: 2010-03-09 01:55:49 -0500 (Tue, 09 Mar 2010) $
042     */
043    public class InternetAddress extends Address implements Cloneable {
044            
045            private static final long serialVersionUID = -7507595530758302903L;
046            
047        /**
048         * The address in RFC822 format.
049         */
050        protected String address;
051    
052        /**
053         * The personal name in RFC2047 format.
054         * Subclasses must ensure that this field is updated if the personal field
055         * is updated; alternatively, it can be invalidated by setting to null
056         * which will cause it to be recomputed.
057         */
058        protected String encodedPersonal;
059    
060        /**
061         * The personal name as a Java String.
062         * Subclasses must ensure that this field is updated if the encodedPersonal field
063         * is updated; alternatively, it can be invalidated by setting to null
064         * which will cause it to be recomputed.
065         */
066        protected String personal;
067    
068        public InternetAddress() {
069        }
070    
071        public InternetAddress(String address) throws AddressException {
072            this(address, true);
073        }
074    
075        public InternetAddress(String address, boolean strict) throws AddressException {
076            // use the parse method to process the address.  This has the wierd side effect of creating a new
077            // InternetAddress instance to create an InternetAddress, but these are lightweight objects and
078            // we need access to multiple pieces of data from the parsing process.
079            AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
080    
081            InternetAddress parsedAddress = parser.parseAddress();
082            // copy the important information, which right now is just the address and
083            // personal info.
084            this.address = parsedAddress.address;
085            this.personal = parsedAddress.personal;
086            this.encodedPersonal = parsedAddress.encodedPersonal;
087        }
088    
089        public InternetAddress(String address, String personal) throws UnsupportedEncodingException {
090            this(address, personal, null);
091        }
092    
093        public InternetAddress(String address, String personal, String charset) throws UnsupportedEncodingException {
094            this.address = address;
095            setPersonal(personal, charset);
096        }
097    
098        /**
099         * Clone this object.
100         *
101         * @return a copy of this object as created by Object.clone()
102         */
103        public Object clone() {
104            try {
105                return super.clone();
106            } catch (CloneNotSupportedException e) {
107                throw new Error();
108            }
109        }
110    
111        /**
112         * Return the type of this address.
113         *
114         * @return the type of this address; always "rfc822"
115         */
116        public String getType() {
117            return "rfc822";
118        }
119    
120        /**
121         * Set the address.
122         * No validation is performed; validate() can be used to check if it is valid.
123         *
124         * @param address the address to set
125         */
126        public void setAddress(String address) {
127            this.address = address;
128        }
129    
130        /**
131         * Set the personal name.
132         * The name is first checked to see if it can be encoded; if this fails then an
133         * UnsupportedEncodingException is thrown and no fields are modified.
134         *
135         * @param name    the new personal name
136         * @param charset the charset to use; see {@link MimeUtility#encodeWord(String, String, String) MimeUtilityencodeWord}
137         * @throws UnsupportedEncodingException if the name cannot be encoded
138         */
139        public void setPersonal(String name, String charset) throws UnsupportedEncodingException {
140            personal = name;
141            if (name != null) {
142                encodedPersonal = MimeUtility.encodeWord(name, charset, null);
143            }
144            else {
145                encodedPersonal = null;
146            }
147        }
148    
149        /**
150         * Set the personal name.
151         * The name is first checked to see if it can be encoded using {@link MimeUtility#encodeWord(String)}; if this fails then an
152         * UnsupportedEncodingException is thrown and no fields are modified.
153         *
154         * @param name the new personal name
155         * @throws UnsupportedEncodingException if the name cannot be encoded
156         */
157        public void setPersonal(String name) throws UnsupportedEncodingException {
158            personal = name;
159            if (name != null) {
160                encodedPersonal = MimeUtility.encodeWord(name);
161            }
162            else {
163                encodedPersonal = null;
164            }
165        }
166    
167        /**
168         * Return the address.
169         *
170         * @return the address
171         */
172        public String getAddress() {
173            return address;
174        }
175    
176        /**
177         * Return the personal name.
178         * If the personal field is null, then an attempt is made to decode the encodedPersonal
179         * field using {@link MimeUtility#decodeWord(String)}; if this is sucessful, then
180         * the personal field is updated with that value and returned; if there is a problem
181         * decoding the text then the raw value from encodedPersonal is returned.
182         *
183         * @return the personal name
184         */
185        public String getPersonal() {
186            if (personal == null && encodedPersonal != null) {
187                try {
188                    personal = MimeUtility.decodeWord(encodedPersonal);
189                } catch (ParseException e) {
190                    return encodedPersonal;
191                } catch (UnsupportedEncodingException e) {
192                    return encodedPersonal;
193                }
194            }
195            return personal;
196        }
197    
198        /**
199         * Return the encoded form of the personal name.
200         * If the encodedPersonal field is null, then an attempt is made to encode the
201         * personal field using {@link MimeUtility#encodeWord(String)}; if this is
202         * successful then the encodedPersonal field is updated with that value and returned;
203         * if there is a problem encoding the text then null is returned.
204         *
205         * @return the encoded form of the personal name
206         */
207        private String getEncodedPersonal() {
208            if (encodedPersonal == null && personal != null) {
209                try {
210                    encodedPersonal = MimeUtility.encodeWord(personal);
211                } catch (UnsupportedEncodingException e) {
212                    // as we could not encode this, return null
213                    return null;
214                }
215            }
216            return encodedPersonal;
217        }
218    
219    
220        /**
221         * Return a string representation of this address using only US-ASCII characters.
222         *
223         * @return a string representation of this address
224         */
225        public String toString() {
226            // group addresses are always returned without modification.
227            if (isGroup()) {
228                return address;
229            }
230    
231            // if we have personal information, then we need to return this in the route-addr form:
232            // "personal <address>".  If there is no personal information, then we typically return
233            // the address without the angle brackets.  However, if the address contains anything other
234            // than atoms, '@', and '.' (e.g., uses domain literals, has specified routes, or uses
235            // quoted strings in the local-part), we bracket the address.
236            String p = getEncodedPersonal();
237            if (p == null) {
238                return formatAddress(address);
239            }
240            else {
241                StringBuffer buf = new StringBuffer(p.length() + 8 + address.length() + 3);
242                buf.append(AddressParser.quoteString(p));
243                buf.append(" <").append(address).append(">");
244                return buf.toString();
245            }
246        }
247    
248        /**
249         * Check the form of an address, and enclose it within brackets
250         * if they are required for this address form.
251         *
252         * @param a      The source address.
253         *
254         * @return A formatted address, which can be the original address string.
255         */
256        private String formatAddress(String a)
257        {
258            // this could be a group address....we don't muck with those.
259            if (address.endsWith(";") && address.indexOf(":") > 0) {
260                return address;
261            }
262    
263            if (AddressParser.containsCharacters(a, "()<>,;:\"[]")) {
264                StringBuffer buf = new StringBuffer(address.length() + 3);
265                buf.append("<").append(address).append(">");
266                return buf.toString();
267            }
268            return address;
269        }
270    
271        /**
272         * Return a string representation of this address using Unicode characters.
273         *
274         * @return a string representation of this address
275         */
276        public String toUnicodeString() {
277            // group addresses are always returned without modification.
278            if (isGroup()) {
279                return address;
280            }
281    
282            // if we have personal information, then we need to return this in the route-addr form:
283            // "personal <address>".  If there is no personal information, then we typically return
284            // the address without the angle brackets.  However, if the address contains anything other
285            // than atoms, '@', and '.' (e.g., uses domain literals, has specified routes, or uses
286            // quoted strings in the local-part), we bracket the address.
287    
288            // NB:  The difference between toString() and toUnicodeString() is the use of getPersonal()
289            // vs. getEncodedPersonal() for the personal portion.  If the personal information contains only
290            // ASCII-7 characters, these are the same.
291            String p = getPersonal();
292            if (p == null) {
293                return formatAddress(address);
294            }
295            else {
296                StringBuffer buf = new StringBuffer(p.length() + 8 + address.length() + 3);
297                buf.append(AddressParser.quoteString(p));
298                buf.append(" <").append(address).append(">");
299                return buf.toString();
300            }
301        }
302    
303        /**
304         * Compares two addresses for equality.
305         * We define this as true if the other object is an InternetAddress
306         * and the two values returned by getAddress() are equal in a
307         * case-insensitive comparison.
308         *
309         * @param o the other object
310         * @return true if the addresses are the same
311         */
312        public boolean equals(Object o) {
313            if (this == o) return true;
314            if (!(o instanceof InternetAddress)) return false;
315    
316            InternetAddress other = (InternetAddress) o;
317            String myAddress = getAddress();
318            return myAddress == null ? (other.getAddress() == null) : myAddress.equalsIgnoreCase(other.getAddress());
319        }
320    
321        /**
322         * Return the hashCode for this address.
323         * We define this to be the hashCode of the address after conversion to lowercase.
324         *
325         * @return a hashCode for this address
326         */
327        public int hashCode() {
328            return (address == null) ? 0 : address.toLowerCase().hashCode();
329        }
330    
331        /**
332         * Return true is this address is an RFC822 group address in the format
333         * <code>phrase ":" [#mailbox] ";"</code>.
334         * We check this by using the presense of a ':' character in the address, and a
335         * ';' as the very last character.
336         *
337         * @return true is this address represents a group
338         */
339        public boolean isGroup() {
340            if (address == null) {
341                return false;
342            }
343    
344            return address.endsWith(";") && address.indexOf(":") > 0;
345        }
346    
347        /**
348         * Return the members of a group address.
349         *
350         * If strict is true and the address does not contain an initial phrase then an AddressException is thrown.
351         * Otherwise the phrase is skipped and the remainder of the address is checked to see if it is a group.
352         * If it is, the content and strict flag are passed to parseHeader to extract the list of addresses;
353         * if it is not a group then null is returned.
354         *
355         * @param strict whether strict RFC822 checking should be performed
356         * @return an array of InternetAddress objects for the group members, or null if this address is not a group
357         * @throws AddressException if there was a problem parsing the header
358         */
359        public InternetAddress[] getGroup(boolean strict) throws AddressException {
360            if (address == null) {
361                return null;
362            }
363    
364            // create an address parser and use it to extract the group information.
365            AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
366            return parser.extractGroupList();
367        }
368    
369        /**
370         * Return an InternetAddress representing the current user.
371         * <P/>
372         * If session is not null, we first look for an address specified in its
373         * "mail.from" property; if this is not set, we look at its "mail.user"
374         * and "mail.host" properties and if both are not null then an address of
375         * the form "${mail.user}@${mail.host}" is created.
376         * If this fails to give an address, then an attempt is made to create
377         * an address by combining the value of the "user.name" System property
378         * with the value returned from InetAddress.getLocalHost().getHostName().
379         * Any SecurityException raised accessing the system property or any
380         * UnknownHostException raised getting the hostname are ignored.
381         * <P/>
382         * Finally, an attempt is made to convert the value obtained above to
383         * an InternetAddress. If this fails, then null is returned.
384         *
385         * @param session used to obtain mail properties
386         * @return an InternetAddress for the current user, or null if it cannot be determined
387         */
388        public static InternetAddress getLocalAddress(Session session) {
389            String host = null;
390            String user = null;
391    
392            // ok, we have several steps for resolving this.  To start with, we could have a from address
393            // configured already, which will be a full InternetAddress string.  If we don't have that, then
394            // we need to resolve a user and host to compose an address from.
395            if (session != null) {
396                String address = session.getProperty("mail.from");
397                // if we got this, we can skip out now
398                if (address != null) {
399                    try {
400                        return new InternetAddress(address);
401                    } catch (AddressException e) {
402                        // invalid address on the from...treat this as an error and return null.
403                        return null;
404                    }
405                }
406    
407                // now try for user and host information.  We have both session and system properties to check here.
408                // we'll just handle the session ones here, and check the system ones below if we're missing information.
409                user = session.getProperty("mail.user");
410                host = session.getProperty("mail.host");
411            }
412    
413            try {
414    
415                // if either user or host is null, then we check non-session sources for the information.
416                if (user == null) {
417                    user = System.getProperty("user.name");
418                }
419    
420                if (host == null) {
421                    host = InetAddress.getLocalHost().getHostName();
422                }
423    
424                if (user != null && host != null) {
425                    // if we have both a user and host, we can create a local address
426                    return new InternetAddress(user + '@' + host);
427                }
428            } catch (AddressException e) {
429                // ignore
430            } catch (UnknownHostException e) {
431                // ignore
432            } catch (SecurityException e) {
433                // ignore
434            }
435            return null;
436        }
437    
438        /**
439         * Convert the supplied addresses into a single String of comma-separated text as
440         * produced by {@link InternetAddress#toString() toString()}.
441         * No line-break detection is performed.
442         *
443         * @param addresses the array of addresses to convert
444         * @return a one-line String of comma-separated addresses
445         */
446        public static String toString(Address[] addresses) {
447            if (addresses == null || addresses.length == 0) {
448                return null;
449            }
450            if (addresses.length == 1) {
451                return addresses[0].toString();
452            } else {
453                StringBuffer buf = new StringBuffer(addresses.length * 32);
454                buf.append(addresses[0].toString());
455                for (int i = 1; i < addresses.length; i++) {
456                    buf.append(", ");
457                    buf.append(addresses[i].toString());
458                }
459                return buf.toString();
460            }
461        }
462    
463        /**
464         * Convert the supplies addresses into a String of comma-separated text,
465         * inserting line-breaks between addresses as needed to restrict the line
466         * length to 72 characters. Splits will only be introduced between addresses
467         * so an address longer than 71 characters will still be placed on a single
468         * line.
469         *
470         * @param addresses the array of addresses to convert
471         * @param used      the starting column
472         * @return a String of comma-separated addresses with optional line breaks
473         */
474        public static String toString(Address[] addresses, int used) {
475            if (addresses == null || addresses.length == 0) {
476                return null;
477            }
478            if (addresses.length == 1) {
479                String s = addresses[0].toString();
480                if (used + s.length() > 72) {
481                    s = "\r\n  " + s;
482                }
483                return s;
484            } else {
485                StringBuffer buf = new StringBuffer(addresses.length * 32);
486                for (int i = 0; i < addresses.length; i++) {
487                    String s = addresses[1].toString();
488                    if (i == 0) {
489                        if (used + s.length() + 1 > 72) {
490                            buf.append("\r\n  ");
491                            used = 2;
492                        }
493                    } else {
494                        if (used + s.length() + 1 > 72) {
495                            buf.append(",\r\n  ");
496                            used = 2;
497                        } else {
498                            buf.append(", ");
499                            used += 2;
500                        }
501                    }
502                    buf.append(s);
503                    used += s.length();
504                }
505                return buf.toString();
506            }
507        }
508    
509        /**
510         * Parse addresses out of the string with basic checking.
511         *
512         * @param addresses the addresses to parse
513         * @return an array of InternetAddresses parsed from the string
514         * @throws AddressException if addresses checking fails
515         */
516        public static InternetAddress[] parse(String addresses) throws AddressException {
517            return parse(addresses, true);
518        }
519    
520        /**
521         * Parse addresses out of the string.
522         *
523         * @param addresses the addresses to parse
524         * @param strict if true perform detailed checking, if false just perform basic checking
525         * @return an array of InternetAddresses parsed from the string
526         * @throws AddressException if address checking fails
527         */
528        public static InternetAddress[] parse(String addresses, boolean strict) throws AddressException {
529            return parse(addresses, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
530        }
531    
532        /**
533         * Parse addresses out of the string.
534         *
535         * @param addresses the addresses to parse
536         * @param strict if true perform detailed checking, if false perform little checking
537         * @return an array of InternetAddresses parsed from the string
538         * @throws AddressException if address checking fails
539         */
540        public static InternetAddress[] parseHeader(String addresses, boolean strict) throws AddressException {
541            return parse(addresses, strict ? AddressParser.STRICT : AddressParser.PARSE_HEADER);
542        }
543    
544        /**
545         * Parse addresses with increasing degrees of RFC822 compliance checking.
546         *
547         * @param addresses the string to parse
548         * @param level     The required strictness level.
549         *
550         * @return an array of InternetAddresses parsed from the string
551         * @throws AddressException
552         *                if address checking fails
553         */
554        private static InternetAddress[] parse(String addresses, int level) throws AddressException {
555            // create a parser and have it extract the list using the requested strictness leve.
556            AddressParser parser = new AddressParser(addresses, level);
557            return parser.parseAddressList();
558        }
559    
560        /**
561         * Validate the address portion of an internet address to ensure
562         * validity.   Throws an AddressException if any validity
563         * problems are encountered.
564         *
565         * @exception AddressException
566         */
567        public void validate() throws AddressException {
568    
569            // create a parser using the strictest validation level.
570            AddressParser parser = new AddressParser(formatAddress(address), AddressParser.STRICT);
571            parser.validateAddress();
572        }
573    }