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    }