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