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