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
64
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
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
102
103 stripSourceRoute(address);
104
105 StringBuffer userSB = new StringBuffer();
106 StringBuffer hostSB = new StringBuffer();
107
108
109
110 try {
111
112
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
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
132
133
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
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
265
266 while (true) {
267 if (address.charAt(pos) == '\"') {
268 resultSB.append('\"');
269
270 pos++;
271 break;
272 }
273 if (address.charAt(pos) == '//') {
274 resultSB.append('//');
275 pos++;
276
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
285
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
300 boolean lastCharDot = false;
301 while (true) {
302
303
304 if (address.charAt(pos) == '//') {
305 resultSB.append('//');
306 pos++;
307
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
321 break;
322 } else {
323
324
325
326
327
328
329
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
352
353 StringBuffer resultSB = new StringBuffer();
354
355 while (true) {
356 if (pos >= address.length()) {
357 break;
358 }
359
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
375 while(address.indexOf("//")>-1){
376 address= address.substring(0,address.indexOf("//")) + address.substring(address.indexOf("//")+1);
377 }
378 StringBuffer resultSB = new StringBuffer();
379
380
381 resultSB.append(address.charAt(pos));
382 pos++;
383
384
385 for (int octet = 0; octet < 4; octet++) {
386
387
388
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
439
440
441
442
443
444
445
446
447
448
449
450
451
452
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 }