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