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