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