1 /* 2 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway 3 * Copyright (C) 2009 Mickael Guessant 4 * 5 * This program is free software; you can redistribute it and/or 6 * modify it under the terms of the GNU General Public License 7 * as published by the Free Software Foundation; either version 2 8 * of the License, or (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program; if not, write to the Free Software 17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 */ 19 package davmail.util; 20 21 import org.apache.commons.codec.DecoderException; 22 import org.apache.commons.codec.binary.Base64; 23 import org.apache.commons.codec.binary.Hex; 24 25 import java.nio.charset.StandardCharsets; 26 import java.text.ParseException; 27 import java.text.SimpleDateFormat; 28 import java.util.ArrayList; 29 import java.util.Calendar; 30 import java.util.List; 31 import java.util.Set; 32 import java.util.regex.Pattern; 33 34 /** 35 * Various string handling methods 36 */ 37 public final class StringUtil { StringUtil()38 private StringUtil() { 39 } 40 41 /** 42 * Return the sub string between startDelimiter and endDelimiter or null. 43 * 44 * @param value String value 45 * @param startDelimiter start delimiter 46 * @param endDelimiter end delimiter 47 * @return token value 48 */ getToken(String value, String startDelimiter, String endDelimiter)49 public static String getToken(String value, String startDelimiter, String endDelimiter) { 50 String token = null; 51 if (value != null) { 52 int startIndex = value.indexOf(startDelimiter); 53 if (startIndex >= 0) { 54 startIndex += startDelimiter.length(); 55 int endIndex = value.indexOf(endDelimiter, startIndex); 56 if (endIndex >= 0) { 57 token = value.substring(startIndex, endIndex); 58 } 59 } 60 } 61 return token; 62 } 63 64 /** 65 * Return the sub string between startDelimiter and endDelimiter or null, 66 * look for last token in string. 67 * 68 * @param value String value 69 * @param startDelimiter start delimiter 70 * @param endDelimiter end delimiter 71 * @return token value 72 */ getLastToken(String value, String startDelimiter, String endDelimiter)73 public static String getLastToken(String value, String startDelimiter, String endDelimiter) { 74 String token = null; 75 if (value != null) { 76 int startIndex = value.lastIndexOf(startDelimiter); 77 if (startIndex >= 0) { 78 startIndex += startDelimiter.length(); 79 int endIndex = value.indexOf(endDelimiter, startIndex); 80 if (endIndex >= 0) { 81 token = value.substring(startIndex, endIndex); 82 } 83 } 84 } 85 return token; 86 } 87 88 /** 89 * Return the sub string between startDelimiter and endDelimiter with newToken. 90 * 91 * @param value String value 92 * @param startDelimiter start delimiter 93 * @param endDelimiter end delimiter 94 * @param newToken new token value 95 * @return token value 96 */ replaceToken(String value, String startDelimiter, String endDelimiter, String newToken)97 public static String replaceToken(String value, String startDelimiter, String endDelimiter, String newToken) { 98 String result = null; 99 if (value != null) { 100 int startIndex = value.indexOf(startDelimiter); 101 if (startIndex >= 0) { 102 startIndex += startDelimiter.length(); 103 int endIndex = value.indexOf(endDelimiter, startIndex); 104 if (endIndex >= 0) { 105 result = value.substring(0, startIndex) + newToken + value.substring(endIndex); 106 } 107 } 108 } 109 return result; 110 } 111 112 /** 113 * Join values with given separator. 114 * 115 * @param values value set 116 * @param separator separator 117 * @return joined values 118 */ join(Set<String> values, String separator)119 public static String join(Set<String> values, String separator) { 120 if (values != null && !values.isEmpty()) { 121 StringBuilder result = new StringBuilder(); 122 for (String value : values) { 123 if (result.length() > 0) { 124 result.append(separator); 125 } 126 result.append(value); 127 } 128 return result.toString(); 129 } else { 130 return null; 131 } 132 } 133 134 static class PatternMap { 135 protected String match; 136 protected String value; 137 protected Pattern pattern; 138 PatternMap(String match, String value)139 protected PatternMap(String match, String value) { 140 this.match = match; 141 this.value = value; 142 pattern = Pattern.compile(match); 143 } 144 PatternMap(String match, String escapedMatch, String value)145 protected PatternMap(String match, String escapedMatch, String value) { 146 this.match = match; 147 this.value = value; 148 pattern = Pattern.compile(escapedMatch); 149 } 150 PatternMap(String match, Pattern pattern, String value)151 protected PatternMap(String match, Pattern pattern, String value) { 152 this.match = match; 153 this.value = value; 154 this.pattern = pattern; 155 } 156 replaceAll(String string)157 protected String replaceAll(String string) { 158 if (string != null && string.contains(match)) { 159 return pattern.matcher(string).replaceAll(value); 160 } else { 161 return string; 162 } 163 } 164 } 165 166 private static final Pattern AMP_PATTERN = Pattern.compile("&"); 167 private static final Pattern PLUS_PATTERN = Pattern.compile("\\+"); 168 169 private static final Pattern QUOTE_PATTERN = Pattern.compile("\""); 170 private static final Pattern CR_PATTERN = Pattern.compile("\r"); 171 private static final Pattern LF_PATTERN = Pattern.compile("\n"); 172 173 private static final List<PatternMap> URLENCODED_PATTERNS = new ArrayList<>(); 174 static { URLENCODED_PATTERNS.add(new PatternMap(String.valueOf((char) 0xF8FF), R))175 URLENCODED_PATTERNS.add(new PatternMap(String.valueOf((char) 0xF8FF), "_xF8FF_")); URLENCODED_PATTERNS.add(new PatternMap(R, R))176 URLENCODED_PATTERNS.add(new PatternMap("%26", "&")); URLENCODED_PATTERNS.add(new PatternMap(R, R))177 URLENCODED_PATTERNS.add(new PatternMap("%2B", "+")); URLENCODED_PATTERNS.add(new PatternMap(R, R))178 URLENCODED_PATTERNS.add(new PatternMap("%3A", ":")); URLENCODED_PATTERNS.add(new PatternMap(R, R))179 URLENCODED_PATTERNS.add(new PatternMap("%3B", ";")); URLENCODED_PATTERNS.add(new PatternMap(R, R))180 URLENCODED_PATTERNS.add(new PatternMap("%3C", "<")); URLENCODED_PATTERNS.add(new PatternMap(R, R))181 URLENCODED_PATTERNS.add(new PatternMap("%3E", ">")); URLENCODED_PATTERNS.add(new PatternMap(R, R))182 URLENCODED_PATTERNS.add(new PatternMap("%22", "\"")); URLENCODED_PATTERNS.add(new PatternMap(R, R))183 URLENCODED_PATTERNS.add(new PatternMap("%23", "#")); URLENCODED_PATTERNS.add(new PatternMap(R, R))184 URLENCODED_PATTERNS.add(new PatternMap("%2A", "*")); URLENCODED_PATTERNS.add(new PatternMap(R, R))185 URLENCODED_PATTERNS.add(new PatternMap("%7C", "|")); URLENCODED_PATTERNS.add(new PatternMap(R, R))186 URLENCODED_PATTERNS.add(new PatternMap("%3F", "?")); URLENCODED_PATTERNS.add(new PatternMap(R, R))187 URLENCODED_PATTERNS.add(new PatternMap("%7E", "~")); 188 189 // CRLF is replaced with LF in response URLENCODED_PATTERNS.add(new PatternMap(R, R))190 URLENCODED_PATTERNS.add(new PatternMap("\n", "_x000D__x000A_")); 191 192 // last replace % URLENCODED_PATTERNS.add(new PatternMap(R, R))193 URLENCODED_PATTERNS.add(new PatternMap("%25", "%")); 194 } 195 196 private static final List<PatternMap> URLENCODE_PATTERNS = new ArrayList<>(); 197 static { 198 // first replace % URLENCODE_PATTERNS.add(new PatternMap(R, R))199 URLENCODE_PATTERNS.add(new PatternMap("%", "%25")); 200 URLENCODE_PATTERNS.add(new PatternMap(R, String.valueOf((char) 0xF8FF)))201 URLENCODE_PATTERNS.add(new PatternMap("_xF8FF_", String.valueOf((char) 0xF8FF))); URLENCODE_PATTERNS.add(new PatternMap(R, AMP_PATTERN, R))202 URLENCODE_PATTERNS.add(new PatternMap("&", AMP_PATTERN, "%26")); URLENCODE_PATTERNS.add(new PatternMap(R, PLUS_PATTERN, R))203 URLENCODE_PATTERNS.add(new PatternMap("+", PLUS_PATTERN, "%2B")); URLENCODE_PATTERNS.add(new PatternMap(R, R))204 URLENCODE_PATTERNS.add(new PatternMap(":", "%3A")); URLENCODE_PATTERNS.add(new PatternMap(R, R))205 URLENCODE_PATTERNS.add(new PatternMap(";", "%3B")); URLENCODE_PATTERNS.add(new PatternMap(R, R))206 URLENCODE_PATTERNS.add(new PatternMap("<", "%3C")); URLENCODE_PATTERNS.add(new PatternMap(R, R))207 URLENCODE_PATTERNS.add(new PatternMap(">", "%3E")); URLENCODE_PATTERNS.add(new PatternMap(R, R))208 URLENCODE_PATTERNS.add(new PatternMap("\"", "%22")); URLENCODE_PATTERNS.add(new PatternMap(R, R))209 URLENCODE_PATTERNS.add(new PatternMap("#", "%23")); URLENCODE_PATTERNS.add(new PatternMap(R, R))210 URLENCODE_PATTERNS.add(new PatternMap("~", "%7E")); URLENCODE_PATTERNS.add(new PatternMap(R, R, R))211 URLENCODE_PATTERNS.add(new PatternMap("*", "\\*", "%2A")); URLENCODE_PATTERNS.add(new PatternMap(R, R, R))212 URLENCODE_PATTERNS.add(new PatternMap("|", "\\|", "%7C")); URLENCODE_PATTERNS.add(new PatternMap(R, R, R))213 URLENCODE_PATTERNS.add(new PatternMap("?", "\\?", "%3F")); 214 URLENCODE_PATTERNS.add(new PatternMap(R, R))215 URLENCODE_PATTERNS.add(new PatternMap("_x000D__x000A_", "\r\n")); 216 217 } 218 219 private static final List<PatternMap> XML_DECODE_PATTERNS = new ArrayList<>(); 220 static { XML_DECODE_PATTERNS.add(new PatternMap(R, R))221 XML_DECODE_PATTERNS.add(new PatternMap("&", "&")); XML_DECODE_PATTERNS.add(new PatternMap(R, R))222 XML_DECODE_PATTERNS.add(new PatternMap("<", "<")); XML_DECODE_PATTERNS.add(new PatternMap(R, R))223 XML_DECODE_PATTERNS.add(new PatternMap(">", ">")); 224 } 225 226 private static final List<PatternMap> XML_ENCODE_PATTERNS = new ArrayList<>(); 227 static { XML_ENCODE_PATTERNS.add(new PatternMap(R, AMP_PATTERN, R))228 XML_ENCODE_PATTERNS.add(new PatternMap("&", AMP_PATTERN, "&")); XML_ENCODE_PATTERNS.add(new PatternMap(R, R))229 XML_ENCODE_PATTERNS.add(new PatternMap("<", "<")); XML_ENCODE_PATTERNS.add(new PatternMap(R, R))230 XML_ENCODE_PATTERNS.add(new PatternMap(">", ">")); 231 } 232 233 private static final Pattern SLASH_PATTERN = Pattern.compile("/"); 234 private static final Pattern UNDERSCORE_PATTERN = Pattern.compile("_"); 235 private static final Pattern DASH_PATTERN = Pattern.compile("-"); 236 237 // WebDav search parameter encode 238 private static final Pattern APOS_PATTERN = Pattern.compile("'"); 239 240 /** 241 * Xml encode content. 242 * 243 * @param name decoded name 244 * @return name encoded name 245 */ xmlEncode(String name)246 public static String xmlEncode(String name) { 247 String result = name; 248 if (result != null) { 249 for (PatternMap patternMap : XML_ENCODE_PATTERNS) { 250 result = patternMap.replaceAll(result); 251 } 252 } 253 return result; 254 } 255 256 /** 257 * Xml encode inside attribute. 258 * 259 * @param name decoded name 260 * @return name encoded name 261 */ xmlEncodeAttribute(String name)262 public static String xmlEncodeAttribute(String name) { 263 String result = xmlEncode(name); 264 if (result != null) { 265 if (result.indexOf('"') >= 0) { 266 result = QUOTE_PATTERN.matcher(result).replaceAll("""); 267 } 268 if (result.indexOf('\r') >= 0) { 269 result = CR_PATTERN.matcher(result).replaceAll("
"); 270 } 271 if (result.indexOf('\n') >= 0) { 272 result = LF_PATTERN.matcher(result).replaceAll("
"); 273 } 274 } 275 return result; 276 } 277 278 /** 279 * Need to decode xml for iCal 280 * 281 * @param name encoded name 282 * @return name decoded name 283 */ xmlDecode(String name)284 public static String xmlDecode(String name) { 285 String result = name; 286 if (result != null) { 287 for (PatternMap patternMap : XML_DECODE_PATTERNS) { 288 result = patternMap.replaceAll(result); 289 } 290 } 291 return result; 292 } 293 294 /** 295 * Convert base64 value to hex. 296 * 297 * @param value base64 value 298 * @return hex value 299 */ 300 @SuppressWarnings("unused") base64ToHex(String value)301 public static String base64ToHex(String value) { 302 String hexValue = null; 303 if (value != null) { 304 hexValue = new String(Hex.encodeHex(Base64.decodeBase64(value.getBytes(StandardCharsets.UTF_8)))); 305 } 306 return hexValue; 307 } 308 309 /** 310 * Convert hex value to base64. 311 * 312 * @param value hex value 313 * @return base64 value 314 * @throws DecoderException on error 315 */ 316 @SuppressWarnings("unused") hexToBase64(String value)317 public static String hexToBase64(String value) throws DecoderException { 318 String base64Value = null; 319 if (value != null) { 320 base64Value = new String(Base64.encodeBase64(Hex.decodeHex(value.toCharArray())), StandardCharsets.UTF_8); 321 } 322 return base64Value; 323 } 324 325 /** 326 * Encode item name to get actual value stored in urlcompname MAPI property. 327 * 328 * @param value decoded value 329 * @return urlcompname encoded value 330 */ encodeUrlcompname(String value)331 public static String encodeUrlcompname(String value) { 332 String result = value; 333 if (result != null) { 334 for (PatternMap patternMap : URLENCODE_PATTERNS) { 335 result = patternMap.replaceAll(result); 336 } 337 } 338 return result; 339 } 340 341 /** 342 * Decode urlcompname to get item name. 343 * 344 * @param urlcompname encoded value 345 * @return decoded value 346 */ decodeUrlcompname(String urlcompname)347 public static String decodeUrlcompname(String urlcompname) { 348 String result = urlcompname; 349 if (result != null) { 350 for (PatternMap patternMap : URLENCODED_PATTERNS) { 351 result = patternMap.replaceAll(result); 352 } 353 } 354 return result; 355 } 356 357 /** 358 * Urlencode plus sign in encoded href. 359 * '+' is decoded as ' ' by URIUtil.decode, the workaround is to force urlencoding to '%2B' first 360 * 361 * @param value encoded href 362 * @return encoded href 363 */ encodePlusSign(String value)364 public static String encodePlusSign(String value) { 365 String result = value; 366 if (result.indexOf('+') >= 0) { 367 result = PLUS_PATTERN.matcher(result).replaceAll("%2B"); 368 } 369 return result; 370 } 371 372 /** 373 * Encode EWS base64 itemId to url compatible value. 374 * 375 * @param value base64 value 376 * @return url compatible value 377 */ base64ToUrl(String value)378 public static String base64ToUrl(String value) { 379 String result = value; 380 if (result != null) { 381 if (result.indexOf('+') >= 0) { 382 result = PLUS_PATTERN.matcher(result).replaceAll("-"); 383 } 384 if (result.indexOf('/') >= 0) { 385 result = SLASH_PATTERN.matcher(result).replaceAll("_"); 386 } 387 } 388 return result; 389 } 390 391 /** 392 * Encode EWS url compatible itemId back to base64 value. 393 * 394 * @param value url compatible value 395 * @return base64 value 396 */ urlToBase64(String value)397 public static String urlToBase64(String value) { 398 String result = value; 399 if (result.indexOf('-') >= 0) { 400 result = DASH_PATTERN.matcher(result).replaceAll("+"); 401 } 402 if (result.indexOf('_') >= 0) { 403 result = UNDERSCORE_PATTERN.matcher(result).replaceAll("/"); 404 } 405 return result; 406 } 407 408 /** 409 * Encode quotes in Dav search parameter. 410 * 411 * @param value search parameter 412 * @return escaped value 413 */ davSearchEncode(String value)414 public static String davSearchEncode(String value) { 415 String result = value; 416 if (result.indexOf('\'') >= 0) { 417 result = APOS_PATTERN.matcher(result).replaceAll("''"); 418 } 419 return result; 420 } 421 422 /** 423 * Get allday date value from zulu timestamp. 424 * 425 * @param value zulu datetime 426 * @return yyyyMMdd allday date value 427 */ convertZuluDateTimeToAllDay(String value)428 public static String convertZuluDateTimeToAllDay(String value) { 429 String result = value; 430 if (value != null && value.length() != 8) { 431 // try to convert datetime value to date value 432 try { 433 Calendar calendar = Calendar.getInstance(); 434 SimpleDateFormat dateParser = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); 435 calendar.setTime(dateParser.parse(value)); 436 calendar.add(Calendar.HOUR_OF_DAY, 12); 437 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); 438 result = dateFormatter.format(calendar.getTime()); 439 } catch (ParseException e) { 440 // ignore 441 } 442 } 443 return result; 444 } 445 446 /** 447 * Remove quotes if present on value. 448 * 449 * @param value input value 450 * @return unquoted string 451 */ removeQuotes(String value)452 public static String removeQuotes(String value) { 453 String result = value; 454 if (result != null) { 455 if (result.startsWith("\"") || result.startsWith("{") || result.startsWith("(")) { 456 result = result.substring(1); 457 } 458 if (result.endsWith("\"") || result.endsWith("}") || result.endsWith(")")) { 459 result = result.substring(0, result.length() - 1); 460 } 461 } 462 return result; 463 } 464 465 } 466