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