View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  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.exchange.ews;
20  
21  import davmail.Settings;
22  import davmail.exception.DavMailAuthenticationException;
23  import davmail.exception.DavMailException;
24  import davmail.exception.HttpNotFoundException;
25  import davmail.exchange.ExchangeSession;
26  import davmail.exchange.VCalendar;
27  import davmail.exchange.VObject;
28  import davmail.exchange.VProperty;
29  import davmail.http.DavGatewayHttpClientFacade;
30  import davmail.util.IOUtil;
31  import davmail.util.StringUtil;
32  import org.apache.commons.httpclient.*;
33  import org.apache.commons.httpclient.methods.*;
34  import org.apache.commons.httpclient.params.HttpClientParams;
35  
36  import javax.mail.MessagingException;
37  import javax.mail.Session;
38  import javax.mail.internet.InternetAddress;
39  import javax.mail.internet.MimeMessage;
40  import javax.mail.util.SharedByteArrayInputStream;
41  import java.io.*;
42  import java.net.HttpURLConnection;
43  import java.text.ParseException;
44  import java.text.SimpleDateFormat;
45  import java.util.*;
46  
47  /**
48   * EWS Exchange adapter.
49   * Compatible with Exchange 2007, 2010 and 2013.
50   */
51  public class EwsExchangeSession extends ExchangeSession {
52  
53      protected static final int PAGE_SIZE = 500;
54  
55      protected static final String ARCHIVE_ROOT = "/archive/";
56  
57      /**
58       * Message types.
59       *
60       * @see <a href="http://msdn.microsoft.com/en-us/library/aa565652%28v=EXCHG.140%29.aspx">http://msdn.microsoft.com/en-us/library/aa565652%28v=EXCHG.140%29.aspx</a>
61       */
62      protected static final Set<String> MESSAGE_TYPES = new HashSet<String>();
63  
64      static {
65          MESSAGE_TYPES.add("Message");
66          MESSAGE_TYPES.add("CalendarItem");
67  
68          MESSAGE_TYPES.add("MeetingMessage");
69          MESSAGE_TYPES.add("MeetingRequest");
70          MESSAGE_TYPES.add("MeetingResponse");
71          MESSAGE_TYPES.add("MeetingCancellation");
72  
73          MESSAGE_TYPES.add("Item");
74          MESSAGE_TYPES.add("PostItem");
75  
76          // exclude types from IMAP
77          //MESSAGE_TYPES.add("Contact");
78          //MESSAGE_TYPES.add("DistributionList");
79          //MESSAGE_TYPES.add("Task");
80  
81          //ReplyToItem
82          //ForwardItem
83          //ReplyAllToItem
84          //AcceptItem
85          //TentativelyAcceptItem
86          //DeclineItem
87          //CancelCalendarItem
88          //RemoveItem
89          //PostReplyItem
90          //SuppressReadReceipt
91          //AcceptSharingInvitation
92      }
93  
94      static final Map<String, String> vTodoToTaskStatusMap = new HashMap<String, String>();
95      static final Map<String, String> taskTovTodoStatusMap = new HashMap<String, String>();
96  
97      static {
98          //taskTovTodoStatusMap.put("NotStarted", null);
99          taskTovTodoStatusMap.put("InProgress", "IN-PROCESS");
100         taskTovTodoStatusMap.put("Completed", "COMPLETED");
101         taskTovTodoStatusMap.put("WaitingOnOthers", "NEEDS-ACTION");
102         taskTovTodoStatusMap.put("Deferred", "CANCELLED");
103 
104         //vTodoToTaskStatusMap.put(null, "NotStarted");
105         vTodoToTaskStatusMap.put("IN-PROCESS", "InProgress");
106         vTodoToTaskStatusMap.put("COMPLETED", "Completed");
107         vTodoToTaskStatusMap.put("NEEDS-ACTION", "WaitingOnOthers");
108         vTodoToTaskStatusMap.put("CANCELLED", "Deferred");
109     }
110 
111     protected Map<String, String> folderIdMap;
112     protected boolean directEws;
113 
114     protected class Folder extends ExchangeSession.Folder {
115         public FolderId folderId;
116     }
117 
118     protected static class FolderPath {
119         protected final String parentPath;
120         protected final String folderName;
121 
122         protected FolderPath(String folderPath) {
123             int slashIndex = folderPath.lastIndexOf('/');
124             if (slashIndex < 0) {
125                 parentPath = "";
126                 folderName = folderPath;
127             } else {
128                 parentPath = folderPath.substring(0, slashIndex);
129                 folderName = folderPath.substring(slashIndex + 1);
130             }
131         }
132     }
133 
134     /**
135      * @inheritDoc
136      */
137     public EwsExchangeSession(String url, String userName, String password) throws IOException {
138         super(url, userName, password);
139     }
140 
141     /**
142      * Override authentication mode test: EWS is never form based.
143      *
144      * @param url        exchange base URL
145      * @param httpClient httpClient instance
146      * @return true if basic authentication detected
147      */
148     @Override
149     protected boolean isBasicAuthentication(HttpClient httpClient, String url) {
150         return !url.toLowerCase().endsWith("/ews/exchange.asmx") && super.isBasicAuthentication(httpClient, url);
151     }
152 
153     @Override
154     protected HttpMethod formLogin(HttpClient httpClient, HttpMethod initmethod, String userName, String password) throws IOException {
155         LOGGER.debug("Form based authentication detected");
156 
157         HttpMethod logonMethod = buildLogonMethod(httpClient, initmethod);
158         if (logonMethod == null) {
159             LOGGER.debug("Authentication form not found at " + initmethod.getURI() + ", will try direct EWS access");
160         } else {
161             logonMethod = postLogonMethod(httpClient, logonMethod, userName, password);
162         }
163 
164         return logonMethod;
165     }
166 
167 
168     /**
169      * Check endpoint url.
170      *
171      * @param endPointUrl endpoint url
172      * @throws IOException on error
173      */
174     protected void checkEndPointUrl(String endPointUrl) throws IOException {
175         GetFolderMethod checkMethod = new GetFolderMethod(BaseShape.ID_ONLY, DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null);
176         int status = executeMethod(checkMethod);
177         if (status == HttpStatus.SC_UNAUTHORIZED) {
178             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
179         } else if (status != HttpStatus.SC_OK) {
180             throw new IOException("Ews endpoint not available at " + checkMethod.getURI().toString() + " status " + status);
181         }
182     }
183 
184     @Override
185     protected void buildSessionInfo(HttpMethod method) throws DavMailException {
186         // no need to check logon method body
187         if (method != null) {
188             method.releaseConnection();
189         }
190         directEws = method == null
191                 || "/ews/services.wsdl".equalsIgnoreCase(method.getPath())
192                 || "/ews/exchange.asmx".equalsIgnoreCase(method.getPath());
193 
194         // options page is not available in direct EWS mode
195         if (!directEws) {
196             // retrieve email and alias from options page
197             getEmailAndAliasFromOptions();
198         }
199 
200         if (email == null || alias == null) {
201             // OWA authentication failed, get email address from login
202             if (userName.indexOf('@') >= 0) {
203                 // userName is email address
204                 email = userName;
205                 alias = userName.substring(0, userName.indexOf('@'));
206             } else {
207                 // userName or domain\\username, rebuild email address
208                 alias = getAliasFromLogin();
209 
210                 // try to get email address with ResolveNames
211                 resolveEmailAddress(userName);
212                 // failover, build from host name
213                 if (email == null) {
214                     email = getAliasFromLogin() + getEmailSuffixFromHostname();
215                 }
216             }
217         }
218 
219         currentMailboxPath = "/users/" + email.toLowerCase();
220 
221         // check EWS access
222         try {
223             checkEndPointUrl("/ews/exchange.asmx");
224             // workaround for Exchange bug: send fake request
225             internalGetFolder("");
226         } catch (IOException e) {
227             // first failover: retry with NTLM
228             DavGatewayHttpClientFacade.addNTLM(httpClient);
229             try {
230                 checkEndPointUrl("/ews/exchange.asmx");
231                 // workaround for Exchange bug: send fake request
232                 internalGetFolder("");
233             } catch (IOException e2) {
234                 LOGGER.debug(e2.getMessage());
235                 try {
236                     // failover, try to retrieve EWS url from autodiscover
237                     checkEndPointUrl(getEwsUrlFromAutoDiscover());
238                     // workaround for Exchange bug: send fake request
239                     internalGetFolder("");
240                 } catch (IOException e3) {
241                     // autodiscover failed and initial exception was authentication failure => throw original exception
242                     if (e instanceof DavMailAuthenticationException) {
243                         throw (DavMailAuthenticationException) e;
244                     }
245                     LOGGER.error(e2.getMessage());
246                     throw new DavMailException("EXCEPTION_EWS_NOT_AVAILABLE");
247                 }
248             }
249         }
250 
251         // enable preemptive authentication on non NTLM endpoints
252         if (!DavGatewayHttpClientFacade.hasNTLMorNegotiate(httpClient)) {
253             httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, true);
254         }
255 
256         // direct EWS: get primary smtp email address with ResolveNames
257         if (directEws) {
258             try {
259                 ResolveNamesMethod resolveNamesMethod = new ResolveNamesMethod(alias);
260                 executeMethod(resolveNamesMethod);
261                 List<EWSMethod.Item> responses = resolveNamesMethod.getResponseItems();
262                 for (EWSMethod.Item response : responses) {
263                     if (alias.equalsIgnoreCase(response.get("Name"))) {
264                         email = response.get("EmailAddress");
265                         currentMailboxPath = "/users/" + email.toLowerCase();
266                     }
267                 }
268             } catch (IOException e) {
269                 LOGGER.warn("Unable to get primary email address with ResolveNames", e);
270             }
271         }
272 
273         try {
274             folderIdMap = new HashMap<String, String>();
275             // load actual well known folder ids
276             folderIdMap.put(internalGetFolder(INBOX).folderId.value, INBOX);
277             folderIdMap.put(internalGetFolder(CALENDAR).folderId.value, CALENDAR);
278             folderIdMap.put(internalGetFolder(CONTACTS).folderId.value, CONTACTS);
279             folderIdMap.put(internalGetFolder(SENT).folderId.value, SENT);
280             folderIdMap.put(internalGetFolder(DRAFTS).folderId.value, DRAFTS);
281             folderIdMap.put(internalGetFolder(TRASH).folderId.value, TRASH);
282             folderIdMap.put(internalGetFolder(JUNK).folderId.value, JUNK);
283             folderIdMap.put(internalGetFolder(UNSENT).folderId.value, UNSENT);
284         } catch (IOException e) {
285             LOGGER.error(e.getMessage(), e);
286             throw new DavMailAuthenticationException("EXCEPTION_EWS_NOT_AVAILABLE");
287         }
288         LOGGER.debug("Current user email is " + email + ", alias is " + alias + " on " + serverVersion);
289     }
290 
291     protected void resolveEmailAddress(String userName) {
292         String searchValue = userName;
293         int index = searchValue.indexOf('\\');
294         if (index >= 0) {
295             searchValue = searchValue.substring(index + 1);
296         }
297         ResolveNamesMethod resolveNamesMethod = new ResolveNamesMethod(searchValue);
298         try {
299             // send a fake request to get server version
300             internalGetFolder("");
301             executeMethod(resolveNamesMethod);
302             List<EWSMethod.Item> responses = resolveNamesMethod.getResponseItems();
303             if (responses.size() == 1) {
304                 email = responses.get(0).get("EmailAddress");
305             }
306 
307         } catch (IOException e) {
308             // ignore
309         }
310     }
311 
312     protected static class AutoDiscoverMethod extends PostMethod {
313         AutoDiscoverMethod(String autodiscoverHost, String userEmail) throws IOException {
314             super("https://" + autodiscoverHost + "/autodiscover/autodiscover.xml");
315             setAutoDiscoverRequestEntity(userEmail);
316         }
317 
318         AutoDiscoverMethod(String userEmail) throws IOException {
319             super("/autodiscover/autodiscover.xml");
320             setAutoDiscoverRequestEntity(userEmail);
321         }
322 
323         void setAutoDiscoverRequestEntity(String userEmail) throws IOException {
324             String body = "<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006\">" +
325                     "<Request>" +
326                     "<EMailAddress>" + userEmail + "</EMailAddress>" +
327                     "<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>" +
328                     "</Request>" +
329                     "</Autodiscover>";
330             setRequestEntity(new StringRequestEntity(body, "text/xml", "UTF-8"));
331         }
332 
333         String ewsUrl;
334 
335         @Override
336         protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
337             Header contentTypeHeader = getResponseHeader("Content-Type");
338             if (contentTypeHeader != null &&
339                     ("text/xml; charset=utf-8".equals(contentTypeHeader.getValue())
340                             || "text/html; charset=utf-8".equals(contentTypeHeader.getValue())
341                     )) {
342                 BufferedReader autodiscoverReader = null;
343                 try {
344                     autodiscoverReader = new BufferedReader(new InputStreamReader(getResponseBodyAsStream(), "UTF-8"));
345                     String line;
346                     // find ews url
347                     //noinspection StatementWithEmptyBody
348                     while ((line = autodiscoverReader.readLine()) != null
349                             && (!line.contains("<EwsUrl>"))
350                             && (!line.contains("</EwsUrl>"))) {
351                     }
352                     if (line != null) {
353                         ewsUrl = line.substring(line.indexOf("<EwsUrl>") + 8, line.indexOf("</EwsUrl>"));
354                     }
355                 } catch (IOException e) {
356                     LOGGER.debug(e);
357                 } finally {
358                     if (autodiscoverReader != null) {
359                         try {
360                             autodiscoverReader.close();
361                         } catch (IOException e) {
362                             LOGGER.debug(e);
363                         }
364                     }
365                 }
366             }
367         }
368     }
369 
370     protected String getEwsUrlFromAutoDiscover() throws DavMailAuthenticationException {
371         String ewsUrl;
372         try {
373             ewsUrl = getEwsUrlFromAutoDiscover(null);
374         } catch (IOException e) {
375             try {
376                 ewsUrl = getEwsUrlFromAutoDiscover("autodiscover." + email.substring(email.indexOf('@') + 1));
377             } catch (IOException e2) {
378                 LOGGER.error(e2.getMessage());
379                 throw new DavMailAuthenticationException("EXCEPTION_EWS_NOT_AVAILABLE");
380             }
381         }
382         return ewsUrl;
383     }
384 
385     protected String getEwsUrlFromAutoDiscover(String autodiscoverHostname) throws IOException {
386         String ewsUrl;
387         AutoDiscoverMethod autoDiscoverMethod;
388         if (autodiscoverHostname != null) {
389             autoDiscoverMethod = new AutoDiscoverMethod(autodiscoverHostname, email);
390         } else {
391             autoDiscoverMethod = new AutoDiscoverMethod(email);
392         }
393         try {
394             int status = DavGatewayHttpClientFacade.executeNoRedirect(httpClient, autoDiscoverMethod);
395             if (status != HttpStatus.SC_OK) {
396                 throw DavGatewayHttpClientFacade.buildHttpException(autoDiscoverMethod);
397             }
398             ewsUrl = autoDiscoverMethod.ewsUrl;
399 
400             // update host name
401             DavGatewayHttpClientFacade.setClientHost(httpClient, ewsUrl);
402 
403             if (ewsUrl == null) {
404                 throw new IOException("Ews url not found");
405             }
406         } finally {
407             autoDiscoverMethod.releaseConnection();
408         }
409         return ewsUrl;
410     }
411 
412     class Message extends ExchangeSession.Message {
413         // message item id
414         ItemId itemId;
415 
416         @Override
417         public String getPermanentId() {
418             return itemId.id;
419         }
420 
421         @Override
422         protected InputStream getMimeHeaders() {
423             InputStream result = null;
424             try {
425                 GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
426                 getItemMethod.addAdditionalProperty(Field.get("messageheaders"));
427                 getItemMethod.addAdditionalProperty(Field.get("from"));
428                 executeMethod(getItemMethod);
429                 EWSMethod.Item item = getItemMethod.getResponseItem();
430 
431                 String messageHeaders = item.get(Field.get("messageheaders").getResponseName());
432                 if (messageHeaders != null
433                         // workaround for broken message headers on Exchange 2010
434                         && messageHeaders.toLowerCase().contains("message-id:")) {
435                     // workaround for messages in Sent folder
436                     if (!messageHeaders.contains("From:")) {
437                         String from = item.get(Field.get("from").getResponseName());
438                         messageHeaders = "From: " + from + '\n' + messageHeaders;
439                     }
440 
441                     result = new ByteArrayInputStream(messageHeaders.getBytes("UTF-8"));
442                 }
443             } catch (Exception e) {
444                 LOGGER.warn(e.getMessage());
445             }
446 
447             return result;
448         }
449     }
450 
451     /**
452      * Message create/update properties
453      *
454      * @param properties flag values map
455      * @return field values
456      */
457     protected List<FieldUpdate> buildProperties(Map<String, String> properties) {
458         ArrayList<FieldUpdate> list = new ArrayList<FieldUpdate>();
459         for (Map.Entry<String, String> entry : properties.entrySet()) {
460             if ("read".equals(entry.getKey())) {
461                 list.add(Field.createFieldUpdate("read", Boolean.toString("1".equals(entry.getValue()))));
462             } else if ("junk".equals(entry.getKey())) {
463                 list.add(Field.createFieldUpdate("junk", entry.getValue()));
464             } else if ("flagged".equals(entry.getKey())) {
465                 list.add(Field.createFieldUpdate("flagStatus", entry.getValue()));
466             } else if ("answered".equals(entry.getKey())) {
467                 list.add(Field.createFieldUpdate("lastVerbExecuted", entry.getValue()));
468                 if ("102".equals(entry.getValue())) {
469                     list.add(Field.createFieldUpdate("iconIndex", "261"));
470                 }
471             } else if ("forwarded".equals(entry.getKey())) {
472                 list.add(Field.createFieldUpdate("lastVerbExecuted", entry.getValue()));
473                 if ("104".equals(entry.getValue())) {
474                     list.add(Field.createFieldUpdate("iconIndex", "262"));
475                 }
476             } else if ("draft".equals(entry.getKey())) {
477                 // note: draft is readonly after create
478                 list.add(Field.createFieldUpdate("messageFlags", entry.getValue()));
479             } else if ("deleted".equals(entry.getKey())) {
480                 list.add(Field.createFieldUpdate("deleted", entry.getValue()));
481             } else if ("datereceived".equals(entry.getKey())) {
482                 list.add(Field.createFieldUpdate("datereceived", entry.getValue()));
483             } else if ("keywords".equals(entry.getKey())) {
484                 list.add(Field.createFieldUpdate("keywords", entry.getValue()));
485             }
486         }
487         return list;
488     }
489 
490     @Override
491     public void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
492         EWSMethod.Item item = new EWSMethod.Item();
493         item.type = "Message";
494         ByteArrayOutputStream baos = new ByteArrayOutputStream();
495         try {
496             mimeMessage.writeTo(baos);
497         } catch (MessagingException e) {
498             throw new IOException(e.getMessage());
499         }
500         baos.close();
501         item.mimeContent = IOUtil.encodeBase64(baos.toByteArray());
502 
503         List<FieldUpdate> fieldUpdates = buildProperties(properties);
504         if (!properties.containsKey("draft")) {
505             // need to force draft flag to false
506             if (properties.containsKey("read")) {
507                 fieldUpdates.add(Field.createFieldUpdate("messageFlags", "1"));
508             } else {
509                 fieldUpdates.add(Field.createFieldUpdate("messageFlags", "0"));
510             }
511         }
512         fieldUpdates.add(Field.createFieldUpdate("urlcompname", messageName));
513         item.setFieldUpdates(fieldUpdates);
514         CreateItemMethod createItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, getFolderId(folderPath), item);
515         executeMethod(createItemMethod);
516     }
517 
518     @Override
519     public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
520         if (properties.containsKey("read") && "urn:content-classes:appointment".equals(message.contentClass)) {
521             properties.remove("read");
522         }
523         if (!properties.isEmpty()) {
524             UpdateItemMethod updateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
525                     ConflictResolution.AlwaysOverwrite,
526                     SendMeetingInvitationsOrCancellations.SendToNone,
527                     ((EwsExchangeSession.Message) message).itemId, buildProperties(properties));
528             executeMethod(updateItemMethod);
529         }
530     }
531 
532     @Override
533     public void deleteMessage(ExchangeSession.Message message) throws IOException {
534         LOGGER.debug("Delete " + message.imapUid);
535         DeleteItemMethod deleteItemMethod = new DeleteItemMethod(((EwsExchangeSession.Message) message).itemId, DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
536         executeMethod(deleteItemMethod);
537     }
538 
539 
540     protected void sendMessage(String itemClass, byte[] messageBody) throws IOException {
541         EWSMethod.Item item = new EWSMethod.Item();
542         item.type = "Message";
543         item.mimeContent = IOUtil.encodeBase64(messageBody);
544         if (itemClass != null) {
545             item.put("ItemClass", itemClass);
546         }
547 
548         MessageDisposition messageDisposition;
549         if (Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) {
550             messageDisposition = MessageDisposition.SendAndSaveCopy;
551         } else {
552             messageDisposition = MessageDisposition.SendOnly;
553         }
554 
555         CreateItemMethod createItemMethod = new CreateItemMethod(messageDisposition, getFolderId(SENT), item);
556         executeMethod(createItemMethod);
557     }
558 
559     @Override
560     public void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException {
561         String itemClass = null;
562         if (mimeMessage.getContentType().startsWith("multipart/report")) {
563             itemClass = "REPORT.IPM.Note.IPNRN";
564         }
565 
566         ByteArrayOutputStream baos = new ByteArrayOutputStream();
567         try {
568             mimeMessage.writeTo(baos);
569         } catch (MessagingException e) {
570             throw new IOException(e.getMessage());
571         }
572         sendMessage(itemClass, baos.toByteArray());
573     }
574 
575     /**
576      * @inheritDoc
577      */
578     @Override
579     protected byte[] getContent(ExchangeSession.Message message) throws IOException {
580         return getContent(((EwsExchangeSession.Message) message).itemId);
581     }
582 
583     /**
584      * Get item content.
585      *
586      * @param itemId EWS item id
587      * @return item content as byte array
588      * @throws IOException on error
589      */
590     protected byte[] getContent(ItemId itemId) throws IOException {
591         GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
592         byte[] mimeContent = null;
593         try {
594             executeMethod(getItemMethod);
595             mimeContent = getItemMethod.getMimeContent();
596         } catch (EWSException e) {
597             LOGGER.warn("GetItem with MimeContent failed: " + e.getMessage());
598         }
599         if (getItemMethod.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
600             throw new HttpNotFoundException("Item " + itemId + " not found");
601         }
602         if (mimeContent == null) {
603             LOGGER.warn("MimeContent not available, trying to rebuild from properties");
604             try {
605                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
606                 getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
607                 getItemMethod.addAdditionalProperty(Field.get("contentclass"));
608                 getItemMethod.addAdditionalProperty(Field.get("message-id"));
609                 getItemMethod.addAdditionalProperty(Field.get("from"));
610                 getItemMethod.addAdditionalProperty(Field.get("to"));
611                 getItemMethod.addAdditionalProperty(Field.get("cc"));
612                 getItemMethod.addAdditionalProperty(Field.get("subject"));
613                 getItemMethod.addAdditionalProperty(Field.get("date"));
614                 getItemMethod.addAdditionalProperty(Field.get("body"));
615                 executeMethod(getItemMethod);
616                 EWSMethod.Item item = getItemMethod.getResponseItem();
617 
618                 if (item == null) {
619                     throw new HttpNotFoundException("Item " + itemId + " not found");
620                 }
621 
622                 MimeMessage mimeMessage = new MimeMessage((Session) null);
623                 mimeMessage.addHeader("Content-class", item.get(Field.get("contentclass").getResponseName()));
624                 mimeMessage.setSentDate(parseDateFromExchange(item.get(Field.get("date").getResponseName())));
625                 mimeMessage.addHeader("From", item.get(Field.get("from").getResponseName()));
626                 mimeMessage.addHeader("To", item.get(Field.get("to").getResponseName()));
627                 mimeMessage.addHeader("Cc", item.get(Field.get("cc").getResponseName()));
628                 mimeMessage.setSubject(item.get(Field.get("subject").getResponseName()));
629                 String propertyValue = item.get(Field.get("body").getResponseName());
630                 if (propertyValue == null) {
631                     propertyValue = "";
632                 }
633                 mimeMessage.setContent(propertyValue, "text/html; charset=UTF-8");
634 
635                 mimeMessage.writeTo(baos);
636                 if (LOGGER.isDebugEnabled()) {
637                     LOGGER.debug("Rebuilt message content: " + new String(baos.toByteArray(), "UTF-8"));
638                 }
639                 mimeContent = baos.toByteArray();
640 
641             } catch (IOException e2) {
642                 LOGGER.warn(e2);
643             } catch (MessagingException e2) {
644                 LOGGER.warn(e2);
645             }
646             if (mimeContent == null) {
647                 throw new IOException("GetItem returned null MimeContent");
648             }
649         }
650         return mimeContent;
651     }
652 
653     protected Message buildMessage(EWSMethod.Item response) throws DavMailException {
654         Message message = new Message();
655 
656         // get item id
657         message.itemId = new ItemId(response);
658 
659         message.permanentUrl = response.get(Field.get("permanenturl").getResponseName());
660 
661         message.size = response.getInt(Field.get("messageSize").getResponseName());
662         message.uid = response.get(Field.get("uid").getResponseName());
663         message.contentClass = response.get(Field.get("contentclass").getResponseName());
664         message.imapUid = response.getLong(Field.get("imapUid").getResponseName());
665         message.read = response.getBoolean(Field.get("read").getResponseName());
666         message.junk = response.getBoolean(Field.get("junk").getResponseName());
667         message.flagged = "2".equals(response.get(Field.get("flagStatus").getResponseName()));
668         message.draft = (response.getInt(Field.get("messageFlags").getResponseName()) & 8) != 0;
669         String lastVerbExecuted = response.get(Field.get("lastVerbExecuted").getResponseName());
670         message.answered = "102".equals(lastVerbExecuted) || "103".equals(lastVerbExecuted);
671         message.forwarded = "104".equals(lastVerbExecuted);
672         message.date = convertDateFromExchange(response.get(Field.get("date").getResponseName()));
673         message.deleted = "1".equals(response.get(Field.get("deleted").getResponseName()));
674 
675         String lastmodified = convertDateFromExchange(response.get(Field.get("lastmodified").getResponseName()));
676         message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date);
677 
678         message.keywords = response.get(Field.get("keywords").getResponseName());
679 
680         if (LOGGER.isDebugEnabled()) {
681             StringBuilder buffer = new StringBuilder();
682             buffer.append("Message");
683             if (message.imapUid != 0) {
684                 buffer.append(" IMAP uid: ").append(message.imapUid);
685             }
686             if (message.uid != null) {
687                 buffer.append(" uid: ").append(message.uid);
688             }
689             buffer.append(" ItemId: ").append(message.itemId.id);
690             buffer.append(" ChangeKey: ").append(message.itemId.changeKey);
691             LOGGER.debug(buffer.toString());
692         }
693         return message;
694     }
695 
696     @Override
697     public MessageList searchMessages(String folderPath, Set<String> attributes, Condition condition) throws IOException {
698         MessageList messages = new MessageList();
699         int maxCount = Settings.getIntProperty("davmail.folderSizeLimit", 0);
700         List<EWSMethod.Item> responses = searchItems(folderPath, attributes, condition, FolderQueryTraversal.SHALLOW, maxCount);
701 
702         for (EWSMethod.Item response : responses) {
703             if (MESSAGE_TYPES.contains(response.type)) {
704                 Message message = buildMessage(response);
705                 message.messageList = messages;
706                 messages.add(message);
707             }
708         }
709         Collections.sort(messages);
710         return messages;
711     }
712 
713     protected List<EWSMethod.Item> searchItems(String folderPath, Set<String> attributes, Condition condition, FolderQueryTraversal folderQueryTraversal, int maxCount) throws IOException {
714         if (maxCount == 0) {
715             // unlimited search
716             return searchItems(folderPath, attributes, condition, folderQueryTraversal);
717         }
718         // limited search, do not use paged search, limit with maxCount, sort by imapUid descending to get latest items
719         int resultCount;
720         List<EWSMethod.Item> results = new ArrayList<EWSMethod.Item>();
721         FindItemMethod findItemMethod;
722 
723         // search items in folder, do not retrieve all properties
724         findItemMethod = new FindItemMethod(folderQueryTraversal, BaseShape.ID_ONLY, getFolderId(folderPath), 0, maxCount);
725         for (String attribute : attributes) {
726             findItemMethod.addAdditionalProperty(Field.get(attribute));
727         }
728         // make sure imapUid is available
729         if (!attributes.contains("imapUid")) {
730             findItemMethod.addAdditionalProperty(Field.get("imapUid"));
731         }
732 
733         // always sort items by imapUid descending to retrieve recent messages first
734         findItemMethod.setFieldOrder(new FieldOrder(Field.get("imapUid"), FieldOrder.Order.Descending));
735 
736         if (condition != null && !condition.isEmpty()) {
737             findItemMethod.setSearchExpression((SearchExpression) condition);
738         }
739         executeMethod(findItemMethod);
740         results.addAll(findItemMethod.getResponseItems());
741         resultCount = results.size();
742         if (resultCount > 0 && LOGGER.isDebugEnabled()) {
743             LOGGER.debug("Folder " + folderPath + " - Search items count: " + resultCount + " maxCount: " + maxCount
744                     + " highest uid: " + results.get(0).get(Field.get("imapUid").getResponseName())
745                     + " lowest uid: " + results.get(resultCount - 1).get(Field.get("imapUid").getResponseName()));
746         }
747 
748 
749         return results;
750     }
751 
752     /**
753      * Paged search, retrieve all items.
754      *
755      * @param folderPath           folder path
756      * @param attributes           attributes
757      * @param condition            search condition
758      * @param folderQueryTraversal search mode
759      * @return items
760      * @throws IOException on error
761      */
762     protected List<EWSMethod.Item> searchItems(String folderPath, Set<String> attributes, Condition condition, FolderQueryTraversal folderQueryTraversal) throws IOException {
763         int resultCount = 0;
764         List<EWSMethod.Item> results = new ArrayList<EWSMethod.Item>();
765         FolderId folderId = getFolderId(folderPath);
766         FindItemMethod findItemMethod;
767         do {
768             // search items in folder, do not retrieve all properties
769             findItemMethod = new FindItemMethod(folderQueryTraversal, BaseShape.ID_ONLY, folderId, resultCount, PAGE_SIZE);
770             for (String attribute : attributes) {
771                 findItemMethod.addAdditionalProperty(Field.get(attribute));
772             }
773             // make sure imapUid is available
774             if (!attributes.contains("imapUid")) {
775                 findItemMethod.addAdditionalProperty(Field.get("imapUid"));
776             }
777 
778             // always sort items by imapUid ascending to retrieve pages in creation order
779             findItemMethod.setFieldOrder(new FieldOrder(Field.get("imapUid"), FieldOrder.Order.Ascending));
780 
781             if (condition != null && !condition.isEmpty()) {
782                 findItemMethod.setSearchExpression((SearchExpression) condition);
783             }
784             executeMethod(findItemMethod);
785 
786             long highestUid = 0;
787             if (resultCount > 0) {
788                 highestUid = Long.parseLong(results.get(resultCount - 1).get(Field.get("imapUid").getResponseName()));
789             }
790             // Only add new result if not already available (concurrent folder changes issue)
791             for (EWSMethod.Item item : findItemMethod.getResponseItems()) {
792                 long imapUid = Long.parseLong(item.get(Field.get("imapUid").getResponseName()));
793                 if (imapUid > highestUid) {
794                     results.add(item);
795                 }
796             }
797             resultCount = results.size();
798             if (resultCount > 0 && LOGGER.isDebugEnabled()) {
799                 LOGGER.debug("Folder " + folderPath + " - Search items current count: " + resultCount + " fetchCount: " + PAGE_SIZE
800                         + " highest uid: " + results.get(resultCount - 1).get(Field.get("imapUid").getResponseName())
801                         + " lowest uid: " + results.get(0).get(Field.get("imapUid").getResponseName()));
802             }
803             if (Thread.interrupted()) {
804                 LOGGER.debug("Folder " + folderPath + " - Search items failed: Interrupted by client");
805                 throw new IOException("Search items failed: Interrupted by client");
806             }
807         } while (!(findItemMethod.includesLastItemInRange));
808         return results;
809     }
810 
811     protected static class MultiCondition extends ExchangeSession.MultiCondition implements SearchExpression {
812         protected MultiCondition(Operator operator, Condition... condition) {
813             super(operator, condition);
814         }
815 
816         public void appendTo(StringBuilder buffer) {
817             int actualConditionCount = 0;
818             for (Condition condition : conditions) {
819                 if (!condition.isEmpty()) {
820                     actualConditionCount++;
821                 }
822             }
823             if (actualConditionCount > 0) {
824                 if (actualConditionCount > 1) {
825                     buffer.append("<t:").append(operator.toString()).append('>');
826                 }
827 
828                 for (Condition condition : conditions) {
829                     condition.appendTo(buffer);
830                 }
831 
832                 if (actualConditionCount > 1) {
833                     buffer.append("</t:").append(operator.toString()).append('>');
834                 }
835             }
836         }
837     }
838 
839     protected static class NotCondition extends ExchangeSession.NotCondition implements SearchExpression {
840         protected NotCondition(Condition condition) {
841             super(condition);
842         }
843 
844         public void appendTo(StringBuilder buffer) {
845             buffer.append("<t:Not>");
846             condition.appendTo(buffer);
847             buffer.append("</t:Not>");
848         }
849     }
850 
851 
852     protected static class AttributeCondition extends ExchangeSession.AttributeCondition implements SearchExpression {
853         protected ContainmentMode containmentMode;
854         protected ContainmentComparison containmentComparison;
855 
856         protected AttributeCondition(String attributeName, Operator operator, String value) {
857             super(attributeName, operator, value);
858         }
859 
860         protected AttributeCondition(String attributeName, Operator operator, String value,
861                                      ContainmentMode containmentMode, ContainmentComparison containmentComparison) {
862             super(attributeName, operator, value);
863             this.containmentMode = containmentMode;
864             this.containmentComparison = containmentComparison;
865         }
866 
867         protected FieldURI getFieldURI() {
868             FieldURI fieldURI = Field.get(attributeName);
869             if (fieldURI == null) {
870                 throw new IllegalArgumentException("Unknown field: " + attributeName);
871             }
872             return fieldURI;
873         }
874 
875         protected Operator getOperator() {
876             return operator;
877         }
878 
879         public void appendTo(StringBuilder buffer) {
880             buffer.append("<t:").append(operator.toString());
881             if (containmentMode != null) {
882                 containmentMode.appendTo(buffer);
883             }
884             if (containmentComparison != null) {
885                 containmentComparison.appendTo(buffer);
886             }
887             buffer.append('>');
888             FieldURI fieldURI = getFieldURI();
889             fieldURI.appendTo(buffer);
890 
891             if (operator != Operator.Contains) {
892                 buffer.append("<t:FieldURIOrConstant>");
893             }
894             buffer.append("<t:Constant Value=\"");
895             // encode urlcompname
896             if (fieldURI instanceof ExtendedFieldURI && "0x10f3".equals(((ExtendedFieldURI) fieldURI).propertyTag)) {
897                 buffer.append(StringUtil.xmlEncodeAttribute(StringUtil.encodeUrlcompname(value)));
898             } else if (fieldURI instanceof ExtendedFieldURI
899                     && ((ExtendedFieldURI) fieldURI).propertyType == ExtendedFieldURI.PropertyType.Integer) {
900                 // check value
901                 try {
902                     //noinspection ResultOfMethodCallIgnored
903                     Integer.parseInt(value);
904                     buffer.append(value);
905                 } catch (NumberFormatException e) {
906                     // invalid value, replace with 0
907                     buffer.append('0');
908                 }
909             } else {
910                 buffer.append(StringUtil.xmlEncodeAttribute(value));
911             }
912             buffer.append("\"/>");
913             if (operator != Operator.Contains) {
914                 buffer.append("</t:FieldURIOrConstant>");
915             }
916 
917             buffer.append("</t:").append(operator.toString()).append('>');
918         }
919 
920         public boolean isMatch(ExchangeSession.Contact contact) {
921             String lowerCaseValue = value.toLowerCase();
922 
923             String actualValue = contact.get(attributeName);
924             if (actualValue == null) {
925                 return false;
926             }
927             actualValue = actualValue.toLowerCase();
928             if (operator == Operator.IsEqualTo) {
929                 return lowerCaseValue.equals(actualValue);
930             } else {
931                 return operator == Operator.Contains && ((containmentMode.equals(ContainmentMode.Substring) && actualValue.contains(lowerCaseValue)) ||
932                         (containmentMode.equals(ContainmentMode.Prefixed) && actualValue.startsWith(lowerCaseValue)));
933             }
934         }
935 
936     }
937 
938     protected static class HeaderCondition extends AttributeCondition {
939 
940         protected HeaderCondition(String attributeName, String value) {
941             super(attributeName, Operator.Contains, value);
942             containmentMode = ContainmentMode.Substring;
943             containmentComparison = ContainmentComparison.IgnoreCase;
944         }
945 
946         @Override
947         protected FieldURI getFieldURI() {
948             return new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, attributeName);
949         }
950 
951     }
952 
953     protected static class IsNullCondition implements ExchangeSession.Condition, SearchExpression {
954         protected final String attributeName;
955 
956         protected IsNullCondition(String attributeName) {
957             this.attributeName = attributeName;
958         }
959 
960         public void appendTo(StringBuilder buffer) {
961             buffer.append("<t:Not><t:Exists>");
962             Field.get(attributeName).appendTo(buffer);
963             buffer.append("</t:Exists></t:Not>");
964         }
965 
966         public boolean isEmpty() {
967             return false;
968         }
969 
970         public boolean isMatch(ExchangeSession.Contact contact) {
971             String actualValue = contact.get(attributeName);
972             return actualValue == null;
973         }
974 
975     }
976 
977     protected static class ExistsCondition implements ExchangeSession.Condition, SearchExpression {
978         protected final String attributeName;
979 
980         protected ExistsCondition(String attributeName) {
981             this.attributeName = attributeName;
982         }
983 
984         public void appendTo(StringBuilder buffer) {
985             buffer.append("<t:Exists>");
986             Field.get(attributeName).appendTo(buffer);
987             buffer.append("</t:Exists>");
988         }
989 
990         public boolean isEmpty() {
991             return false;
992         }
993 
994         public boolean isMatch(ExchangeSession.Contact contact) {
995             String actualValue = contact.get(attributeName);
996             return actualValue == null;
997         }
998 
999     }
1000 
1001     @Override
1002     public ExchangeSession.MultiCondition and(Condition... condition) {
1003         return new MultiCondition(Operator.And, condition);
1004     }
1005 
1006     @Override
1007     public ExchangeSession.MultiCondition or(Condition... condition) {
1008         return new MultiCondition(Operator.Or, condition);
1009     }
1010 
1011     @Override
1012     public Condition not(Condition condition) {
1013         return new NotCondition(condition);
1014     }
1015 
1016     @Override
1017     public Condition isEqualTo(String attributeName, String value) {
1018         return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
1019     }
1020 
1021     @Override
1022     public Condition isEqualTo(String attributeName, int value) {
1023         return new AttributeCondition(attributeName, Operator.IsEqualTo, String.valueOf(value));
1024     }
1025 
1026     @Override
1027     public Condition headerIsEqualTo(String headerName, String value) {
1028         if (serverVersion.startsWith("Exchange201")) {
1029             if ("from".equals(headerName)
1030                     || "to".equals(headerName)
1031                     || "cc".equals(headerName)) {
1032                 return new AttributeCondition("msg" + headerName, Operator.Contains, value, ContainmentMode.Substring, ContainmentComparison.IgnoreCase);
1033             } else if ("message-id".equals(headerName)
1034                     || "bcc".equals(headerName)) {
1035                 return new AttributeCondition(headerName, Operator.Contains, value, ContainmentMode.Substring, ContainmentComparison.IgnoreCase);
1036             } else {
1037                 // Exchange 2010 does not support header search, use PR_TRANSPORT_MESSAGE_HEADERS instead
1038                 return new AttributeCondition("messageheaders", Operator.Contains, headerName + ": " + value, ContainmentMode.Substring, ContainmentComparison.IgnoreCase);
1039             }
1040         } else {
1041             return new HeaderCondition(headerName, value);
1042         }
1043     }
1044 
1045     @Override
1046     public Condition gte(String attributeName, String value) {
1047         return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value);
1048     }
1049 
1050     @Override
1051     public Condition lte(String attributeName, String value) {
1052         return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value);
1053     }
1054 
1055     @Override
1056     public Condition lt(String attributeName, String value) {
1057         return new AttributeCondition(attributeName, Operator.IsLessThan, value);
1058     }
1059 
1060     @Override
1061     public Condition gt(String attributeName, String value) {
1062         return new AttributeCondition(attributeName, Operator.IsGreaterThan, value);
1063     }
1064 
1065     @Override
1066     public Condition contains(String attributeName, String value) {
1067         // workaround for from: and to: headers not searchable over EWS
1068         if ("from".equals(attributeName)) {
1069             attributeName = "msgfrom";
1070         } else if ("to".equals(attributeName)) {
1071             attributeName = "displayto";
1072         } else if ("cc".equals(attributeName)) {
1073             attributeName = "displaycc";
1074         }
1075         return new AttributeCondition(attributeName, Operator.Contains, value, ContainmentMode.Substring, ContainmentComparison.IgnoreCase);
1076     }
1077 
1078     @Override
1079     public Condition startsWith(String attributeName, String value) {
1080         return new AttributeCondition(attributeName, Operator.Contains, value, ContainmentMode.Prefixed, ContainmentComparison.IgnoreCase);
1081     }
1082 
1083     @Override
1084     public Condition isNull(String attributeName) {
1085         return new IsNullCondition(attributeName);
1086     }
1087 
1088     @Override
1089     public Condition exists(String attributeName) {
1090         return new ExistsCondition(attributeName);
1091     }
1092 
1093     @Override
1094     public Condition isTrue(String attributeName) {
1095         return new AttributeCondition(attributeName, Operator.IsEqualTo, "true");
1096     }
1097 
1098     @Override
1099     public Condition isFalse(String attributeName) {
1100         return new AttributeCondition(attributeName, Operator.IsEqualTo, "false");
1101     }
1102 
1103     protected static final HashSet<FieldURI> FOLDER_PROPERTIES = new HashSet<FieldURI>();
1104 
1105     static {
1106         FOLDER_PROPERTIES.add(Field.get("urlcompname"));
1107         FOLDER_PROPERTIES.add(Field.get("folderDisplayName"));
1108         FOLDER_PROPERTIES.add(Field.get("lastmodified"));
1109         FOLDER_PROPERTIES.add(Field.get("folderclass"));
1110         FOLDER_PROPERTIES.add(Field.get("ctag"));
1111         FOLDER_PROPERTIES.add(Field.get("count"));
1112         FOLDER_PROPERTIES.add(Field.get("unread"));
1113         FOLDER_PROPERTIES.add(Field.get("hassubs"));
1114         FOLDER_PROPERTIES.add(Field.get("uidNext"));
1115         FOLDER_PROPERTIES.add(Field.get("highestUid"));
1116     }
1117 
1118     protected Folder buildFolder(EWSMethod.Item item) {
1119         Folder folder = new Folder();
1120         folder.folderId = new FolderId(item);
1121         folder.displayName = item.get(Field.get("folderDisplayName").getResponseName());
1122         folder.folderClass = item.get(Field.get("folderclass").getResponseName());
1123         folder.etag = item.get(Field.get("lastmodified").getResponseName());
1124         folder.ctag = item.get(Field.get("ctag").getResponseName());
1125         folder.count = item.getInt(Field.get("count").getResponseName());
1126         folder.unreadCount = item.getInt(Field.get("unread").getResponseName());
1127         // fake recent value
1128         folder.recent = folder.unreadCount;
1129         folder.hasChildren = item.getBoolean(Field.get("hassubs").getResponseName());
1130         // noInferiors not implemented
1131         folder.uidNext = item.getInt(Field.get("uidNext").getResponseName());
1132         return folder;
1133     }
1134 
1135     /**
1136      * @inheritDoc
1137      */
1138     @Override
1139     public List<ExchangeSession.Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
1140         String baseFolderPath = folderPath;
1141         if (baseFolderPath.startsWith("/users/")) {
1142             int index = baseFolderPath.indexOf('/', "/users/".length());
1143             if (index >= 0) {
1144                 baseFolderPath = baseFolderPath.substring(index + 1);
1145             }
1146         }
1147         List<ExchangeSession.Folder> folders = new ArrayList<ExchangeSession.Folder>();
1148         appendSubFolders(folders, baseFolderPath, getFolderId(folderPath), condition, recursive);
1149         return folders;
1150     }
1151 
1152     protected void appendSubFolders(List<ExchangeSession.Folder> folders,
1153                                     String parentFolderPath, FolderId parentFolderId,
1154                                     Condition condition, boolean recursive) throws IOException {
1155         int resultCount = 0;
1156         FindFolderMethod findFolderMethod;
1157         do {
1158             findFolderMethod = new FindFolderMethod(FolderQueryTraversal.SHALLOW,
1159                     BaseShape.ID_ONLY, parentFolderId, FOLDER_PROPERTIES, (SearchExpression) condition, resultCount, PAGE_SIZE);
1160             executeMethod(findFolderMethod);
1161             for (EWSMethod.Item item : findFolderMethod.getResponseItems()) {
1162                 resultCount++;
1163                 Folder folder = buildFolder(item);
1164                 if (parentFolderPath.length() > 0) {
1165                     if (parentFolderPath.endsWith("/")) {
1166                         folder.folderPath = parentFolderPath + item.get(Field.get("folderDisplayName").getResponseName());
1167                     } else {
1168                         folder.folderPath = parentFolderPath + '/' + item.get(Field.get("folderDisplayName").getResponseName());
1169                     }
1170                 } else if (folderIdMap.get(folder.folderId.value) != null) {
1171                     folder.folderPath = folderIdMap.get(folder.folderId.value);
1172                 } else {
1173                     folder.folderPath = item.get(Field.get("folderDisplayName").getResponseName());
1174                 }
1175                 folders.add(folder);
1176                 if (recursive && folder.hasChildren) {
1177                     appendSubFolders(folders, folder.folderPath, folder.folderId, condition, true);
1178                 }
1179             }
1180         } while (!(findFolderMethod.includesLastItemInRange));
1181     }
1182 
1183     /**
1184      * Get folder by path.
1185      *
1186      * @param folderPath folder path
1187      * @return folder object
1188      * @throws IOException on error
1189      */
1190     @Override
1191     protected EwsExchangeSession.Folder internalGetFolder(String folderPath) throws IOException {
1192         FolderId folderId = getFolderId(folderPath);
1193         GetFolderMethod getFolderMethod = new GetFolderMethod(BaseShape.ID_ONLY, folderId, FOLDER_PROPERTIES);
1194         executeMethod(getFolderMethod);
1195         EWSMethod.Item item = getFolderMethod.getResponseItem();
1196         Folder folder;
1197         if (item != null) {
1198             folder = buildFolder(item);
1199             folder.folderPath = folderPath;
1200         } else {
1201             throw new HttpNotFoundException("Folder " + folderPath + " not found");
1202         }
1203         return folder;
1204     }
1205 
1206     /**
1207      * @inheritDoc
1208      */
1209     @Override
1210     public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException {
1211         FolderPath path = new FolderPath(folderPath);
1212         EWSMethod.Item folder = new EWSMethod.Item();
1213         folder.type = "Folder";
1214         folder.put("FolderClass", folderClass);
1215         folder.put("DisplayName", path.folderName);
1216         // TODO: handle properties
1217         CreateFolderMethod createFolderMethod = new CreateFolderMethod(getFolderId(path.parentPath), folder);
1218         executeMethod(createFolderMethod);
1219         return HttpStatus.SC_CREATED;
1220     }
1221 
1222     /**
1223      * @inheritDoc
1224      */
1225     @Override
1226     public int updateFolder(String folderPath, Map<String, String> properties) throws IOException {
1227         ArrayList<FieldUpdate> updates = new ArrayList<FieldUpdate>();
1228         for (Map.Entry<String, String> entry : properties.entrySet()) {
1229             updates.add(new FieldUpdate(Field.get(entry.getKey()), entry.getValue()));
1230         }
1231         UpdateFolderMethod updateFolderMethod = new UpdateFolderMethod(internalGetFolder(folderPath).folderId, updates);
1232 
1233         executeMethod(updateFolderMethod);
1234         return HttpStatus.SC_CREATED;
1235     }
1236 
1237     /**
1238      * @inheritDoc
1239      */
1240     @Override
1241     public void deleteFolder(String folderPath) throws IOException {
1242         FolderId folderId = getFolderIdIfExists(folderPath);
1243         if (folderId != null) {
1244             DeleteFolderMethod deleteFolderMethod = new DeleteFolderMethod(folderId);
1245             executeMethod(deleteFolderMethod);
1246         } else {
1247             LOGGER.debug("Folder " + folderPath + " not found");
1248         }
1249     }
1250 
1251     /**
1252      * @inheritDoc
1253      */
1254     @Override
1255     public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
1256         MoveItemMethod moveItemMethod = new MoveItemMethod(((EwsExchangeSession.Message) message).itemId, getFolderId(targetFolder));
1257         executeMethod(moveItemMethod);
1258     }
1259 
1260     /**
1261      * @inheritDoc
1262      */
1263     @Override
1264     public void moveMessages(List<ExchangeSession.Message> messages, String targetFolder) throws IOException {
1265         ArrayList<ItemId> itemIds = new ArrayList<ItemId>();
1266         for (ExchangeSession.Message message: messages) {
1267             itemIds.add(((EwsExchangeSession.Message) message).itemId);
1268         }
1269 
1270         MoveItemMethod moveItemMethod = new MoveItemMethod(itemIds, getFolderId(targetFolder));
1271         executeMethod(moveItemMethod);
1272     }
1273 
1274     /**
1275      * @inheritDoc
1276      */
1277     @Override
1278     public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
1279         CopyItemMethod copyItemMethod = new CopyItemMethod(((EwsExchangeSession.Message) message).itemId, getFolderId(targetFolder));
1280         executeMethod(copyItemMethod);
1281     }
1282 
1283     /**
1284      * @inheritDoc
1285      */
1286     @Override
1287     public void copyMessages(List<ExchangeSession.Message> messages, String targetFolder) throws IOException {
1288         ArrayList<ItemId> itemIds = new ArrayList<ItemId>();
1289         for (ExchangeSession.Message message: messages) {
1290             itemIds.add(((EwsExchangeSession.Message) message).itemId);
1291         }
1292 
1293         CopyItemMethod copyItemMethod = new CopyItemMethod(itemIds, getFolderId(targetFolder));
1294         executeMethod(copyItemMethod);
1295     }
1296 
1297     /**
1298      * @inheritDoc
1299      */
1300     @Override
1301     public void moveFolder(String folderPath, String targetFolderPath) throws IOException {
1302         FolderPath path = new FolderPath(folderPath);
1303         FolderPath targetPath = new FolderPath(targetFolderPath);
1304         FolderId folderId = getFolderId(folderPath);
1305         FolderId toFolderId = getFolderId(targetPath.parentPath);
1306         toFolderId.changeKey = null;
1307         // move folder
1308         if (!path.parentPath.equals(targetPath.parentPath)) {
1309             MoveFolderMethod moveFolderMethod = new MoveFolderMethod(folderId, toFolderId);
1310             executeMethod(moveFolderMethod);
1311         }
1312         // rename folder
1313         if (!path.folderName.equals(targetPath.folderName)) {
1314             ArrayList<FieldUpdate> updates = new ArrayList<FieldUpdate>();
1315             updates.add(new FieldUpdate(Field.get("folderDisplayName"), targetPath.folderName));
1316             UpdateFolderMethod updateFolderMethod = new UpdateFolderMethod(folderId, updates);
1317             executeMethod(updateFolderMethod);
1318         }
1319     }
1320 
1321     @Override
1322     public void moveItem(String sourcePath, String targetPath) throws IOException {
1323         FolderPath sourceFolderPath = new FolderPath(sourcePath);
1324         Item item = getItem(sourceFolderPath.parentPath, sourceFolderPath.folderName);
1325         FolderPath targetFolderPath = new FolderPath(targetPath);
1326         FolderId toFolderId = getFolderId(targetFolderPath.parentPath);
1327         MoveItemMethod moveItemMethod = new MoveItemMethod(((Event) item).itemId, toFolderId);
1328         executeMethod(moveItemMethod);
1329     }
1330 
1331     /**
1332      * @inheritDoc
1333      */
1334     @Override
1335     protected void moveToTrash(ExchangeSession.Message message) throws IOException {
1336         MoveItemMethod moveItemMethod = new MoveItemMethod(((EwsExchangeSession.Message) message).itemId, getFolderId(TRASH));
1337         executeMethod(moveItemMethod);
1338     }
1339 
1340     protected class Contact extends ExchangeSession.Contact {
1341         // item id
1342         ItemId itemId;
1343 
1344         protected Contact(EWSMethod.Item response) throws DavMailException {
1345             itemId = new ItemId(response);
1346 
1347             permanentUrl = response.get(Field.get("permanenturl").getResponseName());
1348             etag = response.get(Field.get("etag").getResponseName());
1349             displayName = response.get(Field.get("displayname").getResponseName());
1350             itemName = StringUtil.decodeUrlcompname(response.get(Field.get("urlcompname").getResponseName()));
1351             // workaround for missing urlcompname in Exchange 2010
1352             if (itemName == null) {
1353                 itemName = StringUtil.base64ToUrl(itemId.id) + ".EML";
1354             }
1355             for (String attributeName : CONTACT_ATTRIBUTES) {
1356                 String value = response.get(Field.get(attributeName).getResponseName());
1357                 if (value != null && value.length() > 0) {
1358                     if ("bday".equals(attributeName) || "anniversary".equals(attributeName) || "lastmodified".equals(attributeName) || "datereceived".equals(attributeName)) {
1359                         value = convertDateFromExchange(value);
1360                     }
1361                     put(attributeName, value);
1362                 }
1363             }
1364         }
1365 
1366         /**
1367          * @inheritDoc
1368          */
1369         protected Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
1370             super(folderPath, itemName, properties, etag, noneMatch);
1371         }
1372 
1373         /**
1374          * Empty constructor for GalFind
1375          */
1376         protected Contact() {
1377         }
1378 
1379         protected void buildProperties(List<FieldUpdate> updates) {
1380             for (Map.Entry<String, String> entry : entrySet()) {
1381                 if ("photo".equals(entry.getKey())) {
1382                     updates.add(Field.createFieldUpdate("haspicture", "true"));
1383                 } else if (!entry.getKey().startsWith("email") && !entry.getKey().startsWith("smtpemail")
1384                         && !"fileas".equals(entry.getKey())) {
1385                     updates.add(Field.createFieldUpdate(entry.getKey(), entry.getValue()));
1386                 }
1387             }
1388             if (get("fileas") != null) {
1389                 updates.add(Field.createFieldUpdate("fileas", get("fileas")));
1390             }
1391             // handle email addresses
1392             IndexedFieldUpdate emailFieldUpdate = null;
1393             for (Map.Entry<String, String> entry : entrySet()) {
1394                 if (entry.getKey().startsWith("smtpemail")) {
1395                     if (emailFieldUpdate == null) {
1396                         emailFieldUpdate = new IndexedFieldUpdate("EmailAddresses");
1397                     }
1398                     emailFieldUpdate.addFieldValue(Field.createFieldUpdate(entry.getKey(), entry.getValue()));
1399                 }
1400             }
1401             if (emailFieldUpdate != null) {
1402                 updates.add(emailFieldUpdate);
1403             }
1404         }
1405 
1406 
1407         /**
1408          * Create or update contact
1409          *
1410          * @return action result
1411          * @throws IOException on error
1412          */
1413         public ItemResult createOrUpdate() throws IOException {
1414             String photo = get("photo");
1415 
1416             ItemResult itemResult = new ItemResult();
1417             EWSMethod createOrUpdateItemMethod;
1418 
1419             // first try to load existing event
1420             String currentEtag = null;
1421             ItemId currentItemId = null;
1422             FileAttachment currentFileAttachment = null;
1423             EWSMethod.Item currentItem = getEwsItem(folderPath, itemName);
1424             if (currentItem != null) {
1425                 currentItemId = new ItemId(currentItem);
1426                 currentEtag = currentItem.get(Field.get("etag").getResponseName());
1427 
1428                 // load current picture
1429                 GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, currentItemId, false);
1430                 getItemMethod.addAdditionalProperty(Field.get("attachments"));
1431                 executeMethod(getItemMethod);
1432                 EWSMethod.Item item = getItemMethod.getResponseItem();
1433                 if (item != null) {
1434                     currentFileAttachment = item.getAttachmentByName("ContactPicture.jpg");
1435                 }
1436             }
1437             if ("*".equals(noneMatch)) {
1438                 // create requested
1439                 //noinspection VariableNotUsedInsideIf
1440                 if (currentItemId != null) {
1441                     itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1442                     return itemResult;
1443                 }
1444             } else if (etag != null) {
1445                 // update requested
1446                 if (currentItemId == null || !etag.equals(currentEtag)) {
1447                     itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1448                     return itemResult;
1449                 }
1450             }
1451 
1452             List<FieldUpdate> properties = new ArrayList<FieldUpdate>();
1453             if (currentItemId != null) {
1454                 buildProperties(properties);
1455                 // update
1456                 createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
1457                         ConflictResolution.AlwaysOverwrite,
1458                         SendMeetingInvitationsOrCancellations.SendToNone,
1459                         currentItemId, properties);
1460             } else {
1461                 // create
1462                 EWSMethod.Item newItem = new EWSMethod.Item();
1463                 newItem.type = "Contact";
1464                 // force urlcompname on create
1465                 properties.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
1466                 buildProperties(properties);
1467                 newItem.setFieldUpdates(properties);
1468                 createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, getFolderId(folderPath), newItem);
1469             }
1470             executeMethod(createOrUpdateItemMethod);
1471 
1472             itemResult.status = createOrUpdateItemMethod.getStatusCode();
1473             if (itemResult.status == HttpURLConnection.HTTP_OK) {
1474                 //noinspection VariableNotUsedInsideIf
1475                 if (etag == null) {
1476                     itemResult.status = HttpStatus.SC_CREATED;
1477                     LOGGER.debug("Created contact " + getHref());
1478                 } else {
1479                     LOGGER.debug("Updated contact " + getHref());
1480                 }
1481             } else {
1482                 return itemResult;
1483             }
1484 
1485             ItemId newItemId = new ItemId(createOrUpdateItemMethod.getResponseItem());
1486 
1487             // disable contact picture handling on Exchange 2007
1488             if (!"Exchange2007_SP1".equals(serverVersion)) {
1489                 // first delete current picture
1490                 if (currentFileAttachment != null) {
1491                     DeleteAttachmentMethod deleteAttachmentMethod = new DeleteAttachmentMethod(currentFileAttachment.attachmentId);
1492                     executeMethod(deleteAttachmentMethod);
1493                 }
1494 
1495                 if (photo != null) {
1496                     // convert image to jpeg
1497                     byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90);
1498 
1499                     FileAttachment attachment = new FileAttachment("ContactPicture.jpg", "image/jpeg", IOUtil.encodeBase64AsString(resizedImageBytes));
1500                     attachment.setIsContactPhoto(true);
1501 
1502                     // update photo attachment
1503                     CreateAttachmentMethod createAttachmentMethod = new CreateAttachmentMethod(newItemId, attachment);
1504                     executeMethod(createAttachmentMethod);
1505                 }
1506             }
1507 
1508             GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, newItemId, false);
1509             getItemMethod.addAdditionalProperty(Field.get("etag"));
1510             executeMethod(getItemMethod);
1511             itemResult.etag = getItemMethod.getResponseItem().get(Field.get("etag").getResponseName());
1512 
1513             return itemResult;
1514         }
1515     }
1516 
1517     protected class Event extends ExchangeSession.Event {
1518         // item id
1519         ItemId itemId;
1520         String type;
1521         boolean isException;
1522 
1523         protected Event(String folderPath, EWSMethod.Item response) {
1524             this.folderPath = folderPath;
1525             itemId = new ItemId(response);
1526 
1527             type = response.type;
1528 
1529             permanentUrl = response.get(Field.get("permanenturl").getResponseName());
1530             etag = response.get(Field.get("etag").getResponseName());
1531             displayName = response.get(Field.get("displayname").getResponseName());
1532             subject = response.get(Field.get("subject").getResponseName());
1533             itemName = StringUtil.decodeUrlcompname(response.get(Field.get("urlcompname").getResponseName()));
1534             // workaround for missing urlcompname in Exchange 2010
1535             if (itemName == null) {
1536                 itemName = StringUtil.base64ToUrl(itemId.id) + ".EML";
1537             }
1538             String instancetype = response.get(Field.get("instancetype").getResponseName());
1539             isException = "3".equals(instancetype);
1540         }
1541 
1542         /**
1543          * @inheritDoc
1544          */
1545         protected Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
1546             super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
1547         }
1548 
1549         @Override
1550         public ItemResult createOrUpdate() throws IOException {
1551             if (vCalendar.isTodo() && isMainCalendar(folderPath)) {
1552                 // task item, move to tasks folder
1553                 folderPath = TASKS;
1554             }
1555 
1556             ItemResult itemResult = new ItemResult();
1557             EWSMethod createOrUpdateItemMethod;
1558 
1559             // first try to load existing event
1560             String currentEtag = null;
1561             ItemId currentItemId = null;
1562             String ownerResponseReply = null;
1563 
1564             EWSMethod.Item currentItem = getEwsItem(folderPath, itemName);
1565             if (currentItem != null) {
1566                 currentItemId = new ItemId(currentItem);
1567                 currentEtag = currentItem.get(Field.get("etag").getResponseName());
1568                 LOGGER.debug("Existing item found with etag: " + currentEtag + " client etag: " + etag + " id: " + currentItemId.id);
1569             }
1570             if ("*".equals(noneMatch)) {
1571                 // create requested
1572                 //noinspection VariableNotUsedInsideIf
1573                 if (currentItemId != null) {
1574                     itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1575                     return itemResult;
1576                 }
1577             } else if (etag != null) {
1578                 // update requested
1579                 if (currentItemId == null || !etag.equals(currentEtag)) {
1580                     itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1581                     return itemResult;
1582                 }
1583             }
1584             if (vCalendar.isTodo()) {
1585                 // create or update task method
1586                 EWSMethod.Item newItem = new EWSMethod.Item();
1587                 newItem.type = "Task";
1588                 List<FieldUpdate> updates = new ArrayList<FieldUpdate>();
1589                 updates.add(Field.createFieldUpdate("importance", convertPriorityToExchange(vCalendar.getFirstVeventPropertyValue("PRIORITY"))));
1590                 updates.add(Field.createFieldUpdate("calendaruid", vCalendar.getFirstVeventPropertyValue("UID")));
1591                 // force urlcompname
1592                 updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
1593                 updates.add(Field.createFieldUpdate("subject", vCalendar.getFirstVeventPropertyValue("SUMMARY")));
1594                 updates.add(Field.createFieldUpdate("description", vCalendar.getFirstVeventPropertyValue("DESCRIPTION")));
1595                 updates.add(Field.createFieldUpdate("keywords", vCalendar.getFirstVeventPropertyValue("CATEGORIES")));
1596                 updates.add(Field.createFieldUpdate("startdate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
1597                 updates.add(Field.createFieldUpdate("duedate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
1598                 updates.add(Field.createFieldUpdate("datecompleted", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("COMPLETED"))));
1599 
1600                 updates.add(Field.createFieldUpdate("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
1601                 updates.add(Field.createFieldUpdate("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
1602 
1603                 String percentComplete = vCalendar.getFirstVeventPropertyValue("PERCENT-COMPLETE");
1604                 if (percentComplete == null) {
1605                     percentComplete = "0";
1606                 }
1607                 updates.add(Field.createFieldUpdate("percentcomplete", percentComplete));
1608                 String vTodoStatus = vCalendar.getFirstVeventPropertyValue("STATUS");
1609                 if (vTodoStatus == null) {
1610                     updates.add(Field.createFieldUpdate("taskstatus", "NotStarted"));
1611                 } else {
1612                     updates.add(Field.createFieldUpdate("taskstatus", vTodoToTaskStatusMap.get(vTodoStatus)));
1613                 }
1614 
1615                 //updates.add(Field.createFieldUpdate("iscomplete", "COMPLETED".equals(vTodoStatus)?"True":"False"));
1616 
1617                 if (currentItemId != null) {
1618                     // update
1619                     createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
1620                             ConflictResolution.AutoResolve,
1621                             SendMeetingInvitationsOrCancellations.SendToNone,
1622                             currentItemId, updates);
1623                 } else {
1624                     newItem.setFieldUpdates(updates);
1625                     // create
1626                     createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId(folderPath), newItem);
1627                 }
1628 
1629             } else {
1630 
1631                 if (currentItemId != null) {
1632                     /*Set<FieldUpdate> updates = new HashSet<FieldUpdate>();
1633                     // TODO: update properties instead of brute force delete/add
1634                     updates.add(new FieldUpdate(Field.get("mimeContent"), new String(Base64.encodeBase64AsString(itemContent))));
1635                     // update
1636                     createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
1637                            ConflictResolution.AutoResolve,
1638                            SendMeetingInvitationsOrCancellations.SendToNone,
1639                            currentItemId, updates);*/
1640                     // hard method: delete/create on update
1641                     DeleteItemMethod deleteItemMethod = new DeleteItemMethod(currentItemId, DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
1642                     executeMethod(deleteItemMethod);
1643                 } //else {
1644                 // create
1645                 EWSMethod.Item newItem = new EWSMethod.Item();
1646                 newItem.type = "CalendarItem";
1647                 newItem.mimeContent = IOUtil.encodeBase64(vCalendar.toString());
1648                 ArrayList<FieldUpdate> updates = new ArrayList<FieldUpdate>();
1649                 if (!vCalendar.hasVAlarm()) {
1650                     updates.add(Field.createFieldUpdate("reminderset", "false"));
1651                 }
1652                 //updates.add(Field.createFieldUpdate("outlookmessageclass", "IPM.Appointment"));
1653                 // force urlcompname
1654                 updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
1655                 if (vCalendar.isMeeting()) {
1656                     if (vCalendar.isMeetingOrganizer()) {
1657                         updates.add(Field.createFieldUpdate("apptstateflags", "1"));
1658                     } else {
1659                         updates.add(Field.createFieldUpdate("apptstateflags", "3"));
1660                     }
1661                 } else {
1662                     updates.add(Field.createFieldUpdate("apptstateflags", "0"));
1663                 }
1664                 // store mozilla invitations option
1665                 String xMozSendInvitations = vCalendar.getFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS");
1666                 if (xMozSendInvitations != null) {
1667                     updates.add(Field.createFieldUpdate("xmozsendinvitations", xMozSendInvitations));
1668                 }
1669                 // handle mozilla alarm
1670                 String xMozLastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
1671                 if (xMozLastack != null) {
1672                     updates.add(Field.createFieldUpdate("xmozlastack", xMozLastack));
1673                 }
1674                 String xMozSnoozeTime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
1675                 if (xMozSnoozeTime != null) {
1676                     updates.add(Field.createFieldUpdate("xmozsnoozetime", xMozSnoozeTime));
1677                 }
1678 
1679                 if (vCalendar.isMeeting() && "Exchange2007_SP1".equals(serverVersion)) {
1680                     Set<String> requiredAttendees = new HashSet<String>();
1681                     Set<String> optionalAttendees = new HashSet<String>();
1682                     List<VProperty> attendeeProperties = vCalendar.getFirstVeventProperties("ATTENDEE");
1683                     if (attendeeProperties != null) {
1684                         for (VProperty property : attendeeProperties) {
1685                             String attendeeEmail = vCalendar.getEmailValue(property);
1686                             if (attendeeEmail != null && attendeeEmail.indexOf('@') >= 0) {
1687                                 if (email.equals(attendeeEmail)) {
1688                                     String ownerPartStat = property.getParamValue("PARTSTAT");
1689                                     if ("ACCEPTED".equals(ownerPartStat)) {
1690                                         ownerResponseReply = "AcceptItem";
1691                                         // do not send DeclineItem to avoid deleting target event
1692                                     } else if ("DECLINED".equals(ownerPartStat) ||
1693                                             "TENTATIVE".equals(ownerPartStat)) {
1694                                         ownerResponseReply = "TentativelyAcceptItem";
1695                                     }
1696                                 }
1697                                 InternetAddress internetAddress = new InternetAddress(attendeeEmail, property.getParamValue("CN"));
1698                                 String attendeeRole = property.getParamValue("ROLE");
1699                                 if ("REQ-PARTICIPANT".equals(attendeeRole)) {
1700                                     requiredAttendees.add(internetAddress.toString());
1701                                 } else {
1702                                     optionalAttendees.add(internetAddress.toString());
1703                                 }
1704                             }
1705                         }
1706                     }
1707                     List<VProperty> organizerProperties = vCalendar.getFirstVeventProperties("ORGANIZER");
1708                     if (organizerProperties != null) {
1709                         VProperty property = organizerProperties.get(0);
1710                         String organizerEmail = vCalendar.getEmailValue(property);
1711                         if (organizerEmail != null && organizerEmail.indexOf('@') >= 0) {
1712                             updates.add(Field.createFieldUpdate("from", organizerEmail));
1713                         }
1714                     }
1715 
1716                     if (requiredAttendees.size() > 0) {
1717                         updates.add(Field.createFieldUpdate("to", StringUtil.join(requiredAttendees, ", ")));
1718                     }
1719                     if (optionalAttendees.size() > 0) {
1720                         updates.add(Field.createFieldUpdate("cc", StringUtil.join(optionalAttendees, ", ")));
1721                     }
1722                 }
1723 
1724                 // patch allday date values, only on 2007
1725                 if ("Exchange2007_SP1".equals(serverVersion) && vCalendar.isCdoAllDay()) {
1726                     updates.add(Field.createFieldUpdate("dtstart", convertCalendarDateToExchange(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
1727                     updates.add(Field.createFieldUpdate("dtend", convertCalendarDateToExchange(vCalendar.getFirstVeventPropertyValue("DTEND"))));
1728                 }
1729                 updates.add(Field.createFieldUpdate("busystatus", "BUSY".equals(vCalendar.getFirstVeventPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS")) ? "Busy" : "Free"));
1730                 if ("Exchange2007_SP1".equals(serverVersion) && vCalendar.isCdoAllDay()) {
1731                     updates.add(Field.createFieldUpdate("meetingtimezone", vCalendar.getVTimezone().getPropertyValue("TZID")));
1732                 }
1733 
1734                 newItem.setFieldUpdates(updates);
1735                 createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId(folderPath), newItem);
1736                 // force context Timezone on Exchange 2010 and 2013
1737                 if (serverVersion != null && serverVersion.startsWith("Exchange201")) {
1738                     createOrUpdateItemMethod.setTimezoneContext(EwsExchangeSession.this.getVTimezone().getPropertyValue("TZID"));
1739                 }
1740                 //}
1741                 }
1742             executeMethod(createOrUpdateItemMethod);
1743 
1744             itemResult.status = createOrUpdateItemMethod.getStatusCode();
1745             if (itemResult.status == HttpURLConnection.HTTP_OK) {
1746                 //noinspection VariableNotUsedInsideIf
1747                 if (currentItemId == null) {
1748                     itemResult.status = HttpStatus.SC_CREATED;
1749                     LOGGER.debug("Created event " + getHref());
1750                 } else {
1751                     LOGGER.warn("Overwritten event " + getHref());
1752                 }
1753             }
1754 
1755             // force responsetype on Exchange 2007
1756             if (ownerResponseReply != null) {
1757                 EWSMethod.Item responseTypeItem = new EWSMethod.Item();
1758                 responseTypeItem.referenceItemId = new ItemId("ReferenceItemId", createOrUpdateItemMethod.getResponseItem());
1759                 responseTypeItem.type = ownerResponseReply;
1760                 createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, null, responseTypeItem);
1761                 executeMethod(createOrUpdateItemMethod);
1762 
1763                 // force urlcompname again
1764                 ArrayList<FieldUpdate> updates = new ArrayList<FieldUpdate>();
1765                 updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
1766                 createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
1767                         ConflictResolution.AlwaysOverwrite,
1768                         SendMeetingInvitationsOrCancellations.SendToNone,
1769                         new ItemId(createOrUpdateItemMethod.getResponseItem()),
1770                         updates);
1771                 executeMethod(createOrUpdateItemMethod);
1772             }
1773 
1774             ItemId newItemId = new ItemId(createOrUpdateItemMethod.getResponseItem());
1775             GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, newItemId, false);
1776             getItemMethod.addAdditionalProperty(Field.get("etag"));
1777             executeMethod(getItemMethod);
1778             itemResult.etag = getItemMethod.getResponseItem().get(Field.get("etag").getResponseName());
1779 
1780             return itemResult;
1781 
1782         }
1783 
1784         @Override
1785         public byte[] getEventContent() throws IOException {
1786             byte[] content;
1787             if (LOGGER.isDebugEnabled()) {
1788                 LOGGER.debug("Get event: " + itemName);
1789             }
1790             try {
1791                 GetItemMethod getItemMethod;
1792                 if ("Task".equals(type)) {
1793                     getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
1794                     getItemMethod.addAdditionalProperty(Field.get("importance"));
1795                     getItemMethod.addAdditionalProperty(Field.get("subject"));
1796                     getItemMethod.addAdditionalProperty(Field.get("created"));
1797                     getItemMethod.addAdditionalProperty(Field.get("lastmodified"));
1798                     getItemMethod.addAdditionalProperty(Field.get("calendaruid"));
1799                     getItemMethod.addAdditionalProperty(Field.get("description"));
1800                     getItemMethod.addAdditionalProperty(Field.get("textbody"));
1801                     getItemMethod.addAdditionalProperty(Field.get("percentcomplete"));
1802                     getItemMethod.addAdditionalProperty(Field.get("taskstatus"));
1803                     getItemMethod.addAdditionalProperty(Field.get("startdate"));
1804                     getItemMethod.addAdditionalProperty(Field.get("duedate"));
1805                     getItemMethod.addAdditionalProperty(Field.get("datecompleted"));
1806                     getItemMethod.addAdditionalProperty(Field.get("keywords"));
1807 
1808                 } else if (!"Message".equals(type)
1809                         && !"MeetingCancellation".equals(type)
1810                         && !"MeetingResponse".equals(type)) {
1811                     getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
1812                     getItemMethod.addAdditionalProperty(Field.get("lastmodified"));
1813                     getItemMethod.addAdditionalProperty(Field.get("reminderset"));
1814                     getItemMethod.addAdditionalProperty(Field.get("calendaruid"));
1815                     getItemMethod.addAdditionalProperty(Field.get("myresponsetype"));
1816                     getItemMethod.addAdditionalProperty(Field.get("requiredattendees"));
1817                     getItemMethod.addAdditionalProperty(Field.get("optionalattendees"));
1818                     getItemMethod.addAdditionalProperty(Field.get("modifiedoccurrences"));
1819                     getItemMethod.addAdditionalProperty(Field.get("xmozlastack"));
1820                     getItemMethod.addAdditionalProperty(Field.get("xmozsnoozetime"));
1821                     getItemMethod.addAdditionalProperty(Field.get("xmozsendinvitations"));
1822                 } else {
1823                     getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
1824                 }
1825 
1826                 executeMethod(getItemMethod);
1827                 if ("Task".equals(type)) {
1828                     VCalendar localVCalendar = new VCalendar();
1829                     VObject vTodo = new VObject();
1830                     vTodo.type = "VTODO";
1831                     localVCalendar.setTimezone(getVTimezone());
1832                     vTodo.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
1833                     vTodo.setPropertyValue("CREATED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("created").getResponseName())));
1834                     String calendarUid = getItemMethod.getResponseItem().get(Field.get("calendaruid").getResponseName());
1835                     if (calendarUid == null) {
1836                         // use item id as uid for Exchange created tasks
1837                         calendarUid = itemId.id;
1838                     }
1839                     vTodo.setPropertyValue("UID", calendarUid);
1840                     vTodo.setPropertyValue("SUMMARY", getItemMethod.getResponseItem().get(Field.get("subject").getResponseName()));
1841                     String description = getItemMethod.getResponseItem().get(Field.get("description").getResponseName());
1842                     if (description == null) {
1843                         // Exchange 2013: try to get description from body
1844                         description = getItemMethod.getResponseItem().get(Field.get("textbody").getResponseName());
1845                     }
1846                     vTodo.setPropertyValue("DESCRIPTION", description);
1847                     vTodo.setPropertyValue("PRIORITY", convertPriorityFromExchange(getItemMethod.getResponseItem().get(Field.get("importance").getResponseName())));
1848                     vTodo.setPropertyValue("PERCENT-COMPLETE", getItemMethod.getResponseItem().get(Field.get("percentcomplete").getResponseName()));
1849                     vTodo.setPropertyValue("STATUS", taskTovTodoStatusMap.get(getItemMethod.getResponseItem().get(Field.get("taskstatus").getResponseName())));
1850 
1851                     vTodo.setPropertyValue("DUE;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("duedate").getResponseName())));
1852                     vTodo.setPropertyValue("DTSTART;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("startdate").getResponseName())));
1853                     vTodo.setPropertyValue("COMPLETED;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("datecompleted").getResponseName())));
1854 
1855                     vTodo.setPropertyValue("CATEGORIES", getItemMethod.getResponseItem().get(Field.get("keywords").getResponseName()));
1856 
1857                     localVCalendar.addVObject(vTodo);
1858                     content = localVCalendar.toString().getBytes("UTF-8");
1859                 } else {
1860                     content = getItemMethod.getMimeContent();
1861                     if (content == null) {
1862                         throw new IOException("empty event body");
1863                     }
1864                     if (!"CalendarItem".equals(type)) {
1865                         content = getICS(new SharedByteArrayInputStream(content));
1866                     }
1867                     VCalendar localVCalendar = new VCalendar(content, email, getVTimezone());
1868 
1869                     String calendaruid = getItemMethod.getResponseItem().get(Field.get("calendaruid").getResponseName());
1870 
1871                     if ("Exchange2007_SP1".equals(serverVersion)) {
1872                         // remove additional reminder
1873                         if (!"true".equals(getItemMethod.getResponseItem().get(Field.get("reminderset").getResponseName()))) {
1874                             localVCalendar.removeVAlarm();
1875                         }
1876                         if (calendaruid != null) {
1877                             localVCalendar.setFirstVeventPropertyValue("UID", calendaruid);
1878                         }
1879                     }
1880                     fixAttendees(getItemMethod, localVCalendar.getFirstVevent());
1881                     // fix UID and RECURRENCE-ID, broken at least on Exchange 2007
1882                     List<EWSMethod.Occurrence> occurences = getItemMethod.getResponseItem().getOccurrences();
1883                     if (occurences != null) {
1884                         Iterator<VObject> modifiedOccurrencesIterator = localVCalendar.getModifiedOccurrences().iterator();
1885                         for (EWSMethod.Occurrence occurrence : occurences) {
1886                             if (modifiedOccurrencesIterator.hasNext()) {
1887                                 VObject modifiedOccurrence = modifiedOccurrencesIterator.next();
1888                                 // fix modified occurrences attendees
1889                                 GetItemMethod getOccurrenceMethod = new GetItemMethod(BaseShape.ID_ONLY, occurrence.itemId, false);
1890                                 getOccurrenceMethod.addAdditionalProperty(Field.get("requiredattendees"));
1891                                 getOccurrenceMethod.addAdditionalProperty(Field.get("optionalattendees"));
1892                                 getOccurrenceMethod.addAdditionalProperty(Field.get("modifiedoccurrences"));
1893                                 getOccurrenceMethod.addAdditionalProperty(Field.get("lastmodified"));
1894                                 executeMethod(getOccurrenceMethod);
1895                                 fixAttendees(getOccurrenceMethod, modifiedOccurrence);
1896                                 // LAST-MODIFIED is missing in event content
1897                                 modifiedOccurrence.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getOccurrenceMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
1898 
1899                                 // fix uid, should be the same as main VEVENT
1900                                 if (calendaruid != null) {
1901                                     modifiedOccurrence.setPropertyValue("UID", calendaruid);
1902                                 }
1903 
1904                                 VProperty recurrenceId = modifiedOccurrence.getProperty("RECURRENCE-ID");
1905                                 if (recurrenceId != null) {
1906                                     recurrenceId.removeParam("TZID");
1907                                     recurrenceId.getValues().set(0, convertDateFromExchange(occurrence.originalStart));
1908                                 }
1909                             }
1910                         }
1911                     }
1912                     // LAST-MODIFIED is missing in event content
1913                     localVCalendar.setFirstVeventPropertyValue("LAST-MODIFIED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
1914 
1915                     // restore mozilla invitations option
1916                     localVCalendar.setFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS",
1917                             getItemMethod.getResponseItem().get(Field.get("xmozsendinvitations").getResponseName()));
1918                     // restore mozilla alarm status
1919                     localVCalendar.setFirstVeventPropertyValue("X-MOZ-LASTACK",
1920                             getItemMethod.getResponseItem().get(Field.get("xmozlastack").getResponseName()));
1921                     localVCalendar.setFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME",
1922                             getItemMethod.getResponseItem().get(Field.get("xmozsnoozetime").getResponseName()));
1923                     // overwrite method
1924                     // localVCalendar.setPropertyValue("METHOD", "REQUEST");
1925                     content = localVCalendar.toString().getBytes("UTF-8");
1926                 }
1927             } catch (IOException e) {
1928                 throw buildHttpException(e);
1929             } catch (MessagingException e) {
1930                 throw buildHttpException(e);
1931             }
1932             return content;
1933         }
1934 
1935         protected void fixAttendees(GetItemMethod getItemMethod, VObject vEvent) throws EWSException {
1936             if (getItemMethod.getResponseItem() != null) {
1937                 List<EWSMethod.Attendee> attendees = getItemMethod.getResponseItem().getAttendees();
1938                 if (attendees != null) {
1939                     for (EWSMethod.Attendee attendee : attendees) {
1940                         VProperty attendeeProperty = new VProperty("ATTENDEE", "mailto:" + attendee.email);
1941                         attendeeProperty.addParam("CN", attendee.name);
1942                         String myResponseType = getItemMethod.getResponseItem().get(Field.get("myresponsetype").getResponseName());
1943                         if (email.equalsIgnoreCase(attendee.email) && myResponseType != null) {
1944                             attendeeProperty.addParam("PARTSTAT", EWSMethod.responseTypeToPartstat(myResponseType));
1945                         } else {
1946                             attendeeProperty.addParam("PARTSTAT", attendee.partstat);
1947                         }
1948                         //attendeeProperty.addParam("RSVP", "TRUE");
1949                         attendeeProperty.addParam("ROLE", attendee.role);
1950                         vEvent.addProperty(attendeeProperty);
1951                     }
1952                 }
1953             }
1954         }
1955     }
1956 
1957     @Override
1958     public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
1959         List<ExchangeSession.Contact> contacts = new ArrayList<ExchangeSession.Contact>();
1960         List<EWSMethod.Item> responses = searchItems(folderPath, attributes, condition,
1961                 FolderQueryTraversal.SHALLOW, maxCount);
1962 
1963         for (EWSMethod.Item response : responses) {
1964             contacts.add(new Contact(response));
1965         }
1966         return contacts;
1967     }
1968 
1969     @Override
1970     protected Condition getCalendarItemCondition(Condition dateCondition) {
1971         // tasks in calendar not supported over EWS => do not look for instancetype null
1972         return or(
1973                 // Exchange 2010
1974                 or(isTrue("isrecurring"),
1975                         and(isFalse("isrecurring"), dateCondition)),
1976                 // Exchange 2007
1977                 or(isEqualTo("instancetype", 1),
1978                         and(isEqualTo("instancetype", 0), dateCondition))
1979         );
1980     }
1981 
1982     @Override
1983     public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
1984         return searchEvents(folderPath, ITEM_PROPERTIES,
1985                 and(startsWith("outlookmessageclass", "IPM.Schedule.Meeting."),
1986                         or(isNull("processed"), isFalse("processed"))));
1987     }
1988 
1989     @Override
1990     public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
1991         List<ExchangeSession.Event> events = new ArrayList<ExchangeSession.Event>();
1992         List<EWSMethod.Item> responses = searchItems(folderPath, attributes,
1993                 condition,
1994                 FolderQueryTraversal.SHALLOW, 0);
1995         for (EWSMethod.Item response : responses) {
1996             Event event = new Event(folderPath, response);
1997             if ("Message".equals(event.type)) {
1998                 // TODO: just exclude
1999                 // need to check body
2000                 try {
2001                     event.getEventContent();
2002                     events.add(event);
2003                 } catch (HttpException e) {
2004                     LOGGER.warn("Ignore invalid event " + event.getHref());
2005                 }
2006                 // exclude exceptions
2007             } else if (event.isException) {
2008                 LOGGER.debug("Exclude recurrence exception " + event.getHref());
2009             } else {
2010                 events.add(event);
2011             }
2012 
2013         }
2014 
2015         return events;
2016     }
2017 
2018     /**
2019      * Common item properties
2020      */
2021     protected static final Set<String> ITEM_PROPERTIES = new HashSet<String>();
2022 
2023     static {
2024         ITEM_PROPERTIES.add("etag");
2025         ITEM_PROPERTIES.add("displayname");
2026         // calendar CdoInstanceType
2027         ITEM_PROPERTIES.add("instancetype");
2028         ITEM_PROPERTIES.add("urlcompname");
2029         ITEM_PROPERTIES.add("subject");
2030     }
2031 
2032     protected static final HashSet<String> EVENT_REQUEST_PROPERTIES = new HashSet<String>();
2033 
2034     static {
2035         EVENT_REQUEST_PROPERTIES.add("permanenturl");
2036         EVENT_REQUEST_PROPERTIES.add("etag");
2037         EVENT_REQUEST_PROPERTIES.add("displayname");
2038         EVENT_REQUEST_PROPERTIES.add("subject");
2039         EVENT_REQUEST_PROPERTIES.add("urlcompname");
2040     }
2041 
2042     @Override
2043     protected Set<String> getItemProperties() {
2044         return ITEM_PROPERTIES;
2045     }
2046 
2047     protected EWSMethod.Item getEwsItem(String folderPath, String itemName) throws IOException {
2048         EWSMethod.Item item = null;
2049         String urlcompname = convertItemNameToEML(itemName);
2050         // workaround for missing urlcompname in Exchange 2010
2051         if (isItemId(urlcompname)) {
2052             ItemId itemId = new ItemId(StringUtil.urlToBase64(urlcompname.substring(0, urlcompname.indexOf('.'))));
2053             GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
2054             for (String attribute : EVENT_REQUEST_PROPERTIES) {
2055                 getItemMethod.addAdditionalProperty(Field.get(attribute));
2056             }
2057             executeMethod(getItemMethod);
2058             item = getItemMethod.getResponseItem();
2059         }
2060         // find item by urlcompname
2061         if (item == null) {
2062             List<EWSMethod.Item> responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", urlcompname), FolderQueryTraversal.SHALLOW, 0);
2063             if (!responses.isEmpty()) {
2064                 item = responses.get(0);
2065             }
2066         }
2067         return item;
2068     }
2069 
2070 
2071     @Override
2072     public Item getItem(String folderPath, String itemName) throws IOException {
2073         EWSMethod.Item item = getEwsItem(folderPath, itemName);
2074         if (item == null && isMainCalendar(folderPath)) {
2075             // look for item in task folder, replace extension first
2076             if (itemName.endsWith(".ics")) {
2077                 item = getEwsItem(TASKS, itemName.substring(0, itemName.length() - 3) + "EML");
2078             } else {
2079                 item = getEwsItem(TASKS, itemName);
2080             }
2081         }
2082 
2083         if (item == null) {
2084             throw new HttpNotFoundException(itemName + " not found in " + folderPath);
2085         }
2086 
2087         String itemType = item.type;
2088         if ("Contact".equals(itemType)) {
2089             // retrieve Contact properties
2090             ItemId itemId = new ItemId(item);
2091             GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
2092             for (String attribute : CONTACT_ATTRIBUTES) {
2093                 getItemMethod.addAdditionalProperty(Field.get(attribute));
2094             }
2095             executeMethod(getItemMethod);
2096             item = getItemMethod.getResponseItem();
2097             if (item == null) {
2098                 throw new HttpNotFoundException(itemName + " not found in " + folderPath);
2099             }
2100             return new Contact(item);
2101         } else if ("CalendarItem".equals(itemType)
2102                 || "MeetingMessage".equals(itemType)
2103                 || "MeetingRequest".equals(itemType)
2104                 || "MeetingResponse".equals(itemType)
2105                 || "MeetingCancellation".equals(itemType)
2106                 || "Task".equals(itemType)
2107                 // VTODOs appear as Messages
2108                 || "Message".equals(itemType)) {
2109             Event event = new Event(folderPath, item);
2110             // force item name to client provided name (for tasks)
2111             event.setItemName(itemName);
2112             return event;
2113         } else {
2114             throw new HttpNotFoundException(itemName + " not found in " + folderPath);
2115         }
2116 
2117     }
2118 
2119     @Override
2120     public ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
2121         ContactPhoto contactPhoto;
2122 
2123         GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, ((EwsExchangeSession.Contact) contact).itemId, false);
2124         getItemMethod.addAdditionalProperty(Field.get("attachments"));
2125         executeMethod(getItemMethod);
2126         EWSMethod.Item item = getItemMethod.getResponseItem();
2127         if (item == null) {
2128             throw new IOException("Missing contact picture");
2129         }
2130         FileAttachment attachment = item.getAttachmentByName("ContactPicture.jpg");
2131         if (attachment == null) {
2132             throw new IOException("Missing contact picture");
2133         }
2134         // get attachment content
2135         GetAttachmentMethod getAttachmentMethod = new GetAttachmentMethod(attachment.attachmentId);
2136         executeMethod(getAttachmentMethod);
2137 
2138         contactPhoto = new ContactPhoto();
2139         contactPhoto.content = getAttachmentMethod.getResponseItem().get("Content");
2140         if (attachment.contentType == null) {
2141             contactPhoto.contentType = "image/jpeg";
2142         } else {
2143             contactPhoto.contentType = attachment.contentType;
2144         }
2145 
2146         return contactPhoto;
2147     }
2148 
2149     @Override
2150     public void deleteItem(String folderPath, String itemName) throws IOException {
2151         EWSMethod.Item item = getEwsItem(folderPath, itemName);
2152         if (item == null && isMainCalendar(folderPath)) {
2153             // look for item in task folder
2154             item = getEwsItem(TASKS, itemName);
2155         }
2156         if (item != null) {
2157             DeleteItemMethod deleteItemMethod = new DeleteItemMethod(new ItemId(item), DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
2158             executeMethod(deleteItemMethod);
2159         }
2160     }
2161 
2162     @Override
2163     public void processItem(String folderPath, String itemName) throws IOException {
2164         EWSMethod.Item item = getEwsItem(folderPath, itemName);
2165         if (item != null) {
2166             HashMap<String, String> localProperties = new HashMap<String, String>();
2167             localProperties.put("processed", "1");
2168             localProperties.put("read", "1");
2169             UpdateItemMethod updateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
2170                     ConflictResolution.AlwaysOverwrite,
2171                     SendMeetingInvitationsOrCancellations.SendToNone,
2172                     new ItemId(item), buildProperties(localProperties));
2173             executeMethod(updateItemMethod);
2174         }
2175     }
2176 
2177     @Override
2178     public int sendEvent(String icsBody) throws IOException {
2179         String itemName = UUID.randomUUID().toString() + ".EML";
2180         byte[] mimeContent = new Event(DRAFTS, itemName, "urn:content-classes:calendarmessage", icsBody, null, null).createMimeContent();
2181         if (mimeContent == null) {
2182             // no recipients, cancel
2183             return HttpStatus.SC_NO_CONTENT;
2184         } else {
2185             sendMessage(null, mimeContent);
2186             return HttpStatus.SC_OK;
2187         }
2188     }
2189 
2190     @Override
2191     protected ItemResult internalCreateOrUpdateContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException {
2192         return new Contact(folderPath, itemName, properties, StringUtil.removeQuotes(etag), noneMatch).createOrUpdate();
2193     }
2194 
2195     @Override
2196     protected ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
2197         return new Event(folderPath, itemName, contentClass, icsBody, StringUtil.removeQuotes(etag), noneMatch).createOrUpdate();
2198     }
2199 
2200     @Override
2201     public boolean isSharedFolder(String folderPath) {
2202         return folderPath.startsWith("/") && !folderPath.toLowerCase().startsWith(currentMailboxPath);
2203     }
2204 
2205     @Override
2206     public boolean isMainCalendar(String folderPath) throws IOException {
2207         FolderId currentFolderId = getFolderId(folderPath);
2208         FolderId calendarFolderId = getFolderId("calendar");
2209         return calendarFolderId.name.equals(currentFolderId.name) && calendarFolderId.value.equals(currentFolderId.value);
2210     }
2211 
2212     @Override
2213     protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException {
2214         GetUserAvailabilityMethod getUserAvailabilityMethod = new GetUserAvailabilityMethod(attendee, start, end, interval);
2215         executeMethod(getUserAvailabilityMethod);
2216         return getUserAvailabilityMethod.getMergedFreeBusy();
2217     }
2218 
2219     @Override
2220     protected void loadVtimezone() {
2221 
2222         try {
2223             String timezoneId = null;
2224             if (!"Exchange2007_SP1".equals(serverVersion)) {
2225                 // On Exchange 2010, get user timezone from server
2226                 GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
2227                 executeMethod(getUserConfigurationMethod);
2228                 EWSMethod.Item item = getUserConfigurationMethod.getResponseItem();
2229                 if (item != null) {
2230                     timezoneId = item.get("timezone");
2231                 }
2232             } else if (!directEws) {
2233                 timezoneId = getTimezoneidFromOptions();
2234             }
2235             // failover: use timezone id from settings file
2236             if (timezoneId == null) {
2237                 timezoneId = Settings.getProperty("davmail.timezoneId");
2238             }
2239             // last failover: use GMT
2240             if (timezoneId == null) {
2241                 LOGGER.warn("Unable to get user timezone, using GMT Standard Time. Set davmail.timezoneId setting to override this.");
2242                 timezoneId = "GMT Standard Time";
2243             }
2244 
2245             createCalendarFolder("davmailtemp", null);
2246             EWSMethod.Item item = new EWSMethod.Item();
2247             item.type = "CalendarItem";
2248             if (!"Exchange2007_SP1".equals(serverVersion)) {
2249                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
2250                 dateFormatter.setTimeZone(GMT_TIMEZONE);
2251                 Calendar cal = Calendar.getInstance();
2252                 item.put("Start", dateFormatter.format(cal.getTime()));
2253                 cal.add(Calendar.DAY_OF_MONTH, 1);
2254                 item.put("End", dateFormatter.format(cal.getTime()));
2255                 item.put("StartTimeZone", timezoneId);
2256             } else {
2257                 item.put("MeetingTimeZone", timezoneId);
2258             }
2259             CreateItemMethod createItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId("davmailtemp"), item);
2260             executeMethod(createItemMethod);
2261             item = createItemMethod.getResponseItem();
2262             VCalendar vCalendar = new VCalendar(getContent(new ItemId(item)), email, null);
2263             this.vTimezone = vCalendar.getVTimezone();
2264             // delete temporary folder
2265             deleteFolder("davmailtemp");
2266         } catch (IOException e) {
2267             LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
2268         }
2269     }
2270 
2271     protected String getTimezoneidFromOptions() {
2272         String result = null;
2273         // get time zone setting from html body
2274         BufferedReader optionsPageReader = null;
2275         GetMethod optionsMethod = new GetMethod("/owa/?ae=Options&t=Regional");
2276         try {
2277             DavGatewayHttpClientFacade.executeGetMethod(httpClient, optionsMethod, false);
2278             optionsPageReader = new BufferedReader(new InputStreamReader(optionsMethod.getResponseBodyAsStream(), "UTF-8"));
2279             String line;
2280             // find timezone
2281             //noinspection StatementWithEmptyBody
2282             while ((line = optionsPageReader.readLine()) != null
2283                     && (!line.contains("tblTmZn"))
2284                     && (!line.contains("selTmZn"))) {
2285             }
2286             if (line != null) {
2287                 if (line.contains("tblTmZn")) {
2288                     int start = line.indexOf("oV=\"") + 4;
2289                     int end = line.indexOf('\"', start);
2290                     result = line.substring(start, end);
2291                 } else {
2292                     int end = line.lastIndexOf("\" selected>");
2293                     int start = line.lastIndexOf('\"', end - 1);
2294                     result = line.substring(start + 1, end);
2295                 }
2296             }
2297         } catch (IOException e) {
2298             LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
2299         } finally {
2300             if (optionsPageReader != null) {
2301                 try {
2302                     optionsPageReader.close();
2303                 } catch (IOException e) {
2304                     LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
2305                 }
2306             }
2307             optionsMethod.releaseConnection();
2308         }
2309 
2310         return result;
2311     }
2312 
2313 
2314     protected FolderId getFolderId(String folderPath) throws IOException {
2315         FolderId folderId = getFolderIdIfExists(folderPath);
2316         if (folderId == null) {
2317             throw new HttpNotFoundException("Folder '" + folderPath + "' not found");
2318         }
2319         return folderId;
2320     }
2321 
2322     protected static final String USERS_ROOT = "/users/";
2323 
2324     protected FolderId getFolderIdIfExists(String folderPath) throws IOException {
2325         String lowerCaseFolderPath = folderPath.toLowerCase();
2326         if (lowerCaseFolderPath.equals(currentMailboxPath)) {
2327             return getSubFolderIdIfExists(null, "");
2328         } else if (lowerCaseFolderPath.startsWith(currentMailboxPath + '/')) {
2329             return getSubFolderIdIfExists(null, folderPath.substring(currentMailboxPath.length() + 1));
2330         } else if (folderPath.startsWith("/users/")) {
2331             int slashIndex = folderPath.indexOf('/', USERS_ROOT.length());
2332             String mailbox;
2333             String subFolderPath;
2334             if (slashIndex >= 0) {
2335                 mailbox = folderPath.substring(USERS_ROOT.length(), slashIndex);
2336                 subFolderPath = folderPath.substring(slashIndex + 1);
2337             } else {
2338                 mailbox = folderPath.substring(USERS_ROOT.length());
2339                 subFolderPath = "";
2340             }
2341             return getSubFolderIdIfExists(mailbox, subFolderPath);
2342         } else {
2343             return getSubFolderIdIfExists(null, folderPath);
2344         }
2345     }
2346 
2347     protected FolderId getSubFolderIdIfExists(String mailbox, String folderPath) throws IOException {
2348         String[] folderNames;
2349         FolderId currentFolderId;
2350 
2351         if (folderPath.startsWith(PUBLIC_ROOT)) {
2352             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.publicfoldersroot);
2353             folderNames = folderPath.substring(PUBLIC_ROOT.length()).split("/");
2354         } else if (folderPath.startsWith(ARCHIVE_ROOT)) {
2355             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.archivemsgfolderroot);
2356             folderNames = folderPath.substring(ARCHIVE_ROOT.length()).split("/");
2357         } else if (folderPath.startsWith(INBOX) || folderPath.startsWith(LOWER_CASE_INBOX)) {
2358             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.inbox);
2359             folderNames = folderPath.substring(INBOX.length()).split("/");
2360         } else if (folderPath.startsWith(CALENDAR)) {
2361             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.calendar);
2362             folderNames = folderPath.substring(CALENDAR.length()).split("/");
2363         } else if (folderPath.startsWith(TASKS)) {
2364             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.tasks);
2365             folderNames = folderPath.substring(TASKS.length()).split("/");
2366         } else if (folderPath.startsWith(CONTACTS)) {
2367             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.contacts);
2368             folderNames = folderPath.substring(CONTACTS.length()).split("/");
2369         } else if (folderPath.startsWith(SENT)) {
2370             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.sentitems);
2371             folderNames = folderPath.substring(SENT.length()).split("/");
2372         } else if (folderPath.startsWith(DRAFTS)) {
2373             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.drafts);
2374             folderNames = folderPath.substring(DRAFTS.length()).split("/");
2375         } else if (folderPath.startsWith(TRASH)) {
2376             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.deleteditems);
2377             folderNames = folderPath.substring(TRASH.length()).split("/");
2378         } else if (folderPath.startsWith(JUNK)) {
2379             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.junkemail);
2380             folderNames = folderPath.substring(JUNK.length()).split("/");
2381         } else if (folderPath.startsWith(UNSENT)) {
2382             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.outbox);
2383             folderNames = folderPath.substring(UNSENT.length()).split("/");
2384         } else {
2385             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.msgfolderroot);
2386             folderNames = folderPath.split("/");
2387         }
2388         for (String folderName : folderNames) {
2389             if (folderName.length() > 0) {
2390                 currentFolderId = getSubFolderByName(currentFolderId, folderName);
2391                 if (currentFolderId == null) {
2392                     break;
2393                 }
2394             }
2395         }
2396         return currentFolderId;
2397     }
2398 
2399     protected FolderId getSubFolderByName(FolderId parentFolderId, String folderName) throws IOException {
2400         FolderId folderId = null;
2401         FindFolderMethod findFolderMethod = new FindFolderMethod(
2402                 FolderQueryTraversal.SHALLOW,
2403                 BaseShape.ID_ONLY,
2404                 parentFolderId,
2405                 FOLDER_PROPERTIES,
2406                 new TwoOperandExpression(TwoOperandExpression.Operator.IsEqualTo,
2407                         Field.get("folderDisplayName"), folderName),
2408                 0, 1
2409         );
2410         executeMethod(findFolderMethod);
2411         EWSMethod.Item item = findFolderMethod.getResponseItem();
2412         if (item != null) {
2413             folderId = new FolderId(item);
2414         }
2415         return folderId;
2416     }
2417 
2418     long throttlingTimestamp = 0;
2419 
2420     protected int executeMethod(EWSMethod ewsMethod) throws IOException {
2421         long throttlingDelay = throttlingTimestamp - System.currentTimeMillis();
2422         try {
2423             if (throttlingDelay > 0) {
2424                 LOGGER.warn("Throttling active on server, waiting " + (throttlingDelay / 1000) + " seconds");
2425                 try {
2426                     Thread.sleep(throttlingDelay);
2427                 } catch (InterruptedException e1) {
2428                     LOGGER.error("Throttling delay interrupted " + e1.getMessage());
2429                 }
2430             }
2431             internalExecuteMethod(ewsMethod);
2432         } catch (EWSThrottlingException e) {
2433             // default throttling delay is one minute
2434             throttlingDelay = 60000;
2435             if (ewsMethod.errorValue != null) {
2436                 // server provided a throttling delay, add 10 seconds
2437                 try {
2438                     throttlingDelay = Long.parseLong(ewsMethod.errorValue) + 10000;
2439                 } catch (NumberFormatException e2) {
2440                     LOGGER.error("Unable to parse BackOffMilliseconds " + e2.getMessage());
2441                 }
2442             }
2443             throttlingTimestamp = System.currentTimeMillis() + throttlingDelay;
2444 
2445             LOGGER.warn("Throttling active on server, waiting " + (throttlingDelay / 1000) + " seconds");
2446             try {
2447                 Thread.sleep(throttlingDelay);
2448             } catch (InterruptedException e1) {
2449                 LOGGER.error("Throttling delay interrupted " + e1.getMessage());
2450             }
2451             // retry once
2452             internalExecuteMethod(ewsMethod);
2453         }
2454         return ewsMethod.getStatusCode();
2455     }
2456 
2457     protected void internalExecuteMethod(EWSMethod ewsMethod) throws IOException {
2458         try {
2459             ewsMethod.setServerVersion(serverVersion);
2460             httpClient.executeMethod(ewsMethod);
2461             if (serverVersion == null) {
2462                 serverVersion = ewsMethod.getServerVersion();
2463             }
2464             ewsMethod.checkSuccess();
2465         } finally {
2466             ewsMethod.releaseConnection();
2467         }
2468     }
2469 
2470     protected static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<String, String>();
2471 
2472     static {
2473         GALFIND_ATTRIBUTE_MAP.put("imapUid", "Name");
2474         GALFIND_ATTRIBUTE_MAP.put("cn", "DisplayName");
2475         GALFIND_ATTRIBUTE_MAP.put("givenName", "GivenName");
2476         GALFIND_ATTRIBUTE_MAP.put("sn", "Surname");
2477         GALFIND_ATTRIBUTE_MAP.put("smtpemail1", "EmailAddress");
2478 
2479         GALFIND_ATTRIBUTE_MAP.put("roomnumber", "OfficeLocation");
2480         GALFIND_ATTRIBUTE_MAP.put("street", "BusinessStreet");
2481         GALFIND_ATTRIBUTE_MAP.put("l", "BusinessCity");
2482         GALFIND_ATTRIBUTE_MAP.put("o", "CompanyName");
2483         GALFIND_ATTRIBUTE_MAP.put("postalcode", "BusinessPostalCode");
2484         GALFIND_ATTRIBUTE_MAP.put("st", "BusinessState");
2485         GALFIND_ATTRIBUTE_MAP.put("co", "BusinessCountryOrRegion");
2486 
2487         GALFIND_ATTRIBUTE_MAP.put("manager", "Manager");
2488         GALFIND_ATTRIBUTE_MAP.put("middlename", "Initials");
2489         GALFIND_ATTRIBUTE_MAP.put("title", "JobTitle");
2490         GALFIND_ATTRIBUTE_MAP.put("department", "Department");
2491 
2492         GALFIND_ATTRIBUTE_MAP.put("otherTelephone", "OtherTelephone");
2493         GALFIND_ATTRIBUTE_MAP.put("telephoneNumber", "BusinessPhone");
2494         GALFIND_ATTRIBUTE_MAP.put("mobile", "MobilePhone");
2495         GALFIND_ATTRIBUTE_MAP.put("facsimiletelephonenumber", "BusinessFax");
2496         GALFIND_ATTRIBUTE_MAP.put("secretarycn", "AssistantName");
2497     }
2498 
2499     protected static final HashSet<String> IGNORE_ATTRIBUTE_SET = new HashSet<String>();
2500 
2501     static {
2502         IGNORE_ATTRIBUTE_SET.add("ContactSource");
2503         IGNORE_ATTRIBUTE_SET.add("Culture");
2504         IGNORE_ATTRIBUTE_SET.add("AssistantPhone");
2505     }
2506 
2507     protected Contact buildGalfindContact(EWSMethod.Item response) {
2508         Contact contact = new Contact();
2509         contact.setName(response.get("Name"));
2510         contact.put("imapUid", response.get("Name"));
2511         contact.put("uid", response.get("Name"));
2512         if (LOGGER.isDebugEnabled()) {
2513             for (Map.Entry<String, String> entry : response.entrySet()) {
2514                 String key = entry.getKey();
2515                 if (!IGNORE_ATTRIBUTE_SET.contains(key) && !GALFIND_ATTRIBUTE_MAP.containsValue(key)) {
2516                     LOGGER.debug("Unsupported ResolveNames " + contact.getName() + " response attribute: " + key + " value: " + entry.getValue());
2517                 }
2518             }
2519         }
2520         for (Map.Entry<String, String> entry : GALFIND_ATTRIBUTE_MAP.entrySet()) {
2521             String attributeValue = response.get(entry.getValue());
2522             if (attributeValue != null) {
2523                 contact.put(entry.getKey(), attributeValue);
2524             }
2525         }
2526         return contact;
2527     }
2528 
2529     @Override
2530     public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
2531         Map<String, ExchangeSession.Contact> contacts = new HashMap<String, ExchangeSession.Contact>();
2532         if (condition instanceof MultiCondition) {
2533             List<Condition> conditions = ((ExchangeSession.MultiCondition) condition).getConditions();
2534             Operator operator = ((ExchangeSession.MultiCondition) condition).getOperator();
2535             if (operator == Operator.Or) {
2536                 for (Condition innerCondition : conditions) {
2537                     contacts.putAll(galFind(innerCondition, returningAttributes, sizeLimit));
2538                 }
2539             } else if (operator == Operator.And && !conditions.isEmpty()) {
2540                 Map<String, ExchangeSession.Contact> innerContacts = galFind(conditions.get(0), returningAttributes, sizeLimit);
2541                 for (ExchangeSession.Contact contact : innerContacts.values()) {
2542                     if (condition.isMatch(contact)) {
2543                         contacts.put(contact.getName().toLowerCase(), contact);
2544                     }
2545                 }
2546             }
2547         } else if (condition instanceof AttributeCondition) {
2548             String mappedAttributeName = GALFIND_ATTRIBUTE_MAP.get(((ExchangeSession.AttributeCondition) condition).getAttributeName());
2549             if (mappedAttributeName != null) {
2550                 String value = ((ExchangeSession.AttributeCondition) condition).getValue().toLowerCase();
2551                 Operator operator = ((AttributeCondition) condition).getOperator();
2552                 String searchValue = value;
2553                 if (mappedAttributeName.startsWith("EmailAddress")) {
2554                     searchValue = "smtp:" + searchValue;
2555                 }
2556                 if (operator == Operator.IsEqualTo) {
2557                     searchValue = '=' + searchValue;
2558                 }
2559                 ResolveNamesMethod resolveNamesMethod = new ResolveNamesMethod(searchValue);
2560                 executeMethod(resolveNamesMethod);
2561                 List<EWSMethod.Item> responses = resolveNamesMethod.getResponseItems();
2562                 if (LOGGER.isDebugEnabled()) {
2563                     LOGGER.debug("ResolveNames(" + searchValue + ") returned " + responses.size() + " results");
2564                 }
2565                 for (EWSMethod.Item response : responses) {
2566                     Contact contact = buildGalfindContact(response);
2567                     if (condition.isMatch(contact)) {
2568                         contacts.put(contact.getName().toLowerCase(), contact);
2569                     }
2570                 }
2571             }
2572         }
2573         return contacts;
2574     }
2575 
2576     protected Date parseDateFromExchange(String exchangeDateValue) throws DavMailException {
2577         Date dateValue = null;
2578         if (exchangeDateValue != null) {
2579             try {
2580                 dateValue = getExchangeZuluDateFormat().parse(exchangeDateValue);
2581             } catch (ParseException e) {
2582                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
2583             }
2584         }
2585         return dateValue;
2586     }
2587 
2588     protected String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
2589         String zuluDateValue = null;
2590         if (exchangeDateValue != null) {
2591             try {
2592                 zuluDateValue = getZuluDateFormat().format(getExchangeZuluDateFormat().parse(exchangeDateValue));
2593             } catch (ParseException e) {
2594                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
2595             }
2596         }
2597         return zuluDateValue;
2598     }
2599 
2600     protected String convertCalendarDateToExchange(String vcalendarDateValue) throws DavMailException {
2601         String zuluDateValue = null;
2602         if (vcalendarDateValue != null) {
2603             try {
2604                 SimpleDateFormat dateParser;
2605                 if (vcalendarDateValue.length() == 8) {
2606                     dateParser = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
2607                 } else {
2608                     dateParser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
2609                 }
2610                 dateParser.setTimeZone(GMT_TIMEZONE);
2611                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
2612                 dateFormatter.setTimeZone(GMT_TIMEZONE);
2613                 zuluDateValue = dateFormatter.format(dateParser.parse(vcalendarDateValue));
2614             } catch (ParseException e) {
2615                 throw new DavMailException("EXCEPTION_INVALID_DATE", vcalendarDateValue);
2616             }
2617         }
2618         return zuluDateValue;
2619     }
2620 
2621     protected String convertDateFromExchangeToTaskDate(String exchangeDateValue) throws DavMailException {
2622         String zuluDateValue = null;
2623         if (exchangeDateValue != null) {
2624             try {
2625                 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
2626                 dateFormat.setTimeZone(GMT_TIMEZONE);
2627                 zuluDateValue = dateFormat.format(getExchangeZuluDateFormat().parse(exchangeDateValue));
2628             } catch (ParseException e) {
2629                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
2630             }
2631         }
2632         return zuluDateValue;
2633     }
2634 
2635     protected String convertTaskDateToZulu(String value) {
2636         String result = null;
2637         if (value != null && value.length() > 0) {
2638             try {
2639                 SimpleDateFormat parser;
2640                 if (value.length() == 8) {
2641                     parser = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
2642                     parser.setTimeZone(GMT_TIMEZONE);
2643                 } else if (value.length() == 15) {
2644                     parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
2645                     parser.setTimeZone(GMT_TIMEZONE);
2646                 } else if (value.length() == 16) {
2647                     parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
2648                     parser.setTimeZone(GMT_TIMEZONE);
2649                 } else {
2650                     parser = ExchangeSession.getExchangeZuluDateFormat();
2651                 }
2652                 Calendar calendarValue = Calendar.getInstance(GMT_TIMEZONE);
2653                 calendarValue.setTime(parser.parse(value));
2654                 // zulu time: add 12 hours
2655                 if (value.length() == 16) {
2656                     calendarValue.add(Calendar.HOUR, 12);
2657                 }
2658                 calendarValue.set(Calendar.HOUR, 0);
2659                 calendarValue.set(Calendar.MINUTE, 0);
2660                 calendarValue.set(Calendar.SECOND, 0);
2661                 result = ExchangeSession.getExchangeZuluDateFormat().format(calendarValue.getTime());
2662             } catch (ParseException e) {
2663                 LOGGER.warn("Invalid date: " + value);
2664             }
2665         }
2666 
2667         return result;
2668     }
2669 
2670     /**
2671      * Format date to exchange search format.
2672      *
2673      * @param date date object
2674      * @return formatted search date
2675      */
2676     @Override
2677     public String formatSearchDate(Date date) {
2678         SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
2679         dateFormatter.setTimeZone(GMT_TIMEZONE);
2680         return dateFormatter.format(date);
2681     }
2682 
2683     /**
2684      * Check if itemName is long and base64 encoded.
2685      * User generated item names are usually short
2686      *
2687      * @param itemName item name
2688      * @return true if itemName is an EWS item id
2689      */
2690     protected static boolean isItemId(String itemName) {
2691         return itemName.length() >= 152
2692                 // item name is base64url
2693                 //&& itemName.matches("^([A-Za-z0-9-_]{4})*([A-Za-z0-9-_]{4}|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{2}==)$")
2694                 && itemName.indexOf(' ') < 0;
2695     }
2696 
2697 
2698     protected static final Map<String, String> importanceToPriorityMap = new HashMap<String, String>();
2699 
2700     static {
2701         importanceToPriorityMap.put("High", "1");
2702         importanceToPriorityMap.put("Normal", "5");
2703         importanceToPriorityMap.put("Low", "9");
2704     }
2705 
2706     protected static final Map<String, String> priorityToImportanceMap = new HashMap<String, String>();
2707 
2708     static {
2709         // 0 means undefined, map it to normal
2710         priorityToImportanceMap.put("0", "Normal");
2711 
2712         priorityToImportanceMap.put("1", "High");
2713         priorityToImportanceMap.put("2", "High");
2714         priorityToImportanceMap.put("3", "High");
2715         priorityToImportanceMap.put("4", "Normal");
2716         priorityToImportanceMap.put("5", "Normal");
2717         priorityToImportanceMap.put("6", "Normal");
2718         priorityToImportanceMap.put("7", "Low");
2719         priorityToImportanceMap.put("8", "Low");
2720         priorityToImportanceMap.put("9", "Low");
2721     }
2722 
2723     protected String convertPriorityFromExchange(String exchangeImportanceValue) {
2724         String value = null;
2725         if (exchangeImportanceValue != null) {
2726             value = importanceToPriorityMap.get(exchangeImportanceValue);
2727         }
2728         return value;
2729     }
2730 
2731     protected String convertPriorityToExchange(String vTodoPriorityValue) {
2732         String value = null;
2733         if (vTodoPriorityValue != null) {
2734             value = priorityToImportanceMap.get(vTodoPriorityValue);
2735         }
2736         return value;
2737     }
2738 }
2739