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