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.dav; 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.exception.HttpPreconditionFailedException; 27 import davmail.exception.InsufficientStorageException; 28 import davmail.exception.LoginTimeoutException; 29 import davmail.exception.WebdavNotAvailableException; 30 import davmail.exchange.ExchangeSession; 31 import davmail.exchange.VCalendar; 32 import davmail.exchange.VObject; 33 import davmail.exchange.VProperty; 34 import davmail.exchange.XMLStreamUtil; 35 import davmail.http.HttpClientAdapter; 36 import davmail.http.URIUtil; 37 import davmail.http.request.ExchangePropPatchRequest; 38 import davmail.ui.tray.DavGatewayTray; 39 import davmail.util.IOUtil; 40 import davmail.util.StringUtil; 41 import org.apache.http.Consts; 42 import org.apache.http.HttpResponse; 43 import org.apache.http.HttpStatus; 44 import org.apache.http.NameValuePair; 45 import org.apache.http.client.HttpResponseException; 46 import org.apache.http.client.entity.UrlEncodedFormEntity; 47 import org.apache.http.client.methods.CloseableHttpResponse; 48 import org.apache.http.client.methods.HttpDelete; 49 import org.apache.http.client.methods.HttpGet; 50 import org.apache.http.client.methods.HttpHead; 51 import org.apache.http.client.methods.HttpPost; 52 import org.apache.http.client.methods.HttpPut; 53 import org.apache.http.client.protocol.HttpClientContext; 54 import org.apache.http.client.utils.URIUtils; 55 import org.apache.http.entity.ByteArrayEntity; 56 import org.apache.http.entity.ContentType; 57 import org.apache.http.impl.client.BasicCookieStore; 58 import org.apache.http.impl.client.BasicResponseHandler; 59 import org.apache.http.message.BasicNameValuePair; 60 import org.apache.jackrabbit.webdav.DavException; 61 import org.apache.jackrabbit.webdav.MultiStatus; 62 import org.apache.jackrabbit.webdav.MultiStatusResponse; 63 import org.apache.jackrabbit.webdav.client.methods.HttpCopy; 64 import org.apache.jackrabbit.webdav.client.methods.HttpMove; 65 import org.apache.jackrabbit.webdav.client.methods.HttpPropfind; 66 import org.apache.jackrabbit.webdav.client.methods.HttpProppatch; 67 import org.apache.jackrabbit.webdav.property.DavProperty; 68 import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; 69 import org.apache.jackrabbit.webdav.property.DavPropertySet; 70 import org.apache.jackrabbit.webdav.property.PropEntry; 71 import org.w3c.dom.Node; 72 73 import javax.mail.MessagingException; 74 import javax.mail.Session; 75 import javax.mail.internet.InternetAddress; 76 import javax.mail.internet.MimeMessage; 77 import javax.mail.internet.MimeMultipart; 78 import javax.mail.internet.MimePart; 79 import javax.mail.util.SharedByteArrayInputStream; 80 import javax.xml.stream.XMLStreamException; 81 import javax.xml.stream.XMLStreamReader; 82 import java.io.BufferedReader; 83 import java.io.ByteArrayInputStream; 84 import java.io.ByteArrayOutputStream; 85 import java.io.FilterInputStream; 86 import java.io.IOException; 87 import java.io.InputStream; 88 import java.io.InputStreamReader; 89 import java.net.NoRouteToHostException; 90 import java.net.SocketException; 91 import java.net.URISyntaxException; 92 import java.net.URL; 93 import java.net.UnknownHostException; 94 import java.nio.charset.StandardCharsets; 95 import java.text.ParseException; 96 import java.text.SimpleDateFormat; 97 import java.util.*; 98 import java.util.zip.GZIPInputStream; 99 100 /** 101 * Webdav Exchange adapter. 102 * Compatible with Exchange 2003 and 2007 with webdav available. 103 */ 104 @SuppressWarnings("rawtypes") 105 public class DavExchangeSession extends ExchangeSession { 106 protected enum FolderQueryTraversal { 107 Shallow, Deep 108 } 109 110 protected static final DavPropertyNameSet WELL_KNOWN_FOLDERS = new DavPropertyNameSet(); 111 112 static { 113 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("inbox")); 114 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("deleteditems")); 115 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("sentitems")); 116 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("sendmsg")); 117 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("drafts")); 118 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("calendar")); 119 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("tasks")); 120 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("contacts")); 121 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("outbox")); 122 } 123 124 static final Map<String, String> vTodoToTaskStatusMap = new HashMap<>(); 125 static final Map<String, String> taskTovTodoStatusMap = new HashMap<>(); 126 127 static { 128 //taskTovTodoStatusMap.put("0", null); 129 taskTovTodoStatusMap.put("1", "IN-PROCESS"); 130 taskTovTodoStatusMap.put("2", "COMPLETED"); 131 taskTovTodoStatusMap.put("3", "NEEDS-ACTION"); 132 taskTovTodoStatusMap.put("4", "CANCELLED"); 133 134 //vTodoToTaskStatusMap.put(null, "0"); 135 vTodoToTaskStatusMap.put("IN-PROCESS", "1"); 136 vTodoToTaskStatusMap.put("COMPLETED", "2"); 137 vTodoToTaskStatusMap.put("NEEDS-ACTION", "3"); 138 vTodoToTaskStatusMap.put("CANCELLED", "4"); 139 } 140 141 /** 142 * HttpClient 4 adapter to replace httpClient 143 */ 144 private HttpClientAdapter httpClientAdapter; 145 146 /** 147 * Various standard mail boxes Urls 148 */ 149 protected String inboxUrl; 150 protected String deleteditemsUrl; 151 protected String sentitemsUrl; 152 protected String sendmsgUrl; 153 protected String draftsUrl; 154 protected String calendarUrl; 155 protected String tasksUrl; 156 protected String contactsUrl; 157 protected String outboxUrl; 158 159 protected String inboxName; 160 protected String deleteditemsName; 161 protected String sentitemsName; 162 protected String sendmsgName; 163 protected String draftsName; 164 protected String calendarName; 165 protected String tasksName; 166 protected String contactsName; 167 protected String outboxName; 168 169 protected static final String USERS = "/users/"; 170 171 /** 172 * HttpClient4 conversion. 173 * TODO: move up to ExchangeSession 174 */ getEmailAndAliasFromOptions()175 protected void getEmailAndAliasFromOptions() { 176 // get user mail URL from html body 177 HttpGet optionsMethod = new HttpGet("/owa/?ae=Options&t=About"); 178 try ( 179 CloseableHttpResponse response = httpClientAdapter.execute(optionsMethod, cloneContext()); 180 InputStream inputStream = response.getEntity().getContent(); 181 BufferedReader optionsPageReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) 182 ) { 183 String line; 184 185 // find email and alias 186 //noinspection StatementWithEmptyBody 187 while ((line = optionsPageReader.readLine()) != null 188 && (line.indexOf('[') == -1 189 || line.indexOf('@') == -1 190 || line.indexOf(']') == -1 191 || !line.toLowerCase().contains(MAILBOX_BASE))) { 192 } 193 if (line != null) { 194 int start = line.toLowerCase().lastIndexOf(MAILBOX_BASE) + MAILBOX_BASE.length(); 195 int end = line.indexOf('<', start); 196 alias = line.substring(start, end); 197 end = line.lastIndexOf(']'); 198 start = line.lastIndexOf('[', end) + 1; 199 email = line.substring(start, end); 200 } 201 } catch (IOException e) { 202 LOGGER.error("Error parsing options page at " + optionsMethod.getURI()); 203 } 204 } 205 206 /** 207 * Create a separate Http context to protect session cookies. 208 * 209 * @return HttpClientContext instance with cookies 210 */ cloneContext()211 private HttpClientContext cloneContext() { 212 // Create a local context to avoid cookie reset on error 213 BasicCookieStore cookieStore = new BasicCookieStore(); 214 cookieStore.addCookies(httpClientAdapter.getCookies().toArray(new org.apache.http.cookie.Cookie[0])); 215 HttpClientContext context = HttpClientContext.create(); 216 context.setCookieStore(cookieStore); 217 return context; 218 } 219 220 @Override isExpired()221 public boolean isExpired() throws NoRouteToHostException, UnknownHostException { 222 // experimental: try to reset session timeout 223 if ("Exchange2007".equals(serverVersion)) { 224 HttpGet getMethod = new HttpGet("/owa/"); 225 try (CloseableHttpResponse response = httpClientAdapter.execute(getMethod)) { 226 LOGGER.debug(response.getStatusLine().getStatusCode() + " at /owa/"); 227 } catch (IOException e) { 228 LOGGER.warn(e.getMessage()); 229 } 230 } 231 232 return super.isExpired(); 233 } 234 235 236 /** 237 * Convert logical or relative folder path to exchange folder path. 238 * 239 * @param folderPath folder name 240 * @return folder path 241 */ getFolderPath(String folderPath)242 public String getFolderPath(String folderPath) { 243 String exchangeFolderPath; 244 // IMAP path 245 if (folderPath.startsWith(INBOX)) { 246 exchangeFolderPath = mailPath + inboxName + folderPath.substring(INBOX.length()); 247 } else if (folderPath.startsWith(TRASH)) { 248 exchangeFolderPath = mailPath + deleteditemsName + folderPath.substring(TRASH.length()); 249 } else if (folderPath.startsWith(DRAFTS)) { 250 exchangeFolderPath = mailPath + draftsName + folderPath.substring(DRAFTS.length()); 251 } else if (folderPath.startsWith(SENT)) { 252 exchangeFolderPath = mailPath + sentitemsName + folderPath.substring(SENT.length()); 253 } else if (folderPath.startsWith(SENDMSG)) { 254 exchangeFolderPath = mailPath + sendmsgName + folderPath.substring(SENDMSG.length()); 255 } else if (folderPath.startsWith(CONTACTS)) { 256 exchangeFolderPath = mailPath + contactsName + folderPath.substring(CONTACTS.length()); 257 } else if (folderPath.startsWith(CALENDAR)) { 258 exchangeFolderPath = mailPath + calendarName + folderPath.substring(CALENDAR.length()); 259 } else if (folderPath.startsWith(TASKS)) { 260 exchangeFolderPath = mailPath + tasksName + folderPath.substring(TASKS.length()); 261 } else if (folderPath.startsWith("public")) { 262 exchangeFolderPath = publicFolderUrl + folderPath.substring("public".length()); 263 264 // caldav path 265 } else if (folderPath.startsWith(USERS)) { 266 // get requested principal 267 String principal; 268 String localPath; 269 int principalIndex = folderPath.indexOf('/', USERS.length()); 270 if (principalIndex >= 0) { 271 principal = folderPath.substring(USERS.length(), principalIndex); 272 localPath = folderPath.substring(USERS.length() + principal.length() + 1); 273 if (localPath.startsWith(LOWER_CASE_INBOX) || localPath.startsWith(INBOX) || localPath.startsWith(MIXED_CASE_INBOX)) { 274 localPath = inboxName + localPath.substring(LOWER_CASE_INBOX.length()); 275 } else if (localPath.startsWith(CALENDAR)) { 276 localPath = calendarName + localPath.substring(CALENDAR.length()); 277 } else if (localPath.startsWith(TASKS)) { 278 localPath = tasksName + localPath.substring(TASKS.length()); 279 } else if (localPath.startsWith(CONTACTS)) { 280 localPath = contactsName + localPath.substring(CONTACTS.length()); 281 } else if (localPath.startsWith(ADDRESSBOOK)) { 282 localPath = contactsName + localPath.substring(ADDRESSBOOK.length()); 283 } 284 } else { 285 principal = folderPath.substring(USERS.length()); 286 localPath = ""; 287 } 288 if (principal.length() == 0) { 289 exchangeFolderPath = rootPath; 290 } else if (alias.equalsIgnoreCase(principal) || (email != null && email.equalsIgnoreCase(principal))) { 291 exchangeFolderPath = mailPath + localPath; 292 } else { 293 LOGGER.debug("Detected shared path for principal " + principal + ", user principal is " + email); 294 exchangeFolderPath = rootPath + principal + '/' + localPath; 295 } 296 297 // absolute folder path 298 } else if (folderPath.startsWith("/")) { 299 exchangeFolderPath = folderPath; 300 } else { 301 exchangeFolderPath = mailPath + folderPath; 302 } 303 return exchangeFolderPath; 304 } 305 306 /** 307 * Test if folderPath is inside user mailbox. 308 * 309 * @param folderPath absolute folder path 310 * @return true if folderPath is a public or shared folder 311 */ 312 @Override isSharedFolder(String folderPath)313 public boolean isSharedFolder(String folderPath) { 314 return !getFolderPath(folderPath).toLowerCase().startsWith(mailPath.toLowerCase()); 315 } 316 317 /** 318 * Test if folderPath is main calendar. 319 * 320 * @param folderPath absolute folder path 321 * @return true if folderPath is a public or shared folder 322 */ 323 @Override isMainCalendar(String folderPath)324 public boolean isMainCalendar(String folderPath) { 325 return getFolderPath(folderPath).equalsIgnoreCase(getFolderPath("calendar")); 326 } 327 328 /** 329 * Build base path for cmd commands (galfind, gallookup). 330 * 331 * @return cmd base path 332 */ getCmdBasePath()333 public String getCmdBasePath() { 334 if (("Exchange2003".equals(serverVersion) || PUBLIC_ROOT.equals(publicFolderUrl)) && mailPath != null) { 335 // public folder is not available => try to use mailbox path 336 // Note: This does not work with freebusy, which requires /public/ 337 return mailPath; 338 } else { 339 // use public folder url 340 return publicFolderUrl; 341 } 342 } 343 344 /** 345 * LDAP to Exchange Criteria Map 346 */ 347 static final HashMap<String, String> GALFIND_CRITERIA_MAP = new HashMap<>(); 348 349 static { 350 GALFIND_CRITERIA_MAP.put("imapUid", "AN"); 351 GALFIND_CRITERIA_MAP.put("smtpemail1", "EM"); 352 GALFIND_CRITERIA_MAP.put("cn", "DN"); 353 GALFIND_CRITERIA_MAP.put("givenName", "FN"); 354 GALFIND_CRITERIA_MAP.put("sn", "LN"); 355 GALFIND_CRITERIA_MAP.put("title", "TL"); 356 GALFIND_CRITERIA_MAP.put("o", "CP"); 357 GALFIND_CRITERIA_MAP.put("l", "OF"); 358 GALFIND_CRITERIA_MAP.put("department", "DP"); 359 } 360 361 static final HashSet<String> GALLOOKUP_ATTRIBUTES = new HashSet<>(); 362 363 static { 364 GALLOOKUP_ATTRIBUTES.add("givenName"); 365 GALLOOKUP_ATTRIBUTES.add("initials"); 366 GALLOOKUP_ATTRIBUTES.add("sn"); 367 GALLOOKUP_ATTRIBUTES.add("street"); 368 GALLOOKUP_ATTRIBUTES.add("st"); 369 GALLOOKUP_ATTRIBUTES.add("postalcode"); 370 GALLOOKUP_ATTRIBUTES.add("co"); 371 GALLOOKUP_ATTRIBUTES.add("departement"); 372 GALLOOKUP_ATTRIBUTES.add("mobile"); 373 } 374 375 /** 376 * Exchange to LDAP attribute map 377 */ 378 static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<>(); 379 380 static { 381 GALFIND_ATTRIBUTE_MAP.put("uid", "AN"); 382 GALFIND_ATTRIBUTE_MAP.put("smtpemail1", "EM"); 383 GALFIND_ATTRIBUTE_MAP.put("cn", "DN"); 384 GALFIND_ATTRIBUTE_MAP.put("displayName", "DN"); 385 GALFIND_ATTRIBUTE_MAP.put("telephoneNumber", "PH"); 386 GALFIND_ATTRIBUTE_MAP.put("l", "OFFICE"); 387 GALFIND_ATTRIBUTE_MAP.put("o", "CP"); 388 GALFIND_ATTRIBUTE_MAP.put("title", "TL"); 389 390 GALFIND_ATTRIBUTE_MAP.put("givenName", "first"); 391 GALFIND_ATTRIBUTE_MAP.put("initials", "initials"); 392 GALFIND_ATTRIBUTE_MAP.put("sn", "last"); 393 GALFIND_ATTRIBUTE_MAP.put("street", "street"); 394 GALFIND_ATTRIBUTE_MAP.put("st", "state"); 395 GALFIND_ATTRIBUTE_MAP.put("postalcode", "zip"); 396 GALFIND_ATTRIBUTE_MAP.put("co", "country"); 397 GALFIND_ATTRIBUTE_MAP.put("department", "department"); 398 GALFIND_ATTRIBUTE_MAP.put("mobile", "mobile"); 399 GALFIND_ATTRIBUTE_MAP.put("roomnumber", "office"); 400 } 401 402 boolean disableGalFind; 403 galFind(String query)404 protected Map<String, Map<String, String>> galFind(String query) throws IOException { 405 Map<String, Map<String, String>> results; 406 String path = getCmdBasePath() + "?Cmd=galfind" + query; 407 HttpGet httpGet = new HttpGet(path); 408 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) { 409 results = XMLStreamUtil.getElementContentsAsMap(response.getEntity().getContent(), "item", "AN"); 410 if (LOGGER.isDebugEnabled()) { 411 LOGGER.debug(path + ": " + results.size() + " result(s)"); 412 } 413 } catch (IOException e) { 414 LOGGER.debug("GET " + path + " failed: " + e + ' ' + e.getMessage()); 415 disableGalFind = true; 416 throw e; 417 } 418 return results; 419 } 420 421 422 @Override galFind(Condition condition, Set<String> returningAttributes, int sizeLimit)423 public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException { 424 Map<String, ExchangeSession.Contact> contacts = new HashMap<>(); 425 //noinspection StatementWithEmptyBody 426 if (disableGalFind) { 427 // do nothing 428 } else if (condition instanceof MultiCondition) { 429 List<Condition> conditions = ((ExchangeSession.MultiCondition) condition).getConditions(); 430 Operator operator = ((ExchangeSession.MultiCondition) condition).getOperator(); 431 if (operator == Operator.Or) { 432 for (Condition innerCondition : conditions) { 433 contacts.putAll(galFind(innerCondition, returningAttributes, sizeLimit)); 434 } 435 } else if (operator == Operator.And && !conditions.isEmpty()) { 436 Map<String, ExchangeSession.Contact> innerContacts = galFind(conditions.get(0), returningAttributes, sizeLimit); 437 for (ExchangeSession.Contact contact : innerContacts.values()) { 438 if (condition.isMatch(contact)) { 439 contacts.put(contact.getName().toLowerCase(), contact); 440 } 441 } 442 } 443 } else if (condition instanceof AttributeCondition) { 444 String searchAttributeName = ((ExchangeSession.AttributeCondition) condition).getAttributeName(); 445 String searchAttribute = GALFIND_CRITERIA_MAP.get(searchAttributeName); 446 if (searchAttribute != null) { 447 String searchValue = ((ExchangeSession.AttributeCondition) condition).getValue(); 448 StringBuilder query = new StringBuilder(); 449 if ("EM".equals(searchAttribute)) { 450 // mail search, split 451 int atIndex = searchValue.indexOf('@'); 452 // remove suffix 453 if (atIndex >= 0) { 454 searchValue = searchValue.substring(0, atIndex); 455 } 456 // split firstname.lastname 457 int dotIndex = searchValue.indexOf('.'); 458 if (dotIndex >= 0) { 459 // assume mail starts with firstname 460 query.append("&FN=").append(URIUtil.encodeWithinQuery(searchValue.substring(0, dotIndex))); 461 query.append("&LN=").append(URIUtil.encodeWithinQuery(searchValue.substring(dotIndex + 1))); 462 } else { 463 query.append("&FN=").append(URIUtil.encodeWithinQuery(searchValue)); 464 } 465 } else { 466 query.append('&').append(searchAttribute).append('=').append(URIUtil.encodeWithinQuery(searchValue)); 467 } 468 Map<String, Map<String, String>> results = galFind(query.toString()); 469 for (Map<String, String> result : results.values()) { 470 Contact contact = new Contact(); 471 contact.setName(result.get("AN")); 472 contact.put("imapUid", result.get("AN")); 473 buildGalfindContact(contact, result); 474 if (needGalLookup(searchAttributeName, returningAttributes)) { 475 galLookup(contact); 476 // iCal fix to suit both iCal 3 and 4: move cn to sn, remove cn 477 } else if (returningAttributes.contains("apple-serviceslocator")) { 478 if (contact.get("cn") != null && returningAttributes.contains("sn")) { 479 contact.put("sn", contact.get("cn")); 480 contact.remove("cn"); 481 } 482 } 483 if (condition.isMatch(contact)) { 484 contacts.put(contact.getName().toLowerCase(), contact); 485 } 486 } 487 } 488 489 } 490 return contacts; 491 } 492 needGalLookup(String searchAttributeName, Set<String> returningAttributes)493 protected boolean needGalLookup(String searchAttributeName, Set<String> returningAttributes) { 494 // return all attributes => call gallookup 495 if (returningAttributes == null || returningAttributes.isEmpty()) { 496 return true; 497 // iCal search, do not call gallookup 498 } else if (returningAttributes.contains("apple-serviceslocator")) { 499 return false; 500 // Lightning search, no need to gallookup 501 } else if ("sn".equals(searchAttributeName)) { 502 return returningAttributes.contains("sn"); 503 // search attribute is gallookup attribute, need to fetch value for isMatch 504 } else if (GALLOOKUP_ATTRIBUTES.contains(searchAttributeName)) { 505 return true; 506 } 507 508 for (String attributeName : GALLOOKUP_ATTRIBUTES) { 509 if (returningAttributes.contains(attributeName)) { 510 return true; 511 } 512 } 513 return false; 514 } 515 516 private boolean disableGalLookup; 517 518 /** 519 * Get extended address book information for person with gallookup. 520 * Does not work with Exchange 2007 521 * 522 * @param contact galfind contact 523 */ galLookup(Contact contact)524 public void galLookup(Contact contact) { 525 if (!disableGalLookup) { 526 LOGGER.debug("galLookup(" + contact.get("smtpemail1") + ')'); 527 HttpGet httpGet = new HttpGet(URIUtil.encodePathQuery(getCmdBasePath() + "?Cmd=gallookup&ADDR=" + contact.get("smtpemail1"))); 528 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) { 529 Map<String, Map<String, String>> results = XMLStreamUtil.getElementContentsAsMap(response.getEntity().getContent(), "person", "alias"); 530 // add detailed information 531 if (!results.isEmpty()) { 532 Map<String, String> personGalLookupDetails = results.get(contact.get("uid").toLowerCase()); 533 if (personGalLookupDetails != null) { 534 buildGalfindContact(contact, personGalLookupDetails); 535 } 536 } 537 } catch (IOException e) { 538 LOGGER.warn("Unable to gallookup person: " + contact + ", disable GalLookup"); 539 disableGalLookup = true; 540 } 541 } 542 } 543 buildGalfindContact(Contact contact, Map<String, String> response)544 protected void buildGalfindContact(Contact contact, Map<String, String> response) { 545 for (Map.Entry<String, String> entry : GALFIND_ATTRIBUTE_MAP.entrySet()) { 546 String attributeValue = response.get(entry.getValue()); 547 if (attributeValue != null) { 548 contact.put(entry.getKey(), attributeValue); 549 } 550 } 551 } 552 553 @Override getFreeBusyData(String attendee, String start, String end, int interval)554 protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException { 555 String freebusyUrl = publicFolderUrl + "/?cmd=freebusy" + 556 "&start=" + start + 557 "&end=" + end + 558 "&interval=" + interval + 559 "&u=SMTP:" + attendee; 560 HttpGet httpGet = new HttpGet(freebusyUrl); 561 httpGet.setHeader("Content-Type", "text/xml"); 562 String fbdata; 563 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) { 564 fbdata = StringUtil.getLastToken(new BasicResponseHandler().handleResponse(response), "<a:fbdata>", "</a:fbdata>"); 565 } 566 return fbdata; 567 } 568 DavExchangeSession(HttpClientAdapter httpClientAdapter, java.net.URI uri, String userName)569 public DavExchangeSession(HttpClientAdapter httpClientAdapter, java.net.URI uri, String userName) throws IOException { 570 this.httpClientAdapter = httpClientAdapter; 571 this.userName = userName; 572 buildSessionInfo(uri); 573 } 574 575 576 @Override buildSessionInfo(java.net.URI uri)577 public void buildSessionInfo(java.net.URI uri) throws DavMailException { 578 buildMailPath(uri); 579 580 // get base http mailbox http urls 581 getWellKnownFolders(); 582 } 583 584 static final String BASE_HREF = "<base href=\""; 585 586 /** 587 * Exchange 2003: get mailPath from welcome page 588 * 589 * @param uri current uri 590 * @return mail path from body 591 */ getMailpathFromWelcomePage(java.net.URI uri)592 protected String getMailpathFromWelcomePage(java.net.URI uri) { 593 String welcomePageMailPath = null; 594 // get user mail URL from html body (multi frame) 595 HttpGet method = new HttpGet(uri.toString()); 596 597 try ( 598 CloseableHttpResponse response = httpClientAdapter.execute(method); 599 InputStream inputStream = response.getEntity().getContent(); 600 BufferedReader mainPageReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) 601 ) { 602 String line; 603 //noinspection StatementWithEmptyBody 604 while ((line = mainPageReader.readLine()) != null && !line.toLowerCase().contains(BASE_HREF)) { 605 } 606 if (line != null) { 607 // Exchange 2003 608 int start = line.toLowerCase().indexOf(BASE_HREF) + BASE_HREF.length(); 609 int end = line.indexOf('\"', start); 610 String mailBoxBaseHref = line.substring(start, end); 611 URL baseURL = new URL(mailBoxBaseHref); 612 welcomePageMailPath = URIUtil.decode(baseURL.getPath()); 613 LOGGER.debug("Base href found in body, mailPath is " + welcomePageMailPath); 614 } 615 } catch (IOException e) { 616 LOGGER.error("Error parsing main page at " + method.getURI(), e); 617 } 618 return welcomePageMailPath; 619 } 620 buildMailPath(java.net.URI uri)621 protected void buildMailPath(java.net.URI uri) throws DavMailAuthenticationException { 622 // get mailPath from welcome page on Exchange 2003 623 mailPath = getMailpathFromWelcomePage(uri); 624 625 //noinspection VariableNotUsedInsideIf 626 if (mailPath != null) { 627 // Exchange 2003 628 serverVersion = "Exchange2003"; 629 fixClientHost(uri); 630 checkPublicFolder(); 631 buildEmail(uri.getHost()); 632 } else { 633 // Exchange 2007 : get alias and email from options page 634 serverVersion = "Exchange2007"; 635 636 // Gallookup is an Exchange 2003 only feature 637 disableGalLookup = true; 638 fixClientHost(uri); 639 getEmailAndAliasFromOptions(); 640 641 checkPublicFolder(); 642 643 // failover: try to get email through Webdav and Galfind 644 if (alias == null || email == null) { 645 buildEmail(uri.getHost()); 646 } 647 648 // build standard mailbox link with email 649 mailPath = "/exchange/" + email + '/'; 650 } 651 652 if (mailPath == null || email == null) { 653 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_PASSWORD_EXPIRED"); 654 } 655 LOGGER.debug("Current user email is " + email + ", alias is " + alias + ", mailPath is " + mailPath + " on " + serverVersion); 656 rootPath = mailPath.substring(0, mailPath.lastIndexOf('/', mailPath.length() - 2) + 1); 657 } 658 659 /** 660 * Determine user email through various means. 661 * 662 * @param hostName Exchange server host name for last failover 663 */ buildEmail(String hostName)664 public void buildEmail(String hostName) { 665 String mailBoxPath = getMailboxPath(); 666 // mailPath contains either alias or email 667 if (mailBoxPath != null && mailBoxPath.indexOf('@') >= 0) { 668 email = mailBoxPath; 669 alias = getAliasFromMailboxDisplayName(); 670 if (alias == null) { 671 alias = getAliasFromLogin(); 672 } 673 } else { 674 // use mailbox name as alias 675 alias = mailBoxPath; 676 email = getEmail(alias); 677 if (email == null) { 678 // failover: try to get email from login name 679 alias = getAliasFromLogin(); 680 email = getEmail(alias); 681 } 682 // another failover : get alias from mailPath display name 683 if (email == null) { 684 alias = getAliasFromMailboxDisplayName(); 685 email = getEmail(alias); 686 } 687 if (email == null) { 688 LOGGER.debug("Unable to get user email with alias " + mailBoxPath 689 + " or " + getAliasFromLogin() 690 + " or " + alias 691 ); 692 // last failover: build email from domain name and mailbox display name 693 StringBuilder buffer = new StringBuilder(); 694 // most reliable alias 695 if (mailBoxPath != null) { 696 alias = mailBoxPath; 697 } else { 698 alias = getAliasFromLogin(); 699 } 700 if (alias == null) { 701 alias = "unknown"; 702 } 703 buffer.append(alias); 704 if (alias.indexOf('@') < 0) { 705 buffer.append('@'); 706 if (hostName == null) { 707 hostName = "mail.unknown.com"; 708 } 709 int dotIndex = hostName.indexOf('.'); 710 if (dotIndex >= 0) { 711 buffer.append(hostName.substring(dotIndex + 1)); 712 } 713 } 714 email = buffer.toString(); 715 } 716 } 717 } 718 719 /** 720 * Get user alias from mailbox display name over Webdav. 721 * 722 * @return user alias 723 */ getAliasFromMailboxDisplayName()724 public String getAliasFromMailboxDisplayName() { 725 if (mailPath == null) { 726 return null; 727 } 728 String displayName = null; 729 try { 730 Folder rootFolder = getFolder(""); 731 if (rootFolder == null) { 732 LOGGER.warn(new BundleMessage("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath)); 733 } else { 734 displayName = rootFolder.displayName; 735 } 736 } catch (IOException e) { 737 LOGGER.warn(new BundleMessage("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath)); 738 } 739 return displayName; 740 } 741 742 /** 743 * Get current Exchange alias name from mailbox name 744 * 745 * @return user name 746 */ getMailboxPath()747 protected String getMailboxPath() { 748 if (mailPath == null) { 749 return null; 750 } 751 int index = mailPath.lastIndexOf('/', mailPath.length() - 2); 752 if (index >= 0 && mailPath.endsWith("/")) { 753 return mailPath.substring(index + 1, mailPath.length() - 1); 754 } else { 755 LOGGER.warn(new BundleMessage("EXCEPTION_INVALID_MAIL_PATH", mailPath)); 756 return null; 757 } 758 } 759 760 /** 761 * Get user email from global address list (galfind). 762 * 763 * @param alias user alias 764 * @return user email 765 */ getEmail(String alias)766 public String getEmail(String alias) { 767 String emailResult = null; 768 if (alias != null && !disableGalFind) { 769 try { 770 Map<String, Map<String, String>> results = galFind("&AN=" + URIUtil.encodeWithinQuery(alias)); 771 Map<String, String> result = results.get(alias.toLowerCase()); 772 if (result != null) { 773 emailResult = result.get("EM"); 774 } 775 } catch (IOException e) { 776 // galfind not available 777 disableGalFind = true; 778 LOGGER.debug("getEmail(" + alias + ") failed"); 779 } 780 } 781 return emailResult; 782 } 783 getURIPropertyIfExists(DavPropertySet properties, String alias)784 protected String getURIPropertyIfExists(DavPropertySet properties, String alias) throws IOException { 785 DavProperty property = properties.get(Field.getPropertyName(alias)); 786 if (property == null) { 787 return null; 788 } else { 789 return URIUtil.decode((String) property.getValue()); 790 } 791 } 792 793 // return last folder name from url 794 getFolderName(String url)795 protected String getFolderName(String url) { 796 if (url != null) { 797 if (url.endsWith("/")) { 798 return url.substring(url.lastIndexOf('/', url.length() - 2) + 1, url.length() - 1); 799 } else if (url.indexOf('/') > 0) { 800 return url.substring(url.lastIndexOf('/') + 1); 801 } else { 802 return null; 803 } 804 } else { 805 return null; 806 } 807 } 808 fixClientHost(java.net.URI currentUri)809 protected void fixClientHost(java.net.URI currentUri) { 810 // update client host, workaround for Exchange 2003 mailbox with an Exchange 2007 frontend 811 if (currentUri != null && currentUri.getHost() != null && currentUri.getScheme() != null) { 812 httpClientAdapter.setUri(currentUri); 813 } 814 } 815 checkPublicFolder()816 protected void checkPublicFolder() { 817 // check public folder access 818 try { 819 publicFolderUrl = URIUtils.resolve(httpClientAdapter.getUri(), PUBLIC_ROOT).toString(); 820 DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet(); 821 davPropertyNameSet.add(Field.getPropertyName("displayname")); 822 823 HttpPropfind httpPropfind = new HttpPropfind(publicFolderUrl, davPropertyNameSet, 0); 824 httpClientAdapter.executeDavRequest(httpPropfind); 825 // update public folder URI 826 publicFolderUrl = httpPropfind.getURI().toString(); 827 828 } catch (IOException e) { 829 LOGGER.warn("Public folders not available: " + (e.getMessage() == null ? e : e.getMessage())); 830 // default public folder path 831 publicFolderUrl = PUBLIC_ROOT; 832 } 833 } 834 835 getWellKnownFolders()836 protected void getWellKnownFolders() throws DavMailException { 837 // Retrieve well known URLs 838 try { 839 HttpPropfind httpPropfind = new HttpPropfind(mailPath, WELL_KNOWN_FOLDERS, 0); 840 MultiStatus multiStatus; 841 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) { 842 multiStatus = httpPropfind.getResponseBodyAsMultiStatus(response); 843 } 844 MultiStatusResponse[] responses = multiStatus.getResponses(); 845 if (responses.length == 0) { 846 throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath); 847 } 848 DavPropertySet properties = responses[0].getProperties(org.apache.http.HttpStatus.SC_OK); 849 inboxUrl = getURIPropertyIfExists(properties, "inbox"); 850 inboxName = getFolderName(inboxUrl); 851 deleteditemsUrl = getURIPropertyIfExists(properties, "deleteditems"); 852 deleteditemsName = getFolderName(deleteditemsUrl); 853 sentitemsUrl = getURIPropertyIfExists(properties, "sentitems"); 854 sentitemsName = getFolderName(sentitemsUrl); 855 sendmsgUrl = getURIPropertyIfExists(properties, "sendmsg"); 856 sendmsgName = getFolderName(sendmsgUrl); 857 draftsUrl = getURIPropertyIfExists(properties, "drafts"); 858 draftsName = getFolderName(draftsUrl); 859 calendarUrl = getURIPropertyIfExists(properties, "calendar"); 860 calendarName = getFolderName(calendarUrl); 861 tasksUrl = getURIPropertyIfExists(properties, "tasks"); 862 tasksName = getFolderName(tasksUrl); 863 contactsUrl = getURIPropertyIfExists(properties, "contacts"); 864 contactsName = getFolderName(contactsUrl); 865 outboxUrl = getURIPropertyIfExists(properties, "outbox"); 866 outboxName = getFolderName(outboxUrl); 867 // junk folder not available over webdav 868 869 LOGGER.debug("Inbox URL: " + inboxUrl + 870 " Trash URL: " + deleteditemsUrl + 871 " Sent URL: " + sentitemsUrl + 872 " Send URL: " + sendmsgUrl + 873 " Drafts URL: " + draftsUrl + 874 " Calendar URL: " + calendarUrl + 875 " Tasks URL: " + tasksUrl + 876 " Contacts URL: " + contactsUrl + 877 " Outbox URL: " + outboxUrl + 878 " Public folder URL: " + publicFolderUrl 879 ); 880 } catch (IOException | DavException e) { 881 LOGGER.error(e.getMessage()); 882 throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath); 883 } 884 } 885 886 protected static class MultiCondition extends ExchangeSession.MultiCondition { MultiCondition(Operator operator, Condition... condition)887 protected MultiCondition(Operator operator, Condition... condition) { 888 super(operator, condition); 889 } 890 appendTo(StringBuilder buffer)891 public void appendTo(StringBuilder buffer) { 892 boolean first = true; 893 894 for (Condition condition : conditions) { 895 if (condition != null && !condition.isEmpty()) { 896 if (first) { 897 buffer.append('('); 898 first = false; 899 } else { 900 buffer.append(' ').append(operator).append(' '); 901 } 902 condition.appendTo(buffer); 903 } 904 } 905 // at least one non empty condition 906 if (!first) { 907 buffer.append(')'); 908 } 909 } 910 } 911 912 protected static class NotCondition extends ExchangeSession.NotCondition { NotCondition(Condition condition)913 protected NotCondition(Condition condition) { 914 super(condition); 915 } 916 appendTo(StringBuilder buffer)917 public void appendTo(StringBuilder buffer) { 918 buffer.append("(Not "); 919 condition.appendTo(buffer); 920 buffer.append(')'); 921 } 922 } 923 924 static final Map<Operator, String> OPERATOR_MAP = new HashMap<>(); 925 926 static { OPERATOR_MAP.put(Operator.IsEqualTo, R)927 OPERATOR_MAP.put(Operator.IsEqualTo, " = "); OPERATOR_MAP.put(Operator.IsGreaterThanOrEqualTo, R)928 OPERATOR_MAP.put(Operator.IsGreaterThanOrEqualTo, " >= "); OPERATOR_MAP.put(Operator.IsGreaterThan, R)929 OPERATOR_MAP.put(Operator.IsGreaterThan, " > "); OPERATOR_MAP.put(Operator.IsLessThanOrEqualTo, R)930 OPERATOR_MAP.put(Operator.IsLessThanOrEqualTo, " <= "); OPERATOR_MAP.put(Operator.IsLessThan, R)931 OPERATOR_MAP.put(Operator.IsLessThan, " < "); OPERATOR_MAP.put(Operator.Like, R)932 OPERATOR_MAP.put(Operator.Like, " like "); OPERATOR_MAP.put(Operator.IsNull, R)933 OPERATOR_MAP.put(Operator.IsNull, " is null"); OPERATOR_MAP.put(Operator.IsFalse, R)934 OPERATOR_MAP.put(Operator.IsFalse, " = false"); OPERATOR_MAP.put(Operator.IsTrue, R)935 OPERATOR_MAP.put(Operator.IsTrue, " = true"); OPERATOR_MAP.put(Operator.StartsWith, R)936 OPERATOR_MAP.put(Operator.StartsWith, " = "); OPERATOR_MAP.put(Operator.Contains, R)937 OPERATOR_MAP.put(Operator.Contains, " = "); 938 } 939 940 protected static class AttributeCondition extends ExchangeSession.AttributeCondition { 941 protected boolean isIntValue; 942 AttributeCondition(String attributeName, Operator operator, String value)943 protected AttributeCondition(String attributeName, Operator operator, String value) { 944 super(attributeName, operator, value); 945 } 946 AttributeCondition(String attributeName, Operator operator, int value)947 protected AttributeCondition(String attributeName, Operator operator, int value) { 948 super(attributeName, operator, String.valueOf(value)); 949 isIntValue = true; 950 } 951 appendTo(StringBuilder buffer)952 public void appendTo(StringBuilder buffer) { 953 Field field = Field.get(attributeName); 954 buffer.append('"').append(field.getUri()).append('"'); 955 buffer.append(OPERATOR_MAP.get(operator)); 956 //noinspection VariableNotUsedInsideIf 957 if (field.cast != null) { 958 buffer.append("CAST (\""); 959 } else if (!isIntValue && !field.isIntValue()) { 960 buffer.append('\''); 961 } 962 if (Operator.Like == operator) { 963 buffer.append('%'); 964 } 965 if ("urlcompname".equals(field.alias)) { 966 buffer.append(StringUtil.encodeUrlcompname(StringUtil.davSearchEncode(value))); 967 } else if (field.isIntValue()) { 968 // check value 969 try { 970 Integer.parseInt(value); 971 buffer.append(value); 972 } catch (NumberFormatException e) { 973 // invalid value, replace with 0 974 buffer.append('0'); 975 } 976 } else { 977 buffer.append(StringUtil.davSearchEncode(value)); 978 } 979 if (Operator.Like == operator || Operator.StartsWith == operator) { 980 buffer.append('%'); 981 } 982 if (field.cast != null) { 983 buffer.append("\" as '").append(field.cast).append("')"); 984 } else if (!isIntValue && !field.isIntValue()) { 985 buffer.append('\''); 986 } 987 } 988 isMatch(ExchangeSession.Contact contact)989 public boolean isMatch(ExchangeSession.Contact contact) { 990 String lowerCaseValue = value.toLowerCase(); 991 String actualValue = contact.get(attributeName); 992 Operator actualOperator = operator; 993 // patch for iCal or Lightning search without galLookup 994 if (actualValue == null && ("givenName".equals(attributeName) || "sn".equals(attributeName))) { 995 actualValue = contact.get("cn"); 996 actualOperator = Operator.Like; 997 } 998 if (actualValue == null) { 999 return false; 1000 } 1001 actualValue = actualValue.toLowerCase(); 1002 return (actualOperator == Operator.IsEqualTo && actualValue.equals(lowerCaseValue)) || 1003 (actualOperator == Operator.Like && actualValue.contains(lowerCaseValue)) || 1004 (actualOperator == Operator.StartsWith && actualValue.startsWith(lowerCaseValue)); 1005 } 1006 } 1007 1008 protected static class HeaderCondition extends AttributeCondition { 1009 HeaderCondition(String attributeName, Operator operator, String value)1010 protected HeaderCondition(String attributeName, Operator operator, String value) { 1011 super(attributeName, operator, value); 1012 } 1013 1014 @Override appendTo(StringBuilder buffer)1015 public void appendTo(StringBuilder buffer) { 1016 buffer.append('"').append(Field.getHeader(attributeName).getUri()).append('"'); 1017 buffer.append(OPERATOR_MAP.get(operator)); 1018 buffer.append('\''); 1019 if (Operator.Like == operator) { 1020 buffer.append('%'); 1021 } 1022 buffer.append(value); 1023 if (Operator.Like == operator) { 1024 buffer.append('%'); 1025 } 1026 buffer.append('\''); 1027 } 1028 } 1029 1030 protected static class MonoCondition extends ExchangeSession.MonoCondition { MonoCondition(String attributeName, Operator operator)1031 protected MonoCondition(String attributeName, Operator operator) { 1032 super(attributeName, operator); 1033 } 1034 appendTo(StringBuilder buffer)1035 public void appendTo(StringBuilder buffer) { 1036 buffer.append('"').append(Field.get(attributeName).getUri()).append('"'); 1037 buffer.append(OPERATOR_MAP.get(operator)); 1038 } 1039 } 1040 1041 @Override and(Condition... condition)1042 public ExchangeSession.MultiCondition and(Condition... condition) { 1043 return new MultiCondition(Operator.And, condition); 1044 } 1045 1046 @Override or(Condition... condition)1047 public ExchangeSession.MultiCondition or(Condition... condition) { 1048 return new MultiCondition(Operator.Or, condition); 1049 } 1050 1051 @Override not(Condition condition)1052 public Condition not(Condition condition) { 1053 if (condition == null) { 1054 return null; 1055 } else { 1056 return new NotCondition(condition); 1057 } 1058 } 1059 1060 @Override isEqualTo(String attributeName, String value)1061 public Condition isEqualTo(String attributeName, String value) { 1062 return new AttributeCondition(attributeName, Operator.IsEqualTo, value); 1063 } 1064 1065 @Override isEqualTo(String attributeName, int value)1066 public Condition isEqualTo(String attributeName, int value) { 1067 return new AttributeCondition(attributeName, Operator.IsEqualTo, value); 1068 } 1069 1070 @Override headerIsEqualTo(String headerName, String value)1071 public Condition headerIsEqualTo(String headerName, String value) { 1072 return new HeaderCondition(headerName, Operator.IsEqualTo, value); 1073 } 1074 1075 @Override gte(String attributeName, String value)1076 public Condition gte(String attributeName, String value) { 1077 return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value); 1078 } 1079 1080 @Override lte(String attributeName, String value)1081 public Condition lte(String attributeName, String value) { 1082 return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value); 1083 } 1084 1085 @Override lt(String attributeName, String value)1086 public Condition lt(String attributeName, String value) { 1087 return new AttributeCondition(attributeName, Operator.IsLessThan, value); 1088 } 1089 1090 @Override gt(String attributeName, String value)1091 public Condition gt(String attributeName, String value) { 1092 return new AttributeCondition(attributeName, Operator.IsGreaterThan, value); 1093 } 1094 1095 @Override contains(String attributeName, String value)1096 public Condition contains(String attributeName, String value) { 1097 return new AttributeCondition(attributeName, Operator.Like, value); 1098 } 1099 1100 @Override startsWith(String attributeName, String value)1101 public Condition startsWith(String attributeName, String value) { 1102 return new AttributeCondition(attributeName, Operator.StartsWith, value); 1103 } 1104 1105 @Override isNull(String attributeName)1106 public Condition isNull(String attributeName) { 1107 return new MonoCondition(attributeName, Operator.IsNull); 1108 } 1109 1110 @Override exists(String attributeName)1111 public Condition exists(String attributeName) { 1112 return not(new MonoCondition(attributeName, Operator.IsNull)); 1113 } 1114 1115 @Override isTrue(String attributeName)1116 public Condition isTrue(String attributeName) { 1117 if ("Exchange2003".equals(this.serverVersion) && "deleted".equals(attributeName)) { 1118 return isEqualTo(attributeName, "1"); 1119 } else { 1120 return new MonoCondition(attributeName, Operator.IsTrue); 1121 } 1122 } 1123 1124 @Override isFalse(String attributeName)1125 public Condition isFalse(String attributeName) { 1126 if ("Exchange2003".equals(this.serverVersion) && "deleted".equals(attributeName)) { 1127 return or(isEqualTo(attributeName, "0"), isNull(attributeName)); 1128 } else { 1129 return new MonoCondition(attributeName, Operator.IsFalse); 1130 } 1131 } 1132 1133 /** 1134 * @inheritDoc 1135 */ 1136 public class Message extends ExchangeSession.Message { 1137 1138 @Override getPermanentId()1139 public String getPermanentId() { 1140 return permanentUrl; 1141 } 1142 1143 @Override getMimeHeaders()1144 protected InputStream getMimeHeaders() { 1145 InputStream result = null; 1146 try { 1147 String messageHeaders = getItemProperty(permanentUrl, "messageheaders"); 1148 if (messageHeaders != null) { 1149 final String MS_HEADER = "Microsoft Mail Internet Headers Version 2.0"; 1150 if (messageHeaders.startsWith(MS_HEADER)) { 1151 messageHeaders = messageHeaders.substring(MS_HEADER.length()); 1152 if (!messageHeaders.isEmpty() && messageHeaders.charAt(0) == '\r') { 1153 messageHeaders = messageHeaders.substring(1); 1154 } 1155 if (!messageHeaders.isEmpty() && messageHeaders.charAt(0) == '\n') { 1156 messageHeaders = messageHeaders.substring(1); 1157 } 1158 } 1159 // workaround for messages in Sent folder 1160 if (!messageHeaders.contains("From:")) { 1161 String from = getItemProperty(permanentUrl, "from"); 1162 messageHeaders = "From: " + from + '\n' + messageHeaders; 1163 } 1164 result = new ByteArrayInputStream(messageHeaders.getBytes(StandardCharsets.UTF_8)); 1165 } 1166 } catch (Exception e) { 1167 LOGGER.warn(e.getMessage()); 1168 } 1169 1170 return result; 1171 } 1172 } 1173 1174 1175 /** 1176 * @inheritDoc 1177 */ 1178 public class Contact extends ExchangeSession.Contact { 1179 /** 1180 * Build Contact instance from multistatusResponse info 1181 * 1182 * @param multiStatusResponse response 1183 * @throws IOException on error 1184 * @throws DavMailException on error 1185 */ Contact(MultiStatusResponse multiStatusResponse)1186 public Contact(MultiStatusResponse multiStatusResponse) throws IOException, DavMailException { 1187 setHref(URIUtil.decode(multiStatusResponse.getHref())); 1188 DavPropertySet properties = multiStatusResponse.getProperties(HttpStatus.SC_OK); 1189 permanentUrl = getURLPropertyIfExists(properties, "permanenturl"); 1190 etag = getPropertyIfExists(properties, "etag"); 1191 displayName = getPropertyIfExists(properties, "displayname"); 1192 for (String attributeName : CONTACT_ATTRIBUTES) { 1193 String value = getPropertyIfExists(properties, attributeName); 1194 if (value != null) { 1195 if ("bday".equals(attributeName) || "anniversary".equals(attributeName) 1196 || "lastmodified".equals(attributeName) || "datereceived".equals(attributeName)) { 1197 value = convertDateFromExchange(value); 1198 } else if ("haspicture".equals(attributeName) || "private".equals(attributeName)) { 1199 value = "1".equals(value) ? "true" : "false"; 1200 } 1201 put(attributeName, value); 1202 } 1203 } 1204 } 1205 Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch)1206 public Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) { 1207 super(folderPath, itemName, properties, etag, noneMatch); 1208 } 1209 1210 /** 1211 * Default constructor for galFind 1212 */ Contact()1213 public Contact() { 1214 } 1215 buildProperties()1216 protected Set<PropertyValue> buildProperties() { 1217 Set<PropertyValue> propertyValues = new HashSet<>(); 1218 for (Map.Entry<String, String> entry : entrySet()) { 1219 String key = entry.getKey(); 1220 if (!"photo".equals(key)) { 1221 propertyValues.add(Field.createPropertyValue(key, entry.getValue())); 1222 if (key.startsWith("email")) { 1223 propertyValues.add(Field.createPropertyValue(key + "type", "SMTP")); 1224 } 1225 } 1226 } 1227 1228 return propertyValues; 1229 } 1230 internalCreateOrUpdate(String encodedHref)1231 protected ExchangePropPatchRequest internalCreateOrUpdate(String encodedHref) throws IOException { 1232 ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(encodedHref, buildProperties()); 1233 propPatchRequest.setHeader("Translate", "f"); 1234 if (etag != null) { 1235 propPatchRequest.setHeader("If-Match", etag); 1236 } 1237 if (noneMatch != null) { 1238 propPatchRequest.setHeader("If-None-Match", noneMatch); 1239 } 1240 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) { 1241 LOGGER.debug("internalCreateOrUpdate returned " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase()); 1242 } 1243 return propPatchRequest; 1244 } 1245 1246 /** 1247 * Create or update contact 1248 * 1249 * @return action result 1250 * @throws IOException on error 1251 */ 1252 @Override createOrUpdate()1253 public ItemResult createOrUpdate() throws IOException { 1254 String encodedHref = URIUtil.encodePath(getHref()); 1255 ExchangePropPatchRequest propPatchRequest = internalCreateOrUpdate(encodedHref); 1256 int status = propPatchRequest.getStatusLine().getStatusCode(); 1257 if (status == HttpStatus.SC_MULTI_STATUS) { 1258 try { 1259 status = propPatchRequest.getResponseStatusCode(); 1260 } catch (HttpResponseException e) { 1261 throw new IOException(e.getMessage(), e); 1262 } 1263 //noinspection VariableNotUsedInsideIf 1264 if (status == HttpStatus.SC_CREATED) { 1265 LOGGER.debug("Created contact " + encodedHref); 1266 } else { 1267 LOGGER.debug("Updated contact " + encodedHref); 1268 } 1269 } else if (status == HttpStatus.SC_NOT_FOUND) { 1270 LOGGER.debug("Contact not found at " + encodedHref + ", searching permanenturl by urlcompname"); 1271 // failover, search item by urlcompname 1272 MultiStatusResponse[] responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1); 1273 if (responses.length == 1) { 1274 encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl"); 1275 LOGGER.warn("Contact found, permanenturl is " + encodedHref); 1276 propPatchRequest = internalCreateOrUpdate(encodedHref); 1277 status = propPatchRequest.getStatusLine().getStatusCode(); 1278 if (status == HttpStatus.SC_MULTI_STATUS) { 1279 try { 1280 status = propPatchRequest.getResponseStatusCode(); 1281 } catch (HttpResponseException e) { 1282 throw new IOException(e.getMessage(), e); 1283 } 1284 LOGGER.debug("Updated contact " + encodedHref); 1285 } else { 1286 LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchRequest.getStatusLine()); 1287 } 1288 } 1289 1290 } else { 1291 LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchRequest.getStatusLine().getReasonPhrase()); 1292 } 1293 ItemResult itemResult = new ItemResult(); 1294 // 440 means forbidden on Exchange 1295 if (status == 440) { 1296 status = HttpStatus.SC_FORBIDDEN; 1297 } 1298 itemResult.status = status; 1299 1300 if (status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) { 1301 String contactPictureUrl = URIUtil.encodePath(getHref() + "/ContactPicture.jpg"); 1302 String photo = get("photo"); 1303 if (photo != null) { 1304 try { 1305 final HttpPut httpPut = new HttpPut(contactPictureUrl); 1306 // need to update photo 1307 byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90); 1308 1309 httpPut.setHeader("Overwrite", "t"); 1310 // TODO: required ? 1311 httpPut.setHeader("Content-Type", "image/jpeg"); 1312 httpPut.setEntity(new ByteArrayEntity(resizedImageBytes, ContentType.IMAGE_JPEG)); 1313 1314 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPut)) { 1315 status = response.getStatusLine().getStatusCode(); 1316 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED) { 1317 throw new IOException("Unable to update contact picture: " + status + ' ' + response.getStatusLine().getReasonPhrase()); 1318 } 1319 } 1320 } catch (IOException e) { 1321 LOGGER.error("Error in contact photo create or update", e); 1322 throw e; 1323 } 1324 1325 Set<PropertyValue> picturePropertyValues = new HashSet<>(); 1326 picturePropertyValues.add(Field.createPropertyValue("attachmentContactPhoto", "true")); 1327 // picturePropertyValues.add(Field.createPropertyValue("renderingPosition", "-1")); 1328 picturePropertyValues.add(Field.createPropertyValue("attachExtension", ".jpg")); 1329 1330 final ExchangePropPatchRequest attachmentPropPatchRequest = new ExchangePropPatchRequest(contactPictureUrl, picturePropertyValues); 1331 try (CloseableHttpResponse response = httpClientAdapter.execute(attachmentPropPatchRequest)) { 1332 attachmentPropPatchRequest.handleResponse(response); 1333 status = response.getStatusLine().getStatusCode(); 1334 if (status != HttpStatus.SC_MULTI_STATUS) { 1335 LOGGER.error("Error in contact photo create or update: " + response.getStatusLine().getStatusCode()); 1336 throw new IOException("Unable to update contact picture"); 1337 } 1338 } 1339 1340 } else { 1341 // try to delete picture 1342 HttpDelete httpDelete = new HttpDelete(contactPictureUrl); 1343 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) { 1344 status = response.getStatusLine().getStatusCode(); 1345 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) { 1346 LOGGER.error("Error in contact photo delete: " + status); 1347 throw new IOException("Unable to delete contact picture"); 1348 } 1349 } 1350 } 1351 // need to retrieve new etag 1352 HttpHead headMethod = new HttpHead(URIUtil.encodePath(getHref())); 1353 try (CloseableHttpResponse response = httpClientAdapter.execute(headMethod)) { 1354 if (response.getFirstHeader("ETag") != null) { 1355 itemResult.etag = response.getFirstHeader("ETag").getValue(); 1356 } 1357 } 1358 } 1359 return itemResult; 1360 1361 } 1362 1363 } 1364 1365 /** 1366 * @inheritDoc 1367 */ 1368 public class Event extends ExchangeSession.Event { 1369 protected String instancetype; 1370 1371 /** 1372 * Build Event instance from response info. 1373 * 1374 * @param multiStatusResponse response 1375 * @throws IOException on error 1376 */ Event(MultiStatusResponse multiStatusResponse)1377 public Event(MultiStatusResponse multiStatusResponse) throws IOException { 1378 setHref(URIUtil.decode(multiStatusResponse.getHref())); 1379 DavPropertySet properties = multiStatusResponse.getProperties(HttpStatus.SC_OK); 1380 permanentUrl = getURLPropertyIfExists(properties, "permanenturl"); 1381 etag = getPropertyIfExists(properties, "etag"); 1382 displayName = getPropertyIfExists(properties, "displayname"); 1383 subject = getPropertyIfExists(properties, "subject"); 1384 instancetype = getPropertyIfExists(properties, "instancetype"); 1385 contentClass = getPropertyIfExists(properties, "contentclass"); 1386 } 1387 getPermanentUrl()1388 protected String getPermanentUrl() { 1389 return permanentUrl; 1390 } 1391 Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch)1392 public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException { 1393 super(folderPath, itemName, contentClass, itemBody, etag, noneMatch); 1394 } 1395 getICSFromInternetContentProperty()1396 protected byte[] getICSFromInternetContentProperty() throws IOException, DavException, MessagingException { 1397 byte[] result = null; 1398 // PropFind PR_INTERNET_CONTENT 1399 String propertyValue = getItemProperty(permanentUrl, "internetContent"); 1400 if (propertyValue != null) { 1401 result = getICS(new ByteArrayInputStream(IOUtil.decodeBase64(propertyValue))); 1402 } 1403 return result; 1404 } 1405 1406 /** 1407 * Load ICS content from Exchange server. 1408 * User Translate: f header to get MIME event content and get ICS attachment from it 1409 * 1410 * @return ICS (iCalendar) event 1411 * @throws IOException on error 1412 */ 1413 @Override getEventContent()1414 public byte[] getEventContent() throws IOException { 1415 byte[] result = null; 1416 LOGGER.debug("Get event subject: " + subject + " contentclass: " + contentClass + " href: " + getHref() + " permanentUrl: " + permanentUrl); 1417 // do not try to load tasks MIME body 1418 if (!"urn:content-classes:task".equals(contentClass)) { 1419 // try to get PR_INTERNET_CONTENT 1420 try { 1421 result = getICSFromInternetContentProperty(); 1422 if (result == null) { 1423 HttpGet httpGet = new HttpGet(encodeAndFixUrl(permanentUrl)); 1424 httpGet.setHeader("Content-Type", "text/xml; charset=utf-8"); 1425 httpGet.setHeader("Translate", "f"); 1426 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) { 1427 result = getICS(response.getEntity().getContent()); 1428 } 1429 } 1430 } catch (DavException | IOException | MessagingException e) { 1431 LOGGER.warn(e.getMessage()); 1432 } 1433 } 1434 1435 // failover: rebuild event from MAPI properties 1436 if (result == null) { 1437 try { 1438 result = getICSFromItemProperties(); 1439 } catch (IOException e) { 1440 deleteBroken(); 1441 throw e; 1442 } 1443 } 1444 // debug code 1445 /*if (new String(result).indexOf("VTODO") < 0) { 1446 LOGGER.debug("Original body: " + new String(result)); 1447 result = getICSFromItemProperties(); 1448 LOGGER.debug("Rebuilt body: " + new String(result)); 1449 }*/ 1450 1451 return result; 1452 } 1453 getICSFromItemProperties()1454 private byte[] getICSFromItemProperties() throws HttpNotFoundException { 1455 byte[] result; 1456 1457 // experimental: build VCALENDAR from properties 1458 1459 try { 1460 //MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod); 1461 Set<String> eventProperties = new HashSet<>(); 1462 eventProperties.add("method"); 1463 1464 eventProperties.add("created"); 1465 eventProperties.add("calendarlastmodified"); 1466 eventProperties.add("dtstamp"); 1467 eventProperties.add("calendaruid"); 1468 eventProperties.add("subject"); 1469 eventProperties.add("dtstart"); 1470 eventProperties.add("dtend"); 1471 eventProperties.add("transparent"); 1472 eventProperties.add("organizer"); 1473 eventProperties.add("to"); 1474 eventProperties.add("description"); 1475 eventProperties.add("rrule"); 1476 eventProperties.add("exdate"); 1477 eventProperties.add("sensitivity"); 1478 eventProperties.add("alldayevent"); 1479 eventProperties.add("busystatus"); 1480 eventProperties.add("reminderset"); 1481 eventProperties.add("reminderdelta"); 1482 // task 1483 eventProperties.add("importance"); 1484 eventProperties.add("uid"); 1485 eventProperties.add("taskstatus"); 1486 eventProperties.add("percentcomplete"); 1487 eventProperties.add("keywords"); 1488 eventProperties.add("startdate"); 1489 eventProperties.add("duedate"); 1490 eventProperties.add("datecompleted"); 1491 1492 MultiStatusResponse[] responses = searchItems(folderPath, eventProperties, DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1); 1493 if (responses.length == 0) { 1494 throw new HttpNotFoundException(permanentUrl + " not found"); 1495 } 1496 DavPropertySet davPropertySet = responses[0].getProperties(HttpStatus.SC_OK); 1497 VCalendar localVCalendar = new VCalendar(); 1498 localVCalendar.setPropertyValue("PRODID", "-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN"); 1499 localVCalendar.setPropertyValue("VERSION", "2.0"); 1500 localVCalendar.setPropertyValue("METHOD", getPropertyIfExists(davPropertySet, "method")); 1501 VObject vEvent = new VObject(); 1502 vEvent.setPropertyValue("CREATED", convertDateFromExchange(getPropertyIfExists(davPropertySet, "created"))); 1503 vEvent.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getPropertyIfExists(davPropertySet, "calendarlastmodified"))); 1504 vEvent.setPropertyValue("DTSTAMP", convertDateFromExchange(getPropertyIfExists(davPropertySet, "dtstamp"))); 1505 1506 String uid = getPropertyIfExists(davPropertySet, "calendaruid"); 1507 if (uid == null) { 1508 uid = getPropertyIfExists(davPropertySet, "uid"); 1509 } 1510 vEvent.setPropertyValue("UID", uid); 1511 vEvent.setPropertyValue("SUMMARY", getPropertyIfExists(davPropertySet, "subject")); 1512 vEvent.setPropertyValue("DESCRIPTION", getPropertyIfExists(davPropertySet, "description")); 1513 vEvent.setPropertyValue("PRIORITY", convertPriorityFromExchange(getPropertyIfExists(davPropertySet, "importance"))); 1514 vEvent.setPropertyValue("CATEGORIES", getPropertyIfExists(davPropertySet, "keywords")); 1515 String sensitivity = getPropertyIfExists(davPropertySet, "sensitivity"); 1516 if ("2".equals(sensitivity)) { 1517 vEvent.setPropertyValue("CLASS", "PRIVATE"); 1518 } else if ("3".equals(sensitivity)) { 1519 vEvent.setPropertyValue("CLASS", "CONFIDENTIAL"); 1520 } else if ("0".equals(sensitivity)) { 1521 vEvent.setPropertyValue("CLASS", "PUBLIC"); 1522 } 1523 1524 if (instancetype == null) { 1525 vEvent.type = "VTODO"; 1526 double percentComplete = getDoublePropertyIfExists(davPropertySet, "percentcomplete"); 1527 if (percentComplete > 0) { 1528 vEvent.setPropertyValue("PERCENT-COMPLETE", String.valueOf((int) (percentComplete * 100))); 1529 } 1530 vEvent.setPropertyValue("STATUS", taskTovTodoStatusMap.get(getPropertyIfExists(davPropertySet, "taskstatus"))); 1531 vEvent.setPropertyValue("DUE;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "duedate"))); 1532 vEvent.setPropertyValue("DTSTART;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "startdate"))); 1533 vEvent.setPropertyValue("COMPLETED;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "datecompleted"))); 1534 1535 } else { 1536 vEvent.type = "VEVENT"; 1537 // check mandatory dtstart value 1538 String dtstart = getPropertyIfExists(davPropertySet, "dtstart"); 1539 if (dtstart != null) { 1540 vEvent.setPropertyValue("DTSTART", convertDateFromExchange(dtstart)); 1541 } else { 1542 LOGGER.warn("missing dtstart on item, using fake value. Set davmail.deleteBroken=true to delete broken events"); 1543 vEvent.setPropertyValue("DTSTART", "20000101T000000Z"); 1544 deleteBroken(); 1545 } 1546 // same on DTEND 1547 String dtend = getPropertyIfExists(davPropertySet, "dtend"); 1548 if (dtend != null) { 1549 vEvent.setPropertyValue("DTEND", convertDateFromExchange(dtend)); 1550 } else { 1551 LOGGER.warn("missing dtend on item, using fake value. Set davmail.deleteBroken=true to delete broken events"); 1552 vEvent.setPropertyValue("DTEND", "20000101T010000Z"); 1553 deleteBroken(); 1554 } 1555 vEvent.setPropertyValue("TRANSP", getPropertyIfExists(davPropertySet, "transparent")); 1556 vEvent.setPropertyValue("RRULE", getPropertyIfExists(davPropertySet, "rrule")); 1557 String exdates = getPropertyIfExists(davPropertySet, "exdate"); 1558 if (exdates != null) { 1559 String[] exdatearray = exdates.split(","); 1560 for (String exdate : exdatearray) { 1561 vEvent.addPropertyValue("EXDATE", 1562 StringUtil.convertZuluDateTimeToAllDay(convertDateFromExchange(exdate))); 1563 } 1564 } 1565 String organizer = getPropertyIfExists(davPropertySet, "organizer"); 1566 String organizerEmail = null; 1567 if (organizer != null) { 1568 InternetAddress organizerAddress = new InternetAddress(organizer); 1569 organizerEmail = organizerAddress.getAddress(); 1570 vEvent.setPropertyValue("ORGANIZER", "MAILTO:" + organizerEmail); 1571 } 1572 1573 // Parse attendee list 1574 String toHeader = getPropertyIfExists(davPropertySet, "to"); 1575 if (toHeader != null && !toHeader.equals(organizerEmail)) { 1576 InternetAddress[] attendees = InternetAddress.parseHeader(toHeader, false); 1577 for (InternetAddress attendee : attendees) { 1578 if (!attendee.getAddress().equalsIgnoreCase(organizerEmail)) { 1579 VProperty vProperty = new VProperty("ATTENDEE", attendee.getAddress()); 1580 if (attendee.getPersonal() != null) { 1581 vProperty.addParam("CN", attendee.getPersonal()); 1582 } 1583 vEvent.addProperty(vProperty); 1584 } 1585 } 1586 1587 } 1588 vEvent.setPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT", 1589 "1".equals(getPropertyIfExists(davPropertySet, "alldayevent")) ? "TRUE" : "FALSE"); 1590 vEvent.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS", getPropertyIfExists(davPropertySet, "busystatus")); 1591 1592 if ("1".equals(getPropertyIfExists(davPropertySet, "reminderset"))) { 1593 VObject vAlarm = new VObject(); 1594 vAlarm.type = "VALARM"; 1595 vAlarm.setPropertyValue("ACTION", "DISPLAY"); 1596 vAlarm.setPropertyValue("DISPLAY", "Reminder"); 1597 String reminderdelta = getPropertyIfExists(davPropertySet, "reminderdelta"); 1598 VProperty vProperty = new VProperty("TRIGGER", "-PT" + reminderdelta + 'M'); 1599 vProperty.addParam("VALUE", "DURATION"); 1600 vAlarm.addProperty(vProperty); 1601 vEvent.addVObject(vAlarm); 1602 } 1603 } 1604 1605 localVCalendar.addVObject(vEvent); 1606 result = localVCalendar.toString().getBytes(StandardCharsets.UTF_8); 1607 } catch (MessagingException | IOException e) { 1608 LOGGER.warn("Unable to rebuild event content: " + e.getMessage(), e); 1609 throw new HttpNotFoundException("Unable to get event " + getName() + " subject: " + subject + " at " + permanentUrl + ": " + e.getMessage()); 1610 } 1611 1612 return result; 1613 } 1614 deleteBroken()1615 protected void deleteBroken() { 1616 // try to delete broken event 1617 if (Settings.getBooleanProperty("davmail.deleteBroken")) { 1618 LOGGER.warn("Deleting broken event at: " + permanentUrl); 1619 try { 1620 HttpDelete httpDelete = new HttpDelete(encodeAndFixUrl(permanentUrl)); 1621 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) { 1622 LOGGER.warn("deleteBroken returned " + response.getStatusLine().getStatusCode()); 1623 } 1624 } catch (IOException e) { 1625 LOGGER.warn("Unable to delete broken event at: " + permanentUrl); 1626 } 1627 } 1628 } 1629 internalCreateOrUpdate(String encodedHref, byte[] mimeContent)1630 protected CloseableHttpResponse internalCreateOrUpdate(String encodedHref, byte[] mimeContent) throws IOException { 1631 HttpPut httpPut = new HttpPut(encodedHref); 1632 httpPut.setHeader("Translate", "f"); 1633 httpPut.setHeader("Overwrite", "f"); 1634 if (etag != null) { 1635 httpPut.setHeader("If-Match", etag); 1636 } 1637 if (noneMatch != null) { 1638 httpPut.setHeader("If-None-Match", noneMatch); 1639 } 1640 httpPut.setHeader("Content-Type", "message/rfc822"); 1641 httpPut.setEntity(new ByteArrayEntity(mimeContent, ContentType.getByMimeType("message/rfc822"))); 1642 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPut)) { 1643 return response; 1644 } 1645 } 1646 1647 /** 1648 * @inheritDoc 1649 */ 1650 @Override createOrUpdate()1651 public ItemResult createOrUpdate() throws IOException { 1652 ItemResult itemResult = new ItemResult(); 1653 if (vCalendar.isTodo()) { 1654 if ((mailPath + calendarName).equals(folderPath)) { 1655 folderPath = mailPath + tasksName; 1656 } 1657 String encodedHref = URIUtil.encodePath(getHref()); 1658 Set<PropertyValue> propertyValues = new HashSet<>(); 1659 // set contentclass on create 1660 if (noneMatch != null) { 1661 propertyValues.add(Field.createPropertyValue("contentclass", "urn:content-classes:task")); 1662 propertyValues.add(Field.createPropertyValue("outlookmessageclass", "IPM.Task")); 1663 propertyValues.add(Field.createPropertyValue("calendaruid", vCalendar.getFirstVeventPropertyValue("UID"))); 1664 } 1665 propertyValues.add(Field.createPropertyValue("subject", vCalendar.getFirstVeventPropertyValue("SUMMARY"))); 1666 propertyValues.add(Field.createPropertyValue("description", vCalendar.getFirstVeventPropertyValue("DESCRIPTION"))); 1667 propertyValues.add(Field.createPropertyValue("importance", convertPriorityToExchange(vCalendar.getFirstVeventPropertyValue("PRIORITY")))); 1668 String percentComplete = vCalendar.getFirstVeventPropertyValue("PERCENT-COMPLETE"); 1669 if (percentComplete == null) { 1670 percentComplete = "0"; 1671 } 1672 propertyValues.add(Field.createPropertyValue("percentcomplete", String.valueOf(Double.parseDouble(percentComplete) / 100))); 1673 String taskStatus = vTodoToTaskStatusMap.get(vCalendar.getFirstVeventPropertyValue("STATUS")); 1674 propertyValues.add(Field.createPropertyValue("taskstatus", taskStatus)); 1675 propertyValues.add(Field.createPropertyValue("keywords", vCalendar.getFirstVeventPropertyValue("CATEGORIES"))); 1676 propertyValues.add(Field.createPropertyValue("startdate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART")))); 1677 propertyValues.add(Field.createPropertyValue("duedate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE")))); 1678 propertyValues.add(Field.createPropertyValue("datecompleted", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("COMPLETED")))); 1679 1680 propertyValues.add(Field.createPropertyValue("iscomplete", "2".equals(taskStatus) ? "true" : "false")); 1681 propertyValues.add(Field.createPropertyValue("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART")))); 1682 propertyValues.add(Field.createPropertyValue("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE")))); 1683 1684 ExchangePropPatchRequest propPatchMethod = new ExchangePropPatchRequest(encodedHref, propertyValues); 1685 propPatchMethod.setHeader("Translate", "f"); 1686 if (etag != null) { 1687 propPatchMethod.setHeader("If-Match", etag); 1688 } 1689 if (noneMatch != null) { 1690 propPatchMethod.setHeader("If-None-Match", noneMatch); 1691 } 1692 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) { 1693 int status = response.getStatusLine().getStatusCode(); 1694 1695 if (status == HttpStatus.SC_MULTI_STATUS) { 1696 Item newItem = getItem(folderPath, itemName); 1697 try { 1698 itemResult.status = propPatchMethod.getResponseStatusCode(); 1699 } catch (HttpResponseException e) { 1700 throw new IOException(e.getMessage(), e); 1701 } 1702 itemResult.etag = newItem.etag; 1703 } else { 1704 itemResult.status = status; 1705 } 1706 } 1707 1708 } else { 1709 String encodedHref = URIUtil.encodePath(getHref()); 1710 byte[] mimeContent = createMimeContent(); 1711 HttpResponse httpResponse = internalCreateOrUpdate(encodedHref, mimeContent); 1712 int status = httpResponse.getStatusLine().getStatusCode(); 1713 1714 if (status == HttpStatus.SC_OK) { 1715 LOGGER.debug("Updated event " + encodedHref); 1716 } else if (status == HttpStatus.SC_CREATED) { 1717 LOGGER.debug("Created event " + encodedHref); 1718 } else if (status == HttpStatus.SC_NOT_FOUND) { 1719 LOGGER.debug("Event not found at " + encodedHref + ", searching permanenturl by urlcompname"); 1720 // failover, search item by urlcompname 1721 MultiStatusResponse[] responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1); 1722 if (responses.length == 1) { 1723 encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl"); 1724 LOGGER.warn("Event found, permanenturl is " + encodedHref); 1725 httpResponse = internalCreateOrUpdate(encodedHref, mimeContent); 1726 status = httpResponse.getStatusLine().getStatusCode(); 1727 if (status == HttpStatus.SC_OK) { 1728 LOGGER.debug("Updated event " + encodedHref); 1729 } else { 1730 LOGGER.warn("Unable to create or update event " + status + ' ' + httpResponse.getStatusLine().getReasonPhrase()); 1731 } 1732 } 1733 } else { 1734 LOGGER.warn("Unable to create or update event " + status + ' ' + httpResponse.getStatusLine().getReasonPhrase()); 1735 } 1736 1737 // 440 means forbidden on Exchange 1738 if (status == 440) { 1739 status = HttpStatus.SC_FORBIDDEN; 1740 } else if (status == HttpStatus.SC_UNAUTHORIZED && getHref().startsWith("/public")) { 1741 LOGGER.warn("Ignore 401 unauthorized on public event"); 1742 status = HttpStatus.SC_OK; 1743 } 1744 itemResult.status = status; 1745 if (httpResponse.getFirstHeader("GetETag") != null) { 1746 itemResult.etag = httpResponse.getFirstHeader("GetETag").getValue(); 1747 } 1748 1749 // trigger activeSync push event, only if davmail.forceActiveSyncUpdate setting is true 1750 if ((status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) && 1751 (Settings.getBooleanProperty("davmail.forceActiveSyncUpdate"))) { 1752 ArrayList<PropEntry> propertyList = new ArrayList<>(); 1753 // Set contentclass to make ActiveSync happy 1754 propertyList.add(Field.createDavProperty("contentclass", contentClass)); 1755 // ... but also set PR_INTERNET_CONTENT to preserve custom properties 1756 propertyList.add(Field.createDavProperty("internetContent", IOUtil.encodeBase64AsString(mimeContent))); 1757 HttpProppatch propPatchMethod = new HttpProppatch(encodedHref, propertyList); 1758 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) { 1759 int patchStatus = response.getStatusLine().getStatusCode(); 1760 if (patchStatus != HttpStatus.SC_MULTI_STATUS) { 1761 LOGGER.warn("Unable to patch event to trigger activeSync push"); 1762 } else { 1763 // need to retrieve new etag 1764 Item newItem = getItem(folderPath, itemName); 1765 itemResult.etag = newItem.etag; 1766 } 1767 } 1768 } 1769 } 1770 return itemResult; 1771 } 1772 1773 1774 } 1775 buildFolder(MultiStatusResponse entity)1776 protected Folder buildFolder(MultiStatusResponse entity) throws IOException { 1777 String href = URIUtil.decode(entity.getHref()); 1778 Folder folder = new Folder(); 1779 DavPropertySet properties = entity.getProperties(HttpStatus.SC_OK); 1780 folder.displayName = getPropertyIfExists(properties, "displayname"); 1781 folder.folderClass = getPropertyIfExists(properties, "folderclass"); 1782 folder.hasChildren = "1".equals(getPropertyIfExists(properties, "hassubs")); 1783 folder.noInferiors = "1".equals(getPropertyIfExists(properties, "nosubs")); 1784 folder.count = getIntPropertyIfExists(properties, "count"); 1785 folder.unreadCount = getIntPropertyIfExists(properties, "unreadcount"); 1786 // fake recent value 1787 folder.recent = folder.unreadCount; 1788 folder.ctag = getPropertyIfExists(properties, "contenttag"); 1789 folder.etag = getPropertyIfExists(properties, "lastmodified"); 1790 1791 folder.uidNext = getIntPropertyIfExists(properties, "uidNext"); 1792 1793 // replace well known folder names 1794 if (inboxUrl != null && href.startsWith(inboxUrl)) { 1795 folder.folderPath = href.replaceFirst(inboxUrl, INBOX); 1796 } else if (sentitemsUrl != null && href.startsWith(sentitemsUrl)) { 1797 folder.folderPath = href.replaceFirst(sentitemsUrl, SENT); 1798 } else if (draftsUrl != null && href.startsWith(draftsUrl)) { 1799 folder.folderPath = href.replaceFirst(draftsUrl, DRAFTS); 1800 } else if (deleteditemsUrl != null && href.startsWith(deleteditemsUrl)) { 1801 folder.folderPath = href.replaceFirst(deleteditemsUrl, TRASH); 1802 } else if (calendarUrl != null && href.startsWith(calendarUrl)) { 1803 folder.folderPath = href.replaceFirst(calendarUrl, CALENDAR); 1804 } else if (contactsUrl != null && href.startsWith(contactsUrl)) { 1805 folder.folderPath = href.replaceFirst(contactsUrl, CONTACTS); 1806 } else { 1807 int index = href.indexOf(mailPath.substring(0, mailPath.length() - 1)); 1808 if (index >= 0) { 1809 if (index + mailPath.length() > href.length()) { 1810 folder.folderPath = ""; 1811 } else { 1812 folder.folderPath = href.substring(index + mailPath.length()); 1813 } 1814 } else { 1815 try { 1816 java.net.URI folderURI = new java.net.URI(href); 1817 folder.folderPath = folderURI.getPath(); 1818 if (folder.folderPath == null) { 1819 throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href); 1820 } 1821 } catch (URISyntaxException e) { 1822 throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href); 1823 } 1824 } 1825 } 1826 if (folder.folderPath.endsWith("/")) { 1827 folder.folderPath = folder.folderPath.substring(0, folder.folderPath.length() - 1); 1828 } 1829 return folder; 1830 } 1831 1832 protected static final Set<String> FOLDER_PROPERTIES = new HashSet<>(); 1833 1834 static { 1835 FOLDER_PROPERTIES.add("displayname"); 1836 FOLDER_PROPERTIES.add("folderclass"); 1837 FOLDER_PROPERTIES.add("hassubs"); 1838 FOLDER_PROPERTIES.add("nosubs"); 1839 FOLDER_PROPERTIES.add("count"); 1840 FOLDER_PROPERTIES.add("unreadcount"); 1841 FOLDER_PROPERTIES.add("contenttag"); 1842 FOLDER_PROPERTIES.add("lastmodified"); 1843 FOLDER_PROPERTIES.add("uidNext"); 1844 } 1845 1846 protected static final DavPropertyNameSet FOLDER_PROPERTIES_NAME_SET = new DavPropertyNameSet(); 1847 1848 static { 1849 for (String attribute : FOLDER_PROPERTIES) { Field.getPropertyName(attribute)1850 FOLDER_PROPERTIES_NAME_SET.add(Field.getPropertyName(attribute)); 1851 } 1852 } 1853 1854 /** 1855 * @inheritDoc 1856 */ 1857 @Override internalGetFolder(String folderPath)1858 protected Folder internalGetFolder(String folderPath) throws IOException { 1859 MultiStatus multiStatus = httpClientAdapter.executeDavRequest(new HttpPropfind( 1860 URIUtil.encodePath(getFolderPath(folderPath)), 1861 FOLDER_PROPERTIES_NAME_SET, 0)); 1862 MultiStatusResponse[] responses = multiStatus.getResponses(); 1863 1864 Folder folder = null; 1865 if (responses.length > 0) { 1866 folder = buildFolder(responses[0]); 1867 folder.folderPath = folderPath; 1868 } 1869 return folder; 1870 } 1871 1872 /** 1873 * @inheritDoc 1874 */ 1875 @Override getSubFolders(String folderPath, Condition condition, boolean recursive)1876 public List<Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException { 1877 boolean isPublic = folderPath.startsWith("/public"); 1878 FolderQueryTraversal mode = (!isPublic && recursive) ? FolderQueryTraversal.Deep : FolderQueryTraversal.Shallow; 1879 List<Folder> folders = new ArrayList<>(); 1880 1881 MultiStatusResponse[] responses = searchItems(folderPath, FOLDER_PROPERTIES, and(isTrue("isfolder"), isFalse("ishidden"), condition), mode, 0); 1882 1883 for (MultiStatusResponse response : responses) { 1884 Folder folder = buildFolder(response); 1885 folders.add(buildFolder(response)); 1886 if (isPublic && recursive) { 1887 getSubFolders(folder.folderPath, condition, recursive); 1888 } 1889 } 1890 return folders; 1891 } 1892 1893 /** 1894 * @inheritDoc 1895 */ 1896 @Override createFolder(String folderPath, String folderClass, Map<String, String> properties)1897 public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException { 1898 Set<PropertyValue> propertyValues = new HashSet<>(); 1899 if (properties != null) { 1900 for (Map.Entry<String, String> entry : properties.entrySet()) { 1901 propertyValues.add(Field.createPropertyValue(entry.getKey(), entry.getValue())); 1902 } 1903 } 1904 propertyValues.add(Field.createPropertyValue("folderclass", folderClass)); 1905 1906 // standard MkColMethod does not take properties, override ExchangePropPatchRequest instead 1907 ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues) { 1908 @Override 1909 public String getMethod() { 1910 return "MKCOL"; 1911 } 1912 }; 1913 int status; 1914 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) { 1915 propPatchRequest.handleResponse(response); 1916 status = response.getStatusLine().getStatusCode(); 1917 if (status == HttpStatus.SC_MULTI_STATUS) { 1918 status = propPatchRequest.getResponseStatusCode(); 1919 } else if (status == HttpStatus.SC_METHOD_NOT_ALLOWED) { 1920 LOGGER.info("Folder " + folderPath + " already exists"); 1921 } 1922 } catch (HttpResponseException e) { 1923 throw new IOException(e.getMessage(), e); 1924 } 1925 LOGGER.debug("Create folder " + folderPath + " returned " + status); 1926 return status; 1927 } 1928 1929 /** 1930 * @inheritDoc 1931 */ 1932 @Override updateFolder(String folderPath, Map<String, String> properties)1933 public int updateFolder(String folderPath, Map<String, String> properties) throws IOException { 1934 Set<PropertyValue> propertyValues = new HashSet<>(); 1935 if (properties != null) { 1936 for (Map.Entry<String, String> entry : properties.entrySet()) { 1937 propertyValues.add(Field.createPropertyValue(entry.getKey(), entry.getValue())); 1938 } 1939 } 1940 1941 ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues); 1942 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) { 1943 propPatchRequest.handleResponse(response); 1944 int status = response.getStatusLine().getStatusCode(); 1945 if (status == HttpStatus.SC_MULTI_STATUS) { 1946 try { 1947 status = propPatchRequest.getResponseStatusCode(); 1948 } catch (HttpResponseException e) { 1949 throw new IOException(e.getMessage(), e); 1950 } 1951 } 1952 1953 return status; 1954 } 1955 } 1956 1957 /** 1958 * @inheritDoc 1959 */ 1960 @Override deleteFolder(String folderPath)1961 public void deleteFolder(String folderPath) throws IOException { 1962 HttpDelete httpDelete = new HttpDelete(URIUtil.encodePath(getFolderPath(folderPath))); 1963 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) { 1964 int status = response.getStatusLine().getStatusCode(); 1965 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) { 1966 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response); 1967 } 1968 } 1969 } 1970 1971 /** 1972 * @inheritDoc 1973 */ 1974 @Override moveFolder(String folderPath, String targetPath)1975 public void moveFolder(String folderPath, String targetPath) throws IOException { 1976 HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(folderPath)), 1977 URIUtil.encodePath(getFolderPath(targetPath)), false); 1978 try (CloseableHttpResponse response = httpClientAdapter.execute(httpMove)) { 1979 int statusCode = response.getStatusLine().getStatusCode(); 1980 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) { 1981 throw new HttpPreconditionFailedException(BundleMessage.format("EXCEPTION_UNABLE_TO_MOVE_FOLDER")); 1982 } else if (statusCode != HttpStatus.SC_CREATED) { 1983 throw HttpClientAdapter.buildHttpResponseException(httpMove, response); 1984 } else if (folderPath.equalsIgnoreCase("/users/" + getEmail() + "/calendar")) { 1985 // calendar renamed, need to reload well known folders 1986 getWellKnownFolders(); 1987 } 1988 } 1989 } 1990 1991 /** 1992 * @inheritDoc 1993 */ 1994 @Override moveItem(String sourcePath, String targetPath)1995 public void moveItem(String sourcePath, String targetPath) throws IOException { 1996 HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(sourcePath)), 1997 URIUtil.encodePath(getFolderPath(targetPath)), false); 1998 moveItem(httpMove); 1999 } 2000 moveItem(HttpMove httpMove)2001 protected void moveItem(HttpMove httpMove) throws IOException { 2002 try (CloseableHttpResponse response = httpClientAdapter.execute(httpMove)) { 2003 int statusCode = response.getStatusLine().getStatusCode(); 2004 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) { 2005 throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_ITEM"); 2006 } else if (statusCode != HttpStatus.SC_CREATED && statusCode != HttpStatus.SC_OK) { 2007 throw HttpClientAdapter.buildHttpResponseException(httpMove, response); 2008 } 2009 } 2010 } 2011 getPropertyIfExists(DavPropertySet properties, String alias)2012 protected String getPropertyIfExists(DavPropertySet properties, String alias) { 2013 DavProperty property = properties.get(Field.getResponsePropertyName(alias)); 2014 if (property == null) { 2015 return null; 2016 } else { 2017 Object value = property.getValue(); 2018 if (value instanceof Node) { 2019 return ((Node) value).getTextContent(); 2020 } else if (value instanceof List) { 2021 StringBuilder buffer = new StringBuilder(); 2022 for (Object node : (List) value) { 2023 if (buffer.length() > 0) { 2024 buffer.append(','); 2025 } 2026 if (node instanceof Node) { 2027 // jackrabbit 2028 buffer.append(((Node) node).getTextContent()); 2029 } else { 2030 // ExchangeDavMethod 2031 buffer.append(node); 2032 } 2033 } 2034 return buffer.toString(); 2035 } else { 2036 return (String) value; 2037 } 2038 } 2039 } 2040 getURLPropertyIfExists(DavPropertySet properties, @SuppressWarnings(R) String alias)2041 protected String getURLPropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) throws IOException { 2042 String result = getPropertyIfExists(properties, alias); 2043 if (result != null) { 2044 result = URIUtil.decode(result); 2045 } 2046 return result; 2047 } 2048 getIntPropertyIfExists(DavPropertySet properties, String alias)2049 protected int getIntPropertyIfExists(DavPropertySet properties, String alias) { 2050 DavProperty property = properties.get(Field.getPropertyName(alias)); 2051 if (property == null) { 2052 return 0; 2053 } else { 2054 return Integer.parseInt((String) property.getValue()); 2055 } 2056 } 2057 getLongPropertyIfExists(DavPropertySet properties, @SuppressWarnings(R) String alias)2058 protected long getLongPropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) { 2059 DavProperty property = properties.get(Field.getPropertyName(alias)); 2060 if (property == null) { 2061 return 0; 2062 } else { 2063 return Long.parseLong((String) property.getValue()); 2064 } 2065 } 2066 getDoublePropertyIfExists(DavPropertySet properties, @SuppressWarnings(R) String alias)2067 protected double getDoublePropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) { 2068 DavProperty property = properties.get(Field.getResponsePropertyName(alias)); 2069 if (property == null) { 2070 return 0; 2071 } else { 2072 return Double.parseDouble((String) property.getValue()); 2073 } 2074 } 2075 getBinaryPropertyIfExists(DavPropertySet properties, @SuppressWarnings(R) String alias)2076 protected byte[] getBinaryPropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) { 2077 byte[] property = null; 2078 String base64Property = getPropertyIfExists(properties, alias); 2079 if (base64Property != null) { 2080 property = IOUtil.decodeBase64(base64Property); 2081 } 2082 return property; 2083 } 2084 2085 buildMessage(MultiStatusResponse responseEntity)2086 protected Message buildMessage(MultiStatusResponse responseEntity) throws IOException { 2087 Message message = new Message(); 2088 message.messageUrl = URIUtil.decode(responseEntity.getHref()); 2089 DavPropertySet properties = responseEntity.getProperties(HttpStatus.SC_OK); 2090 2091 message.permanentUrl = getURLPropertyIfExists(properties, "permanenturl"); 2092 message.size = getIntPropertyIfExists(properties, "messageSize"); 2093 message.uid = getPropertyIfExists(properties, "uid"); 2094 message.contentClass = getPropertyIfExists(properties, "contentclass"); 2095 message.imapUid = getLongPropertyIfExists(properties, "imapUid"); 2096 message.read = "1".equals(getPropertyIfExists(properties, "read")); 2097 message.junk = "1".equals(getPropertyIfExists(properties, "junk")); 2098 message.flagged = "2".equals(getPropertyIfExists(properties, "flagStatus")); 2099 message.draft = (getIntPropertyIfExists(properties, "messageFlags") & 8) != 0; 2100 String lastVerbExecuted = getPropertyIfExists(properties, "lastVerbExecuted"); 2101 message.answered = "102".equals(lastVerbExecuted) || "103".equals(lastVerbExecuted); 2102 message.forwarded = "104".equals(lastVerbExecuted); 2103 message.date = convertDateFromExchange(getPropertyIfExists(properties, "date")); 2104 message.deleted = "1".equals(getPropertyIfExists(properties, "deleted")); 2105 2106 String lastmodified = convertDateFromExchange(getPropertyIfExists(properties, "lastmodified")); 2107 message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date); 2108 2109 message.keywords = getPropertyIfExists(properties, "keywords"); 2110 2111 if (LOGGER.isDebugEnabled()) { 2112 StringBuilder buffer = new StringBuilder(); 2113 buffer.append("Message"); 2114 if (message.imapUid != 0) { 2115 buffer.append(" IMAP uid: ").append(message.imapUid); 2116 } 2117 if (message.uid != null) { 2118 buffer.append(" uid: ").append(message.uid); 2119 } 2120 buffer.append(" href: ").append(responseEntity.getHref()).append(" permanenturl:").append(message.permanentUrl); 2121 LOGGER.debug(buffer.toString()); 2122 } 2123 return message; 2124 } 2125 2126 @Override searchMessages(String folderPath, Set<String> attributes, Condition condition)2127 public MessageList searchMessages(String folderPath, Set<String> attributes, Condition condition) throws IOException { 2128 MessageList messages = new MessageList(); 2129 int maxCount = Settings.getIntProperty("davmail.folderSizeLimit", 0); 2130 MultiStatusResponse[] responses = searchItems(folderPath, attributes, and(isFalse("isfolder"), isFalse("ishidden"), condition), FolderQueryTraversal.Shallow, maxCount); 2131 2132 for (MultiStatusResponse response : responses) { 2133 Message message = buildMessage(response); 2134 message.messageList = messages; 2135 messages.add(message); 2136 } 2137 Collections.sort(messages); 2138 return messages; 2139 } 2140 2141 /** 2142 * @inheritDoc 2143 */ 2144 @Override searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount)2145 public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException { 2146 List<ExchangeSession.Contact> contacts = new ArrayList<>(); 2147 MultiStatusResponse[] responses = searchItems(folderPath, attributes, 2148 and(isEqualTo("outlookmessageclass", "IPM.Contact"), isFalse("isfolder"), isFalse("ishidden"), condition), 2149 FolderQueryTraversal.Shallow, maxCount); 2150 for (MultiStatusResponse response : responses) { 2151 contacts.add(new Contact(response)); 2152 } 2153 return contacts; 2154 } 2155 2156 /** 2157 * Common item properties 2158 */ 2159 protected static final Set<String> ITEM_PROPERTIES = new HashSet<>(); 2160 2161 static { 2162 ITEM_PROPERTIES.add("etag"); 2163 ITEM_PROPERTIES.add("displayname"); 2164 // calendar CdoInstanceType 2165 ITEM_PROPERTIES.add("instancetype"); 2166 ITEM_PROPERTIES.add("urlcompname"); 2167 ITEM_PROPERTIES.add("subject"); 2168 ITEM_PROPERTIES.add("contentclass"); 2169 } 2170 2171 @Override getItemProperties()2172 protected Set<String> getItemProperties() { 2173 return ITEM_PROPERTIES; 2174 } 2175 2176 2177 /** 2178 * @inheritDoc 2179 */ 2180 @Override getEventMessages(String folderPath)2181 public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException { 2182 return searchEvents(folderPath, ITEM_PROPERTIES, 2183 and(isEqualTo("contentclass", "urn:content-classes:calendarmessage"), 2184 or(isNull("processed"), isFalse("processed")))); 2185 } 2186 2187 2188 @Override searchEvents(String folderPath, Set<String> attributes, Condition condition)2189 public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException { 2190 List<ExchangeSession.Event> events = new ArrayList<>(); 2191 MultiStatusResponse[] responses = searchItems(folderPath, attributes, and(isFalse("isfolder"), isFalse("ishidden"), condition), FolderQueryTraversal.Shallow, 0); 2192 for (MultiStatusResponse response : responses) { 2193 String instancetype = getPropertyIfExists(response.getProperties(HttpStatus.SC_OK), "instancetype"); 2194 Event event = new Event(response); 2195 //noinspection VariableNotUsedInsideIf 2196 if (instancetype == null) { 2197 // check ics content 2198 try { 2199 event.getBody(); 2200 // getBody success => add event or task 2201 events.add(event); 2202 } catch (IOException e) { 2203 // invalid event: exclude from list 2204 LOGGER.warn("Invalid event " + event.displayName + " found at " + response.getHref(), e); 2205 } 2206 } else { 2207 events.add(event); 2208 } 2209 } 2210 return events; 2211 } 2212 2213 @Override getCalendarItemCondition(Condition dateCondition)2214 protected Condition getCalendarItemCondition(Condition dateCondition) { 2215 boolean caldavEnableLegacyTasks = Settings.getBooleanProperty("davmail.caldavEnableLegacyTasks", false); 2216 if (caldavEnableLegacyTasks) { 2217 // return tasks created in calendar folder 2218 return or(isNull("instancetype"), 2219 isEqualTo("instancetype", 1), 2220 and(isEqualTo("instancetype", 0), dateCondition)); 2221 } else { 2222 // instancetype 0 single appointment / 1 master recurring appointment 2223 return and(or(isEqualTo("outlookmessageclass", "IPM.Appointment"), isEqualTo("outlookmessageclass", "IPM.Appointment.MeetingEvent")), 2224 or(isEqualTo("instancetype", 1), 2225 and(isEqualTo("instancetype", 0), dateCondition))); 2226 } 2227 } 2228 searchItems(String folderPath, Set<String> attributes, Condition condition, FolderQueryTraversal folderQueryTraversal, int maxCount)2229 protected MultiStatusResponse[] searchItems(String folderPath, Set<String> attributes, Condition condition, 2230 FolderQueryTraversal folderQueryTraversal, int maxCount) throws IOException { 2231 String folderUrl; 2232 if (folderPath.startsWith("http")) { 2233 folderUrl = folderPath; 2234 } else { 2235 folderUrl = getFolderPath(folderPath); 2236 } 2237 StringBuilder searchRequest = new StringBuilder(); 2238 searchRequest.append("SELECT ") 2239 .append(Field.getRequestPropertyString("permanenturl")); 2240 if (attributes != null) { 2241 for (String attribute : attributes) { 2242 searchRequest.append(',').append(Field.getRequestPropertyString(attribute)); 2243 } 2244 } 2245 searchRequest.append(" FROM SCOPE('").append(folderQueryTraversal).append(" TRAVERSAL OF \"").append(folderUrl).append("\"')"); 2246 if (condition != null) { 2247 searchRequest.append(" WHERE "); 2248 condition.appendTo(searchRequest); 2249 } 2250 searchRequest.append(" ORDER BY ").append(Field.getRequestPropertyString("imapUid")).append(" DESC"); 2251 DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_QUERY", searchRequest)); 2252 MultiStatusResponse[] responses = httpClientAdapter.executeSearchRequest( 2253 encodeAndFixUrl(folderUrl), searchRequest.toString(), maxCount); 2254 DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_RESULT", responses.length)); 2255 return responses; 2256 } 2257 2258 protected static final Set<String> EVENT_REQUEST_PROPERTIES = new HashSet<>(); 2259 2260 static { 2261 EVENT_REQUEST_PROPERTIES.add("permanenturl"); 2262 EVENT_REQUEST_PROPERTIES.add("urlcompname"); 2263 EVENT_REQUEST_PROPERTIES.add("etag"); 2264 EVENT_REQUEST_PROPERTIES.add("contentclass"); 2265 EVENT_REQUEST_PROPERTIES.add("displayname"); 2266 EVENT_REQUEST_PROPERTIES.add("subject"); 2267 } 2268 2269 protected static final DavPropertyNameSet EVENT_REQUEST_PROPERTIES_NAME_SET = new DavPropertyNameSet(); 2270 2271 static { 2272 for (String attribute : EVENT_REQUEST_PROPERTIES) { Field.getPropertyName(attribute)2273 EVENT_REQUEST_PROPERTIES_NAME_SET.add(Field.getPropertyName(attribute)); 2274 } 2275 2276 } 2277 2278 @Override getItem(String folderPath, String itemName)2279 public Item getItem(String folderPath, String itemName) throws IOException { 2280 String emlItemName = convertItemNameToEML(itemName); 2281 String itemPath = getFolderPath(folderPath) + '/' + emlItemName; 2282 MultiStatusResponse[] responses = null; 2283 try { 2284 HttpPropfind httpPropfind = new HttpPropfind(URIUtil.encodePath(itemPath), EVENT_REQUEST_PROPERTIES_NAME_SET, 0); 2285 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) { 2286 responses = httpPropfind.getResponseBodyAsMultiStatus(response).getResponses(); 2287 } catch (HttpNotFoundException | DavException e) { 2288 // ignore 2289 } 2290 if (responses == null || responses.length == 0 && isMainCalendar(folderPath)) { 2291 if (itemName.endsWith(".ics")) { 2292 itemName = itemName.substring(0, itemName.length() - 3) + "EML"; 2293 } 2294 // look for item in tasks folder 2295 HttpPropfind taskHttpPropfind = new HttpPropfind(URIUtil.encodePath(getFolderPath(TASKS) + '/' + emlItemName), EVENT_REQUEST_PROPERTIES_NAME_SET, 0); 2296 try (CloseableHttpResponse response = httpClientAdapter.execute(taskHttpPropfind)) { 2297 responses = taskHttpPropfind.getResponseBodyAsMultiStatus(response).getResponses(); 2298 } catch (HttpNotFoundException | DavException e) { 2299 // ignore 2300 } 2301 } 2302 if (responses == null || responses.length == 0) { 2303 throw new HttpNotFoundException(itemPath + " not found"); 2304 } 2305 } catch (HttpNotFoundException e) { 2306 try { 2307 LOGGER.debug(itemPath + " not found, searching by urlcompname"); 2308 // failover: try to get event by displayname 2309 responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", emlItemName), FolderQueryTraversal.Shallow, 1); 2310 if (responses.length == 0 && isMainCalendar(folderPath)) { 2311 responses = searchItems(TASKS, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", emlItemName), FolderQueryTraversal.Shallow, 1); 2312 } 2313 if (responses.length == 0) { 2314 throw new HttpNotFoundException(itemPath + " not found"); 2315 } 2316 } catch (HttpNotFoundException e2) { 2317 LOGGER.debug("last failover: search all items"); 2318 List<ExchangeSession.Event> events = getAllEvents(folderPath); 2319 for (ExchangeSession.Event event : events) { 2320 if (itemName.equals(event.getName())) { 2321 HttpPropfind permanentHttpPropfind = new HttpPropfind(encodeAndFixUrl(((DavExchangeSession.Event) event).getPermanentUrl()), EVENT_REQUEST_PROPERTIES_NAME_SET, 0); 2322 try (CloseableHttpResponse response = httpClientAdapter.execute(permanentHttpPropfind)) { 2323 responses = permanentHttpPropfind.getResponseBodyAsMultiStatus(response).getResponses(); 2324 } catch (DavException e3) { 2325 // ignore 2326 } 2327 break; 2328 } 2329 } 2330 if (responses == null || responses.length == 0) { 2331 throw new HttpNotFoundException(itemPath + " not found"); 2332 } 2333 LOGGER.warn("search by urlcompname failed, actual value is " + getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "urlcompname")); 2334 } 2335 } 2336 // build item 2337 String contentClass = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "contentclass"); 2338 String urlcompname = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "urlcompname"); 2339 if ("urn:content-classes:person".equals(contentClass)) { 2340 // retrieve Contact properties 2341 List<ExchangeSession.Contact> contacts = searchContacts(folderPath, CONTACT_ATTRIBUTES, 2342 isEqualTo("urlcompname", StringUtil.decodeUrlcompname(urlcompname)), 1); 2343 if (contacts.isEmpty()) { 2344 LOGGER.warn("Item found, but unable to build contact"); 2345 throw new HttpNotFoundException(itemPath + " not found"); 2346 } 2347 return contacts.get(0); 2348 } else if ("urn:content-classes:appointment".equals(contentClass) 2349 || "urn:content-classes:calendarmessage".equals(contentClass) 2350 || "urn:content-classes:task".equals(contentClass)) { 2351 return new Event(responses[0]); 2352 } else { 2353 LOGGER.warn("wrong contentclass on item " + itemPath + ": " + contentClass); 2354 // return item anyway 2355 return new Event(responses[0]); 2356 } 2357 2358 } 2359 2360 @Override getContactPhoto(ExchangeSession.Contact contact)2361 public ExchangeSession.ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException { 2362 ContactPhoto contactPhoto; 2363 final HttpGet httpGet = new HttpGet(URIUtil.encodePath(contact.getHref()) + "/ContactPicture.jpg"); 2364 httpGet.setHeader("Translate", "f"); 2365 httpGet.setHeader("Accept-Encoding", "gzip"); 2366 2367 InputStream inputStream = null; 2368 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) { 2369 if (HttpClientAdapter.isGzipEncoded(response)) { 2370 inputStream = (new GZIPInputStream(response.getEntity().getContent())); 2371 } else { 2372 inputStream = response.getEntity().getContent(); 2373 } 2374 2375 contactPhoto = new ContactPhoto(); 2376 contactPhoto.contentType = "image/jpeg"; 2377 2378 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 2379 InputStream partInputStream = inputStream; 2380 IOUtil.write(partInputStream, baos); 2381 contactPhoto.content = IOUtil.encodeBase64AsString(baos.toByteArray()); 2382 } finally { 2383 if (inputStream != null) { 2384 try { 2385 inputStream.close(); 2386 } catch (IOException e) { 2387 LOGGER.debug(e); 2388 } 2389 } 2390 } 2391 return contactPhoto; 2392 } 2393 2394 @Override sendEvent(String icsBody)2395 public int sendEvent(String icsBody) throws IOException { 2396 String itemName = UUID.randomUUID().toString() + ".EML"; 2397 byte[] mimeContent = (new Event(getFolderPath(DRAFTS), itemName, "urn:content-classes:calendarmessage", icsBody, null, null)).createMimeContent(); 2398 if (mimeContent == null) { 2399 // no recipients, cancel 2400 return HttpStatus.SC_NO_CONTENT; 2401 } else { 2402 sendMessage(mimeContent); 2403 return HttpStatus.SC_OK; 2404 } 2405 } 2406 2407 @Override deleteItem(String folderPath, String itemName)2408 public void deleteItem(String folderPath, String itemName) throws IOException { 2409 String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName)); 2410 HttpDelete httpDelete = new HttpDelete(eventPath); 2411 int status; 2412 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) { 2413 status = response.getStatusLine().getStatusCode(); 2414 } 2415 if (status == HttpStatus.SC_NOT_FOUND && isMainCalendar(folderPath)) { 2416 // retry in tasks folder 2417 eventPath = URIUtil.encodePath(getFolderPath(TASKS) + '/' + convertItemNameToEML(itemName)); 2418 httpDelete = new HttpDelete(eventPath); 2419 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) { 2420 status = response.getStatusLine().getStatusCode(); 2421 } 2422 } 2423 if (status == HttpStatus.SC_NOT_FOUND) { 2424 LOGGER.debug("Unable to delete " + itemName + ": item not found"); 2425 } 2426 } 2427 2428 @Override processItem(String folderPath, String itemName)2429 public void processItem(String folderPath, String itemName) throws IOException { 2430 String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName)); 2431 // do not delete calendar messages, mark read and processed 2432 ArrayList<PropEntry> list = new ArrayList<>(); 2433 list.add(Field.createDavProperty("processed", "true")); 2434 list.add(Field.createDavProperty("read", "1")); 2435 HttpProppatch patchMethod = new HttpProppatch(eventPath, list); 2436 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) { 2437 LOGGER.debug("Processed " + itemName + " " + response.getStatusLine().getStatusCode()); 2438 } 2439 } 2440 2441 @Override internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch)2442 public ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException { 2443 return new Event(getFolderPath(folderPath), itemName, contentClass, icsBody, etag, noneMatch).createOrUpdate(); 2444 } 2445 2446 /** 2447 * create a fake event to get VTIMEZONE body 2448 */ 2449 @Override loadVtimezone()2450 protected void loadVtimezone() { 2451 try { 2452 // create temporary folder 2453 String folderPath = getFolderPath("davmailtemp"); 2454 createCalendarFolder(folderPath, null); 2455 2456 String fakeEventUrl = null; 2457 if ("Exchange2003".equals(serverVersion)) { 2458 HttpPost httpPost = new HttpPost(URIUtil.encodePath(folderPath)); 2459 ArrayList<NameValuePair> parameters = new ArrayList<>(); 2460 parameters.add(new BasicNameValuePair("Cmd", "saveappt")); 2461 parameters.add(new BasicNameValuePair("FORMTYPE", "appointment")); 2462 httpPost.setEntity(new UrlEncodedFormEntity(parameters, Consts.UTF_8)); 2463 2464 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPost)) { 2465 // create fake event 2466 int statusCode = response.getStatusLine().getStatusCode(); 2467 if (statusCode == HttpStatus.SC_OK) { 2468 fakeEventUrl = StringUtil.getToken(new BasicResponseHandler().handleResponse(response), "<span id=\"itemHREF\">", "</span>"); 2469 if (fakeEventUrl != null) { 2470 fakeEventUrl = URIUtil.decode(fakeEventUrl); 2471 } 2472 } 2473 } 2474 } 2475 // failover for Exchange 2007, use PROPPATCH with forced timezone 2476 if (fakeEventUrl == null) { 2477 ArrayList<PropEntry> propertyList = new ArrayList<>(); 2478 propertyList.add(Field.createDavProperty("contentclass", "urn:content-classes:appointment")); 2479 propertyList.add(Field.createDavProperty("outlookmessageclass", "IPM.Appointment")); 2480 propertyList.add(Field.createDavProperty("instancetype", "0")); 2481 2482 // get forced timezone id from settings 2483 String timezoneId = Settings.getProperty("davmail.timezoneId"); 2484 if (timezoneId == null) { 2485 // get timezoneid from OWA settings 2486 timezoneId = getTimezoneIdFromExchange(); 2487 } 2488 // without a timezoneId, use Exchange timezone 2489 if (timezoneId != null) { 2490 propertyList.add(Field.createDavProperty("timezoneid", timezoneId)); 2491 } 2492 String patchMethodUrl = folderPath + '/' + UUID.randomUUID().toString() + ".EML"; 2493 HttpProppatch patchMethod = new HttpProppatch(URIUtil.encodePath(patchMethodUrl), propertyList); 2494 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) { 2495 int statusCode = response.getStatusLine().getStatusCode(); 2496 if (statusCode == HttpStatus.SC_MULTI_STATUS) { 2497 fakeEventUrl = patchMethodUrl; 2498 } 2499 } 2500 } 2501 if (fakeEventUrl != null) { 2502 // get fake event body 2503 HttpGet httpGet = new HttpGet(URIUtil.encodePath(fakeEventUrl)); 2504 httpGet.setHeader("Translate", "f"); 2505 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) { 2506 this.vTimezone = new VObject("BEGIN:VTIMEZONE" + 2507 StringUtil.getToken(new BasicResponseHandler().handleResponse(response), "BEGIN:VTIMEZONE", "END:VTIMEZONE") + 2508 "END:VTIMEZONE\r\n"); 2509 } 2510 } 2511 2512 // delete temporary folder 2513 deleteFolder("davmailtemp"); 2514 } catch (IOException e) { 2515 LOGGER.warn("Unable to get VTIMEZONE info: " + e, e); 2516 } 2517 } 2518 getTimezoneIdFromExchange()2519 protected String getTimezoneIdFromExchange() { 2520 String timezoneId = null; 2521 String timezoneName = null; 2522 try { 2523 Set<String> attributes = new HashSet<>(); 2524 attributes.add("roamingdictionary"); 2525 2526 MultiStatusResponse[] responses = searchItems("/users/" + getEmail() + "/NON_IPM_SUBTREE", attributes, isEqualTo("messageclass", "IPM.Configuration.OWA.UserOptions"), DavExchangeSession.FolderQueryTraversal.Deep, 1); 2527 if (responses.length == 1) { 2528 byte[] roamingdictionary = getBinaryPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "roamingdictionary"); 2529 if (roamingdictionary != null) { 2530 timezoneName = getTimezoneNameFromRoamingDictionary(roamingdictionary); 2531 if (timezoneName != null) { 2532 timezoneId = ResourceBundle.getBundle("timezoneids").getString(timezoneName); 2533 } 2534 } 2535 } 2536 } catch (MissingResourceException e) { 2537 LOGGER.warn("Unable to retrieve Exchange timezone id for name " + timezoneName); 2538 } catch (IOException e) { 2539 LOGGER.warn("Unable to retrieve Exchange timezone id: " + e.getMessage(), e); 2540 } 2541 return timezoneId; 2542 } 2543 getTimezoneNameFromRoamingDictionary(byte[] roamingdictionary)2544 protected String getTimezoneNameFromRoamingDictionary(byte[] roamingdictionary) { 2545 String timezoneName = null; 2546 XMLStreamReader reader; 2547 try { 2548 reader = XMLStreamUtil.createXMLStreamReader(roamingdictionary); 2549 while (reader.hasNext()) { 2550 reader.next(); 2551 if (XMLStreamUtil.isStartTag(reader, "e") 2552 && "18-timezone".equals(reader.getAttributeValue(null, "k"))) { 2553 String value = reader.getAttributeValue(null, "v"); 2554 if (value != null && value.startsWith("18-")) { 2555 timezoneName = value.substring(3); 2556 } 2557 } 2558 } 2559 2560 } catch (XMLStreamException e) { 2561 LOGGER.error("Error while parsing RoamingDictionary: " + e, e); 2562 } 2563 return timezoneName; 2564 } 2565 2566 @Override buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch)2567 protected Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) { 2568 return new Contact(getFolderPath(folderPath), itemName, properties, etag, noneMatch); 2569 } 2570 buildProperties(Map<String, String> properties)2571 protected List<PropEntry> buildProperties(Map<String, String> properties) { 2572 ArrayList<PropEntry> list = new ArrayList<>(); 2573 if (properties != null) { 2574 for (Map.Entry<String, String> entry : properties.entrySet()) { 2575 if ("read".equals(entry.getKey())) { 2576 list.add(Field.createDavProperty("read", entry.getValue())); 2577 } else if ("junk".equals(entry.getKey())) { 2578 list.add(Field.createDavProperty("junk", entry.getValue())); 2579 } else if ("flagged".equals(entry.getKey())) { 2580 list.add(Field.createDavProperty("flagStatus", entry.getValue())); 2581 } else if ("answered".equals(entry.getKey())) { 2582 list.add(Field.createDavProperty("lastVerbExecuted", entry.getValue())); 2583 if ("102".equals(entry.getValue())) { 2584 list.add(Field.createDavProperty("iconIndex", "261")); 2585 } 2586 } else if ("forwarded".equals(entry.getKey())) { 2587 list.add(Field.createDavProperty("lastVerbExecuted", entry.getValue())); 2588 if ("104".equals(entry.getValue())) { 2589 list.add(Field.createDavProperty("iconIndex", "262")); 2590 } 2591 } else if ("bcc".equals(entry.getKey())) { 2592 list.add(Field.createDavProperty("bcc", entry.getValue())); 2593 } else if ("deleted".equals(entry.getKey())) { 2594 list.add(Field.createDavProperty("deleted", entry.getValue())); 2595 } else if ("datereceived".equals(entry.getKey())) { 2596 list.add(Field.createDavProperty("datereceived", entry.getValue())); 2597 } else if ("keywords".equals(entry.getKey())) { 2598 list.add(Field.createDavProperty("keywords", entry.getValue())); 2599 } 2600 } 2601 } 2602 return list; 2603 } 2604 2605 /** 2606 * Create message in specified folder. 2607 * Will overwrite an existing message with same messageName in the same folder 2608 * 2609 * @param folderPath Exchange folder path 2610 * @param messageName message name 2611 * @param properties message properties (flags) 2612 * @param mimeMessage MIME message 2613 * @throws IOException when unable to create message 2614 */ 2615 @Override createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage)2616 public void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException { 2617 String messageUrl = URIUtil.encodePathQuery(getFolderPath(folderPath) + '/' + messageName); 2618 2619 List<PropEntry> davProperties = buildProperties(properties); 2620 2621 if (properties != null && properties.containsKey("draft")) { 2622 // note: draft is readonly after create, create the message first with requested messageFlags 2623 davProperties.add(Field.createDavProperty("messageFlags", properties.get("draft"))); 2624 } 2625 if (properties != null && properties.containsKey("mailOverrideFormat")) { 2626 davProperties.add(Field.createDavProperty("mailOverrideFormat", properties.get("mailOverrideFormat"))); 2627 } 2628 if (properties != null && properties.containsKey("messageFormat")) { 2629 davProperties.add(Field.createDavProperty("messageFormat", properties.get("messageFormat"))); 2630 } 2631 if (!davProperties.isEmpty()) { 2632 HttpProppatch httpProppatch = new HttpProppatch(messageUrl, davProperties); 2633 try (CloseableHttpResponse response = httpClientAdapter.execute(httpProppatch)) { 2634 // update message with blind carbon copy and other flags 2635 int statusCode = response.getStatusLine().getStatusCode(); 2636 if (statusCode != HttpStatus.SC_MULTI_STATUS) { 2637 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', response.getStatusLine().getReasonPhrase()); 2638 } 2639 2640 } 2641 } 2642 2643 // update message body 2644 HttpPut putmethod = new HttpPut(messageUrl); 2645 putmethod.setHeader("Translate", "f"); 2646 putmethod.setHeader("Content-Type", "message/rfc822"); 2647 2648 try { 2649 // use same encoding as client socket reader 2650 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 2651 mimeMessage.writeTo(baos); 2652 baos.close(); 2653 putmethod.setEntity(new ByteArrayEntity(baos.toByteArray())); 2654 2655 int code; 2656 String reasonPhrase; 2657 try (CloseableHttpResponse response = httpClientAdapter.execute(putmethod)) { 2658 code = response.getStatusLine().getStatusCode(); 2659 reasonPhrase = response.getStatusLine().getReasonPhrase(); 2660 } 2661 2662 // workaround for misconfigured Exchange server 2663 if (code == HttpStatus.SC_NOT_ACCEPTABLE) { 2664 LOGGER.warn("Draft message creation failed, failover to property update. Note: attachments are lost"); 2665 2666 ArrayList<PropEntry> propertyList = new ArrayList<>(); 2667 propertyList.add(Field.createDavProperty("to", mimeMessage.getHeader("to", ","))); 2668 propertyList.add(Field.createDavProperty("cc", mimeMessage.getHeader("cc", ","))); 2669 propertyList.add(Field.createDavProperty("message-id", mimeMessage.getHeader("message-id", ","))); 2670 2671 MimePart mimePart = mimeMessage; 2672 if (mimeMessage.getContent() instanceof MimeMultipart) { 2673 MimeMultipart multiPart = (MimeMultipart) mimeMessage.getContent(); 2674 for (int i = 0; i < multiPart.getCount(); i++) { 2675 String contentType = multiPart.getBodyPart(i).getContentType(); 2676 if (contentType.startsWith("text/")) { 2677 mimePart = (MimePart) multiPart.getBodyPart(i); 2678 break; 2679 } 2680 } 2681 } 2682 2683 String contentType = mimePart.getContentType(); 2684 2685 if (contentType.startsWith("text/plain")) { 2686 propertyList.add(Field.createDavProperty("description", (String) mimePart.getContent())); 2687 } else if (contentType.startsWith("text/html")) { 2688 propertyList.add(Field.createDavProperty("htmldescription", (String) mimePart.getContent())); 2689 } else { 2690 LOGGER.warn("Unsupported content type: " + contentType.replaceAll("[\n\r\t]", "_") + " message body will be empty"); 2691 } 2692 2693 propertyList.add(Field.createDavProperty("subject", mimeMessage.getHeader("subject", ","))); 2694 HttpProppatch propPatchMethod = new HttpProppatch(messageUrl, propertyList); 2695 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) { 2696 int patchStatus = response.getStatusLine().getStatusCode(); 2697 if (patchStatus == HttpStatus.SC_MULTI_STATUS) { 2698 code = HttpStatus.SC_OK; 2699 } 2700 } 2701 } 2702 2703 2704 if (code != HttpStatus.SC_OK && code != HttpStatus.SC_CREATED) { 2705 2706 // first delete draft message 2707 if (!davProperties.isEmpty()) { 2708 HttpDelete httpDelete = new HttpDelete(messageUrl); 2709 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) { 2710 int status = response.getStatusLine().getStatusCode(); 2711 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) { 2712 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response); 2713 } 2714 } catch (IOException e) { 2715 LOGGER.warn("Unable to delete draft message"); 2716 } 2717 } 2718 if (code == HttpStatus.SC_INSUFFICIENT_STORAGE) { 2719 throw new InsufficientStorageException(reasonPhrase); 2720 } else { 2721 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, code, ' ', reasonPhrase); 2722 } 2723 } 2724 } catch (MessagingException e) { 2725 throw new IOException(e.getMessage()); 2726 } finally { 2727 putmethod.releaseConnection(); 2728 } 2729 2730 try { 2731 // need to update bcc after put 2732 if (mimeMessage.getHeader("Bcc") != null) { 2733 davProperties = new ArrayList<>(); 2734 davProperties.add(Field.createDavProperty("bcc", mimeMessage.getHeader("Bcc", ","))); 2735 HttpProppatch httpProppatch = new HttpProppatch(messageUrl, davProperties); 2736 // update message with blind carbon copy 2737 try (CloseableHttpResponse response = httpClientAdapter.execute(httpProppatch)) { 2738 int statusCode = response.getStatusLine().getStatusCode(); 2739 if (statusCode != HttpStatus.SC_MULTI_STATUS) { 2740 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', response.getStatusLine().getReasonPhrase()); 2741 } 2742 } 2743 } 2744 } catch (MessagingException e) { 2745 throw new IOException(e.getMessage()); 2746 } 2747 2748 } 2749 2750 /** 2751 * @inheritDoc 2752 */ 2753 @Override updateMessage(ExchangeSession.Message message, Map<String, String> properties)2754 public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException { 2755 HttpProppatch patchMethod = new HttpProppatch(encodeAndFixUrl(message.permanentUrl), buildProperties(properties)) { 2756 @Override 2757 public MultiStatus getResponseBodyAsMultiStatus(HttpResponse response) { 2758 // ignore response body, sometimes invalid with exchange mapi properties 2759 throw new UnsupportedOperationException(); 2760 } 2761 }; 2762 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) { 2763 int statusCode = response.getStatusLine().getStatusCode(); 2764 if (statusCode != HttpStatus.SC_MULTI_STATUS) { 2765 throw new DavMailException("EXCEPTION_UNABLE_TO_UPDATE_MESSAGE"); 2766 } 2767 } 2768 } 2769 2770 /** 2771 * @inheritDoc 2772 */ 2773 @Override deleteMessage(ExchangeSession.Message message)2774 public void deleteMessage(ExchangeSession.Message message) throws IOException { 2775 LOGGER.debug("Delete " + message.permanentUrl + " (" + message.messageUrl + ')'); 2776 HttpDelete httpDelete = new HttpDelete(encodeAndFixUrl(message.permanentUrl)); 2777 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) { 2778 int status = response.getStatusLine().getStatusCode(); 2779 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) { 2780 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response); 2781 } 2782 } 2783 } 2784 2785 /** 2786 * Send message. 2787 * 2788 * @param messageBody MIME message body 2789 * @throws IOException on error 2790 */ sendMessage(byte[] messageBody)2791 public void sendMessage(byte[] messageBody) throws IOException { 2792 try { 2793 sendMessage(new MimeMessage(null, new SharedByteArrayInputStream(messageBody))); 2794 } catch (MessagingException e) { 2795 throw new IOException(e.getMessage()); 2796 } 2797 } 2798 2799 //protected static final long MAPI_SEND_NO_RICH_INFO = 0x00010000L; 2800 protected static final long ENCODING_PREFERENCE = 0x00020000L; 2801 protected static final long ENCODING_MIME = 0x00040000L; 2802 //protected static final long BODY_ENCODING_HTML = 0x00080000L; 2803 protected static final long BODY_ENCODING_TEXT_AND_HTML = 0x00100000L; 2804 //protected static final long MAC_ATTACH_ENCODING_UUENCODE = 0x00200000L; 2805 //protected static final long MAC_ATTACH_ENCODING_APPLESINGLE = 0x00400000L; 2806 //protected static final long MAC_ATTACH_ENCODING_APPLEDOUBLE = 0x00600000L; 2807 //protected static final long OOP_DONT_LOOKUP = 0x10000000L; 2808 2809 @Override sendMessage(MimeMessage mimeMessage)2810 public void sendMessage(MimeMessage mimeMessage) throws IOException { 2811 try { 2812 // need to create draft first 2813 String itemName = UUID.randomUUID().toString() + ".EML"; 2814 HashMap<String, String> properties = new HashMap<>(); 2815 properties.put("draft", "9"); 2816 String contentType = mimeMessage.getContentType(); 2817 if (contentType != null && contentType.startsWith("text/plain")) { 2818 properties.put("messageFormat", "1"); 2819 } else { 2820 properties.put("mailOverrideFormat", String.valueOf(ENCODING_PREFERENCE | ENCODING_MIME | BODY_ENCODING_TEXT_AND_HTML)); 2821 properties.put("messageFormat", "2"); 2822 } 2823 createMessage(DRAFTS, itemName, properties, mimeMessage); 2824 HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(DRAFTS + '/' + itemName)), 2825 URIUtil.encodePath(getFolderPath(SENDMSG)), false); 2826 // set header if saveInSent is disabled 2827 if (!Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) { 2828 httpMove.setHeader("Saveinsent", "f"); 2829 } 2830 moveItem(httpMove); 2831 } catch (MessagingException e) { 2832 throw new IOException(e.getMessage()); 2833 } 2834 } 2835 2836 // wrong hostname fix flag 2837 protected boolean restoreHostName; 2838 2839 /** 2840 * @inheritDoc 2841 */ 2842 @Override getContent(ExchangeSession.Message message)2843 protected byte[] getContent(ExchangeSession.Message message) throws IOException { 2844 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 2845 InputStream contentInputStream; 2846 try { 2847 try { 2848 try { 2849 contentInputStream = getContentInputStream(message.messageUrl); 2850 } catch (UnknownHostException e) { 2851 // failover for misconfigured Exchange server, replace host name in url 2852 restoreHostName = true; 2853 contentInputStream = getContentInputStream(message.messageUrl); 2854 } 2855 } catch (HttpNotFoundException e) { 2856 LOGGER.debug("Message not found at: " + message.messageUrl + ", retrying with permanenturl"); 2857 contentInputStream = getContentInputStream(message.permanentUrl); 2858 } 2859 2860 try { 2861 IOUtil.write(contentInputStream, baos); 2862 } finally { 2863 contentInputStream.close(); 2864 } 2865 2866 } catch (LoginTimeoutException | SocketException e) { 2867 // throw error on expired session 2868 LOGGER.warn(e.getMessage()); 2869 throw e; 2870 } // throw error on broken connection 2871 catch (IOException e) { 2872 LOGGER.warn("Broken message at: " + message.messageUrl + " permanentUrl: " + message.permanentUrl + ", trying to rebuild from properties"); 2873 2874 try { 2875 DavPropertyNameSet messageProperties = new DavPropertyNameSet(); 2876 messageProperties.add(Field.getPropertyName("contentclass")); 2877 messageProperties.add(Field.getPropertyName("message-id")); 2878 messageProperties.add(Field.getPropertyName("from")); 2879 messageProperties.add(Field.getPropertyName("to")); 2880 messageProperties.add(Field.getPropertyName("cc")); 2881 messageProperties.add(Field.getPropertyName("subject")); 2882 messageProperties.add(Field.getPropertyName("date")); 2883 messageProperties.add(Field.getPropertyName("htmldescription")); 2884 messageProperties.add(Field.getPropertyName("body")); 2885 HttpPropfind httpPropfind = new HttpPropfind(encodeAndFixUrl(message.permanentUrl), messageProperties, 0); 2886 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) { 2887 MultiStatus responses = httpPropfind.getResponseBodyAsMultiStatus(response); 2888 if (responses.getResponses().length > 0) { 2889 MimeMessage mimeMessage = new MimeMessage((Session) null); 2890 2891 DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK); 2892 String propertyValue = getPropertyIfExists(properties, "contentclass"); 2893 if (propertyValue != null) { 2894 mimeMessage.addHeader("Content-class", propertyValue); 2895 } 2896 propertyValue = getPropertyIfExists(properties, "date"); 2897 if (propertyValue != null) { 2898 mimeMessage.setSentDate(parseDateFromExchange(propertyValue)); 2899 } 2900 propertyValue = getPropertyIfExists(properties, "from"); 2901 if (propertyValue != null) { 2902 mimeMessage.addHeader("From", propertyValue); 2903 } 2904 propertyValue = getPropertyIfExists(properties, "to"); 2905 if (propertyValue != null) { 2906 mimeMessage.addHeader("To", propertyValue); 2907 } 2908 propertyValue = getPropertyIfExists(properties, "cc"); 2909 if (propertyValue != null) { 2910 mimeMessage.addHeader("Cc", propertyValue); 2911 } 2912 propertyValue = getPropertyIfExists(properties, "subject"); 2913 if (propertyValue != null) { 2914 mimeMessage.setSubject(propertyValue); 2915 } 2916 propertyValue = getPropertyIfExists(properties, "htmldescription"); 2917 if (propertyValue != null) { 2918 mimeMessage.setContent(propertyValue, "text/html; charset=UTF-8"); 2919 } else { 2920 propertyValue = getPropertyIfExists(properties, "body"); 2921 if (propertyValue != null) { 2922 mimeMessage.setText(propertyValue); 2923 } 2924 } 2925 mimeMessage.writeTo(baos); 2926 } 2927 } 2928 if (LOGGER.isDebugEnabled()) { 2929 LOGGER.debug("Rebuilt message content: " + new String(baos.toByteArray(), StandardCharsets.UTF_8)); 2930 } 2931 } catch (IOException | DavException | MessagingException e2) { 2932 LOGGER.warn(e2); 2933 } 2934 // other exception 2935 if (baos.size() == 0 && Settings.getBooleanProperty("davmail.deleteBroken")) { 2936 LOGGER.warn("Deleting broken message at: " + message.messageUrl + " permanentUrl: " + message.permanentUrl); 2937 try { 2938 message.delete(); 2939 } catch (IOException ioe) { 2940 LOGGER.warn("Unable to delete broken message at: " + message.permanentUrl); 2941 } 2942 throw e; 2943 } 2944 } 2945 2946 return baos.toByteArray(); 2947 } 2948 2949 /** 2950 * sometimes permanenturis inside items are wrong after an Exchange version migration 2951 * need to restore base uri to actual public Exchange uri 2952 * 2953 * @param url input uri 2954 * @return fixed uri 2955 * @throws IOException on error 2956 */ encodeAndFixUrl(String url)2957 protected String encodeAndFixUrl(String url) throws IOException { 2958 String fixedurl = URIUtil.encodePath(url); 2959 // sometimes permanenturis inside items are wrong after an Exchange version migration 2960 // need to restore base uri to actual public Exchange uri 2961 if (restoreHostName && fixedurl.startsWith("http")) { 2962 try { 2963 return URIUtils.rewriteURI(new java.net.URI(fixedurl), URIUtils.extractHost(httpClientAdapter.getUri())).toString(); 2964 } catch (URISyntaxException e) { 2965 throw new IOException(e.getMessage(), e); 2966 } 2967 } 2968 return fixedurl; 2969 } 2970 getContentInputStream(String url)2971 protected InputStream getContentInputStream(String url) throws IOException { 2972 String encodedUrl = encodeAndFixUrl(url); 2973 2974 final HttpGet httpGet = new HttpGet(encodedUrl); 2975 httpGet.setHeader("Content-Type", "text/xml; charset=utf-8"); 2976 httpGet.setHeader("Translate", "f"); 2977 httpGet.setHeader("Accept-Encoding", "gzip"); 2978 2979 InputStream inputStream; 2980 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) { 2981 if (HttpClientAdapter.isGzipEncoded(response)) { 2982 inputStream = new GZIPInputStream(response.getEntity().getContent()); 2983 } else { 2984 inputStream = response.getEntity().getContent(); 2985 } 2986 inputStream = new FilterInputStream(inputStream) { 2987 int totalCount; 2988 int lastLogCount; 2989 2990 @Override 2991 public int read(byte[] buffer, int offset, int length) throws IOException { 2992 int count = super.read(buffer, offset, length); 2993 totalCount += count; 2994 if (totalCount - lastLogCount > 1024 * 128) { 2995 DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), httpGet.getURI())); 2996 DavGatewayTray.switchIcon(); 2997 lastLogCount = totalCount; 2998 } 2999 return count; 3000 } 3001 3002 @Override 3003 public void close() throws IOException { 3004 try { 3005 super.close(); 3006 } finally { 3007 httpGet.releaseConnection(); 3008 } 3009 } 3010 }; 3011 3012 } catch (IOException e) { 3013 LOGGER.warn("Unable to retrieve message at: " + url); 3014 throw e; 3015 } 3016 return inputStream; 3017 } 3018 3019 /** 3020 * @inheritDoc 3021 */ 3022 @Override moveMessage(ExchangeSession.Message message, String targetFolder)3023 public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException { 3024 try { 3025 moveMessage(message.permanentUrl, targetFolder); 3026 } catch (HttpNotFoundException e) { 3027 LOGGER.debug("404 not found at permanenturl: " + message.permanentUrl + ", retry with messageurl"); 3028 moveMessage(message.messageUrl, targetFolder); 3029 } 3030 } 3031 moveMessage(String sourceUrl, String targetFolder)3032 protected void moveMessage(String sourceUrl, String targetFolder) throws IOException { 3033 String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID().toString(); 3034 HttpMove method = new HttpMove(URIUtil.encodePath(sourceUrl), targetPath, false); 3035 // allow rename if a message with the same name exists 3036 method.setHeader("Allow-Rename", "t"); 3037 try (CloseableHttpResponse response = httpClientAdapter.execute(method)) { 3038 int statusCode = response.getStatusLine().getStatusCode(); 3039 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED || 3040 statusCode == HttpStatus.SC_CONFLICT) { 3041 throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_MESSAGE"); 3042 } else if (statusCode != HttpStatus.SC_CREATED) { 3043 throw HttpClientAdapter.buildHttpResponseException(method, response); 3044 } 3045 } finally { 3046 method.releaseConnection(); 3047 } 3048 } 3049 3050 /** 3051 * @inheritDoc 3052 */ 3053 @Override copyMessage(ExchangeSession.Message message, String targetFolder)3054 public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException { 3055 try { 3056 copyMessage(message.permanentUrl, targetFolder); 3057 } catch (HttpNotFoundException e) { 3058 LOGGER.debug("404 not found at permanenturl: " + message.permanentUrl + ", retry with messageurl"); 3059 copyMessage(message.messageUrl, targetFolder); 3060 } 3061 } 3062 copyMessage(String sourceUrl, String targetFolder)3063 protected void copyMessage(String sourceUrl, String targetFolder) throws IOException { 3064 String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID().toString(); 3065 HttpCopy httpCopy = new HttpCopy(URIUtil.encodePath(sourceUrl), targetPath, false, false); 3066 // allow rename if a message with the same name exists 3067 httpCopy.addHeader("Allow-Rename", "t"); 3068 try (CloseableHttpResponse response = httpClientAdapter.execute(httpCopy)) { 3069 int statusCode = response.getStatusLine().getStatusCode(); 3070 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) { 3071 throw new DavMailException("EXCEPTION_UNABLE_TO_COPY_MESSAGE"); 3072 } else if (statusCode != HttpStatus.SC_CREATED) { 3073 throw HttpClientAdapter.buildHttpResponseException(httpCopy, response); 3074 } 3075 } 3076 } 3077 3078 @Override moveToTrash(ExchangeSession.Message message)3079 protected void moveToTrash(ExchangeSession.Message message) throws IOException { 3080 String destination = URIUtil.encodePath(deleteditemsUrl) + '/' + UUID.randomUUID().toString(); 3081 LOGGER.debug("Deleting : " + message.permanentUrl + " to " + destination); 3082 HttpMove method = new HttpMove(encodeAndFixUrl(message.permanentUrl), destination, false); 3083 method.addHeader("Allow-rename", "t"); 3084 3085 try (CloseableHttpResponse response = httpClientAdapter.execute(method)) { 3086 int status = response.getStatusLine().getStatusCode(); 3087 // do not throw error if already deleted 3088 if (status != HttpStatus.SC_CREATED && status != HttpStatus.SC_NOT_FOUND) { 3089 throw HttpClientAdapter.buildHttpResponseException(method, response); 3090 } 3091 if (response.getFirstHeader("Location") != null) { 3092 destination = method.getFirstHeader("Location").getValue(); 3093 } 3094 } 3095 3096 LOGGER.debug("Deleted to :" + destination); 3097 } 3098 getItemProperty(String permanentUrl, String propertyName)3099 protected String getItemProperty(String permanentUrl, String propertyName) throws IOException, DavException { 3100 String result = null; 3101 DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet(); 3102 davPropertyNameSet.add(Field.getPropertyName(propertyName)); 3103 HttpPropfind propFindMethod = new HttpPropfind(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0); 3104 MultiStatus responses; 3105 try (CloseableHttpResponse response = httpClientAdapter.execute(propFindMethod)) { 3106 responses = propFindMethod.getResponseBodyAsMultiStatus(response); 3107 } catch (UnknownHostException e) { 3108 // failover for misconfigured Exchange server, replace host name in url 3109 restoreHostName = true; 3110 propFindMethod = new HttpPropfind(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0); 3111 try (CloseableHttpResponse response = httpClientAdapter.execute(propFindMethod)) { 3112 responses = propFindMethod.getResponseBodyAsMultiStatus(response); 3113 } 3114 } 3115 3116 if (responses.getResponses().length > 0) { 3117 DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK); 3118 result = getPropertyIfExists(properties, propertyName); 3119 } 3120 3121 return result; 3122 } 3123 convertDateFromExchange(String exchangeDateValue)3124 protected String convertDateFromExchange(String exchangeDateValue) throws DavMailException { 3125 String zuluDateValue = null; 3126 if (exchangeDateValue != null) { 3127 try { 3128 zuluDateValue = getZuluDateFormat().format(getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue)); 3129 } catch (ParseException e) { 3130 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue); 3131 } 3132 } 3133 return zuluDateValue; 3134 } 3135 3136 protected static final Map<String, String> importanceToPriorityMap = new HashMap<>(); 3137 3138 static { 3139 importanceToPriorityMap.put("high", "1"); 3140 importanceToPriorityMap.put("normal", "5"); 3141 importanceToPriorityMap.put("low", "9"); 3142 } 3143 3144 protected static final Map<String, String> priorityToImportanceMap = new HashMap<>(); 3145 3146 static { 3147 priorityToImportanceMap.put("1", "high"); 3148 priorityToImportanceMap.put("5", "normal"); 3149 priorityToImportanceMap.put("9", "low"); 3150 } 3151 convertPriorityFromExchange(String exchangeImportanceValue)3152 protected String convertPriorityFromExchange(String exchangeImportanceValue) { 3153 String value = null; 3154 if (exchangeImportanceValue != null) { 3155 value = importanceToPriorityMap.get(exchangeImportanceValue); 3156 } 3157 return value; 3158 } 3159 convertPriorityToExchange(String vTodoPriorityValue)3160 protected String convertPriorityToExchange(String vTodoPriorityValue) { 3161 String value = null; 3162 if (vTodoPriorityValue != null) { 3163 value = priorityToImportanceMap.get(vTodoPriorityValue); 3164 } 3165 return value; 3166 } 3167 3168 3169 @Override close()3170 public void close() { 3171 httpClientAdapter.close(); 3172 } 3173 3174 /** 3175 * Format date to exchange search format. 3176 * 3177 * @param date date object 3178 * @return formatted search date 3179 */ 3180 @Override formatSearchDate(Date date)3181 public String formatSearchDate(Date date) { 3182 SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS, Locale.ENGLISH); 3183 dateFormatter.setTimeZone(GMT_TIMEZONE); 3184 return dateFormatter.format(date); 3185 } 3186 convertTaskDateToZulu(String value)3187 protected String convertTaskDateToZulu(String value) { 3188 String result = null; 3189 if (value != null && value.length() > 0) { 3190 try { 3191 SimpleDateFormat parser = ExchangeSession.getExchangeDateFormat(value); 3192 3193 Calendar calendarValue = Calendar.getInstance(GMT_TIMEZONE); 3194 calendarValue.setTime(parser.parse(value)); 3195 // zulu time: add 12 hours 3196 if (value.length() == 16) { 3197 calendarValue.add(Calendar.HOUR, 12); 3198 } 3199 calendarValue.set(Calendar.HOUR, 0); 3200 calendarValue.set(Calendar.MINUTE, 0); 3201 calendarValue.set(Calendar.SECOND, 0); 3202 result = ExchangeSession.getExchangeZuluDateFormatMillisecond().format(calendarValue.getTime()); 3203 } catch (ParseException e) { 3204 LOGGER.warn("Invalid date: " + value); 3205 } 3206 } 3207 3208 return result; 3209 } 3210 convertDateFromExchangeToTaskDate(String exchangeDateValue)3211 protected String convertDateFromExchangeToTaskDate(String exchangeDateValue) throws DavMailException { 3212 String result = null; 3213 if (exchangeDateValue != null) { 3214 try { 3215 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH); 3216 dateFormat.setTimeZone(GMT_TIMEZONE); 3217 result = dateFormat.format(getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue)); 3218 } catch (ParseException e) { 3219 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue); 3220 } 3221 } 3222 return result; 3223 } 3224 parseDateFromExchange(String exchangeDateValue)3225 protected Date parseDateFromExchange(String exchangeDateValue) throws DavMailException { 3226 Date result = null; 3227 if (exchangeDateValue != null) { 3228 try { 3229 result = getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue); 3230 } catch (ParseException e) { 3231 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue); 3232 } 3233 } 3234 return result; 3235 } 3236 } 3237