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