1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20 package javax.mail.internet;
21
22 import java.text.FieldPosition;
23 import java.text.NumberFormat;
24 import java.text.ParseException;
25 import java.text.ParsePosition;
26 import java.text.SimpleDateFormat;
27 import java.util.Calendar;
28 import java.util.Date;
29 import java.util.GregorianCalendar;
30 import java.util.Locale;
31 import java.util.TimeZone;
32
33 /**
34 * Formats ths date as specified by
35 * draft-ietf-drums-msg-fmt-08 dated January 26, 2000
36 * which supercedes RFC822.
37 * <p/>
38 * <p/>
39 * The format used is <code>EEE, d MMM yyyy HH:mm:ss Z</code> and
40 * locale is always US-ASCII.
41 *
42 * @version $Rev: 628009 $ $Date: 2008-02-15 05:53:02 -0500 (Fri, 15 Feb 2008) $
43 */
44 public class MailDateFormat extends SimpleDateFormat {
45 public MailDateFormat() {
46 super("EEE, d MMM yyyy HH:mm:ss Z (z)", Locale.US);
47 }
48
49 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition position) {
50 return super.format(date, buffer, position);
51 }
52
53 /**
54 * Parse a Mail date into a Date object. This uses fairly
55 * lenient rules for the format because the Mail standards
56 * for dates accept multiple formats.
57 *
58 * @param string The input string.
59 * @param position The position argument.
60 *
61 * @return The Date object with the information inside.
62 */
63 public Date parse(String string, ParsePosition position) {
64 MailDateParser parser = new MailDateParser(string, position);
65 try {
66 return parser.parse(isLenient());
67 } catch (ParseException e) {
68 e.printStackTrace();
69 // just return a null for any parsing errors
70 return null;
71 }
72 }
73
74 /**
75 * The calendar cannot be set
76 * @param calendar
77 * @throws UnsupportedOperationException
78 */
79 public void setCalendar(Calendar calendar) {
80 throw new UnsupportedOperationException();
81 }
82
83 /**
84 * The format cannot be set
85 * @param format
86 * @throws UnsupportedOperationException
87 */
88 public void setNumberFormat(NumberFormat format) {
89 throw new UnsupportedOperationException();
90 }
91
92
93 // utility class for handling date parsing issues
94 class MailDateParser {
95 // our list of defined whitespace characters
96 static final String whitespace = " \t\r\n";
97
98 // current parsing position
99 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 }