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