View Javadoc

1   /****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  
21  package org.apache.mailet;
22  
23  import java.util.Locale;
24  import javax.mail.internet.AddressException;
25  import javax.mail.internet.InternetAddress;
26  
27  /**
28   * A representation of an email address.
29   * 
30   * <p>This class encapsulates functionality to access different
31   * parts of an email address without dealing with its parsing.</p>
32   *
33   * <p>A MailAddress is an address specified in the MAIL FROM and
34   * RCPT TO commands in SMTP sessions.  These are either passed by
35   * an external server to the mailet-compliant SMTP server, or they
36   * are created programmatically by the mailet-compliant server to
37   * send to another (external) SMTP server.  Mailets and matchers
38   * use the MailAddress for the purpose of evaluating the sender
39   * and recipient(s) of a message.</p>
40   *
41   * <p>MailAddress parses an email address as defined in RFC 821
42   * (SMTP) p. 30 and 31 where addresses are defined in BNF convention.
43   * As the mailet API does not support the aged "SMTP-relayed mail"
44   * addressing protocol, this leaves all addresses to be a {@code <mailbox>},
45   * as per the spec. 
46   *
47   * <p>This class is a good way to validate email addresses as there are
48   * some valid addresses which would fail with a simpler approach
49   * to parsing address. It also removes the parsing burden from
50   * mailets and matchers that might not realize the flexibility of an
51   * SMTP address. For instance, "serge@home"@lokitech.com is a valid
52   * SMTP address (the quoted text serge@home is the local-part and
53   * lokitech.com is the domain). This means all current parsing to date
54   * is incorrect as we just find the first '@' and use that to separate
55   * local-part from domain.</p>
56   *
57   * <p>This parses an address as per the BNF specification for <mailbox>
58   * from RFC 821 on page 30 and 31, section 4.1.2. COMMAND SYNTAX.
59   * http://www.freesoft.org/CIE/RFC/821/15.htm</p>
60   *
61   * @version 1.0
62   */
63  public class MailAddress implements java.io.Serializable {
64      /**
65       *  We hardcode the serialVersionUID 
66       *  This version (2779163542539434916L) retains compatibility back to
67       *  Mailet version 1.2 (James 1.2) so that MailAddress will be
68       *  deserializable and mail doesn't get lost after an upgrade.
69       */
70      public static final long serialVersionUID = 2779163542539434916L;
71  
72      private final static char[] SPECIAL =
73      {'<', '>', '(', ')', '[', ']', '\\', '.', ',', ';', ':', '@', '\"'};
74  
75      private String localPart = null;
76      private String domain = null;
77      //Used for parsing
78      private int pos = 0;
79  
80      /**
81       * Strips source routing. According to RFC-2821 it is a valid approach
82       * to handle mails containing RFC-821 source-route information.
83       * 
84       * @param address the address to strip
85       */
86      private void stripSourceRoute(String address) {
87          if (pos < address.length()) {
88              if (address.charAt(pos)=='@') { 
89                  int i = address.indexOf(':');
90                  if (i != -1) {
91                      pos = i+1;
92                  }
93              }
94          }
95      }
96      
97      /**
98       * Constructs a MailAddress by parsing the provided address.
99       *
100      * @param address the email address, compliant to the RFC2822 3.4.1. addr-spec specification
101      * @throws AddressException if the parse failed
102      */
103     public MailAddress(String address) throws AddressException {
104         address = address.trim();
105 
106         // Test if mail address has source routing information (RFC-821) and get rid of it!!
107         //must be called first!! (or at least prior to updating pos)
108         stripSourceRoute(address);
109 
110         StringBuffer localPartSB = new StringBuffer();
111         StringBuffer domainSB = new StringBuffer();
112         //Begin parsing
113         //<mailbox> ::= <local-part> "@" <domain>
114 
115         try {
116             //parse local-part
117             //<local-part> ::= <dot-string> | <quoted-string>
118             if (address.charAt(pos) == '\"') {
119                 localPartSB.append(parseQuotedLocalPart(address));
120                 if (localPartSB.toString().length() == 2) {
121                     throw new AddressException("No quoted local-part (user account) found at position " + (pos + 2) + " in '" + address + "'",address,pos+2);
122                 }
123             } else {
124                 localPartSB.append(parseUnquotedLocalPart(address));
125                 if (localPartSB.toString().length() == 0) {
126                     throw new AddressException("No local-part (user account) found at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
127                 }
128             }
129 
130             //find @
131             if (pos >= address.length() || address.charAt(pos) != '@') {
132                 throw new AddressException("Did not find @ between local-part and domain at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
133             }
134             pos++;
135 
136             //parse domain
137             //<domain> ::=  <element> | <element> "." <domain>
138             //<element> ::= <name> | "#" <number> | "[" <dotnum> "]"
139             while (true) {
140                 if (address.charAt(pos) == '#') {
141                     domainSB.append(parseNumber(address));
142                 } else if (address.charAt(pos) == '[') {
143                     domainSB.append(parseDomainLiteral(address));
144                 } else {
145                     domainSB.append(parseDomain(address));
146                 }
147                 if (pos >= address.length()) {
148                     break;
149                 }
150                 if (address.charAt(pos) == '.') {
151                     domainSB.append('.');
152                     pos++;
153                     continue;
154                 }
155                 break;
156             }
157 
158             if (domainSB.toString().length() == 0) {
159                 throw new AddressException("No domain found at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
160             }
161         } catch (IndexOutOfBoundsException ioobe) {
162             throw new AddressException("Out of data at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
163         }
164 
165         localPart = localPartSB.toString();
166         domain = domainSB.toString();
167     }
168 
169     /**
170      * Constructs a MailAddress with the provided local part and domain.
171      *
172      * @param localPart the local-part portion. This is a domain dependent string.
173      *        In addresses, it is simply interpreted on the particular host as a
174      *        name of a particular mailbox. per RFC2822 3.4.1. addr-spec specification
175      * @param domain the domain portion. This identifies the point to which the mail
176      *        is delivered  per RFC2822 3.4.1. addr-spec specification
177      * @throws AddressException if the parse failed
178      */
179     public MailAddress(String localPart, String domain) throws AddressException {
180         this(new InternetAddress(localPart+"@"+domain));
181     }
182 
183     /**
184      * Constructs a MailAddress from an InternetAddress, using only the
185      * email address portion (an "addr-spec", not "name-addr", as
186      * defined in the RFC2822 3.4. Address Specification)
187      * 
188      * @param address the address
189      * @throws AddressException if the parse failed
190      */
191     public MailAddress(InternetAddress address) throws AddressException {
192         this(address.getAddress());
193     }
194 
195     /**
196      * Returns the host part.
197      *
198      * @return the host part of this email address. If the host is of the
199      *         dotNum form (e.g. [yyy.yyy.yyy.yyy]), then strip the braces first.
200      * @deprecated use {@link #getDomain()}, whose name was changed to
201      *              align with RFC2822 3.4.1. addr-spec specification
202      */
203     public String getHost() {
204         return getDomain();
205     }
206     
207     /**
208      * Returns the domain part per RFC2822 3.4.1. addr-spec specification.
209      *
210      * @return the domain part of this email address. If the domain is of
211      * the domain-literal form  (e.g. [yyy.yyy.yyy.yyy]), the braces will
212      * have been stripped returning the raw IP address.
213      */
214     public String getDomain() {
215         if (!(domain.startsWith("[") && domain.endsWith("]"))) {
216             return domain;
217         } 
218         return domain.substring(1, domain.length() -1);
219     }
220 
221     /**
222      * Returns the user part.
223      *
224      * @return the user part of this email address
225      * @deprecated use {@link #getLocalPart()}, whose name was changed to
226      *             align with the RFC2822 3.4.1. addr-spec specification
227      */
228     public String getUser() {
229         return getLocalPart();
230     }
231     
232     /**
233      * Returns the local-part per RFC2822 3.4.1. addr-spec specification.
234      *
235      * @return  the local-part of this email address as defined by the
236      *          RFC2822 3.4.1. addr-spec specification. 
237      *          The local-part portion is a domain dependent string.
238      *          In addresses, it is simply interpreted on the particular
239      *          host as a name of a particular mailbox
240      *          (the part before the "@" character)
241      */
242     public String getLocalPart() {
243         return localPart;
244     }
245 
246     public String toString() {
247         StringBuffer addressBuffer =
248             new StringBuffer(128)
249                     .append(localPart)
250                     .append("@")
251                     .append(domain);
252         return addressBuffer.toString();
253     }
254     
255     /**
256      * Returns an InternetAddress representing the same address
257      * as this MailAddress.
258      * 
259      * @return the address
260      */
261     public InternetAddress toInternetAddress() {
262         try {
263             return new InternetAddress(toString());
264         } catch (javax.mail.internet.AddressException ae) {
265             //impossible really
266             return null;
267         }
268     }
269 
270     /**
271      * Indicates whether some other object is "equal to" this one.
272      * 
273      * Note that this implementation breaks the general contract of the
274      * <code>equals</code> method by allowing an instance to equal to a
275      * <code>String</code>. It is recommended that implementations avoid
276      * relying on this design which may be removed in a future release.
277      * 
278      * @returns true if the given object is equal to this one, false otherwise
279      */
280     public boolean equals(Object obj) {
281         if (obj == null) {
282             return false;
283         } else if (obj instanceof String) {
284             String theString = (String)obj;
285             return toString().equalsIgnoreCase(theString);
286         } else if (obj instanceof MailAddress) {
287             MailAddress addr = (MailAddress)obj;
288             return getLocalPart().equalsIgnoreCase(addr.getLocalPart()) && getDomain().equalsIgnoreCase(addr.getDomain());
289         }
290         return false;
291     }
292 
293     /**
294      * Returns a hash code value for this object.
295      * <p>
296      * This method is implemented by returning the hash code of the canonical
297      * string representation of this address, so that all instances representing
298      * the same address will return an identical hash code.
299      *
300      * @return the hashcode.
301      */
302     public int hashCode() {
303         return toString().toLowerCase(Locale.US).hashCode();
304     }
305 
306     private String parseQuotedLocalPart(String address) throws AddressException {
307         StringBuffer resultSB = new StringBuffer();
308         resultSB.append('\"');
309         pos++;
310         //<quoted-string> ::=  """ <qtext> """
311         //<qtext> ::=  "\" <x> | "\" <x> <qtext> | <q> | <q> <qtext>
312         while (true) {
313             if (address.charAt(pos) == '\"') {
314                 resultSB.append('\"');
315                 //end of quoted string... move forward
316                 pos++;
317                 break;
318             }
319             if (address.charAt(pos) == '\\') {
320                 resultSB.append('\\');
321                 pos++;
322                 //<x> ::= any one of the 128 ASCII characters (no exceptions)
323                 char x = address.charAt(pos);
324                 if (x < 0 || x > 127) {
325                     throw new AddressException("Invalid \\ syntaxed character at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
326                 }
327                 resultSB.append(x);
328                 pos++;
329             } else {
330                 //<q> ::= any one of the 128 ASCII characters except <CR>,
331                 //<LF>, quote ("), or backslash (\)
332                 char q = address.charAt(pos);
333                 if (q <= 0 || q == '\n' || q == '\r' || q == '\"' || q == '\\') {
334                     throw new AddressException("Unquoted local-part (user account) must be one of the 128 ASCI characters exception <CR>, <LF>, quote (\"), or backslash (\\) at position " + (pos + 1) + " in '" + address + "'");
335                 }
336                 resultSB.append(q);
337                 pos++;
338             }
339         }
340         return resultSB.toString();
341     }
342 
343     private String parseUnquotedLocalPart(String address) throws AddressException {
344         StringBuffer resultSB = new StringBuffer();
345         //<dot-string> ::= <string> | <string> "." <dot-string>
346         boolean lastCharDot = false;
347         while (true) {
348             //<string> ::= <char> | <char> <string>
349             //<char> ::= <c> | "\" <x>
350             if (address.charAt(pos) == '\\') {
351                 resultSB.append('\\');
352                 pos++;
353                 //<x> ::= any one of the 128 ASCII characters (no exceptions)
354                 char x = address.charAt(pos);
355                 if (x < 0 || x > 127) {
356                     throw new AddressException("Invalid \\ syntaxed character at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
357                 }
358                 resultSB.append(x);
359                 pos++;
360                 lastCharDot = false;
361             } else if (address.charAt(pos) == '.') {
362                 resultSB.append('.');
363                 pos++;
364                 lastCharDot = true;
365             } else if (address.charAt(pos) == '@') {
366                 //End of local-part
367                 break;
368             } else {
369                 //<c> ::= any one of the 128 ASCII characters, but not any
370                 //    <special> or <SP>
371                 //<special> ::= "<" | ">" | "(" | ")" | "[" | "]" | "\" | "."
372                 //    | "," | ";" | ":" | "@"  """ | the control
373                 //    characters (ASCII codes 0 through 31 inclusive and
374                 //    127)
375                 //<SP> ::= the space character (ASCII code 32)
376                 char c = address.charAt(pos);
377                 if (c <= 31 || c >= 127 || c == ' ') {
378                     throw new AddressException("Invalid character in local-part (user account) at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
379                 }
380                 for (int i = 0; i < SPECIAL.length; i++) {
381                     if (c == SPECIAL[i]) {
382                         throw new AddressException("Invalid character in local-part (user account) at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
383                     }
384                 }
385                 resultSB.append(c);
386                 pos++;
387                 lastCharDot = false;
388             }
389         }
390         if (lastCharDot) {
391             throw new AddressException("local-part (user account) ended with a \".\", which is invalid in address '" + address + "'",address,pos);
392         }
393         return resultSB.toString();
394     }
395 
396     private String parseNumber(String address) throws AddressException {
397         //<number> ::= <d> | <d> <number>
398 
399         StringBuffer resultSB = new StringBuffer();
400         //We keep the position from the class level pos field
401         while (true) {
402             if (pos >= address.length()) {
403                 break;
404             }
405             //<d> ::= any one of the ten digits 0 through 9
406             char d = address.charAt(pos);
407             if (d == '.') {
408                 break;
409             }
410             if (d < '0' || d > '9') {
411                 throw new AddressException("In domain, did not find a number in # address at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
412             }
413             resultSB.append(d);
414             pos++;
415         }
416         return resultSB.toString();
417     }
418 
419     private String parseDomainLiteral(String address) throws AddressException {
420         //throw away all irrelevant '\' they're not necessary for escaping of '.' or digits, and are illegal as part of the domain-literal
421         while(address.indexOf("\\")>-1){
422              address= address.substring(0,address.indexOf("\\")) + address.substring(address.indexOf("\\")+1);
423         }
424         StringBuffer resultSB = new StringBuffer();
425         //we were passed the string with pos pointing the the [ char.
426         // take the first char ([), put it in the result buffer and increment pos
427         resultSB.append(address.charAt(pos));
428         pos++;
429 
430         //<dotnum> ::= <snum> "." <snum> "." <snum> "." <snum>
431         for (int octet = 0; octet < 4; octet++) {
432             //<snum> ::= one, two, or three digits representing a decimal
433             //                      integer value in the range 0 through 255
434             //<d> ::= any one of the ten digits 0 through 9
435             StringBuffer snumSB = new StringBuffer();
436             for (int digits = 0; digits < 3; digits++) {
437                 char d = address.charAt(pos);
438                 if (d == '.') {
439                     break;
440                 }
441                 if (d == ']') {
442                     break;
443                 }
444                 if (d < '0' || d > '9') {
445                     throw new AddressException("Invalid number at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
446                 }
447                 snumSB.append(d);
448                 pos++;
449             }
450             if (snumSB.toString().length() == 0) {
451                 throw new AddressException("Number not found at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
452             }
453             try {
454                 int snum = Integer.parseInt(snumSB.toString());
455                 if (snum > 255) {
456                     throw new AddressException("Invalid number at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
457                 }
458             } catch (NumberFormatException nfe) {
459                 throw new AddressException("Invalid number at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
460             }
461             resultSB.append(snumSB.toString());
462             if (address.charAt(pos) == ']') {
463                 if (octet < 3) {
464                     throw new AddressException("End of number reached too quickly at " + (pos + 1) + " in '" + address + "'",address,pos+1);
465                 } 
466                 break;
467             }
468             if (address.charAt(pos) == '.') {
469                 resultSB.append('.');
470                 pos++;
471             }
472         }
473         if (address.charAt(pos) != ']') {
474             throw new AddressException("Did not find closing bracket \"]\" in domain at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
475         }
476         resultSB.append(']');
477         pos++;
478         return resultSB.toString();
479     }
480 
481     private String parseDomain(String address) throws AddressException {
482         StringBuffer resultSB = new StringBuffer();
483         //<name> ::= <a> <ldh-str> <let-dig>
484         //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
485         //<let-dig> ::= <a> | <d>
486         //<let-dig-hyp> ::= <a> | <d> | "-"
487         //<a> ::= any one of the 52 alphabetic characters A through Z
488         //  in upper case and a through z in lower case
489         //<d> ::= any one of the ten digits 0 through 9
490 
491         // basically, this is a series of letters, digits, and hyphens,
492         // but it can't start with a digit or hypthen
493         // and can't end with a hyphen
494 
495         // in practice though, we should relax this as domain names can start
496         // with digits as well as letters.  So only check that doesn't start
497         // or end with hyphen.
498         while (true) {
499             if (pos >= address.length()) {
500                 break;
501             }
502             char ch = address.charAt(pos);
503             if ((ch >= '0' && ch <= '9') ||
504                 (ch >= 'a' && ch <= 'z') ||
505                 (ch >= 'A' && ch <= 'Z') ||
506                 (ch == '-')) {
507                 resultSB.append(ch);
508                 pos++;
509                 continue;
510             }
511             if (ch == '.') {
512                 break;
513             }
514             throw new AddressException("Invalid character at " + pos + " in '" + address + "'",address,pos);
515         }
516         String result = resultSB.toString();
517         if (result.startsWith("-") || result.endsWith("-")) {
518             throw new AddressException("Domain name cannot begin or end with a hyphen \"-\" at position " + (pos + 1) + " in '" + address + "'",address,pos+1);
519         }
520         return result;
521     }
522 }