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