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