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
62
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
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
100
101 stripSourceRoute(address);
102
103 StringBuffer userSB = new StringBuffer();
104 StringBuffer hostSB = new StringBuffer();
105
106
107
108 try {
109
110
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
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
130
131
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
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
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
259
260 while (true) {
261 if (address.charAt(pos) == '\"') {
262 resultSB.append('\"');
263
264 pos++;
265 break;
266 }
267 if (address.charAt(pos) == '//') {
268 resultSB.append('//');
269 pos++;
270
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
279
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
294 boolean lastCharDot = false;
295 while (true) {
296
297
298 if (address.charAt(pos) == '//') {
299 resultSB.append('//');
300 pos++;
301
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
315 break;
316 } else {
317
318
319
320
321
322
323
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
346
347 StringBuffer resultSB = new StringBuffer();
348
349 while (true) {
350 if (pos >= address.length()) {
351 break;
352 }
353
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
369 while(address.indexOf("//")>-1){
370 address= address.substring(0,address.indexOf("//")) + address.substring(address.indexOf("//")+1);
371 }
372 StringBuffer resultSB = new StringBuffer();
373
374
375 resultSB.append(address.charAt(pos));
376 pos++;
377
378
379 for (int octet = 0; octet < 4; octet++) {
380
381
382
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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
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 }