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 020package org.apache.james.mime4j.util; 021 022import java.text.DateFormat; 023import java.text.FieldPosition; 024import java.text.SimpleDateFormat; 025import java.util.Date; 026import java.util.GregorianCalendar; 027import java.util.Locale; 028import java.util.Random; 029import java.util.TimeZone; 030 031/** 032 * A utility class, which provides some MIME related application logic. 033 */ 034public 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}