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 (< and >);
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 }