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