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("&amp;", "&"));
XML_DECODE_PATTERNS.add(new PatternMap(R, R))222         XML_DECODE_PATTERNS.add(new PatternMap("&lt;", "<"));
XML_DECODE_PATTERNS.add(new PatternMap(R, R))223         XML_DECODE_PATTERNS.add(new PatternMap("&gt;", ">"));
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, "&amp;"));
XML_ENCODE_PATTERNS.add(new PatternMap(R, R))229         XML_ENCODE_PATTERNS.add(new PatternMap("<", "&lt;"));
XML_ENCODE_PATTERNS.add(new PatternMap(R, R))230         XML_ENCODE_PATTERNS.add(new PatternMap(">", "&gt;"));
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("&#x22;");
267             }
268             if (result.indexOf('\r') >= 0) {
269                 result = CR_PATTERN.matcher(result).replaceAll("&#x0D;");
270             }
271             if (result.indexOf('\n') >= 0) {
272                 result = LF_PATTERN.matcher(result).replaceAll("&#x0A;");
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