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    }