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 */ 019 020 package javax.mail.internet; 021 022 import java.text.FieldPosition; 023 import java.text.NumberFormat; 024 import java.text.ParseException; 025 import java.text.ParsePosition; 026 import java.text.SimpleDateFormat; 027 import java.util.Calendar; 028 import java.util.Date; 029 import java.util.GregorianCalendar; 030 import java.util.Locale; 031 import java.util.TimeZone; 032 033 /** 034 * Formats ths date as specified by 035 * draft-ietf-drums-msg-fmt-08 dated January 26, 2000 036 * which supercedes RFC822. 037 * <p/> 038 * <p/> 039 * The format used is <code>EEE, d MMM yyyy HH:mm:ss Z</code> and 040 * locale is always US-ASCII. 041 * 042 * @version $Rev: 920714 $ $Date: 2010-03-09 01:55:49 -0500 (Tue, 09 Mar 2010) $ 043 */ 044 public class MailDateFormat extends SimpleDateFormat { 045 046 private static final long serialVersionUID = -8148227605210628779L; 047 048 public MailDateFormat() { 049 super("EEE, d MMM yyyy HH:mm:ss Z (z)", Locale.US); 050 } 051 052 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition position) { 053 return super.format(date, buffer, position); 054 } 055 056 /** 057 * Parse a Mail date into a Date object. This uses fairly 058 * lenient rules for the format because the Mail standards 059 * for dates accept multiple formats. 060 * 061 * @param string The input string. 062 * @param position The position argument. 063 * 064 * @return The Date object with the information inside. 065 */ 066 public Date parse(String string, ParsePosition position) { 067 MailDateParser parser = new MailDateParser(string, position); 068 try { 069 return parser.parse(isLenient()); 070 } catch (ParseException e) { 071 e.printStackTrace(); 072 // just return a null for any parsing errors 073 return null; 074 } 075 } 076 077 /** 078 * The calendar cannot be set 079 * @param calendar 080 * @throws UnsupportedOperationException 081 */ 082 public void setCalendar(Calendar calendar) { 083 throw new UnsupportedOperationException(); 084 } 085 086 /** 087 * The format cannot be set 088 * @param format 089 * @throws UnsupportedOperationException 090 */ 091 public void setNumberFormat(NumberFormat format) { 092 throw new UnsupportedOperationException(); 093 } 094 095 096 // utility class for handling date parsing issues 097 class MailDateParser { 098 // our list of defined whitespace characters 099 static final String whitespace = " \t\r\n"; 100 101 // current parsing position 102 int current; 103 // our end parsing position 104 int endOffset; 105 // the date source string 106 String source; 107 // The parsing position. We update this as we move along and 108 // also for any parsing errors 109 ParsePosition pos; 110 111 public MailDateParser(String source, ParsePosition pos) 112 { 113 this.source = source; 114 this.pos = pos; 115 // we start using the providing parsing index. 116 this.current = pos.getIndex(); 117 this.endOffset = source.length(); 118 } 119 120 /** 121 * Parse the timestamp, returning a date object. 122 * 123 * @param lenient The lenient setting from the Formatter object. 124 * 125 * @return A Date object based off of parsing the date string. 126 * @exception ParseException 127 */ 128 public Date parse(boolean lenient) throws ParseException { 129 // we just skip over any next date format, which means scanning ahead until we 130 // find the first numeric character 131 locateNumeric(); 132 // the day can be either 1 or two digits 133 int day = parseNumber(1, 2); 134 // step over the delimiter 135 skipDateDelimiter(); 136 // parse off the month (which is in character format) 137 int month = parseMonth(); 138 // step over the delimiter 139 skipDateDelimiter(); 140 // now pull of the year, which can be either 2-digit or 4-digit 141 int year = parseYear(); 142 // white space is required here 143 skipRequiredWhiteSpace(); 144 // accept a 1 or 2 digit hour 145 int hour = parseNumber(1, 2); 146 skipRequiredChar(':'); 147 // the minutes must be two digit 148 int minutes = parseNumber(2, 2); 149 150 // the seconds are optional, but the ":" tells us if they are to 151 // be expected. 152 int seconds = 0; 153 if (skipOptionalChar(':')) { 154 seconds = parseNumber(2, 2); 155 } 156 // skip over the white space 157 skipWhiteSpace(); 158 // and finally the timezone information 159 int offset = parseTimeZone(); 160 161 // set the index of how far we've parsed this 162 pos.setIndex(current); 163 164 // create a calendar for creating the date 165 Calendar greg = new GregorianCalendar(TimeZone.getTimeZone("GMT")); 166 // we inherit the leniency rules 167 greg.setLenient(lenient); 168 greg.set(year, month, day, hour, minutes, seconds); 169 // now adjust by the offset. This seems a little strange, but we 170 // need to negate the offset because this is a UTC calendar, so we need to 171 // apply the reverse adjustment. for example, for the EST timezone, the offset 172 // value will be -300 (5 hours). If the time was 15:00:00, the UTC adjusted time 173 // needs to be 20:00:00, so we subract -300 minutes. 174 greg.add(Calendar.MINUTE, -offset); 175 // now return this timestamp. 176 return greg.getTime(); 177 } 178 179 180 /** 181 * Skip over a position where there's a required value 182 * expected. 183 * 184 * @param ch The required character. 185 * 186 * @exception ParseException 187 */ 188 private void skipRequiredChar(char ch) throws ParseException { 189 if (current >= endOffset) { 190 parseError("Delimiter '" + ch + "' expected"); 191 } 192 if (source.charAt(current) != ch) { 193 parseError("Delimiter '" + ch + "' expected"); 194 } 195 current++; 196 } 197 198 199 /** 200 * Skip over a position where iff the position matches the 201 * character 202 * 203 * @param ch The required character. 204 * 205 * @return true if the character was there, false otherwise. 206 * @exception ParseException 207 */ 208 private boolean skipOptionalChar(char ch) { 209 if (current >= endOffset) { 210 return false; 211 } 212 if (source.charAt(current) != ch) { 213 return false; 214 } 215 current++; 216 return true; 217 } 218 219 220 /** 221 * Skip over any white space characters until we find 222 * the next real bit of information. Will scan completely to the 223 * end, if necessary. 224 */ 225 private void skipWhiteSpace() { 226 while (current < endOffset) { 227 // if this is not in the white space list, then success. 228 if (whitespace.indexOf(source.charAt(current)) < 0) { 229 return; 230 } 231 current++; 232 } 233 234 // everything used up, just return 235 } 236 237 238 /** 239 * Skip over any non-white space characters until we find 240 * either a whitespace char or the end of the data. 241 */ 242 private void skipNonWhiteSpace() { 243 while (current < endOffset) { 244 // if this is not in the white space list, then success. 245 if (whitespace.indexOf(source.charAt(current)) >= 0) { 246 return; 247 } 248 current++; 249 } 250 251 // everything used up, just return 252 } 253 254 255 /** 256 * Skip over any white space characters until we find 257 * the next real bit of information. Will scan completely to the 258 * end, if necessary. 259 */ 260 private void skipRequiredWhiteSpace() throws ParseException { 261 int start = current; 262 263 while (current < endOffset) { 264 // if this is not in the white space list, then success. 265 if (whitespace.indexOf(source.charAt(current)) < 0) { 266 // we must have at least one white space character 267 if (start == current) { 268 parseError("White space character expected"); 269 } 270 return; 271 } 272 current++; 273 } 274 // everything used up, just return, but make sure we had at least one 275 // white space 276 if (start == current) { 277 parseError("White space character expected"); 278 } 279 } 280 281 private void parseError(String message) throws ParseException { 282 // we've got an error, set the index to the end. 283 pos.setErrorIndex(current); 284 throw new ParseException(message, current); 285 } 286 287 288 /** 289 * Locate an expected numeric field. 290 * 291 * @exception ParseException 292 */ 293 private void locateNumeric() throws ParseException { 294 while (current < endOffset) { 295 // found a digit? we're done 296 if (Character.isDigit(source.charAt(current))) { 297 return; 298 } 299 current++; 300 } 301 // we've got an error, set the index to the end. 302 parseError("Number field expected"); 303 } 304 305 306 /** 307 * Parse out an expected numeric field. 308 * 309 * @param minDigits The minimum number of digits we expect in this filed. 310 * @param maxDigits The maximum number of digits expected. Parsing will 311 * stop at the first non-digit character. An exception will 312 * be thrown if the field contained more than maxDigits 313 * in it. 314 * 315 * @return The parsed numeric value. 316 * @exception ParseException 317 */ 318 private int parseNumber(int minDigits, int maxDigits) throws ParseException { 319 int start = current; 320 int accumulator = 0; 321 while (current < endOffset) { 322 char ch = source.charAt(current); 323 // if this is not a digit character, then quit 324 if (!Character.isDigit(ch)) { 325 break; 326 } 327 // add the digit value into the accumulator 328 accumulator = accumulator * 10 + Character.digit(ch, 10); 329 current++; 330 } 331 332 int fieldLength = current - start; 333 if (fieldLength < minDigits || fieldLength > maxDigits) { 334 parseError("Invalid number field"); 335 } 336 337 return accumulator; 338 } 339 340 /** 341 * Skip a delimiter between the date portions of the 342 * string. The IMAP internal date format uses "-", so 343 * we either accept a single "-" or any number of white 344 * space characters (at least one required). 345 * 346 * @exception ParseException 347 */ 348 private void skipDateDelimiter() throws ParseException { 349 if (current >= endOffset) { 350 parseError("Invalid date field delimiter"); 351 } 352 353 if (source.charAt(current) == '-') { 354 current++; 355 } 356 else { 357 // must be at least a single whitespace character 358 skipRequiredWhiteSpace(); 359 } 360 } 361 362 363 /** 364 * Parse a character month name into the date month 365 * offset. 366 * 367 * @return 368 * @exception ParseException 369 */ 370 private int parseMonth() throws ParseException { 371 if ((endOffset - current) < 3) { 372 parseError("Invalid month"); 373 } 374 375 int monthOffset = 0; 376 String month = source.substring(current, current + 3).toLowerCase(); 377 378 if (month.equals("jan")) { 379 monthOffset = 0; 380 } 381 else if (month.equals("feb")) { 382 monthOffset = 1; 383 } 384 else if (month.equals("mar")) { 385 monthOffset = 2; 386 } 387 else if (month.equals("apr")) { 388 monthOffset = 3; 389 } 390 else if (month.equals("may")) { 391 monthOffset = 4; 392 } 393 else if (month.equals("jun")) { 394 monthOffset = 5; 395 } 396 else if (month.equals("jul")) { 397 monthOffset = 6; 398 } 399 else if (month.equals("aug")) { 400 monthOffset = 7; 401 } 402 else if (month.equals("sep")) { 403 monthOffset = 8; 404 } 405 else if (month.equals("oct")) { 406 monthOffset = 9; 407 } 408 else if (month.equals("nov")) { 409 monthOffset = 10; 410 } 411 else if (month.equals("dec")) { 412 monthOffset = 11; 413 } 414 else { 415 parseError("Invalid month"); 416 } 417 418 // ok, this is valid. Update the position and return it 419 current += 3; 420 return monthOffset; 421 } 422 423 /** 424 * Parse off a year field that might be expressed as 425 * either 2 or 4 digits. 426 * 427 * @return The numeric value of the year. 428 * @exception ParseException 429 */ 430 private int parseYear() throws ParseException { 431 // the year is between 2 to 4 digits 432 int year = parseNumber(2, 4); 433 434 // the two digit years get some sort of adjustment attempted. 435 if (year < 50) { 436 year += 2000; 437 } 438 else if (year < 100) { 439 year += 1990; 440 } 441 return year; 442 } 443 444 445 /** 446 * Parse all of the different timezone options. 447 * 448 * @return The timezone offset. 449 * @exception ParseException 450 */ 451 private int parseTimeZone() throws ParseException { 452 if (current >= endOffset) { 453 parseError("Missing time zone"); 454 } 455 456 // get the first non-blank. If this is a sign character, this 457 // is a zone offset. 458 char sign = source.charAt(current); 459 460 if (sign == '-' || sign == '+') { 461 // need to step over the sign character 462 current++; 463 // a numeric timezone is always a 4 digit number, but 464 // expressed as minutes/seconds. I'm too lazy to write a 465 // different parser that will bound on just a couple of characters, so 466 // we'll grab this as a single value and adjust 467 int zoneInfo = parseNumber(4, 4); 468 469 int offset = (zoneInfo / 100) * 60 + (zoneInfo % 100); 470 // negate this, if we have a negativeo offset 471 if (sign == '-') { 472 offset = -offset; 473 } 474 return offset; 475 } 476 else { 477 // need to parse this out using the obsolete zone names. This will be 478 // either a 3-character code (defined set), or a single character military 479 // zone designation. 480 int start = current; 481 skipNonWhiteSpace(); 482 String name = source.substring(start, current).toUpperCase(); 483 484 if (name.length() == 1) { 485 return militaryZoneOffset(name); 486 } 487 else if (name.length() <= 3) { 488 return namedZoneOffset(name); 489 } 490 else { 491 parseError("Invalid time zone"); 492 } 493 return 0; 494 } 495 } 496 497 498 /** 499 * Parse the obsolete mail timezone specifiers. The 500 * allowed set of timezones are terribly US centric. 501 * That's the spec. The preferred timezone form is 502 * the +/-mmss form. 503 * 504 * @param name The input name. 505 * 506 * @return The standard timezone offset for the specifier. 507 * @exception ParseException 508 */ 509 private int namedZoneOffset(String name) throws ParseException { 510 511 // NOTE: This is "UT", NOT "UTC" 512 if (name.equals("UT")) { 513 return 0; 514 } 515 else if (name.equals("GMT")) { 516 return 0; 517 } 518 else if (name.equals("EST")) { 519 return -300; 520 } 521 else if (name.equals("EDT")) { 522 return -240; 523 } 524 else if (name.equals("CST")) { 525 return -360; 526 } 527 else if (name.equals("CDT")) { 528 return -300; 529 } 530 else if (name.equals("MST")) { 531 return -420; 532 } 533 else if (name.equals("MDT")) { 534 return -360; 535 } 536 else if (name.equals("PST")) { 537 return -480; 538 } 539 else if (name.equals("PDT")) { 540 return -420; 541 } 542 else { 543 parseError("Invalid time zone"); 544 return 0; 545 } 546 } 547 548 549 /** 550 * Parse a single-character military timezone. 551 * 552 * @param name The one-character name. 553 * 554 * @return The offset corresponding to the military designation. 555 */ 556 private int militaryZoneOffset(String name) throws ParseException { 557 switch (Character.toUpperCase(name.charAt(0))) { 558 case 'A': 559 return 60; 560 case 'B': 561 return 120; 562 case 'C': 563 return 180; 564 case 'D': 565 return 240; 566 case 'E': 567 return 300; 568 case 'F': 569 return 360; 570 case 'G': 571 return 420; 572 case 'H': 573 return 480; 574 case 'I': 575 return 540; 576 case 'K': 577 return 600; 578 case 'L': 579 return 660; 580 case 'M': 581 return 720; 582 case 'N': 583 return -60; 584 case 'O': 585 return -120; 586 case 'P': 587 return -180; 588 case 'Q': 589 return -240; 590 case 'R': 591 return -300; 592 case 'S': 593 return -360; 594 case 'T': 595 return -420; 596 case 'U': 597 return -480; 598 case 'V': 599 return -540; 600 case 'W': 601 return -600; 602 case 'X': 603 return -660; 604 case 'Y': 605 return -720; 606 case 'Z': 607 return 0; 608 default: 609 parseError("Invalid time zone"); 610 return 0; 611 } 612 } 613 } 614 }