View Javadoc
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