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 org.apache.james.mime4j.util;
021    
022    import java.text.DateFormat;
023    import java.text.FieldPosition;
024    import java.text.SimpleDateFormat;
025    import java.util.Date;
026    import java.util.GregorianCalendar;
027    import java.util.Locale;
028    import java.util.Random;
029    import java.util.TimeZone;
030    
031    /**
032     * A utility class, which provides some MIME related application logic.
033     */
034    public final class MimeUtil {
035    
036        /**
037         * The <code>quoted-printable</code> encoding.
038         */
039        public static final String ENC_QUOTED_PRINTABLE = "quoted-printable";
040        /**
041         * The <code>binary</code> encoding.
042         */
043        public static final String ENC_BINARY = "binary";
044        /**
045         * The <code>base64</code> encoding.
046         */
047        public static final String ENC_BASE64 = "base64";
048        /**
049         * The <code>8bit</code> encoding.
050         */
051        public static final String ENC_8BIT = "8bit";
052        /**
053         * The <code>7bit</code> encoding.
054         */
055        public static final String ENC_7BIT = "7bit";
056    
057        // used to create unique ids
058        private static final Random random = new Random();
059    
060        // used to create unique ids
061        private static int counter = 0;
062    
063        private MimeUtil() {
064            // this is an utility class to be used statically.
065            // this constructor protect from instantiation.
066        }
067    
068        /**
069         * Returns, whether the given two MIME types are identical.
070         */
071        public static boolean isSameMimeType(String pType1, String pType2) {
072            return pType1 != null  &&  pType2 != null  &&  pType1.equalsIgnoreCase(pType2);
073        }
074    
075        /**
076         * Returns true, if the given MIME type is that of a message.
077         */
078        public static boolean isMessage(String pMimeType) {
079            return pMimeType != null  &&  pMimeType.equalsIgnoreCase("message/rfc822");
080        }
081    
082        /**
083         * Return true, if the given MIME type indicates a multipart entity.
084         */
085        public static boolean isMultipart(String pMimeType) {
086            return pMimeType != null  &&  pMimeType.toLowerCase().startsWith("multipart/");
087        }
088    
089        /**
090         * Returns, whether the given transfer-encoding is "base64".
091         */
092        public static boolean isBase64Encoding(String pTransferEncoding) {
093            return ENC_BASE64.equalsIgnoreCase(pTransferEncoding);
094        }
095    
096        /**
097         * Returns, whether the given transfer-encoding is "quoted-printable".
098         */
099        public static boolean isQuotedPrintableEncoded(String pTransferEncoding) {
100            return ENC_QUOTED_PRINTABLE.equalsIgnoreCase(pTransferEncoding);
101        }
102    
103        /**
104         * Creates a new unique message boundary string that can be used as boundary
105         * parameter for the Content-Type header field of a message.
106         *
107         * @return a new unique message boundary string.
108         */
109        /* TODO - From rfc2045:
110         * Since the hyphen character ("-") may be represented as itself in the
111         * Quoted-Printable encoding, care must be taken, when encapsulating a
112         * quoted-printable encoded body inside one or more multipart entities,
113         * to ensure that the boundary delimiter does not appear anywhere in the
114         * encoded body.  (A good strategy is to choose a boundary that includes
115         * a character sequence such as "=_" which can never appear in a
116         * quoted-printable body.  See the definition of multipart messages in
117         * RFC 2046.)
118         */
119        public static String createUniqueBoundary() {
120            StringBuilder sb = new StringBuilder();
121            sb.append("-=Part.");
122            sb.append(Integer.toHexString(nextCounterValue()));
123            sb.append('.');
124            sb.append(Long.toHexString(random.nextLong()));
125            sb.append('.');
126            sb.append(Long.toHexString(System.currentTimeMillis()));
127            sb.append('.');
128            sb.append(Long.toHexString(random.nextLong()));
129            sb.append("=-");
130            return sb.toString();
131        }
132    
133        /**
134         * Creates a new unique message identifier that can be used in message
135         * header field such as Message-ID or In-Reply-To. If the given host name is
136         * not <code>null</code> it will be used as suffix for the message ID
137         * (following an at sign).
138         *
139         * The resulting string is enclosed in angle brackets (&lt; and &gt;);
140         *
141         * @param hostName host name to be included in the message ID or
142         *            <code>null</code> if no host name should be included.
143         * @return a new unique message identifier.
144         */
145        public static String createUniqueMessageId(String hostName) {
146            StringBuilder sb = new StringBuilder("<Mime4j.");
147            sb.append(Integer.toHexString(nextCounterValue()));
148            sb.append('.');
149            sb.append(Long.toHexString(random.nextLong()));
150            sb.append('.');
151            sb.append(Long.toHexString(System.currentTimeMillis()));
152            if (hostName != null) {
153                sb.append('@');
154                sb.append(hostName);
155            }
156            sb.append('>');
157            return sb.toString();
158        }
159    
160        /**
161         * Formats the specified date into a RFC 822 date-time string.
162         *
163         * @param date
164         *            date to be formatted into a string.
165         * @param zone
166         *            the time zone to use or <code>null</code> to use the default
167         *            time zone.
168         * @return the formatted time string.
169         */
170        public static String formatDate(Date date, TimeZone zone) {
171            DateFormat df = RFC822_DATE_FORMAT.get();
172    
173            if (zone == null) {
174                df.setTimeZone(TimeZone.getDefault());
175            } else {
176                df.setTimeZone(zone);
177            }
178    
179            return df.format(date);
180        }
181    
182        /**
183         * Splits the specified string into a multiple-line representation with
184         * lines no longer than 76 characters (because the line might contain
185         * encoded words; see <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC
186         * 2047</a> section 2). If the string contains non-whitespace sequences
187         * longer than 76 characters a line break is inserted at the whitespace
188         * character following the sequence resulting in a line longer than 76
189         * characters.
190         *
191         * @param s
192         *            string to split.
193         * @param usedCharacters
194         *            number of characters already used up. Usually the number of
195         *            characters for header field name plus colon and one space.
196         * @return a multiple-line representation of the given string.
197         */
198        public static String fold(String s, int usedCharacters) {
199            final int maxCharacters = 76;
200    
201            final int length = s.length();
202            if (usedCharacters + length <= maxCharacters)
203                return s;
204    
205            StringBuilder sb = new StringBuilder();
206    
207            int lastLineBreak = -usedCharacters;
208            int wspIdx = indexOfWsp(s, 0);
209            while (true) {
210                if (wspIdx == length) {
211                    sb.append(s.substring(Math.max(0, lastLineBreak)));
212                    return sb.toString();
213                }
214    
215                int nextWspIdx = indexOfWsp(s, wspIdx + 1);
216    
217                if (nextWspIdx - lastLineBreak > maxCharacters) {
218                    sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
219                    sb.append("\r\n");
220                    lastLineBreak = wspIdx;
221                }
222    
223                wspIdx = nextWspIdx;
224            }
225        }
226    
227        /**
228         * Unfold a multiple-line representation into a single line.
229         *
230         * @param s
231         *            string to unfold.
232         * @return unfolded string.
233         */
234        public static String unfold(String s) {
235            final int length = s.length();
236            for (int idx = 0; idx < length; idx++) {
237                char c = s.charAt(idx);
238                if (c == '\r' || c == '\n') {
239                    return unfold0(s, idx);
240                }
241            }
242    
243            return s;
244        }
245    
246        private static String unfold0(String s, int crlfIdx) {
247            final int length = s.length();
248            StringBuilder sb = new StringBuilder(length);
249    
250            if (crlfIdx > 0) {
251                sb.append(s.substring(0, crlfIdx));
252            }
253    
254            for (int idx = crlfIdx + 1; idx < length; idx++) {
255                char c = s.charAt(idx);
256                if (c != '\r' && c != '\n') {
257                    sb.append(c);
258                }
259            }
260    
261            return sb.toString();
262        }
263    
264        private static int indexOfWsp(String s, int fromIndex) {
265            final int len = s.length();
266            for (int index = fromIndex; index < len; index++) {
267                char c = s.charAt(index);
268                if (c == ' ' || c == '\t')
269                    return index;
270            }
271            return len;
272        }
273    
274        private static synchronized int nextCounterValue() {
275            return counter++;
276        }
277    
278        private static final ThreadLocal<DateFormat> RFC822_DATE_FORMAT = new ThreadLocal<DateFormat>() {
279            @Override
280            protected DateFormat initialValue() {
281                return new Rfc822DateFormat();
282            }
283        };
284    
285        private static final class Rfc822DateFormat extends SimpleDateFormat {
286            private static final long serialVersionUID = 1L;
287    
288            public Rfc822DateFormat() {
289                super("EEE, d MMM yyyy HH:mm:ss ", Locale.US);
290            }
291    
292            @Override
293            public StringBuffer format(Date date, StringBuffer toAppendTo,
294                    FieldPosition pos) {
295                StringBuffer sb = super.format(date, toAppendTo, pos);
296    
297                int zoneMillis = calendar.get(GregorianCalendar.ZONE_OFFSET);
298                int dstMillis = calendar.get(GregorianCalendar.DST_OFFSET);
299                int minutes = (zoneMillis + dstMillis) / 1000 / 60;
300    
301                if (minutes < 0) {
302                    sb.append('-');
303                    minutes = -minutes;
304                } else {
305                    sb.append('+');
306                }
307    
308                sb.append(String.format("%02d%02d", minutes / 60, minutes % 60));
309    
310                return sb;
311            }
312        }
313    }