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 }