/projects/james-2.2.0/src/java/org/apache/mailet/MailAddress.java
Java | 448 lines | 294 code | 24 blank | 130 comment | 73 complexity | 5abd259c0ae12295a0c39cc788863d5e MD5 | raw file
1/***********************************************************************
2 * Copyright (c) 2000-2004 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
18package org.apache.mailet;
19
20import java.util.Locale;
21import javax.mail.internet.InternetAddress;
22import 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 */
60public 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 * <p>Construct a MailAddress parsing the provided <code>String</code> object.</p>
75 *
76 * <p>The <code>personal</code> variable is left empty.</p>
77 *
78 * @param address the email address compliant to the RFC822 format
79 * @throws ParseException if the parse failed
80 */
81 public MailAddress(String address) throws ParseException {
82 address = address.trim();
83 StringBuffer userSB = new StringBuffer();
84 StringBuffer hostSB = new StringBuffer();
85 //Begin parsing
86 //<mailbox> ::= <local-part> "@" <domain>
87
88 try {
89 //parse local-part
90 //<local-part> ::= <dot-string> | <quoted-string>
91 if (address.charAt(pos) == '\"') {
92 userSB.append(parseQuotedLocalPart(address));
93 } else {
94 userSB.append(parseUnquotedLocalPart(address));
95 }
96 if (userSB.toString().length() == 0) {
97 throw new ParseException("No local-part (user account) found at position " + (pos + 1));
98 }
99
100 //find @
101 if (pos >= address.length() || address.charAt(pos) != '@') {
102 throw new ParseException("Did not find @ between local-part and domain at position " + (pos + 1));
103 }
104 pos++;
105
106 //parse domain
107 //<domain> ::= <element> | <element> "." <domain>
108 //<element> ::= <name> | "#" <number> | "[" <dotnum> "]"
109 while (true) {
110 if (address.charAt(pos) == '#') {
111 hostSB.append(parseNumber(address));
112 } else if (address.charAt(pos) == '[') {
113 hostSB.append(parseDotNum(address));
114 } else {
115 hostSB.append(parseDomainName(address));
116 }
117 if (pos >= address.length()) {
118 break;
119 }
120 if (address.charAt(pos) == '.') {
121 hostSB.append('.');
122 pos++;
123 continue;
124 }
125 break;
126 }
127
128 if (hostSB.toString().length() == 0) {
129 throw new ParseException("No domain found at position " + (pos + 1));
130 }
131 } catch (IndexOutOfBoundsException ioobe) {
132 throw new ParseException("Out of data at position " + (pos + 1));
133 }
134
135 user = userSB.toString();
136 host = hostSB.toString();
137 }
138
139 /**
140 * Construct a MailAddress with the provided personal name and email
141 * address.
142 *
143 * @param user the username or account name on the mail server
144 * @param host the server that should accept messages for this user
145 * @throws ParseException if the parse failed
146 */
147 public MailAddress(String newUser, String newHost) throws ParseException {
148 /* NEEDS TO BE REWORKED TO VALIDATE EACH CHAR */
149 user = newUser;
150 host = newHost;
151 }
152
153 /**
154 * Constructs a MailAddress from a JavaMail InternetAddress, using only the
155 * email address portion, discarding the personal name.
156 */
157 public MailAddress(InternetAddress address) throws ParseException {
158 this(address.getAddress());
159 }
160
161 /**
162 * Return the host part.
163 *
164 * @return a <code>String</code> object representing the host part
165 * of this email address. If the host is of the dotNum form
166 * (e.g. [yyy.yyy.yyy.yyy]) then strip the braces first.
167 */
168 public String getHost() {
169 if (!(host.startsWith("[") && host.endsWith("]"))) {
170 return host;
171 } else {
172 return host.substring(1, host.length() -1);
173 }
174 }
175
176 /**
177 * Return the user part.
178 *
179 * @return a <code>String</code> object representing the user part
180 * of this email address.
181 * @throws AddressException if the parse failed
182 */
183 public String getUser() {
184 return user;
185 }
186
187 public String toString() {
188 StringBuffer addressBuffer =
189 new StringBuffer(128)
190 .append(user)
191 .append("@")
192 .append(host);
193 return addressBuffer.toString();
194 }
195
196 public InternetAddress toInternetAddress() {
197 try {
198 return new InternetAddress(toString());
199 } catch (javax.mail.internet.AddressException ae) {
200 //impossible really
201 return null;
202 }
203 }
204
205 public boolean equals(Object obj) {
206 if (obj == null) {
207 return false;
208 } else if (obj instanceof String) {
209 String theString = (String)obj;
210 return toString().equalsIgnoreCase(theString);
211 } else if (obj instanceof MailAddress) {
212 MailAddress addr = (MailAddress)obj;
213 return getUser().equalsIgnoreCase(addr.getUser()) && getHost().equalsIgnoreCase(addr.getHost());
214 }
215 return false;
216 }
217
218 /**
219 * Return a hashCode for this object which should be identical for addresses
220 * which are equivalent. This is implemented by obtaining the default
221 * hashcode of the String representation of the MailAddress. Without this
222 * explicit definition, the default hashCode will create different hashcodes
223 * for separate object instances.
224 *
225 * @return the hashcode.
226 */
227 public int hashCode() {
228 return toString().toLowerCase(Locale.US).hashCode();
229 }
230
231 private String parseQuotedLocalPart(String address) throws ParseException {
232 StringBuffer resultSB = new StringBuffer();
233 resultSB.append('\"');
234 pos++;
235 //<quoted-string> ::= """ <qtext> """
236 //<qtext> ::= "\" <x> | "\" <x> <qtext> | <q> | <q> <qtext>
237 while (true) {
238 if (address.charAt(pos) == '\"') {
239 resultSB.append('\"');
240 //end of quoted string... move forward
241 pos++;
242 break;
243 }
244 if (address.charAt(pos) == '\\') {
245 resultSB.append('\\');
246 pos++;
247 //<x> ::= any one of the 128 ASCII characters (no exceptions)
248 char x = address.charAt(pos);
249 if (x < 0 || x > 127) {
250 throw new ParseException("Invalid \\ syntaxed character at position " + (pos + 1));
251 }
252 resultSB.append(x);
253 pos++;
254 } else {
255 //<q> ::= any one of the 128 ASCII characters except <CR>,
256 //<LF>, quote ("), or backslash (\)
257 char q = address.charAt(pos);
258 if (q <= 0 || q == '\n' || q == '\r' || q == '\"' || q == '\\') {
259 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));
260 }
261 resultSB.append(q);
262 pos++;
263 }
264 }
265 return resultSB.toString();
266 }
267
268 private String parseUnquotedLocalPart(String address) throws ParseException {
269 StringBuffer resultSB = new StringBuffer();
270 //<dot-string> ::= <string> | <string> "." <dot-string>
271 boolean lastCharDot = false;
272 while (true) {
273 //<string> ::= <char> | <char> <string>
274 //<char> ::= <c> | "\" <x>
275 if (address.charAt(pos) == '\\') {
276 resultSB.append('\\');
277 pos++;
278 //<x> ::= any one of the 128 ASCII characters (no exceptions)
279 char x = address.charAt(pos);
280 if (x < 0 || x > 127) {
281 throw new ParseException("Invalid \\ syntaxed character at position " + (pos + 1));
282 }
283 resultSB.append(x);
284 pos++;
285 lastCharDot = false;
286 } else if (address.charAt(pos) == '.') {
287 resultSB.append('.');
288 pos++;
289 lastCharDot = true;
290 } else if (address.charAt(pos) == '@') {
291 //End of local-part
292 break;
293 } else {
294 //<c> ::= any one of the 128 ASCII characters, but not any
295 // <special> or <SP>
296 //<special> ::= "<" | ">" | "(" | ")" | "[" | "]" | "\" | "."
297 // | "," | ";" | ":" | "@" """ | the control
298 // characters (ASCII codes 0 through 31 inclusive and
299 // 127)
300 //<SP> ::= the space character (ASCII code 32)
301 char c = address.charAt(pos);
302 if (c <= 31 || c >= 127 || c == ' ') {
303 throw new ParseException("Invalid character in local-part (user account) at position " + (pos + 1));
304 }
305 for (int i = 0; i < SPECIAL.length; i++) {
306 if (c == SPECIAL[i]) {
307 throw new ParseException("Invalid character in local-part (user account) at position " + (pos + 1));
308 }
309 }
310 resultSB.append(c);
311 pos++;
312 lastCharDot = false;
313 }
314 }
315 if (lastCharDot) {
316 throw new ParseException("local-part (user account) ended with a \".\", which is invalid.");
317 }
318 return resultSB.toString();
319 }
320
321 private String parseNumber(String address) throws ParseException {
322 //<number> ::= <d> | <d> <number>
323
324 StringBuffer resultSB = new StringBuffer();
325 //We keep the position from the class level pos field
326 while (true) {
327 if (pos >= address.length()) {
328 break;
329 }
330 //<d> ::= any one of the ten digits 0 through 9
331 char d = address.charAt(pos);
332 if (d == '.') {
333 break;
334 }
335 if (d < '0' || d > '9') {
336 throw new ParseException("In domain, did not find a number in # address at position " + (pos + 1));
337 }
338 resultSB.append(d);
339 pos++;
340 }
341 return resultSB.toString();
342 }
343
344 private String parseDotNum(String address) throws ParseException {
345 //throw away all irrelevant '\' they're not necessary for escaping of '.' or digits, and are illegal as part of the domain-literal
346 while(address.indexOf("\\")>-1){
347 address= address.substring(0,address.indexOf("\\")) + address.substring(address.indexOf("\\")+1);
348 }
349 StringBuffer resultSB = new StringBuffer();
350 //we were passed the string with pos pointing the the [ char.
351 // take the first char ([), put it in the result buffer and increment pos
352 resultSB.append(address.charAt(pos));
353 pos++;
354
355 //<dotnum> ::= <snum> "." <snum> "." <snum> "." <snum>
356 for (int octet = 0; octet < 4; octet++) {
357 //<snum> ::= one, two, or three digits representing a decimal
358 // integer value in the range 0 through 255
359 //<d> ::= any one of the ten digits 0 through 9
360 StringBuffer snumSB = new StringBuffer();
361 for (int digits = 0; digits < 3; digits++) {
362 char d = address.charAt(pos);
363 if (d == '.') {
364 break;
365 }
366 if (d == ']') {
367 break;
368 }
369 if (d < '0' || d > '9') {
370 throw new ParseException("Invalid number at position " + (pos + 1));
371 }
372 snumSB.append(d);
373 pos++;
374 }
375 if (snumSB.toString().length() == 0) {
376 throw new ParseException("Number not found at position " + (pos + 1));
377 }
378 try {
379 int snum = Integer.parseInt(snumSB.toString());
380 if (snum > 255) {
381 throw new ParseException("Invalid number at position " + (pos + 1));
382 }
383 } catch (NumberFormatException nfe) {
384 throw new ParseException("Invalid number at position " + (pos + 1));
385 }
386 resultSB.append(snumSB.toString());
387 if (address.charAt(pos) == ']') {
388 if (octet < 3) {
389 throw new ParseException("End of number reached too quickly at " + (pos + 1));
390 } else {
391 break;
392 }
393 }
394 if (address.charAt(pos) == '.') {
395 resultSB.append('.');
396 pos++;
397 }
398 }
399 if (address.charAt(pos) != ']') {
400 throw new ParseException("Did not find closing bracket \"]\" in domain at position " + (pos + 1));
401 }
402 resultSB.append(']');
403 pos++;
404 return resultSB.toString();
405 }
406
407 private String parseDomainName(String address) throws ParseException {
408 StringBuffer resultSB = new StringBuffer();
409 //<name> ::= <a> <ldh-str> <let-dig>
410 //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
411 //<let-dig> ::= <a> | <d>
412 //<let-dig-hyp> ::= <a> | <d> | "-"
413 //<a> ::= any one of the 52 alphabetic characters A through Z
414 // in upper case and a through z in lower case
415 //<d> ::= any one of the ten digits 0 through 9
416
417 // basically, this is a series of letters, digits, and hyphens,
418 // but it can't start with a digit or hypthen
419 // and can't end with a hyphen
420
421 // in practice though, we should relax this as domain names can start
422 // with digits as well as letters. So only check that doesn't start
423 // or end with hyphen.
424 while (true) {
425 if (pos >= address.length()) {
426 break;
427 }
428 char ch = address.charAt(pos);
429 if ((ch >= '0' && ch <= '9') ||
430 (ch >= 'a' && ch <= 'z') ||
431 (ch >= 'A' && ch <= 'Z') ||
432 (ch == '-')) {
433 resultSB.append(ch);
434 pos++;
435 continue;
436 }
437 if (ch == '.') {
438 break;
439 }
440 throw new ParseException("Invalid character at " + pos);
441 }
442 String result = resultSB.toString();
443 if (result.startsWith("-") || result.endsWith("-")) {
444 throw new ParseException("Domain name cannot begin or end with a hyphen \"-\" at position " + (pos + 1));
445 }
446 return result;
447 }
448}