001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.mailet;
019:
020: import java.util.Locale;
021: import javax.mail.internet.InternetAddress;
022: import javax.mail.internet.ParseException;
023:
024: /**
025: * A representation of an email address.
026: * <p>This class encapsulates functionalities to access to different
027: * parts of an email address without dealing with its parsing.</p>
028: *
029: * <p>A MailAddress is an address specified in the MAIL FROM and
030: * RCPT TO commands in SMTP sessions. These are either passed by
031: * an external server to the mailet-compliant SMTP server, or they
032: * are created programmatically by the mailet-compliant server to
033: * send to another (external) SMTP server. Mailets and matchers
034: * use the MailAddress for the purpose of evaluating the sender
035: * and recipient(s) of a message.</p>
036: *
037: * <p>MailAddress parses an email address as defined in RFC 821
038: * (SMTP) p. 30 and 31 where addresses are defined in BNF convention.
039: * As the mailet API does not support the aged "SMTP-relayed mail"
040: * addressing protocol, this leaves all addresses to be a <mailbox>,
041: * as per the spec. The MailAddress's "user" is the <local-part> of
042: * the <mailbox> and "host" is the <domain> of the mailbox.</p>
043: *
044: * <p>This class is a good way to validate email addresses as there are
045: * some valid addresses which would fail with a simpler approach
046: * to parsing address. It also removes parsing burden from
047: * mailets and matchers that might not realize the flexibility of an
048: * SMTP address. For instance, "serge@home"@lokitech.com is a valid
049: * SMTP address (the quoted text serge@home is the user and
050: * lokitech.com is the host). This means all current parsing to date
051: * is incorrect as we just find the first @ and use that to separate
052: * user from host.</p>
053: *
054: * <p>This parses an address as per the BNF specification for <mailbox>
055: * from RFC 821 on page 30 and 31, section 4.1.2. COMMAND SYNTAX.
056: * http://www.freesoft.org/CIE/RFC/821/15.htm</p>
057: *
058: * @version 1.0
059: */
060: public class MailAddress implements java.io.Serializable {
061: //We hardcode the serialVersionUID so that from James 1.2 on,
062: // MailAddress will be deserializable (so your mail doesn't get lost)
063: public static final long serialVersionUID = 2779163542539434916L;
064:
065: private final static char[] SPECIAL = { '<', '>', '(', ')', '[',
066: ']', '\\', '.', ',', ';', ':', '@', '\"' };
067:
068: private String user = null;
069: private String host = null;
070: //Used for parsing
071: private int pos = 0;
072:
073: /**
074: * strip source routing, according to RFC-2821 it is an allowed approach to handle mails
075: * contaning RFC-821 source-route information
076: */
077: private void stripSourceRoute(String address) throws ParseException {
078: if (pos < address.length()) {
079: if (address.charAt(pos) == '@') {
080: int i = address.indexOf(':');
081: if (i != -1) {
082: pos = i + 1;
083: }
084: }
085: }
086: }
087:
088: /**
089: * <p>Construct a MailAddress parsing the provided <code>String</code> object.</p>
090: *
091: * <p>The <code>personal</code> variable is left empty.</p>
092: *
093: * @param address the email address compliant to the RFC822 format
094: * @throws ParseException if the parse failed
095: */
096: public MailAddress(String address) throws ParseException {
097: address = address.trim();
098:
099: // Test if mail address has source routing information (RFC-821) and get rid of it!!
100: //must be called first!! (or at least prior to updating pos)
101: stripSourceRoute(address);
102:
103: StringBuffer userSB = new StringBuffer();
104: StringBuffer hostSB = new StringBuffer();
105: //Begin parsing
106: //<mailbox> ::= <local-part> "@" <domain>
107:
108: try {
109: //parse local-part
110: //<local-part> ::= <dot-string> | <quoted-string>
111: if (address.charAt(pos) == '\"') {
112: userSB.append(parseQuotedLocalPart(address));
113: if (userSB.toString().length() == 2) {
114: throw new ParseException(
115: "No quoted local-part (user account) found at position "
116: + (pos + 2));
117: }
118: } else {
119: userSB.append(parseUnquotedLocalPart(address));
120: if (userSB.toString().length() == 0) {
121: throw new ParseException(
122: "No local-part (user account) found at position "
123: + (pos + 1));
124: }
125: }
126:
127: //find @
128: if (pos >= address.length() || address.charAt(pos) != '@') {
129: throw new ParseException(
130: "Did not find @ between local-part and domain at position "
131: + (pos + 1));
132: }
133: pos++;
134:
135: //parse domain
136: //<domain> ::= <element> | <element> "." <domain>
137: //<element> ::= <name> | "#" <number> | "[" <dotnum> "]"
138: while (true) {
139: if (address.charAt(pos) == '#') {
140: hostSB.append(parseNumber(address));
141: } else if (address.charAt(pos) == '[') {
142: hostSB.append(parseDotNum(address));
143: } else {
144: hostSB.append(parseDomainName(address));
145: }
146: if (pos >= address.length()) {
147: break;
148: }
149: if (address.charAt(pos) == '.') {
150: hostSB.append('.');
151: pos++;
152: continue;
153: }
154: break;
155: }
156:
157: if (hostSB.toString().length() == 0) {
158: throw new ParseException("No domain found at position "
159: + (pos + 1));
160: }
161: } catch (IndexOutOfBoundsException ioobe) {
162: throw new ParseException("Out of data at position "
163: + (pos + 1));
164: }
165:
166: user = userSB.toString();
167: host = hostSB.toString();
168: }
169:
170: /**
171: * Construct a MailAddress with the provided personal name and email
172: * address.
173: *
174: * @param user the username or account name on the mail server
175: * @param host the server that should accept messages for this user
176: * @throws ParseException if the parse failed
177: */
178: public MailAddress(String newUser, String newHost)
179: throws ParseException {
180: this (newUser + "@" + newHost);
181: }
182:
183: /**
184: * Constructs a MailAddress from a JavaMail InternetAddress, using only the
185: * email address portion, discarding the personal name.
186: */
187: public MailAddress(InternetAddress address) throws ParseException {
188: this (address.getAddress());
189: }
190:
191: /**
192: * Return the host part.
193: *
194: * @return a <code>String</code> object representing the host part
195: * of this email address. If the host is of the dotNum form
196: * (e.g. [yyy.yyy.yyy.yyy]) then strip the braces first.
197: */
198: public String getHost() {
199: if (!(host.startsWith("[") && host.endsWith("]"))) {
200: return host;
201: } else {
202: return host.substring(1, host.length() - 1);
203: }
204: }
205:
206: /**
207: * Return the user part.
208: *
209: * @return a <code>String</code> object representing the user part
210: * of this email address.
211: * @throws AddressException if the parse failed
212: */
213: public String getUser() {
214: return user;
215: }
216:
217: public String toString() {
218: StringBuffer addressBuffer = new StringBuffer(128).append(user)
219: .append("@").append(host);
220: return addressBuffer.toString();
221: }
222:
223: /**
224: * Return MailAddress as InternetAddress
225: *
226: * @return the address
227: */
228: public InternetAddress toInternetAddress() {
229: try {
230: return new InternetAddress(toString());
231: } catch (javax.mail.internet.AddressException ae) {
232: //impossible really
233: return null;
234: }
235: }
236:
237: public boolean equals(Object obj) {
238: if (obj == null) {
239: return false;
240: } else if (obj instanceof String) {
241: String theString = (String) obj;
242: return toString().equalsIgnoreCase(theString);
243: } else if (obj instanceof MailAddress) {
244: MailAddress addr = (MailAddress) obj;
245: return getUser().equalsIgnoreCase(addr.getUser())
246: && getHost().equalsIgnoreCase(addr.getHost());
247: }
248: return false;
249: }
250:
251: /**
252: * Return a hashCode for this object which should be identical for addresses
253: * which are equivalent. This is implemented by obtaining the default
254: * hashcode of the String representation of the MailAddress. Without this
255: * explicit definition, the default hashCode will create different hashcodes
256: * for separate object instances.
257: *
258: * @return the hashcode.
259: */
260: public int hashCode() {
261: return toString().toLowerCase(Locale.US).hashCode();
262: }
263:
264: private String parseQuotedLocalPart(String address)
265: throws ParseException {
266: StringBuffer resultSB = new StringBuffer();
267: resultSB.append('\"');
268: pos++;
269: //<quoted-string> ::= """ <qtext> """
270: //<qtext> ::= "\" <x> | "\" <x> <qtext> | <q> | <q> <qtext>
271: while (true) {
272: if (address.charAt(pos) == '\"') {
273: resultSB.append('\"');
274: //end of quoted string... move forward
275: pos++;
276: break;
277: }
278: if (address.charAt(pos) == '\\') {
279: resultSB.append('\\');
280: pos++;
281: //<x> ::= any one of the 128 ASCII characters (no exceptions)
282: char x = address.charAt(pos);
283: if (x < 0 || x > 127) {
284: throw new ParseException(
285: "Invalid \\ syntaxed character at position "
286: + (pos + 1));
287: }
288: resultSB.append(x);
289: pos++;
290: } else {
291: //<q> ::= any one of the 128 ASCII characters except <CR>,
292: //<LF>, quote ("), or backslash (\)
293: char q = address.charAt(pos);
294: if (q <= 0 || q == '\n' || q == '\r' || q == '\"'
295: || q == '\\') {
296: throw new ParseException(
297: "Unquoted local-part (user account) must be one of the 128 ASCI characters exception <CR>, <LF>, quote (\"), or backslash (\\) at position "
298: + (pos + 1));
299: }
300: resultSB.append(q);
301: pos++;
302: }
303: }
304: return resultSB.toString();
305: }
306:
307: private String parseUnquotedLocalPart(String address)
308: throws ParseException {
309: StringBuffer resultSB = new StringBuffer();
310: //<dot-string> ::= <string> | <string> "." <dot-string>
311: boolean lastCharDot = false;
312: while (true) {
313: //<string> ::= <char> | <char> <string>
314: //<char> ::= <c> | "\" <x>
315: if (address.charAt(pos) == '\\') {
316: resultSB.append('\\');
317: pos++;
318: //<x> ::= any one of the 128 ASCII characters (no exceptions)
319: char x = address.charAt(pos);
320: if (x < 0 || x > 127) {
321: throw new ParseException(
322: "Invalid \\ syntaxed character at position "
323: + (pos + 1));
324: }
325: resultSB.append(x);
326: pos++;
327: lastCharDot = false;
328: } else if (address.charAt(pos) == '.') {
329: resultSB.append('.');
330: pos++;
331: lastCharDot = true;
332: } else if (address.charAt(pos) == '@') {
333: //End of local-part
334: break;
335: } else {
336: //<c> ::= any one of the 128 ASCII characters, but not any
337: // <special> or <SP>
338: //<special> ::= "<" | ">" | "(" | ")" | "[" | "]" | "\" | "."
339: // | "," | ";" | ":" | "@" """ | the control
340: // characters (ASCII codes 0 through 31 inclusive and
341: // 127)
342: //<SP> ::= the space character (ASCII code 32)
343: char c = address.charAt(pos);
344: if (c <= 31 || c >= 127 || c == ' ') {
345: throw new ParseException(
346: "Invalid character in local-part (user account) at position "
347: + (pos + 1));
348: }
349: for (int i = 0; i < SPECIAL.length; i++) {
350: if (c == SPECIAL[i]) {
351: throw new ParseException(
352: "Invalid character in local-part (user account) at position "
353: + (pos + 1));
354: }
355: }
356: resultSB.append(c);
357: pos++;
358: lastCharDot = false;
359: }
360: }
361: if (lastCharDot) {
362: throw new ParseException(
363: "local-part (user account) ended with a \".\", which is invalid.");
364: }
365: return resultSB.toString();
366: }
367:
368: private String parseNumber(String address) throws ParseException {
369: //<number> ::= <d> | <d> <number>
370:
371: StringBuffer resultSB = new StringBuffer();
372: //We keep the position from the class level pos field
373: while (true) {
374: if (pos >= address.length()) {
375: break;
376: }
377: //<d> ::= any one of the ten digits 0 through 9
378: char d = address.charAt(pos);
379: if (d == '.') {
380: break;
381: }
382: if (d < '0' || d > '9') {
383: throw new ParseException(
384: "In domain, did not find a number in # address at position "
385: + (pos + 1));
386: }
387: resultSB.append(d);
388: pos++;
389: }
390: return resultSB.toString();
391: }
392:
393: private String parseDotNum(String address) throws ParseException {
394: //throw away all irrelevant '\' they're not necessary for escaping of '.' or digits, and are illegal as part of the domain-literal
395: while (address.indexOf("\\") > -1) {
396: address = address.substring(0, address.indexOf("\\"))
397: + address.substring(address.indexOf("\\") + 1);
398: }
399: StringBuffer resultSB = new StringBuffer();
400: //we were passed the string with pos pointing the the [ char.
401: // take the first char ([), put it in the result buffer and increment pos
402: resultSB.append(address.charAt(pos));
403: pos++;
404:
405: //<dotnum> ::= <snum> "." <snum> "." <snum> "." <snum>
406: for (int octet = 0; octet < 4; octet++) {
407: //<snum> ::= one, two, or three digits representing a decimal
408: // integer value in the range 0 through 255
409: //<d> ::= any one of the ten digits 0 through 9
410: StringBuffer snumSB = new StringBuffer();
411: for (int digits = 0; digits < 3; digits++) {
412: char d = address.charAt(pos);
413: if (d == '.') {
414: break;
415: }
416: if (d == ']') {
417: break;
418: }
419: if (d < '0' || d > '9') {
420: throw new ParseException(
421: "Invalid number at position " + (pos + 1));
422: }
423: snumSB.append(d);
424: pos++;
425: }
426: if (snumSB.toString().length() == 0) {
427: throw new ParseException(
428: "Number not found at position " + (pos + 1));
429: }
430: try {
431: int snum = Integer.parseInt(snumSB.toString());
432: if (snum > 255) {
433: throw new ParseException(
434: "Invalid number at position " + (pos + 1));
435: }
436: } catch (NumberFormatException nfe) {
437: throw new ParseException("Invalid number at position "
438: + (pos + 1));
439: }
440: resultSB.append(snumSB.toString());
441: if (address.charAt(pos) == ']') {
442: if (octet < 3) {
443: throw new ParseException(
444: "End of number reached too quickly at "
445: + (pos + 1));
446: } else {
447: break;
448: }
449: }
450: if (address.charAt(pos) == '.') {
451: resultSB.append('.');
452: pos++;
453: }
454: }
455: if (address.charAt(pos) != ']') {
456: throw new ParseException(
457: "Did not find closing bracket \"]\" in domain at position "
458: + (pos + 1));
459: }
460: resultSB.append(']');
461: pos++;
462: return resultSB.toString();
463: }
464:
465: private String parseDomainName(String address)
466: throws ParseException {
467: StringBuffer resultSB = new StringBuffer();
468: //<name> ::= <a> <ldh-str> <let-dig>
469: //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
470: //<let-dig> ::= <a> | <d>
471: //<let-dig-hyp> ::= <a> | <d> | "-"
472: //<a> ::= any one of the 52 alphabetic characters A through Z
473: // in upper case and a through z in lower case
474: //<d> ::= any one of the ten digits 0 through 9
475:
476: // basically, this is a series of letters, digits, and hyphens,
477: // but it can't start with a digit or hypthen
478: // and can't end with a hyphen
479:
480: // in practice though, we should relax this as domain names can start
481: // with digits as well as letters. So only check that doesn't start
482: // or end with hyphen.
483: while (true) {
484: if (pos >= address.length()) {
485: break;
486: }
487: char ch = address.charAt(pos);
488: if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z')
489: || (ch >= 'A' && ch <= 'Z') || (ch == '-')) {
490: resultSB.append(ch);
491: pos++;
492: continue;
493: }
494: if (ch == '.') {
495: break;
496: }
497: throw new ParseException("Invalid character at " + pos);
498: }
499: String result = resultSB.toString();
500: if (result.startsWith("-") || result.endsWith("-")) {
501: throw new ParseException(
502: "Domain name cannot begin or end with a hyphen \"-\" at position "
503: + (pos + 1));
504: }
505: return result;
506: }
507: }
|