View Javadoc

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