1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package davmail.exchange.graph;
21
22 import davmail.BundleMessage;
23 import davmail.Settings;
24 import davmail.exception.DavMailException;
25 import davmail.exception.HttpForbiddenException;
26 import davmail.exception.HttpNotFoundException;
27 import davmail.exception.HttpPreconditionFailedException;
28 import davmail.exchange.ExchangeSession;
29 import davmail.exchange.VCalendar;
30 import davmail.exchange.VObject;
31 import davmail.exchange.VProperty;
32 import davmail.exchange.auth.O365Token;
33 import davmail.http.HttpClientAdapter;
34 import davmail.http.URIUtil;
35 import davmail.ui.NotificationDialog;
36 import davmail.ui.tray.DavGatewayTray;
37 import davmail.util.DateUtil;
38 import davmail.util.IOUtil;
39 import davmail.util.StringUtil;
40 import org.apache.http.Header;
41 import org.apache.http.HttpStatus;
42 import org.apache.http.client.methods.CloseableHttpResponse;
43 import org.apache.http.client.methods.HttpDelete;
44 import org.apache.http.client.methods.HttpGet;
45 import org.apache.http.client.methods.HttpPatch;
46 import org.apache.http.client.methods.HttpPost;
47 import org.apache.http.client.methods.HttpPut;
48 import org.apache.http.client.methods.HttpRequestBase;
49 import org.codehaus.jettison.json.JSONArray;
50 import org.codehaus.jettison.json.JSONException;
51 import org.codehaus.jettison.json.JSONObject;
52 import org.htmlcleaner.HtmlCleaner;
53 import org.htmlcleaner.TagNode;
54
55 import javax.mail.MessagingException;
56 import javax.mail.internet.MimeMessage;
57 import javax.mail.internet.MimeMultipart;
58 import javax.mail.internet.MimePart;
59 import javax.mail.internet.MimeUtility;
60 import javax.mail.util.SharedByteArrayInputStream;
61 import java.io.ByteArrayInputStream;
62 import java.io.ByteArrayOutputStream;
63 import java.io.FilterInputStream;
64 import java.io.IOException;
65 import java.io.InputStream;
66 import java.io.StringReader;
67 import java.net.NoRouteToHostException;
68 import java.net.URI;
69 import java.net.UnknownHostException;
70 import java.nio.charset.StandardCharsets;
71 import java.text.ParseException;
72 import java.text.SimpleDateFormat;
73 import java.util.ArrayList;
74 import java.util.Collections;
75 import java.util.Date;
76 import java.util.HashMap;
77 import java.util.HashSet;
78 import java.util.Iterator;
79 import java.util.List;
80 import java.util.Locale;
81 import java.util.Map;
82 import java.util.MissingResourceException;
83 import java.util.NoSuchElementException;
84 import java.util.Set;
85 import java.util.TimeZone;
86 import java.util.UUID;
87 import java.util.zip.GZIPInputStream;
88
89 import static davmail.exchange.graph.GraphObject.convertTimezoneFromExchange;
90
91
92
93
94 public class GraphExchangeSession extends ExchangeSession {
95
96 static final Map<String, String> partstatToResponseMap = new HashMap<>();
97 static final Map<String, String> responseTypeToPartstatMap = new HashMap<>();
98 static final Map<String, String> statusToBusyStatusMap = new HashMap<>();
99
100 static {
101 partstatToResponseMap.put("ACCEPTED", "accepted");
102 partstatToResponseMap.put("TENTATIVE", "tentativelyAccepted");
103 partstatToResponseMap.put("DECLINED", "declined");
104 partstatToResponseMap.put("NEEDS-ACTION", "notResponded");
105
106 responseTypeToPartstatMap.put("accepted", "ACCEPTED");
107 responseTypeToPartstatMap.put("organizer", "ACCEPTED");
108 responseTypeToPartstatMap.put("tentativelyAccepted", "TENTATIVE");
109 responseTypeToPartstatMap.put("declined", "DECLINED");
110 responseTypeToPartstatMap.put("none", "NEEDS-ACTION");
111 responseTypeToPartstatMap.put("notResponded", "NEEDS-ACTION");
112
113 statusToBusyStatusMap.put("TENTATIVE", "Tentative");
114 statusToBusyStatusMap.put("CONFIRMED", "Busy");
115
116 }
117
118 protected Map<String, String> urlcompnameToIdMap = new HashMap<>();
119
120
121
122
123 protected class Folder extends ExchangeSession.Folder {
124 public FolderId folderId;
125 protected String specialFlag = "";
126
127 protected boolean isDefaultCalendar = false;
128
129 protected void setSpecialFlag(String specialFlag) {
130 this.specialFlag = "\\" + specialFlag + " ";
131 }
132
133
134
135
136
137
138 @Override
139 public String getFlags() {
140 if (noInferiors) {
141 return specialFlag + "\\NoInferiors";
142 } else if (hasChildren) {
143 return specialFlag + "\\HasChildren";
144 } else {
145 return specialFlag + "\\HasNoChildren";
146 }
147 }
148 }
149
150 protected class Event extends ExchangeSession.Event {
151
152 public FolderId folderId;
153
154 public String id;
155
156 protected GraphObject graphObject;
157
158 public Event(String folderPath, FolderId folderId, GraphObject graphObject) {
159 this.folderPath = folderPath;
160 this.folderId = folderId;
161
162 if (FolderId.IPF_TASK.equals(graphObject.optString("objecttype"))) {
163
164 try {
165 this.folderId = getFolderId(TASKS);
166 } catch (IOException e) {
167 LOGGER.warn("Unable to replace folder with tasks");
168 }
169 displayName = graphObject.optString("summary");
170 subject = graphObject.optString("summary");
171 } else {
172 displayName = graphObject.optString("subject");
173 subject = graphObject.optString("subject");
174 }
175
176 this.graphObject = graphObject;
177
178 id = graphObject.optString("id");
179 etag = graphObject.optString("changeKey");
180
181
182 itemName = StringUtil.base64ToUrl(id) + ".EML";
183 }
184
185 public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
186 super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
187 folderId = getFolderId(folderPath);
188 }
189
190 public Event(FolderId folderId, byte[] content) throws IOException {
191 vCalendar = new VCalendar(content, email, getVTimezone());
192 this.folderId = folderId;
193 }
194
195 @Override
196 public byte[] getEventContent() throws IOException {
197 byte[] content;
198 if (LOGGER.isDebugEnabled()) {
199 LOGGER.debug("Get event: " + itemName);
200 }
201 try {
202 if (vCalendar != null) {
203 return vCalendar.toString().getBytes(StandardCharsets.UTF_8);
204 } else if (folderId.isTask()) {
205 VCalendar localVCalendar = new VCalendar();
206 VObject vTodo = new VObject();
207 vTodo.type = "VTODO";
208 localVCalendar.setTimezone(getVTimezone());
209 vTodo.setPropertyValue("LAST-MODIFIED", graphObject.optString("lastModifiedDateTime"));
210 vTodo.setPropertyValue("CREATED", graphObject.optString("createdDateTime"));
211
212 vTodo.setPropertyValue("UID", graphObject.optString("id"));
213 vTodo.setPropertyValue("TITLE", graphObject.optString("summary"));
214 vTodo.setPropertyValue("SUMMARY", graphObject.optString("summary"));
215
216 vTodo.addProperty(convertBodyToVproperty("DESCRIPTION", graphObject));
217
218 vTodo.setPropertyValue("PRIORITY", graphObject.getTaskPriority());
219
220
221 vTodo.setPropertyValue("STATUS", graphObject.getVTodoStatusFromTask());
222
223 vTodo.setPropertyValue("DUE;VALUE=DATE", convertDateTimeTimeZoneToTaskDate(graphObject.optDateTimeTimeZone("dueDateTime")));
224 vTodo.setPropertyValue("DTSTART;VALUE=DATE", convertDateTimeTimeZoneToTaskDate(graphObject.optDateTimeTimeZone("startDateTime")));
225 vTodo.setPropertyValue("COMPLETED;VALUE=DATE", convertDateTimeTimeZoneToTaskDate(graphObject.optDateTimeTimeZone("completedDateTime")));
226
227 vTodo.setPropertyValue("CATEGORIES", graphObject.optString("categories"));
228
229
230
231 localVCalendar.addVObject(vTodo);
232 content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
233 } else {
234
235
236
237 VCalendar localVCalendar = new VCalendar();
238
239 localVCalendar.setEmail(getCalendarEmail(folderPath));
240
241 String originalStartTimeZone = graphObject.optString("originalStartTimeZone");
242 if (originalStartTimeZone != null && !"tzone://Microsoft/Custom".equals(originalStartTimeZone)) {
243 localVCalendar.setTimezone(getVTimezone(originalStartTimeZone));
244 } else {
245 localVCalendar.setTimezone(getVTimezone());
246 }
247 localVCalendar.addVObject(buildVEvent(graphObject));
248
249 handleException(localVCalendar, graphObject);
250
251 handleRecurrence(localVCalendar, graphObject);
252
253 content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
254 }
255 } catch (Exception e) {
256 throw new IOException(e.getMessage(), e);
257 }
258 return content;
259 }
260
261 private void handleException(VCalendar localVCalendar, GraphObject graphObject) throws DavMailException, JSONException {
262 JSONArray cancelledOccurrences = graphObject.optJSONArray("cancelledOccurrences");
263 if (cancelledOccurrences != null) {
264 HashSet<String> exDateValues = new HashSet<>();
265 VProperty startDate = localVCalendar.getFirstVevent().getProperty("DTSTART");
266 for (int i = 0; i < cancelledOccurrences.length(); i++) {
267 String cancelledOccurrence = null;
268 try {
269 cancelledOccurrence = cancelledOccurrences.getString(i);
270 cancelledOccurrence = cancelledOccurrence.substring(cancelledOccurrence.lastIndexOf('.') + 1);
271 String cancelledDate = convertDateFromExchange(cancelledOccurrence);
272
273 exDateValues.add(cancelledDate.substring(0, 8) + startDate.getValue().substring(8));
274
275 } catch (IndexOutOfBoundsException | JSONException e) {
276 LOGGER.warn("Invalid cancelled occurrence: " + cancelledOccurrence);
277 }
278 }
279
280 VProperty exDate = new VProperty("EXDATE", StringUtil.join(exDateValues, ","));
281 exDate.setParam("TZID", startDate.getParamValue("TZID"));
282 localVCalendar.addFirstVeventProperty(exDate);
283 }
284
285 JSONArray exceptionOccurrences = graphObject.optJSONArray("exceptionOccurrences");
286 if (exceptionOccurrences != null) {
287 for (int i = 0; i < exceptionOccurrences.length(); i++) {
288 GraphObject exceptionOccurrence = new GraphObject(exceptionOccurrences.optJSONObject(i)
289
290 .put("iCalUId", graphObject.optString("iCalUId")));
291 VObject vEvent = buildVEvent(exceptionOccurrence);
292 vEvent.addProperty(exceptionOccurrence.getRecurrenceId());
293 localVCalendar.addVObject(vEvent);
294 }
295 }
296 }
297
298 private VObject buildVEvent(GraphObject jsonEvent) throws DavMailException, JSONException {
299 VObject vEvent = new VObject();
300 vEvent.type = "VEVENT";
301
302 String iCalUId = jsonEvent.optString("transactionId");
303 if (iCalUId == null) {
304
305 iCalUId = jsonEvent.optString("iCalUId");
306 }
307 vEvent.setPropertyValue("UID", iCalUId);
308 vEvent.setPropertyValue("SUMMARY", jsonEvent.optString("subject"));
309
310 vEvent.addProperty(convertBodyToVproperty("DESCRIPTION", jsonEvent));
311
312 vEvent.setPropertyValue("LAST-MODIFIED", jsonEvent.optString("lastModifiedDateTime"));
313 vEvent.setPropertyValue("DTSTAMP", jsonEvent.optString("lastModifiedDateTime"));
314
315
316 String originalStartTimeZone = jsonEvent.optString("originalStartTimeZone");
317 vEvent.addProperty(convertDateTimeTimeZoneToVproperty("DTSTART", jsonEvent.optJSONObject("start"), DateUtil.getExchangeTimeZone(originalStartTimeZone)));
318 vEvent.addProperty(convertDateTimeTimeZoneToVproperty("DTEND", jsonEvent.optJSONObject("end"), DateUtil.getExchangeTimeZone(originalStartTimeZone)));
319
320 vEvent.setPropertyValue("LOCATION", jsonEvent.optString("location", "displayName"));
321 vEvent.setPropertyValue("CATEGORIES", jsonEvent.optString("categories"));
322
323 vEvent.setPropertyValue("CLASS", convertClassFromExchange(jsonEvent.optString("sensitivity")));
324
325
326 String showAs = jsonEvent.optString("showAs");
327 if (showAs != null) {
328 vEvent.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS", showAs.toUpperCase());
329 }
330 String isAllDay = jsonEvent.optString("isAllDay");
331 if (isAllDay != null) {
332 vEvent.setPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT", isAllDay.toUpperCase());
333 }
334 String responseRequested = jsonEvent.optString("responseRequested");
335 if (responseRequested != null) {
336 vEvent.setPropertyValue("X-MICROSOFT-CDO-ISRESPONSEREQUESTED", responseRequested.toUpperCase());
337 }
338
339 if (jsonEvent.optBoolean("isReminderOn")) {
340 VObject vAlarm = new VObject();
341 vAlarm.type = "VALARM";
342 vAlarm.addPropertyValue("ACTION", "DISPLAY");
343 int reminderMinutesBeforeStart = jsonEvent.optInt("reminderMinutesBeforeStart");
344 if (reminderMinutesBeforeStart > 0) {
345 vAlarm.addPropertyValue("TRIGGER", "-PT" + reminderMinutesBeforeStart + "M");
346 }
347 vEvent.addVObject(vAlarm);
348 }
349
350 vEvent.setPropertyValue("X-MOZ-SEND-INVITATIONS", jsonEvent.optString("xmozsendinvitations"));
351 vEvent.setPropertyValue("X-MOZ-LASTACK", jsonEvent.optString("xmozlastack"));
352 vEvent.setPropertyValue("X-MOZ-SNOOZE-TIME", jsonEvent.optString("xmozsnoozetime"));
353
354 vEvent.setPropertyValue("X-MICROSOFT-DISALLOW-COUNTER", jsonEvent.optBoolean("allowNewTimeProposals") ? "FALSE" : "TRUE");
355
356 setAttendees(vEvent, jsonEvent);
357
358 return vEvent;
359 }
360
361 private void handleRecurrence(VCalendar localVCalendar, GraphObject graphObject) throws JSONException, DavMailException {
362
363 JSONObject recurrence = graphObject.optJSONObject("recurrence");
364 if (recurrence != null) {
365 StringBuilder rruleValue = new StringBuilder();
366 JSONObject pattern = recurrence.getJSONObject("pattern");
367 JSONObject range = recurrence.getJSONObject("range");
368
369 String patternType = pattern.getString("type");
370 int interval = pattern.getInt("interval");
371
372 String index = pattern.optString("index", null);
373
374 if ("first".equals(index)) {
375 index = "1";
376 } else if ("second".equals(index)) {
377 index = "2";
378 } else if ("third".equals(index)) {
379 index = "3";
380 } else if ("fourth".equals(index)) {
381 index = "4";
382 } else if ("last".equals(index)) {
383 index = "-1";
384 }
385
386 String month = pattern.getString("month");
387 if ("0".equals(month)) {
388 month = null;
389 }
390
391 String firstDayOfWeek = pattern.getString("firstDayOfWeek");
392
393 String dayOfMonth = pattern.getString("dayOfMonth");
394 if ("0".equals(dayOfMonth)) {
395 dayOfMonth = null;
396 }
397
398 JSONArray daysOfWeek = pattern.optJSONArray("daysOfWeek");
399 String rangeType = range.getString("type");
400
401 rruleValue.append("FREQ=");
402 if (patternType.startsWith("absolute") || patternType.startsWith("relative")) {
403 rruleValue.append(patternType.substring(8).toUpperCase());
404 } else {
405 rruleValue.append(patternType.toUpperCase());
406 }
407 if (rangeType.equals("endDate")) {
408 String endDate = buildUntilDate(range.getString("endDate"), graphObject.optJSONObject("start"));
409 rruleValue.append(";UNTIL=").append(endDate);
410 } else if (rangeType.equals("numbered")) {
411 int numberOfOccurrences = range.getInt("numberOfOccurrences");
412 rruleValue.append(";COUNT=").append(numberOfOccurrences);
413 }
414 if (interval > 0) {
415 rruleValue.append(";INTERVAL=").append(interval);
416 }
417 if (dayOfMonth != null && !dayOfMonth.isEmpty()) {
418 rruleValue.append(";BYMONTHDAY=").append(dayOfMonth);
419 }
420 if (month != null && !month.isEmpty()) {
421 rruleValue.append(";BYMONTH=").append(month);
422 }
423 if (daysOfWeek != null && daysOfWeek.length() > 0) {
424 ArrayList<String> days = new ArrayList<>();
425 for (int i = 0; i < daysOfWeek.length(); i++) {
426 StringBuilder byDay = new StringBuilder();
427 if (index != null && !"weekly".equals(patternType)) {
428 byDay.append(index);
429 }
430 byDay.append(daysOfWeek.getString(i).substring(0, 2).toUpperCase());
431 days.add(byDay.toString());
432 }
433 rruleValue.append(";BYDAY=").append(String.join(",", days));
434 }
435
436 if ("weekly".equals(patternType) && firstDayOfWeek.length() >= 2) {
437 rruleValue.append(";WKST=").append(firstDayOfWeek.substring(0, 2).toUpperCase());
438 }
439
440 localVCalendar.addFirstVeventProperty(new VProperty("RRULE", rruleValue.toString()));
441 }
442 }
443
444 private String buildUntilDate(String date, JSONObject startDate) throws DavMailException {
445 String result = null;
446 if (date != null && date.length() == 10) {
447 String startDateTimeZone = startDate.optString("timeZone");
448 String startDateDateTime = startDate.optString("dateTime");
449
450 String untilDateTime = date + startDateDateTime.substring(10);
451
452 SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
453 SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
454 formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
455 parser.setTimeZone(TimeZone.getTimeZone(convertTimezoneFromExchange(startDateTimeZone)));
456 try {
457 result = formatter.format(parser.parse(untilDateTime));
458 } catch (ParseException e) {
459 throw new DavMailException("EXCEPTION_INVALID_DATE", date);
460 }
461 }
462 return result;
463 }
464
465 private String convertOriginalStartDate(String originalStart) throws DavMailException {
466 String result = originalStart;
467
468 if (originalStart != null && !originalStart.endsWith("Z")) {
469 SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
470 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
471 formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
472 try {
473 result = formatter.format(parser.parse(originalStart));
474 } catch (ParseException e) {
475 throw new DavMailException("EXCEPTION_INVALID_DATE", originalStart);
476 }
477 }
478 return result;
479 }
480
481
482 private void setAttendees(VObject vEvent, GraphObject jsonEvent) throws JSONException {
483
484 JSONObject organizer = jsonEvent.optJSONObject("organizer");
485 if (organizer != null) {
486 vEvent.addProperty(convertEmailAddressToVproperty("ORGANIZER", organizer.optJSONObject("emailAddress")));
487 }
488
489 JSONArray attendees = jsonEvent.optJSONArray("attendees");
490 if (attendees != null) {
491 for (int i = 0; i < attendees.length(); i++) {
492 JSONObject attendee = attendees.getJSONObject(i);
493 JSONObject emailAddress = attendee.getJSONObject("emailAddress");
494 VProperty attendeeProperty = convertEmailAddressToVproperty("ATTENDEE", emailAddress);
495
496
497 String responseType = attendee.getJSONObject("status").optString("response");
498 String myResponseType = graphObject.optString("responseStatus", "response");
499
500
501 if (email.equalsIgnoreCase(emailAddress.optString("address")) && myResponseType != null) {
502 attendeeProperty.addParam("PARTSTAT", responseTypeToPartstat(myResponseType));
503 } else {
504 attendeeProperty.addParam("PARTSTAT", responseTypeToPartstat(responseType));
505 }
506
507 String type = attendee.optString("type");
508 if ("required".equals(type)) {
509 attendeeProperty.addParam("ROLE", "REQ-PARTICIPANT");
510 } else if ("optional".equals(type)) {
511 attendeeProperty.addParam("ROLE", "OPT-PARTICIPANT");
512 }
513
514 vEvent.addProperty(attendeeProperty);
515 }
516 }
517 }
518
519
520
521
522
523
524
525 private String responseTypeToPartstat(String responseType) {
526
527 if ("accepted".equals(responseType) || "organizer".equals(responseType)) {
528 return "ACCEPTED";
529 } else if ("tentativelyAccepted".equals(responseType)) {
530 return "TENTATIVE";
531 } else if ("declined".equals(responseType)) {
532 return "DECLINED";
533 } else {
534 return "NEEDS-ACTION";
535 }
536 }
537
538 @Override
539 public ItemResult createOrUpdate() throws IOException {
540 if (vCalendar.isTodo() && isMainCalendar(folderPath)) {
541
542 folderId = getFolderId(TASKS);
543 }
544
545 String currentItemId = null;
546 String currentEtag = null;
547 boolean isExistingEvent = false;
548 boolean isMeetingResponse = false;
549 boolean isMozSendInvitations = false;
550 boolean isMozDismiss = false;
551
552 boolean isOrganizer = false;
553 boolean isMeeting = false;
554
555 JSONObject existingJsonEvent = getEventIfExists(folderId, itemName);
556 if (existingJsonEvent != null) {
557 isExistingEvent = true;
558
559 GraphObject currentItem = new GraphObject(existingJsonEvent);
560 currentItemId = existingJsonEvent.optString("id", null);
561 currentEtag = new GraphObject(existingJsonEvent).optString("changeKey");
562
563 String myResponseType = currentItem.optString("responseStatus", "response");
564
565 String currentAttendeeStatus = responseTypeToPartstatMap.get(myResponseType);
566 String newAttendeeStatus = vCalendar.getAttendeeStatus();
567
568 isOrganizer = currentItem.optBoolean("isOrganizer");
569 isMeeting = currentItem.optJSONArray("attendees") != null;
570
571 isMeetingResponse = vCalendar.isMeeting() && !isOrganizer
572 && newAttendeeStatus != null
573 && !newAttendeeStatus.equals(currentAttendeeStatus)
574
575 && partstatToResponseMap.get(newAttendeeStatus) != null;
576
577
578 String newmozlastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
579 String currentmozlastack = currentItem.optString("xmozlastack");
580 boolean ismozack = newmozlastack != null && !newmozlastack.equals(currentmozlastack);
581
582 String newmozsnoozetime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
583 String currentmozsnoozetime = currentItem.optString("xmozsnoozetime");
584 boolean ismozsnooze = newmozsnoozetime != null && !newmozsnoozetime.equals(currentmozsnoozetime);
585
586 isMozSendInvitations = (newmozlastack == null && newmozsnoozetime == null)
587 || !(ismozack || ismozsnooze);
588 isMozDismiss = ismozack || ismozsnooze;
589
590 LOGGER.debug("Existing item found with etag: " + currentEtag + " client etag: " + etag + " id: " + currentItemId);
591 }
592
593 ItemResult itemResult = new ItemResult();
594 if (isMeetingResponse || isMozDismiss) {
595 LOGGER.debug("Ignore etag check, meeting response or dismiss");
596 } else if ("*".equals(noneMatch)) {
597
598 if (isExistingEvent) {
599 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
600 return itemResult;
601 }
602 } else if (etag != null) {
603
604 if (!isExistingEvent || !etag.equals(currentEtag)) {
605 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
606 return itemResult;
607 }
608 }
609
610 VObject vEvent = vCalendar.getFirstVevent();
611 GraphObject graphResponse;
612 try {
613 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder();
614
615 if (isExistingEvent && isMeetingResponse) {
616 graphResponse = sendMeetingResponse(currentItemId);
617 } else if (isExistingEvent && isMozDismiss) {
618 graphResponse = mozDismissEvent(currentItemId);
619 } else if (folderId.isTask()) {
620 graphResponse = createOrUpdateTask(currentItemId);
621 } else if (isExistingEvent && isMeeting && !isOrganizer) {
622 graphResponse = updateReminder(currentItemId);
623 } else {
624
625 GraphObject newGraphEvent = buildJsonEvent(vEvent);
626
627
628 newGraphEvent.put("urlcompname", convertItemNameToEML(itemName));
629
630
631 String iCalUId = vEvent.getPropertyValue("UID");
632 if (!isExistingEvent && iCalUId != null && !iCalUId.isEmpty()) {
633 newGraphEvent.put("transactionId", iCalUId);
634 }
635
636
637 newGraphEvent.put("isReminderOn", vCalendar.hasVAlarm());
638 newGraphEvent.put("reminderMinutesBeforeStart", vCalendar.getReminderMinutesBeforeStart());
639
640 if (vCalendar.isMeeting() && Settings.getBooleanProperty("davmail.caldav.enableOnlineMeeting", true)) {
641
642 newGraphEvent.put("isOnlineMeeting", true);
643 }
644 String disaLLowCounter = vEvent.getPropertyValue("X-MICROSOFT-DISALLOW-COUNTER");
645 newGraphEvent.put("allowNewTimeProposals", !"TRUE".equals(disaLLowCounter));
646
647 convertRruleToGraph(newGraphEvent, vEvent.getProperty("RRULE"));
648
649
650 String xMozSendInvitations = vCalendar.getFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS");
651 if (xMozSendInvitations != null) {
652 newGraphEvent.put("xmozsendinvitations", xMozSendInvitations);
653 }
654
655 String xMozLastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
656 if (xMozLastack != null) {
657 newGraphEvent.put("xmozlastack", xMozLastack);
658 }
659 String xMozSnoozeTime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
660 if (xMozSnoozeTime != null) {
661 newGraphEvent.put("xmozsnoozetime", xMozSnoozeTime);
662 }
663
664 newGraphEvent.put("isAllDay", vCalendar.isCdoAllDay());
665
666
667 String status = vCalendar.getFirstVeventPropertyValue("STATUS");
668 if ("TENTATIVE".equals(status)) {
669
670 newGraphEvent.put("showAs", "tentative");
671 } else {
672
673
674 newGraphEvent.put("showAs", "BUSY".equals(vCalendar.getFirstVeventPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS")) ? "busy" : "free");
675 }
676
677 if (isExistingEvent) {
678 graphRequestBuilder.setMethod(HttpPatch.METHOD_NAME)
679 .setMailbox(folderId.mailbox)
680 .setObjectType("events")
681 .setObjectId(currentItemId)
682 .setJsonBody(newGraphEvent);
683 } else {
684 graphRequestBuilder.setMethod(HttpPost.METHOD_NAME)
685 .setMailbox(folderId.mailbox)
686 .setObjectType("calendars")
687 .setObjectId(folderId.id)
688 .setChildType("events")
689 .setJsonBody(newGraphEvent);
690 }
691 graphResponse = executeGraphRequest(graphRequestBuilder);
692
693
694 currentItemId = graphResponse.optString("id");
695 if (existingJsonEvent == null) {
696
697 existingJsonEvent = graphResponse.jsonObject;
698 }
699
700
701 List<VProperty> exdateProperty = vEvent.getProperties("EXDATE");
702 if (exdateProperty != null && !exdateProperty.isEmpty()) {
703 for (VProperty exdate : exdateProperty) {
704 String exdateTzid = exdate.getParamValue("TZID");
705 String exDateValue = vCalendar.convertCalendarDateToGraph(exdate.getValue(), exdateTzid);
706 deleteEventOccurrence(currentItemId, exDateValue);
707 }
708 }
709
710 handleModifiedOccurrences(vCalendar, existingJsonEvent);
711
712
713 graphResponse = executeGraphRequest(new GraphRequestBuilder()
714 .setMethod(HttpGet.METHOD_NAME)
715 .setMailbox(folderId.mailbox)
716 .setObjectType("events")
717 .setObjectId(currentItemId)
718 .setSelect("id"));
719 }
720
721 itemResult.status = graphResponse.statusCode;
722 if (itemResult.status == HttpStatus.SC_ACCEPTED) {
723 LOGGER.debug("Sent meeting response");
724 itemResult.status = HttpStatus.SC_OK;
725 }
726
727 itemResult.etag = graphResponse.optString("changeKey");
728
729
730 urlcompnameToIdMap.put(itemName, graphResponse.optString("id"));
731
732 itemResult.itemName = itemName;
733 itemResult.etag = graphResponse.optString("changeKey");
734
735 } catch (JSONException e) {
736 throw new IOException(e);
737 }
738
739 return itemResult;
740 }
741
742 private GraphObject updateReminder(String currentItemId) throws JSONException, IOException {
743 LOGGER.debug("Update on existing meeting, not organizer, not a meeting response or dismiss: allow reminder updates only");
744
745
746 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder().setMethod(HttpPatch.METHOD_NAME)
747 .setMailbox(folderId.mailbox)
748 .setObjectType("events")
749 .setObjectId(currentItemId)
750 .setJsonBody(new GraphObject().put("isReminderOn", vCalendar.hasVAlarm())
751 .put("reminderMinutesBeforeStart", vCalendar.getReminderMinutesBeforeStart())
752 );
753 return executeGraphRequest(graphRequestBuilder);
754 }
755
756 protected GraphObject createOrUpdateTask(String currentItemId) throws IOException, JSONException {
757 JSONObject jsonTask = buildJsonTask(vCalendar.getFirstVevent());
758
759 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder();
760
761 if (currentItemId == null) {
762 graphRequestBuilder
763 .setMethod(HttpPost.METHOD_NAME)
764 .setMailbox(folderId.mailbox)
765 .setObjectType("todo/lists")
766 .setObjectId(folderId.id)
767 .setChildType("tasks")
768 .setChildId(currentItemId)
769 .setJsonBody(jsonTask);
770 } else {
771 graphRequestBuilder
772 .setMethod(HttpPatch.METHOD_NAME)
773 .setMailbox(folderId.mailbox)
774 .setObjectType("todo/lists")
775 .setObjectId(folderId.id)
776 .setChildType("tasks")
777 .setChildId(currentItemId)
778 .setJsonBody(jsonTask);
779 }
780 return executeGraphRequest(graphRequestBuilder);
781 }
782
783 private GraphObject mozDismissEvent(String currentItemId) throws IOException, JSONException {
784
785 String newmozlastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
786 String newmozsnoozetime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
787
788 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder().setMethod(HttpPatch.METHOD_NAME)
789 .setMailbox(folderId.mailbox)
790 .setObjectType("events")
791 .setObjectId(currentItemId)
792 .setJsonBody(new GraphObject()
793 .put("xmozlastack", newmozlastack)
794 .put("xmozsnoozetime", newmozsnoozetime)
795 );
796
797 return executeGraphRequest(graphRequestBuilder);
798 }
799
800 protected GraphObject sendMeetingResponse(String currentItemId) throws IOException {
801
802 String body = null;
803 boolean sendResponse = true;
804
805 if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
806 String vEventSubject = vCalendar.getFirstVeventPropertyValue("SUMMARY");
807 if (vEventSubject == null) {
808 vEventSubject = BundleMessage.format("MEETING_REQUEST");
809 }
810
811 String status = vCalendar.getAttendeeStatus();
812 String notificationSubject = (status != null) ? (BundleMessage.format(status) + vEventSubject) : subject;
813
814 NotificationDialog notificationDialog = new NotificationDialog(notificationSubject, "");
815 if (!notificationDialog.getSendNotification()) {
816 LOGGER.debug("Notification canceled by user");
817 sendResponse = false;
818 }
819
820 body = notificationDialog.getBody();
821 }
822
823 try {
824 JSONObject jsonBody = new JSONObject();
825 jsonBody.put("sendResponse", sendResponse);
826 if (body != null && !body.isEmpty()) {
827 jsonBody.put("comment", body);
828 }
829 String action = "accept";
830 String attendeeStatus = vCalendar.getAttendeeStatus();
831 if ("ACCEPTED".equals(attendeeStatus)) {
832 action = "accept";
833 } else if ("DECLINED".equals(attendeeStatus)) {
834 action = "decline";
835 } else if ("TENTATIVE".equals(attendeeStatus)) {
836 action = "tentativelyAccept";
837 }
838
839 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
840 .setMailbox(folderId.mailbox)
841 .setObjectType("events")
842 .setObjectId(currentItemId)
843 .setAction(action)
844 .setJsonBody(jsonBody);
845
846 return executeGraphRequest(graphRequestBuilder);
847 } catch (JSONException e) {
848 throw new IOException(e);
849 }
850
851 }
852
853
854
855
856
857
858
859
860 private void convertRruleToGraph(GraphObject jsonEvent, VProperty rrule) throws JSONException, DavMailException {
861 if (rrule != null) {
862 JSONObject start = jsonEvent.optJSONObject("start");
863 if (start == null) {
864
865 start = jsonEvent.optJSONObject("startDateTime");
866 }
867 String startDate = start.getString("dateTime").substring(0, 10);
868 String startTimeZone = start.optString("timeZone");
869
870
871 Map<String, String> rrules = rrule.getValuesAsMap();
872 String frequency = rrules.get("FREQ");
873 String until = rrules.get("UNTIL");
874 String count = rrules.get("COUNT");
875 int interval = rrules.containsKey("INTERVAL") ? Integer.parseInt(rrules.get("INTERVAL")) : 1;
876 String byDay = rrules.get("BYDAY");
877 String byMonthDay = rrules.get("BYMONTHDAY");
878 String byMonth = rrules.get("BYMONTH");
879 String wkst = rrules.get("WKST");
880
881
882 JSONObject range;
883 if (until != null) {
884
885 String endDate = convertUntilToEndDate(until, startTimeZone);
886 range = new JSONObject().put("type", "endDate").put("startDate", startDate)
887 .put("endDate", endDate).put("recurrenceTimeZone", startTimeZone);
888 } else if (count != null) {
889
890 range = new JSONObject().put("type", "numbered").put("startDate", startDate)
891 .put("numberOfOccurrences", Integer.parseInt(count));
892 } else {
893 range = new JSONObject().put("type", "noEnd").put("startDate", startDate).put("endDate", "0001-01-01");
894 }
895
896
897 JSONObject pattern = new JSONObject().put("interval", interval);
898
899 if ("DAILY".equals(frequency)) {
900 pattern.put("type", "daily").put("dayOfMonth", 0);
901 } else if ("WEEKLY".equals(frequency)) {
902 pattern.put("type", "weekly").put("daysOfWeek", byDay != null ? convertByDayToArray(byDay) : new JSONArray().put(getDayOfWeek(startDate)));
903 if (wkst != null) {
904 pattern.put("firstDayOfWeek", convertCaldavDayToGraph(wkst));
905 }
906 } else if ("MONTHLY".equals(frequency)) {
907 if (byDay != null) {
908 pattern.put("type", "relativeMonthly");
909 setRelativePattern(pattern, byDay);
910 } else {
911 pattern.put("type", "absoluteMonthly");
912 pattern.put("dayOfMonth", byMonthDay != null ? Integer.parseInt(byMonthDay) : Integer.parseInt(startDate.substring(8, 10)));
913 }
914 } else if ("YEARLY".equals(frequency)) {
915 if (byDay != null) {
916 pattern.put("type", "relativeYearly");
917 setRelativePattern(pattern, byDay);
918 } else {
919 pattern.put("type", "absoluteYearly")
920 .put("dayOfMonth", byMonthDay != null ? Integer.parseInt(byMonthDay) : Integer.parseInt(startDate.substring(8, 10)));
921 }
922 if (byMonth != null) {
923 pattern.put("month", Integer.parseInt(byMonth));
924 } else {
925 pattern.put("month", Integer.parseInt(startDate.substring(5, 7)));
926 }
927 }
928
929 jsonEvent.put("recurrence", new JSONObject().put("pattern", pattern).put("range", range));
930 }
931 }
932
933 private JSONArray convertByDayToArray(String byDay) {
934 JSONArray daysOfWeek = new JSONArray();
935 for (String day : byDay.split(",")) {
936
937 daysOfWeek.put(convertCaldavDayToGraph(day.replaceAll("^-?\\d+", "")));
938 }
939 return daysOfWeek;
940 }
941
942 private String convertCaldavDayToGraph(String weekDay) {
943 switch (weekDay) {
944 case "MO":
945 return "monday";
946 case "TU":
947 return "tuesday";
948 case "WE":
949 return "wednesday";
950 case "TH":
951 return "thursday";
952 case "FR":
953 return "friday";
954 case "SA":
955 return "saturday";
956 case "SU":
957 return "sunday";
958 default:
959 return weekDay.toLowerCase();
960 }
961 }
962
963 private void setRelativePattern(JSONObject pattern, String byDay) throws JSONException {
964
965 String firstDay = byDay.split(",")[0];
966 int i = 0;
967 while (i < firstDay.length() && (Character.isDigit(firstDay.charAt(i)) || firstDay.charAt(i) == '-')) {
968 i++;
969 }
970 String indexStr = firstDay.substring(0, i);
971 if (!indexStr.isEmpty()) {
972 pattern.put("index", convertIndex(Integer.parseInt(indexStr)));
973 }
974 pattern.put("daysOfWeek", convertByDayToArray(byDay));
975 }
976
977 private String convertIndex(int index) {
978 switch (index) {
979 case 1:
980 return "first";
981 case 2:
982 return "second";
983 case 3:
984 return "third";
985 case 4:
986 return "fourth";
987 case -1:
988 return "last";
989 default:
990 return "first";
991 }
992 }
993
994 private String convertUntilToEndDate(String until, String timeZone) throws DavMailException {
995 try {
996 SimpleDateFormat parser;
997 if (until.length() == 8) {
998 parser = new SimpleDateFormat("yyyyMMdd");
999 parser.setTimeZone(TimeZone.getTimeZone(convertTimezoneFromExchange(timeZone)));
1000 } else if (until.endsWith("Z")) {
1001 parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
1002 parser.setTimeZone(TimeZone.getTimeZone("UTC"));
1003 } else {
1004 parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
1005 parser.setTimeZone(TimeZone.getTimeZone(convertTimezoneFromExchange(timeZone)));
1006 }
1007 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
1008 formatter.setTimeZone(TimeZone.getTimeZone(convertTimezoneFromExchange(timeZone)));
1009 return formatter.format(parser.parse(until));
1010 } catch (ParseException e) {
1011 throw new DavMailException("EXCEPTION_INVALID_DATE", until);
1012 }
1013 }
1014
1015 private String getDayOfWeek(String date) throws DavMailException {
1016 if (date != null) {
1017 try {
1018 SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd");
1019 parser.setTimeZone(TimeZone.getTimeZone("UTC"));
1020 SimpleDateFormat formatter = new SimpleDateFormat("EEEE", Locale.ENGLISH);
1021 formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
1022 return formatter.format(parser.parse(date));
1023 } catch (ParseException e) {
1024 throw new DavMailException("EXCEPTION_INVALID_DATE", date);
1025 }
1026 }
1027 return null;
1028 }
1029
1030 private void handleModifiedOccurrences(VCalendar vCalendar, JSONObject existingJsonEvent) throws IOException, JSONException {
1031 for (VObject modifiedOccurrence : vCalendar.getModifiedOccurrences()) {
1032 VProperty originalDateProperty = modifiedOccurrence.getProperty("RECURRENCE-ID");
1033 String originalDateZulu;
1034 try {
1035 originalDateZulu = vCalendar.convertCalendarDateToExchangeZulu(originalDateProperty.getValue(), originalDateProperty.getParamValue("TZID"));
1036 } catch (IOException e) {
1037 throw new DavMailException("EXCEPTION_INVALID_DATE", originalDateProperty.getValue());
1038 }
1039 LOGGER.debug("Looking for occurrence " + originalDateZulu);
1040
1041 JSONArray exceptionOccurrences = existingJsonEvent.optJSONArray("exceptionOccurrences");
1042 boolean occurrenceFound = false;
1043 if (exceptionOccurrences != null) {
1044 for (int i = 0; i < exceptionOccurrences.length(); i++) {
1045 JSONObject exceptionOccurrence = exceptionOccurrences.optJSONObject(i);
1046 String exceptionOriginalStart = convertOriginalStartDate(exceptionOccurrence.optString("originalStart"));
1047 LOGGER.debug("Looking at occurrence " + exceptionOriginalStart + " for " + originalDateZulu);
1048 if (originalDateZulu.equals(exceptionOriginalStart)) {
1049 updateExceptionOccurrence(modifiedOccurrence, exceptionOccurrence.getString("id"));
1050 occurrenceFound = true;
1051 break;
1052 }
1053 }
1054 }
1055 if (!occurrenceFound) {
1056 createNewModifiedOccurrence(modifiedOccurrence, existingJsonEvent, originalDateZulu);
1057 }
1058 }
1059 }
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069 private void createNewModifiedOccurrence(VObject modifiedOccurrence, JSONObject existingJsonEvent, String originalDateZulu) throws IOException, JSONException {
1070
1071 String startDateTime = originalDateZulu.substring(0, 10) + "T00:00:00.0000000";
1072 String endDateTime = originalDateZulu.substring(0, 10) + "T23:59:59.9999999";
1073
1074 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder().setMethod(HttpGet.METHOD_NAME)
1075 .setMailbox(folderId.mailbox)
1076 .setObjectType("events")
1077 .setObjectId(existingJsonEvent.optString("id"))
1078 .setChildType("instances")
1079 .setStartDateTime(startDateTime)
1080 .setEndDateTime(endDateTime));
1081
1082 JSONArray occurrences = graphResponse.optJSONArray("value");
1083 if (occurrences != null && occurrences.length() > 0) {
1084 for (int i = 0; i < occurrences.length(); i++) {
1085 JSONObject occurrence = occurrences.getJSONObject(i);
1086 String occurrenceId = occurrence.optString("id");
1087 if (occurrenceId != null) {
1088 updateExceptionOccurrence(modifiedOccurrence, occurrenceId);
1089 }
1090 }
1091 } else {
1092 LOGGER.warn("No occurrence found for " + originalDateZulu);
1093 }
1094 }
1095
1096 private void updateExceptionOccurrence(VObject modifiedOccurrence, String exceptionOccurrenceId) throws IOException, JSONException {
1097 LOGGER.debug("Updating occurrence " + modifiedOccurrence.getPropertyValue("SUMMARY") + " " + modifiedOccurrence.getPropertyValue("RECURRENCE-ID"));
1098
1099 GraphObject graphEventOccurrence = buildJsonEvent(modifiedOccurrence);
1100
1101 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
1102 .setMethod(HttpPatch.METHOD_NAME)
1103 .setMailbox(folderId.mailbox)
1104 .setObjectType("events")
1105 .setObjectId(exceptionOccurrenceId)
1106 .setJsonBody(graphEventOccurrence));
1107
1108 LOGGER.debug("Updated occurrence: " + graphResponse.jsonObject.toString());
1109 }
1110
1111 private GraphObject buildJsonEvent(VObject vEvent) throws JSONException, IOException {
1112 GraphObject newGraphEvent = new GraphObject();
1113
1114 newGraphEvent.put("subject", vEvent.getPropertyValue("SUMMARY"));
1115
1116
1117 VProperty dtStart = vEvent.getProperty("DTSTART");
1118 String dtStartTzid = dtStart.getParamValue("TZID");
1119 newGraphEvent.put("start", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(dtStart.getValue(), dtStartTzid)).put("timeZone", dtStartTzid));
1120
1121 VProperty dtEnd = vEvent.getProperty("DTEND");
1122 String dtEndTzid = dtEnd.getParamValue("TZID");
1123 newGraphEvent.put("end", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(dtEnd.getValue(), dtEndTzid)).put("timeZone", dtEndTzid));
1124
1125 VProperty descriptionProperty = vEvent.getProperty("DESCRIPTION");
1126 String description = null;
1127 if (descriptionProperty != null) {
1128
1129 description = descriptionProperty.getParamValue("ALTREP");
1130 }
1131 if (description != null && description.startsWith("data:text/html,")) {
1132 description = URIUtil.decode(description.replaceFirst("data:text/html,", ""));
1133 newGraphEvent.put("body", new JSONObject().put("content", description).put("contentType", "html"));
1134 } else if (descriptionProperty != null) {
1135 description = descriptionProperty.getValue();
1136 newGraphEvent.put("body", new JSONObject().put("content", description).put("contentType", "text"));
1137 }
1138
1139 String location = vEvent.getPropertyValue("LOCATION");
1140 newGraphEvent.put("location", new JSONObject().put("displayName", location));
1141
1142 newGraphEvent.setCategories(vEvent.getPropertyValue("CATEGORIES"));
1143
1144 List<VProperty> categories = vEvent.getProperties("CATEGORIES");
1145 if (categories != null) {
1146 HashSet<String> categoryValues = new HashSet<>();
1147 for (VProperty category : categories) {
1148 categoryValues.add(category.getValue());
1149 }
1150 newGraphEvent.setCategories(StringUtil.join(categoryValues, ","));
1151 }
1152
1153 if (vCalendar.isMeeting()) {
1154
1155 JSONArray attendees = new JSONArray();
1156 newGraphEvent.put("attendees", attendees);
1157
1158 List<VProperty> attendeeProperties = vEvent.getProperties("ATTENDEE");
1159 if (attendeeProperties != null) {
1160 for (VProperty property : attendeeProperties) {
1161 String attendeeEmail = vCalendar.getEmailValue(property);
1162 if (attendeeEmail != null && attendeeEmail.indexOf('@') >= 0) {
1163 String cn = property.getParamValue("CN");
1164 JSONObject jsonAttendee = new JSONObject()
1165 .put("emailAddress", new JSONObject().put("name", cn)
1166 .put("address", attendeeEmail));
1167
1168 String attendeeRole = property.getParamValue("ROLE");
1169 if ("REQ-PARTICIPANT".equals(attendeeRole)) {
1170 jsonAttendee.put("type", "required");
1171 } else {
1172 jsonAttendee.put("type", "optional");
1173 }
1174 attendees.put(jsonAttendee);
1175 }
1176 }
1177 }
1178 }
1179
1180 return newGraphEvent;
1181 }
1182
1183 private void deleteEventOccurrence(String id, String exDateValue) throws IOException, JSONException {
1184 String startDateTime = exDateValue.substring(0, 10) + "T00:00:00.0000000";
1185 String endDateTime = exDateValue.substring(0, 10) + "T23:59:59.9999999";
1186 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder().setMethod(HttpGet.METHOD_NAME)
1187 .setMailbox(folderId.mailbox)
1188 .setObjectType("events")
1189 .setObjectId(id)
1190 .setChildType("instances")
1191 .setStartDateTime(startDateTime)
1192 .setEndDateTime(endDateTime));
1193
1194 JSONArray occurrences = graphResponse.optJSONArray("value");
1195 if (occurrences != null && occurrences.length() > 0) {
1196 for (int i = 0; i < occurrences.length(); i++) {
1197 JSONObject occurrence = occurrences.getJSONObject(i);
1198 String occurrenceId = occurrence.optString("id");
1199 if (occurrenceId != null) {
1200 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpDelete.METHOD_NAME)
1201 .setMailbox(folderId.mailbox)
1202 .setObjectType("events")
1203 .setObjectId(occurrenceId));
1204 }
1205 }
1206 }
1207 }
1208
1209 private JSONObject buildJsonTask(VObject vTodo) throws JSONException, IOException {
1210 JSONObject jsonEvent = new JSONObject();
1211 GraphObject localGraphObject = new GraphObject(jsonEvent);
1212
1213 localGraphObject.put("summary", vTodo.getPropertyValue("SUMMARY"));
1214
1215 localGraphObject.setTaskImportanceFromVTodo(vTodo);
1216 localGraphObject.setTaskStatusFromVTodo(vTodo);
1217
1218
1219 VProperty descriptionProperty = vTodo.getProperty("DESCRIPTION");
1220 String description = null;
1221 if (descriptionProperty != null) {
1222 description = vTodo.getProperty("DESCRIPTION").getParamValue("ALTREP");
1223 }
1224 if (description != null && description.startsWith("data:text/html,")) {
1225 description = URIUtil.decode(description.replaceFirst("data:text/html,", ""));
1226 jsonEvent.put("body", new JSONObject().put("content", description).put("contentType", "html"));
1227 } else {
1228 description = vTodo.getPropertyValue("DESCRIPTION");
1229 jsonEvent.put("body", new JSONObject().put("content", description).put("contentType", "text"));
1230 }
1231
1232 VProperty dtStart = vTodo.getProperty("DTSTART");
1233 if (dtStart != null) {
1234 String dtStartTzid = dtStart.getParamValue("TZID");
1235 if (dtStartTzid == null) {
1236 dtStartTzid = vCalendar.getVTimezone().getPropertyValue("TZID");
1237 }
1238 jsonEvent.put("startDateTime", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(dtStart.getValue(), dtStartTzid)).put("timeZone", dtStartTzid));
1239 }
1240
1241 VProperty due = vTodo.getProperty("DUE");
1242 if (due != null) {
1243 String dueTzid = due.getParamValue("TZID");
1244 if (dueTzid == null) {
1245 dueTzid = vCalendar.getVTimezone().getPropertyValue("TZID");
1246 }
1247 jsonEvent.put("dueDateTime", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(due.getValue(), dueTzid)).put("timeZone", dueTzid));
1248 }
1249
1250 VProperty completed = vTodo.getProperty("COMPLETED");
1251 if (completed != null) {
1252 String completedTzid = completed.getParamValue("TZID");
1253 if (completedTzid == null) {
1254 completedTzid = vCalendar.getVTimezone().getPropertyValue("TZID");
1255 }
1256 jsonEvent.put("completedDateTime", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(completed.getValue(), completedTzid)).put("timeZone", completedTzid));
1257 }
1258
1259 localGraphObject.setCategories(vTodo.getPropertyValue("CATEGORIES"));
1260
1261 List<VProperty> categories = vTodo.getProperties("CATEGORIES");
1262 if (categories != null) {
1263 HashSet<String> categoryValues = new HashSet<>();
1264 for (VProperty category : categories) {
1265 categoryValues.add(category.getValue());
1266 }
1267 localGraphObject.setCategories(StringUtil.join(categoryValues, ","));
1268 }
1269
1270 return jsonEvent;
1271 }
1272
1273 }
1274
1275
1276
1277
1278
1279
1280
1281 @Override
1282 public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
1283 boolean isExpired = false;
1284 try {
1285 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpGet.METHOD_NAME).setObjectType("mailFolders").setSelect("id"));
1286 } catch (UnknownHostException | NoRouteToHostException exc) {
1287 throw exc;
1288 } catch (IOException e) {
1289 isExpired = true;
1290 }
1291
1292 return isExpired;
1293 }
1294
1295 private String convertHtmlToText(String htmlText) {
1296 StringBuilder builder = new StringBuilder();
1297
1298 HtmlCleaner cleaner = new HtmlCleaner();
1299 cleaner.getProperties().setDeserializeEntities(true);
1300 try {
1301 TagNode node = cleaner.clean(new StringReader(htmlText));
1302 for (TagNode childNode : node.getAllElementsList(true)) {
1303 builder.append(childNode.getText());
1304 }
1305 } catch (IOException e) {
1306 LOGGER.error("Error converting html to text", e);
1307 }
1308 return builder.toString();
1309 }
1310
1311 private VProperty convertBodyToVproperty(String propertyName, GraphObject graphObject) {
1312 JSONObject jsonBody = graphObject.optJSONObject("body");
1313
1314 if (jsonBody == null) {
1315 return new VProperty(propertyName, "");
1316 } else {
1317
1318 String content = jsonBody.optString("content");
1319 String contentType = jsonBody.optString("contentType");
1320 VProperty vProperty;
1321
1322 if ("text".equals(contentType)) {
1323 vProperty = new VProperty(propertyName, content);
1324 } else {
1325
1326 if (content != null) {
1327 vProperty = new VProperty(propertyName, convertHtmlToText(content));
1328
1329 content = content.replace("\n", "").replace("\r", "");
1330 vProperty.addParam("ALTREP", "data:text/html," + URIUtil.encodeWithinQuery(content));
1331 } else {
1332 vProperty = new VProperty(propertyName, null);
1333 }
1334
1335 }
1336 return vProperty;
1337 }
1338 }
1339
1340 private VProperty convertDateTimeTimeZoneToVproperty(String vPropertyName, JSONObject jsonDateTimeTimeZone, String originalStartTimeZone) throws DavMailException {
1341
1342 if (jsonDateTimeTimeZone != null) {
1343 String timeZone = jsonDateTimeTimeZone.optString("timeZone");
1344 String dateTime = convertDateFromExchange(jsonDateTimeTimeZone.optString("dateTime"));
1345
1346 if (originalStartTimeZone != null && !timeZone.equals(originalStartTimeZone)) {
1347 LOGGER.debug("originalStartTimeZone different from requested timeZone: " + originalStartTimeZone + " vs " + timeZone);
1348
1349 SimpleDateFormat parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
1350 SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
1351 parser.setTimeZone(DateUtil.getTimeZone(timeZone));
1352 formatter.setTimeZone(DateUtil.getTimeZone(originalStartTimeZone));
1353 try {
1354 dateTime = formatter.format(parser.parse(dateTime));
1355 timeZone = originalStartTimeZone;
1356 } catch (ParseException e) {
1357 LOGGER.warn("Unable to convert to original timezone: " + dateTime + ", " + originalStartTimeZone);
1358 }
1359 }
1360
1361 VProperty vProperty = new VProperty(vPropertyName, dateTime);
1362 vProperty.addParam("TZID", timeZone);
1363 return vProperty;
1364 }
1365 return new VProperty(vPropertyName, null);
1366 }
1367
1368 private VProperty convertEmailAddressToVproperty(String propertyName, JSONObject jsonEmailAddress) {
1369 VProperty attendeeProperty = new VProperty(propertyName, "mailto:" + jsonEmailAddress.optString("address"));
1370 attendeeProperty.addParam("CN", jsonEmailAddress.optString("name"));
1371 return attendeeProperty;
1372 }
1373
1374 private String convertDateTimeTimeZoneToTaskDate(Date exchangeDateValue) {
1375 String zuluDateValue = null;
1376 if (exchangeDateValue != null) {
1377 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
1378 dateFormat.setTimeZone(GMT_TIMEZONE);
1379 zuluDateValue = dateFormat.format(exchangeDateValue);
1380 }
1381 return zuluDateValue;
1382
1383 }
1384
1385 protected class Contact extends ExchangeSession.Contact {
1386
1387 FolderId folderId;
1388 String id;
1389
1390 protected Contact(GraphObject response) throws DavMailException {
1391 id = response.optString("id");
1392 etag = response.optString("@odata.etag");
1393
1394 displayName = response.optString("displayname");
1395
1396 itemName = StringUtil.decodeUrlcompname(response.optString("urlcompname"));
1397
1398 if (itemName == null) {
1399 itemName = StringUtil.base64ToUrl(id) + ".EML";
1400 }
1401 put("uid", response.optString("uid"));
1402
1403 for (GraphField attribute : CONTACT_ATTRIBUTES) {
1404 String alias = attribute.getAlias();
1405 if (!alias.startsWith("smtpemail")) {
1406 String value = response.optString(attribute);
1407 if (value != null && !value.isEmpty()) {
1408 put(alias, value);
1409 }
1410 }
1411 }
1412
1413 JSONArray emailAddresses = response.optJSONArray("emailAddresses");
1414 if (emailAddresses != null) {
1415 for (int i = 0; i < emailAddresses.length(); i++) {
1416 JSONObject emailAddress = emailAddresses.optJSONObject(i);
1417 if (emailAddress != null) {
1418 String email = emailAddress.optString("address");
1419 String type = emailAddress.optString("type");
1420 if (email != null && !email.isEmpty()) {
1421 if ("other".equals(type)) {
1422 put("smtpemail3", email);
1423 } else if ("personal".equals(type)) {
1424 put("smtpemail2", email);
1425 } else if ("work".equals(type)) {
1426 put("smtpemail1", email);
1427 }
1428 }
1429 }
1430 }
1431
1432 for (int i = 0; i < emailAddresses.length(); i++) {
1433 JSONObject emailAddress = emailAddresses.optJSONObject(i);
1434 if (emailAddress != null) {
1435 String email = emailAddress.optString("address");
1436 String type = emailAddress.optString("type");
1437 if (email != null && !email.isEmpty()) {
1438 if ("unknown".equals(type)) {
1439 if (get("smtpemail1") == null) {
1440 put("smtpemail1", email);
1441 } else if (get("smtpemail2") == null) {
1442 put("smtpemail2", email);
1443 } else if (get("smtpemail3") == null) {
1444 put("smtpemail3", email);
1445 }
1446 }
1447 }
1448 }
1449 }
1450 }
1451 }
1452
1453 protected Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
1454 super(folderPath, itemName, properties, etag, noneMatch);
1455 }
1456
1457
1458
1459
1460 protected Contact() {
1461 }
1462
1463
1464
1465
1466
1467
1468
1469
1470 @Override
1471 public ItemResult createOrUpdate() throws IOException {
1472
1473 FolderId folderId = getFolderId(folderPath);
1474 String id = null;
1475 String currentEtag = null;
1476 JSONObject jsonContact = getContactIfExists(folderId, itemName);
1477 if (jsonContact != null) {
1478 id = jsonContact.optString("id", null);
1479 currentEtag = new GraphObject(jsonContact).optString("changeKey");
1480 }
1481
1482 ItemResult itemResult = new ItemResult();
1483 if ("*".equals(noneMatch)) {
1484
1485 if (id != null) {
1486 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1487 return itemResult;
1488 }
1489 } else if (etag != null) {
1490
1491 if (id == null || !etag.equals(currentEtag)) {
1492 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1493 return itemResult;
1494 }
1495 }
1496
1497 try {
1498 JSONObject jsonObject = new JSONObject();
1499 GraphObject graphObject = new GraphObject(jsonObject);
1500 for (Map.Entry<String, String> entry : entrySet()) {
1501 if ("keywords".equals(entry.getKey())) {
1502 graphObject.setCategories(entry.getValue());
1503 } else if ("bday".equals(entry.getKey())) {
1504 graphObject.put(entry.getKey(), convertZuluToIso(entry.getValue()));
1505 } else if ("anniversary".equals(entry.getKey())) {
1506 graphObject.put(entry.getKey(), convertZuluToDate(entry.getValue()));
1507 } else if ("photo".equals(entry.getKey())) {
1508 LOGGER.debug("Contact has a photo");
1509 } else if (!entry.getKey().startsWith("email") && !entry.getKey().startsWith("smtpemail")
1510 && !"usersmimecertificate".equals(entry.getKey())
1511 && !"msexchangecertificate".equals(entry.getKey())
1512 && !"pager".equals(entry.getKey()) && !"otherTelephone".equals(entry.getKey())
1513 && !"fileas".equals(entry.getKey()) && !"outlookmessageclass".equals(entry.getKey())
1514 && !"subject".equals(entry.getKey())
1515 ) {
1516 graphObject.put(entry.getKey(), entry.getValue());
1517 }
1518 }
1519
1520
1521 String pager = get("pager");
1522 if (pager == null) {
1523 pager = get("otherTelephone");
1524 }
1525 graphObject.put("pager", pager);
1526
1527
1528 graphObject.put("urlcompname", convertItemNameToEML(itemName));
1529
1530
1531 JSONArray emailAddresses = new JSONArray();
1532 String smtpemail1 = get("smtpemail1");
1533 if (smtpemail1 != null) {
1534 JSONObject emailAddress = new JSONObject();
1535 emailAddress.put("address", smtpemail1);
1536 emailAddress.put("type", "work");
1537 emailAddresses.put(emailAddress);
1538 }
1539
1540 String smtpemail2 = get("smtpemail2");
1541 if (smtpemail2 != null) {
1542 JSONObject emailAddress = new JSONObject();
1543 emailAddress.put("address", smtpemail2);
1544 emailAddress.put("type", "personal");
1545 emailAddresses.put(emailAddress);
1546 }
1547
1548 String smtpemail3 = get("smtpemail3");
1549 if (smtpemail3 != null) {
1550 JSONObject emailAddress = new JSONObject();
1551 emailAddress.put("address", smtpemail3);
1552 emailAddress.put("type", "other");
1553 emailAddresses.put(emailAddress);
1554 }
1555 graphObject.put("emailAddresses", emailAddresses);
1556
1557 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder();
1558 if (id == null) {
1559 graphRequestBuilder.setMethod(HttpPost.METHOD_NAME)
1560 .setMailbox(folderId.mailbox)
1561 .setObjectType("contactFolders")
1562 .setObjectId(folderId.id)
1563 .setChildType("contacts")
1564 .setJsonBody(jsonObject);
1565 } else {
1566 graphRequestBuilder.setMethod(HttpPatch.METHOD_NAME)
1567 .setMailbox(folderId.mailbox)
1568 .setObjectType("contactFolders")
1569 .setObjectId(folderId.id)
1570 .setChildType("contacts")
1571 .setChildId(id)
1572 .setJsonBody(jsonObject);
1573 }
1574
1575 GraphObject graphResponse = executeGraphRequest(graphRequestBuilder);
1576
1577 if (LOGGER.isDebugEnabled()) {
1578 LOGGER.debug(graphResponse.toString(4));
1579 }
1580
1581 itemResult.status = graphResponse.statusCode;
1582
1583 updatePhoto(folderId, graphResponse.optString("id"));
1584
1585
1586 graphResponse = new GraphObject(getContactIfExists(folderId, itemName));
1587
1588 itemResult.itemName = itemName;
1589 itemResult.etag = graphResponse.optString("etag");
1590
1591 } catch (JSONException e) {
1592 throw new IOException(e);
1593 }
1594 if (itemResult.status == HttpStatus.SC_CREATED) {
1595 LOGGER.debug("Created contact " + getHref());
1596 } else {
1597 LOGGER.debug("Updated contact " + getHref());
1598 }
1599
1600 return itemResult;
1601 }
1602
1603 private void updatePhoto(FolderId folderId, String contactId) throws IOException {
1604 String photo = get("photo");
1605 if (photo != null) {
1606
1607 byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90);
1608
1609
1610 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
1611 .setMethod(HttpPut.METHOD_NAME)
1612 .setMailbox(folderId.mailbox)
1613 .setObjectType("contactFolders")
1614 .setObjectId(folderId.id)
1615 .setChildType("contacts")
1616 .setChildId(contactId)
1617 .setChildSuffix("photo/$value")
1618 .setContentType("image/jpeg")
1619 .setMimeContent(resizedImageBytes));
1620
1621 if (LOGGER.isDebugEnabled()) {
1622 LOGGER.debug(jsonResponse);
1623 }
1624 } else {
1625
1626 executeJsonRequest(new GraphRequestBuilder()
1627 .setMethod(HttpDelete.METHOD_NAME)
1628 .setMailbox(folderId.mailbox)
1629 .setObjectType("contactFolders")
1630 .setObjectId(folderId.id)
1631 .setChildType("contacts")
1632 .setChildId(contactId)
1633 .setChildSuffix("photo"));
1634 }
1635 }
1636 }
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646 private String convertZuluToIso(String value) {
1647 if (value != null) {
1648 return value.replace(".000Z", "Z");
1649 } else {
1650 return null;
1651 }
1652 }
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664 private String convertZuluToDate(String value) {
1665 if (value != null && value.contains("T")) {
1666 return value.substring(0, value.indexOf("T"));
1667 } else {
1668 return value;
1669 }
1670 }
1671
1672
1673 @SuppressWarnings("SpellCheckingInspection")
1674 public enum WellKnownFolderName {
1675 archive,
1676 deleteditems,
1677 calendar, contacts, tasks,
1678 drafts, inbox, outbox, sentitems, junkemail,
1679 msgfolderroot,
1680 searchfolders
1681 }
1682
1683
1684 protected static HashMap<String, String> wellKnownFolderMap = new HashMap<>();
1685
1686 static {
1687 wellKnownFolderMap.put(WellKnownFolderName.inbox.name(), ExchangeSession.INBOX);
1688 wellKnownFolderMap.put(WellKnownFolderName.archive.name(), ExchangeSession.ARCHIVE);
1689 wellKnownFolderMap.put(WellKnownFolderName.drafts.name(), ExchangeSession.DRAFTS);
1690 wellKnownFolderMap.put(WellKnownFolderName.junkemail.name(), ExchangeSession.JUNK);
1691 wellKnownFolderMap.put(WellKnownFolderName.sentitems.name(), ExchangeSession.SENT);
1692 wellKnownFolderMap.put(WellKnownFolderName.deleteditems.name(), ExchangeSession.TRASH);
1693 }
1694
1695 protected static final HashSet<GraphField> IMAP_MESSAGE_ATTRIBUTES = new HashSet<>();
1696
1697 static {
1698
1699 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("permanenturl"));
1700 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("changeKey"));
1701 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("isDraft"));
1702 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("isRead"));
1703 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("receivedDateTime"));
1704 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("lastModifiedDateTime"));
1705
1706 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("urlcompname"));
1707 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("uid"));
1708 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("messageSize"));
1709 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("imapUid"));
1710 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("junk"));
1711 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("flagStatus"));
1712 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("messageFlags"));
1713 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("lastVerbExecuted"));
1714 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("read"));
1715 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("deleted"));
1716 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("date"));
1717 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("lastmodified"));
1718
1719 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("contentclass"));
1720 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("keywords"));
1721
1722
1723 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("messageheaders"));
1724 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("outlookmessageclass"));
1725 }
1726
1727 protected static final HashSet<GraphField> CONTACT_ATTRIBUTES = new HashSet<>();
1728
1729 static {
1730 CONTACT_ATTRIBUTES.add(GraphField.get("uid"));
1731
1732 CONTACT_ATTRIBUTES.add(GraphField.get("imapUid"));
1733
1734 CONTACT_ATTRIBUTES.add(GraphField.get("urlcompname"));
1735 CONTACT_ATTRIBUTES.add(GraphField.get("keywords"));
1736
1737 CONTACT_ATTRIBUTES.add(GraphField.get("extensionattribute1"));
1738 CONTACT_ATTRIBUTES.add(GraphField.get("extensionattribute2"));
1739 CONTACT_ATTRIBUTES.add(GraphField.get("extensionattribute3"));
1740 CONTACT_ATTRIBUTES.add(GraphField.get("extensionattribute4"));
1741 CONTACT_ATTRIBUTES.add(GraphField.get("bday"));
1742 CONTACT_ATTRIBUTES.add(GraphField.get("anniversary"));
1743 CONTACT_ATTRIBUTES.add(GraphField.get("businesshomepage"));
1744 CONTACT_ATTRIBUTES.add(GraphField.get("personalHomePage"));
1745 CONTACT_ATTRIBUTES.add(GraphField.get("cn"));
1746 CONTACT_ATTRIBUTES.add(GraphField.get("co"));
1747 CONTACT_ATTRIBUTES.add(GraphField.get("department"));
1748 CONTACT_ATTRIBUTES.add(GraphField.get("smtpemail1"));
1749 CONTACT_ATTRIBUTES.add(GraphField.get("smtpemail2"));
1750 CONTACT_ATTRIBUTES.add(GraphField.get("smtpemail3"));
1751 CONTACT_ATTRIBUTES.add(GraphField.get("facsimiletelephonenumber"));
1752 CONTACT_ATTRIBUTES.add(GraphField.get("givenName"));
1753 CONTACT_ATTRIBUTES.add(GraphField.get("homeCity"));
1754 CONTACT_ATTRIBUTES.add(GraphField.get("homeCountry"));
1755 CONTACT_ATTRIBUTES.add(GraphField.get("homePhone"));
1756 CONTACT_ATTRIBUTES.add(GraphField.get("homePostalCode"));
1757 CONTACT_ATTRIBUTES.add(GraphField.get("homeState"));
1758 CONTACT_ATTRIBUTES.add(GraphField.get("homeStreet"));
1759 CONTACT_ATTRIBUTES.add(GraphField.get("homepostofficebox"));
1760 CONTACT_ATTRIBUTES.add(GraphField.get("l"));
1761 CONTACT_ATTRIBUTES.add(GraphField.get("manager"));
1762 CONTACT_ATTRIBUTES.add(GraphField.get("mobile"));
1763 CONTACT_ATTRIBUTES.add(GraphField.get("namesuffix"));
1764 CONTACT_ATTRIBUTES.add(GraphField.get("nickname"));
1765 CONTACT_ATTRIBUTES.add(GraphField.get("o"));
1766 CONTACT_ATTRIBUTES.add(GraphField.get("pager"));
1767 CONTACT_ATTRIBUTES.add(GraphField.get("personaltitle"));
1768 CONTACT_ATTRIBUTES.add(GraphField.get("postalcode"));
1769 CONTACT_ATTRIBUTES.add(GraphField.get("postofficebox"));
1770 CONTACT_ATTRIBUTES.add(GraphField.get("profession"));
1771 CONTACT_ATTRIBUTES.add(GraphField.get("roomnumber"));
1772 CONTACT_ATTRIBUTES.add(GraphField.get("secretarycn"));
1773 CONTACT_ATTRIBUTES.add(GraphField.get("sn"));
1774 CONTACT_ATTRIBUTES.add(GraphField.get("spousecn"));
1775 CONTACT_ATTRIBUTES.add(GraphField.get("st"));
1776 CONTACT_ATTRIBUTES.add(GraphField.get("street"));
1777 CONTACT_ATTRIBUTES.add(GraphField.get("telephoneNumber"));
1778 CONTACT_ATTRIBUTES.add(GraphField.get("title"));
1779 CONTACT_ATTRIBUTES.add(GraphField.get("description"));
1780 CONTACT_ATTRIBUTES.add(GraphField.get("im"));
1781 CONTACT_ATTRIBUTES.add(GraphField.get("middlename"));
1782 CONTACT_ATTRIBUTES.add(GraphField.get("lastmodified"));
1783 CONTACT_ATTRIBUTES.add(GraphField.get("otherstreet"));
1784 CONTACT_ATTRIBUTES.add(GraphField.get("otherstate"));
1785 CONTACT_ATTRIBUTES.add(GraphField.get("otherpostofficebox"));
1786 CONTACT_ATTRIBUTES.add(GraphField.get("otherpostalcode"));
1787 CONTACT_ATTRIBUTES.add(GraphField.get("othercountry"));
1788 CONTACT_ATTRIBUTES.add(GraphField.get("othercity"));
1789 CONTACT_ATTRIBUTES.add(GraphField.get("haspicture"));
1790 CONTACT_ATTRIBUTES.add(GraphField.get("othermobile"));
1791 CONTACT_ATTRIBUTES.add(GraphField.get("otherTelephone"));
1792 CONTACT_ATTRIBUTES.add(GraphField.get("gender"));
1793 CONTACT_ATTRIBUTES.add(GraphField.get("private"));
1794 CONTACT_ATTRIBUTES.add(GraphField.get("sensitivity"));
1795 CONTACT_ATTRIBUTES.add(GraphField.get("fburl"));
1796
1797
1798
1799 }
1800
1801 private static final Set<GraphField> TODO_PROPERTIES = new HashSet<>();
1802
1803 static {
1804
1805 TODO_PROPERTIES.add(GraphField.get("id"));
1806 TODO_PROPERTIES.add(GraphField.get("summary"));
1807 TODO_PROPERTIES.add(GraphField.get("body"));
1808 TODO_PROPERTIES.add(GraphField.get("lastModifiedDateTime"));
1809 TODO_PROPERTIES.add(GraphField.get("createdDateTime"));
1810 TODO_PROPERTIES.add(GraphField.get("importance"));
1811 TODO_PROPERTIES.add(GraphField.get("status"));
1812 TODO_PROPERTIES.add(GraphField.get("dueDateTime"));
1813 TODO_PROPERTIES.add(GraphField.get("startDateTime"));
1814 TODO_PROPERTIES.add(GraphField.get("completedDateTime"));
1815 TODO_PROPERTIES.add(GraphField.get("categories"));
1816 }
1817
1818
1819
1820
1821 protected static final HashSet<GraphField> EVENT_LIST_ATTRIBUTES = new HashSet<>();
1822 protected static final HashSet<GraphField> EVENT_ATTRIBUTES = new HashSet<>();
1823
1824 static {
1825 EVENT_LIST_ATTRIBUTES.add(GraphField.get("id"));
1826 EVENT_LIST_ATTRIBUTES.add(GraphField.get("urlcompname"));
1827 EVENT_LIST_ATTRIBUTES.add(GraphField.get("changeKey"));
1828
1829 EVENT_ATTRIBUTES.add(GraphField.get("urlcompname"));
1830 EVENT_ATTRIBUTES.add(GraphField.get("allowNewTimeProposals"));
1831 EVENT_ATTRIBUTES.add(GraphField.get("attendees"));
1832 EVENT_ATTRIBUTES.add(GraphField.get("bodyPreview"));
1833 EVENT_ATTRIBUTES.add(GraphField.get("body"));
1834 EVENT_ATTRIBUTES.add(GraphField.get("cancelledOccurrences"));
1835 EVENT_ATTRIBUTES.add(GraphField.get("categories"));
1836 EVENT_ATTRIBUTES.add(GraphField.get("changeKey"));
1837 EVENT_ATTRIBUTES.add(GraphField.get("createdDateTime"));
1838 EVENT_ATTRIBUTES.add(GraphField.get("end"));
1839 EVENT_ATTRIBUTES.add(GraphField.get("exceptionOccurrences"));
1840 EVENT_ATTRIBUTES.add(GraphField.get("hasAttachments"));
1841 EVENT_ATTRIBUTES.add(GraphField.get("iCalUId"));
1842 EVENT_ATTRIBUTES.add(GraphField.get("transactionId"));
1843 EVENT_ATTRIBUTES.add(GraphField.get("id"));
1844 EVENT_ATTRIBUTES.add(GraphField.get("importance"));
1845 EVENT_ATTRIBUTES.add(GraphField.get("isAllDay"));
1846 EVENT_ATTRIBUTES.add(GraphField.get("isOnlineMeeting"));
1847 EVENT_ATTRIBUTES.add(GraphField.get("isOrganizer"));
1848 EVENT_ATTRIBUTES.add(GraphField.get("isReminderOn"));
1849 EVENT_ATTRIBUTES.add(GraphField.get("lastModifiedDateTime"));
1850 EVENT_ATTRIBUTES.add(GraphField.get("location"));
1851 EVENT_ATTRIBUTES.add(GraphField.get("organizer"));
1852 EVENT_ATTRIBUTES.add(GraphField.get("originalStartTimeZone"));
1853 EVENT_ATTRIBUTES.add(GraphField.get("originalStart"));
1854 EVENT_ATTRIBUTES.add(GraphField.get("recurrence"));
1855 EVENT_ATTRIBUTES.add(GraphField.get("reminderMinutesBeforeStart"));
1856 EVENT_ATTRIBUTES.add(GraphField.get("responseRequested"));
1857 EVENT_ATTRIBUTES.add(GraphField.get("responseStatus"));
1858 EVENT_ATTRIBUTES.add(GraphField.get("sensitivity"));
1859 EVENT_ATTRIBUTES.add(GraphField.get("showAs"));
1860 EVENT_ATTRIBUTES.add(GraphField.get("start"));
1861 EVENT_ATTRIBUTES.add(GraphField.get("subject"));
1862 EVENT_ATTRIBUTES.add(GraphField.get("type"));
1863
1864 EVENT_ATTRIBUTES.add(GraphField.get("xmozlastack"));
1865 EVENT_ATTRIBUTES.add(GraphField.get("xmozsnoozetime"));
1866 }
1867
1868 protected static class FolderId {
1869 protected static final String IPF_NOTE = "IPF.Note";
1870 protected static final String IPF_CONTACT = "IPF.Contact";
1871 protected static final String IPF_APPOINTMENT = "IPF.Appointment";
1872 protected static final String IPF_TASK = "IPF.Task";
1873
1874
1875 protected String mailbox;
1876 protected String id;
1877 protected String parentFolderId;
1878 protected String folderClass;
1879
1880 public FolderId() {
1881 }
1882
1883 public FolderId(String mailbox, String id) {
1884 this.mailbox = mailbox;
1885 this.id = id;
1886 }
1887
1888 public FolderId(String mailbox, String id, String folderClass) {
1889 this.mailbox = mailbox;
1890 this.id = id;
1891 this.folderClass = folderClass;
1892 }
1893
1894 public FolderId(String mailbox, WellKnownFolderName wellKnownFolderName) {
1895 this.mailbox = mailbox;
1896 this.id = wellKnownFolderName.name();
1897 }
1898
1899 public FolderId(String mailbox, WellKnownFolderName wellKnownFolderName, String folderClass) {
1900 this.mailbox = mailbox;
1901 this.id = wellKnownFolderName.name();
1902 this.folderClass = folderClass;
1903 }
1904
1905 public String getMailboxName() {
1906 if (mailbox == null) {
1907 return "me";
1908 } else {
1909 return mailbox;
1910 }
1911 }
1912
1913 public boolean isMail() {
1914 return IPF_NOTE.equals(folderClass);
1915 }
1916
1917 public boolean isCalendar() {
1918 return IPF_APPOINTMENT.equals(folderClass);
1919 }
1920
1921 public boolean isContact() {
1922 return IPF_CONTACT.equals(folderClass);
1923 }
1924
1925 public boolean isTask() {
1926 return IPF_TASK.equals(folderClass);
1927 }
1928 }
1929
1930 HttpClientAdapter httpClient;
1931 O365Token token;
1932
1933
1934
1935
1936 protected static final HashSet<GraphField> FOLDER_PROPERTIES = new HashSet<>();
1937
1938 static {
1939
1940 FOLDER_PROPERTIES.add(GraphField.get("folderlastmodified"));
1941 FOLDER_PROPERTIES.add(GraphField.get("folderclass"));
1942 FOLDER_PROPERTIES.add(GraphField.get("ctag"));
1943 FOLDER_PROPERTIES.add(GraphField.get("uidNext"));
1944 }
1945
1946 public GraphExchangeSession(HttpClientAdapter httpClient, O365Token token, String userName) throws IOException {
1947 this.httpClient = httpClient;
1948 this.token = token;
1949 this.userName = userName;
1950
1951 buildSessionInfo(httpClient.getUri());
1952 }
1953
1954 @Override
1955 public void close() {
1956 httpClient.close();
1957 }
1958
1959
1960
1961
1962
1963
1964
1965
1966 @Override
1967 public String formatSearchDate(Date date) {
1968 SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
1969 dateFormatter.setTimeZone(GMT_TIMEZONE);
1970 return dateFormatter.format(date);
1971 }
1972
1973 @Override
1974 protected void buildSessionInfo(URI uri) throws IOException {
1975 currentMailboxPath = "/users/" + userName.toLowerCase();
1976
1977
1978 email = userName;
1979 alias = userName.substring(0, email.indexOf("@"));
1980
1981 LOGGER.debug("Current user email is " + email + ", alias is " + alias);
1982 }
1983
1984 @Override
1985 public ExchangeSession.Message createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
1986 byte[] mimeContent = IOUtil.encodeBase64(mimeMessage);
1987
1988
1989
1990 boolean isDraft = properties != null && ("8".equals(properties.get("draft")) || "9".equals(properties.get("draft")));
1991
1992
1993
1994 FolderId folderId = getFolderId(folderPath);
1995
1996
1997 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
1998 .setMethod(HttpPost.METHOD_NAME)
1999 .setContentType("text/plain")
2000 .setMimeContent(mimeContent)
2001 .setChildType("messages"));
2002 if (isDraft) {
2003 try {
2004
2005 applyMessageProperties(graphResponse, properties);
2006 graphResponse = executeGraphRequest(new GraphRequestBuilder()
2007 .setMethod(HttpPatch.METHOD_NAME)
2008 .setMailbox(folderId.mailbox)
2009 .setObjectType("messages")
2010 .setObjectId(graphResponse.optString("id"))
2011 .setJsonBody(graphResponse.jsonObject));
2012
2013 graphResponse = executeGraphRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
2014 .setMailbox(folderId.mailbox)
2015 .setObjectType("messages")
2016 .setObjectId(graphResponse.optString("id"))
2017 .setChildType("move")
2018 .setJsonBody(new JSONObject().put("destinationId", folderId.id)));
2019 } catch (JSONException e) {
2020 throw new IOException(e);
2021 }
2022 } else {
2023 String draftMessageId = null;
2024 try {
2025
2026 draftMessageId = graphResponse.getString("id");
2027
2028
2029 graphResponse.put("messageFlags", "4");
2030
2031 graphResponse.put("read", false);
2032 applyMessageProperties(graphResponse, properties);
2033
2034
2035 graphResponse = executeGraphRequest(new GraphRequestBuilder()
2036 .setMethod(HttpPost.METHOD_NAME)
2037 .setMailbox(folderId.mailbox)
2038 .setObjectType("mailFolders")
2039 .setObjectId(folderId.id)
2040 .setJsonBody(graphResponse.jsonObject)
2041 .setChildType("messages"));
2042
2043 } catch (JSONException e) {
2044 throw new IOException(e);
2045 } finally {
2046
2047 if (draftMessageId != null) {
2048 executeJsonRequest(new GraphRequestBuilder()
2049 .setMethod(HttpDelete.METHOD_NAME)
2050 .setObjectType("messages")
2051 .setObjectId(draftMessageId));
2052 }
2053 }
2054
2055 }
2056 return buildMessage(executeJsonRequest(new GraphRequestBuilder()
2057 .setMethod(HttpGet.METHOD_NAME)
2058 .setObjectType("messages")
2059 .setMailbox(folderId.mailbox)
2060 .setObjectId(graphResponse.optString("id"))
2061 .setSelectFields(IMAP_MESSAGE_ATTRIBUTES)));
2062 }
2063
2064 private void applyMessageProperties(GraphObject graphResponse, Map<String, String> properties) throws JSONException {
2065 if (properties != null) {
2066 for (Map.Entry<String, String> entry : properties.entrySet()) {
2067 if ("read".equals(entry.getKey())) {
2068 graphResponse.put(entry.getKey(), "1".equals(entry.getValue()));
2069 } else if ("junk".equals(entry.getKey())) {
2070 graphResponse.put(entry.getKey(), entry.getValue());
2071 } else if ("flagged".equals(entry.getKey())) {
2072 graphResponse.put("flagStatus", entry.getValue());
2073 } else if ("answered".equals(entry.getKey())) {
2074 graphResponse.put("lastVerbExecuted", entry.getValue());
2075 if ("102".equals(entry.getValue())) {
2076 graphResponse.put("iconIndex", "261");
2077 }
2078 } else if ("forwarded".equals(entry.getKey())) {
2079 graphResponse.put("lastVerbExecuted", entry.getValue());
2080 if ("104".equals(entry.getValue())) {
2081 graphResponse.put("iconIndex", "262");
2082 }
2083 } else if ("deleted".equals(entry.getKey())) {
2084 graphResponse.put(entry.getKey(), entry.getValue());
2085 } else if ("datereceived".equals(entry.getKey())) {
2086 graphResponse.put(entry.getKey(), entry.getValue());
2087 } else if ("keywords".equals(entry.getKey())) {
2088 graphResponse.setCategories(entry.getValue());
2089 }
2090 }
2091 }
2092 }
2093
2094 class Message extends ExchangeSession.Message {
2095 protected FolderId folderId;
2096 protected String id;
2097 protected String changeKey;
2098
2099 @Override
2100 public String getPermanentId() {
2101 return id;
2102 }
2103
2104 @Override
2105 protected InputStream getMimeHeaders() {
2106 InputStream result = null;
2107 try {
2108 HashSet<GraphField> selectFields = new HashSet<>();
2109 selectFields.add(GraphField.get("from"));
2110 selectFields.add(GraphField.get("messageheaders"));
2111
2112 GraphObject graphResponse = new GraphObject(executeJsonRequest(new GraphRequestBuilder()
2113 .setMethod(HttpGet.METHOD_NAME)
2114 .setMailbox(folderId.mailbox)
2115 .setObjectType("messages")
2116 .setObjectId(id)
2117 .setSelectFields(selectFields)));
2118
2119 String messageHeaders = graphResponse.optString("messageheaders");
2120
2121
2122 if (messageHeaders != null
2123
2124 && messageHeaders.toLowerCase().contains("message-id:")) {
2125 String from = graphResponse.optString("from");
2126
2127 if (from != null && !messageHeaders.contains("From:")) {
2128 messageHeaders = "From: " + MimeUtility.encodeText(from, "UTF-8", null) + '\r' + '\n' + messageHeaders;
2129 }
2130
2131 result = new ByteArrayInputStream(messageHeaders.getBytes(StandardCharsets.UTF_8));
2132 }
2133 } catch (Exception e) {
2134 LOGGER.warn(e.getMessage());
2135 }
2136
2137 return result;
2138
2139 }
2140 }
2141
2142 private Message buildMessage(JSONObject response) {
2143 Message message = new Message();
2144 GraphObject graphResponse = new GraphObject(response);
2145
2146 try {
2147
2148 message.id = graphResponse.getString("id");
2149 message.changeKey = graphResponse.getString("changeKey");
2150
2151 message.read = graphResponse.getBoolean("isRead");
2152 message.draft = graphResponse.getBoolean("isDraft");
2153 message.date = graphResponse.getString("receivedDateTime");
2154
2155 String lastmodified = graphResponse.optString("lastModifiedDateTime");
2156 message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date);
2157
2158 message.keywords = graphResponse.optString("keywords");
2159
2160 } catch (JSONException e) {
2161 LOGGER.warn("Error parsing message " + e.getMessage(), e);
2162 }
2163
2164 JSONArray singleValueExtendedProperties = response.optJSONArray("singleValueExtendedProperties");
2165 if (singleValueExtendedProperties != null) {
2166 for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
2167 try {
2168 JSONObject responseValue = singleValueExtendedProperties.getJSONObject(i);
2169 String responseId = responseValue.optString("id");
2170 if (GraphField.getGraphId("imapUid").equals(responseId)) {
2171 message.imapUid = responseValue.getLong("value");
2172 } else if (GraphField.getGraphId("messageSize").equals(responseId)) {
2173 message.size = responseValue.getInt("value");
2174 } else if (GraphField.getGraphId("uid").equals(responseId)) {
2175 message.uid = responseValue.getString("value");
2176 } else if (GraphField.getGraphId("permanenturl").equals(responseId)) {
2177 message.permanentUrl = responseValue.getString("value");
2178 } else if (GraphField.getGraphId("lastVerbExecuted").equals(responseId)) {
2179 String lastVerbExecuted = responseValue.getString("value");
2180 message.answered = "102".equals(lastVerbExecuted) || "103".equals(lastVerbExecuted);
2181 message.forwarded = "104".equals(lastVerbExecuted);
2182 } else if (GraphField.getGraphId("contentclass").equals(responseId)) {
2183 message.contentClass = responseValue.getString("value");
2184 } else if (GraphField.getGraphId("junk").equals(responseId)) {
2185 message.junk = "1".equals(responseValue.getString("value"));
2186 } else if (GraphField.getGraphId("flagStatus").equals(responseId)) {
2187 message.flagged = "2".equals(responseValue.getString("value"));
2188 } else if (GraphField.getGraphId("deleted").equals(responseId)) {
2189 message.deleted = "1".equals(responseValue.getString("value"));
2190 }
2191
2192 } catch (JSONException e) {
2193 LOGGER.warn("Error parsing json response value");
2194 }
2195 }
2196 }
2197
2198 JSONArray multiValueExtendedProperties = response.optJSONArray("multiValueExtendedProperties");
2199 if (multiValueExtendedProperties != null) {
2200 for (int i = 0; i < multiValueExtendedProperties.length(); i++) {
2201 try {
2202 JSONObject responseValue = multiValueExtendedProperties.getJSONObject(i);
2203 String responseId = responseValue.optString("id");
2204 if (GraphField.get("keywords").getGraphId().equals(responseId)) {
2205 JSONArray keywordsJsonArray = responseValue.getJSONArray("value");
2206 HashSet<String> keywords = new HashSet<>();
2207 for (int j = 0; j < keywordsJsonArray.length(); j++) {
2208 keywords.add(keywordsJsonArray.getString(j));
2209 }
2210 message.keywords = StringUtil.join(keywords, ",");
2211 }
2212
2213 } catch (JSONException e) {
2214 LOGGER.warn("Error parsing json response value");
2215 }
2216 }
2217 }
2218
2219 if (LOGGER.isDebugEnabled()) {
2220 StringBuilder buffer = new StringBuilder();
2221 buffer.append("Message");
2222 if (message.imapUid != 0) {
2223 buffer.append(" IMAP uid: ").append(message.imapUid);
2224 }
2225 if (message.uid != null) {
2226 buffer.append(" uid: ").append(message.uid);
2227 }
2228 buffer.append(" ItemId: ").append(message.id);
2229 buffer.append(" ChangeKey: ").append(message.changeKey);
2230 LOGGER.debug(buffer.toString());
2231 }
2232
2233 return message;
2234
2235 }
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245 protected static String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
2246
2247 if (exchangeDateValue == null) {
2248 return null;
2249 } else {
2250 StringBuilder buffer = new StringBuilder();
2251 if (exchangeDateValue.length() >= 21 || exchangeDateValue.length() == 20 || exchangeDateValue.length() == 10) {
2252 for (int i = 0; i < exchangeDateValue.length(); i++) {
2253
2254 if (i == 4 || i == 7 || i == 13 || i == 16) {
2255 i++;
2256 }
2257 if (i == 19) {
2258
2259 if (exchangeDateValue.endsWith("Z")) {
2260 buffer.append('Z');
2261 }
2262 break;
2263 } else {
2264 buffer.append(exchangeDateValue.charAt(i));
2265 }
2266 }
2267 if (exchangeDateValue.length() == 10) {
2268 buffer.append("T000000Z");
2269 }
2270 } else {
2271 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
2272 }
2273 return buffer.toString();
2274 }
2275 }
2276
2277 @Override
2278 public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
2279 try {
2280 GraphObject graphObject = new GraphObject(new JSONObject());
2281
2282 applyMessageProperties(graphObject, properties);
2283 try {
2284 executeJsonRequest(new GraphRequestBuilder()
2285 .setMethod(HttpPatch.METHOD_NAME)
2286 .setMailbox(((Message) message).folderId.mailbox)
2287 .setObjectType("messages")
2288 .setObjectId(((Message) message).id)
2289 .setJsonBody(graphObject.jsonObject));
2290 } catch (HttpPreconditionFailedException e) {
2291 LOGGER.debug("Received HTTP 412 Precondition Failed");
2292
2293
2294 executeJsonRequest(new GraphRequestBuilder()
2295 .setMethod(HttpGet.METHOD_NAME)
2296 .setMailbox(((Message) message).folderId.mailbox)
2297 .setObjectType("messages")
2298 .setObjectId(((Message) message).id)
2299 .setSelect("id"));
2300
2301
2302 executeJsonRequest(new GraphRequestBuilder()
2303 .setMethod(HttpPatch.METHOD_NAME)
2304 .setMailbox(((Message) message).folderId.mailbox)
2305 .setObjectType("messages")
2306 .setObjectId(((Message) message).id)
2307 .setJsonBody(graphObject.jsonObject));
2308
2309 }
2310 } catch (JSONException e) {
2311 throw new IOException(e);
2312 }
2313 }
2314
2315 @Override
2316 public void deleteMessage(ExchangeSession.Message message) throws IOException {
2317 executeJsonRequest(new GraphRequestBuilder()
2318 .setMethod(HttpDelete.METHOD_NAME)
2319 .setMailbox(((Message) message).folderId.mailbox)
2320 .setObjectType("messages")
2321 .setObjectId(((Message) message).id));
2322 }
2323
2324 @Override
2325 protected byte[] getContent(ExchangeSession.Message message) throws IOException {
2326 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder()
2327 .setMethod(HttpGet.METHOD_NAME)
2328 .setMailbox(((Message) message).folderId.mailbox)
2329 .setObjectType("messages")
2330 .setObjectId(message.getPermanentId())
2331 .setChildType("$value")
2332 .setAccessToken(token.getAccessToken());
2333
2334
2335 byte[] mimeContent;
2336 try (
2337 CloseableHttpResponse response = httpClient.execute(graphRequestBuilder.build());
2338 InputStream inputStream = response.getEntity().getContent()
2339 ) {
2340
2341 FilterInputStream filterInputStream = new FilterInputStream(inputStream) {
2342 int totalCount;
2343 int lastLogCount;
2344
2345 @Override
2346 public int read(byte[] buffer, int offset, int length) throws IOException {
2347 int count = super.read(buffer, offset, length);
2348 totalCount += count;
2349 if (totalCount - lastLogCount > 1024 * 128) {
2350 DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), message.getPermanentId()));
2351 DavGatewayTray.switchIcon();
2352 lastLogCount = totalCount;
2353 }
2354
2355
2356
2357 return count;
2358 }
2359 };
2360 if (HttpClientAdapter.isGzipEncoded(response)) {
2361 mimeContent = IOUtil.readFully(new GZIPInputStream(filterInputStream));
2362 } else {
2363 mimeContent = IOUtil.readFully(filterInputStream);
2364 }
2365 }
2366 return mimeContent;
2367 }
2368
2369 @Override
2370 public MessageList searchMessages(String folderName, Set<String> attributes, Condition condition) throws IOException {
2371 MessageList messageList = new MessageList();
2372 FolderId folderId = getFolderId(folderName);
2373
2374 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
2375 .setMethod(HttpGet.METHOD_NAME)
2376 .setMailbox(folderId.mailbox)
2377 .setObjectType("mailFolders")
2378 .setObjectId(folderId.id)
2379 .setChildType("messages")
2380 .setSelectFields(IMAP_MESSAGE_ATTRIBUTES)
2381 .setFilter(condition);
2382 int maxCount = Settings.getIntProperty("davmail.folderSizeLimit", 0);
2383 if (maxCount == 0) {
2384 maxCount = Integer.MAX_VALUE;
2385 }
2386 LOGGER.debug("searchMessages " + folderId.getMailboxName() + " " + folderName);
2387 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
2388
2389 while (graphIterator.hasNext() && messageList.size() < maxCount) {
2390 Message message = buildMessage(graphIterator.next());
2391 message.messageList = messageList;
2392 message.folderId = folderId;
2393 messageList.add(message);
2394 }
2395 Collections.sort(messageList);
2396 return messageList;
2397 }
2398
2399 static class AttributeCondition extends ExchangeSession.AttributeCondition {
2400
2401 protected AttributeCondition(String attributeName, Operator operator, String value) {
2402 super(attributeName, operator, value);
2403 }
2404
2405 protected Operator getOperator() {
2406 return operator;
2407 }
2408
2409 protected GraphField getField() {
2410 GraphField fieldURI = GraphField.get(attributeName);
2411
2412
2413 if (fieldURI == null) {
2414 throw new IllegalArgumentException("Unknown field: " + attributeName);
2415 }
2416 return fieldURI;
2417 }
2418
2419 private String convertOperator(Operator operator) {
2420 if (Operator.IsEqualTo.equals(operator)) {
2421 return "eq";
2422 } else if (Operator.IsGreaterThan.equals(operator)) {
2423 return "gt";
2424 } else if (Operator.IsGreaterThanOrEqualTo.equals(operator)) {
2425 return "ge";
2426 } else if (Operator.IsLessThan.equals(operator)) {
2427 return "lt";
2428 } else if (Operator.IsLessThanOrEqualTo.equals(operator)) {
2429 return "le";
2430 } else {
2431 LOGGER.warn("Unsupported operator: " + operator + ", switch to equals");
2432 return "eq";
2433 }
2434 }
2435
2436 @Override
2437 public void appendTo(StringBuilder buffer) {
2438 GraphField field = getField();
2439 String graphId = field.getGraphId();
2440 if (field.isExtended()) {
2441 if (field.isInternetHeaders()) {
2442
2443 buffer.append("singleValueExtendedProperties/any(ep:ep/id eq 'String 0x007D' and contains(ep/value, '")
2444 .append(attributeName).append(": ").append(StringUtil.escapeQuotes(value)).append("'))");
2445 } else if (field.isNumber()) {
2446
2447 int intValue = 0;
2448 try {
2449 intValue = Integer.parseInt(value);
2450 } catch (NumberFormatException e) {
2451
2452 LOGGER.warn("Invalid integer value for " + graphId + " " + value);
2453 }
2454 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2455 .append("' and cast(ep/value, Edm.Int32) ").append(convertOperator(operator)).append(" ").append(intValue).append(")");
2456 } else if (Operator.Contains.equals(operator)) {
2457 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2458 .append("' and contains(ep/value,'").append(StringUtil.escapeQuotes(value)).append("'))");
2459 } else if (Operator.StartsWith.equals(operator)) {
2460 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2461 .append("' and startswith(ep/value,'").append(StringUtil.escapeQuotes(value)).append("'))");
2462 } else if (field.isBinary()) {
2463 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2464 .append("' and cast(ep/value,Edm.Binary) ").append(convertOperator(operator)).append(" binary'").append(StringUtil.escapeQuotes(value)).append("')");
2465 } else if (field.isDate()) {
2466 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2467 .append("' and cast(ep/value,Edm.DateTimeOffset) ").append(convertOperator(operator)).append(" datetimeoffset'").append(StringUtil.escapeQuotes(value)).append("')");
2468 } else if (field.isBoolean()) {
2469 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2470 .append("' and cast(ep/value,Edm.Boolean) ").append(convertOperator(operator)).append(" ").append(value).append(")");
2471 } else {
2472 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2473 .append("' and ep/value ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("')");
2474 }
2475 } else if (field.isMultiValued()) {
2476 buffer.append(graphId).append("/any(a:a ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("')");
2477 } else if ("body".equals(graphId)) {
2478
2479 buffer.append("contains(").append(graphId).append("/content,'").append(StringUtil.escapeQuotes(value)).append("')");
2480 } else if (Operator.Contains.equals(operator)) {
2481
2482 buffer.append("contains(").append(graphId).append(",'").append(StringUtil.escapeQuotes(value)).append("')");
2483 } else if (Operator.StartsWith.equals(operator)) {
2484 buffer.append("startswith(").append(graphId).append(",'").append(StringUtil.escapeQuotes(value)).append("')");
2485 } else if (field.isDate() || field.isBoolean()) {
2486 buffer.append(graphId).append(" ").append(convertOperator(operator)).append(" ").append(value);
2487 } else if ("start".equals(graphId) || "end".equals(graphId)) {
2488 buffer.append(graphId).append("/dateTime ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("'");
2489 } else {
2490 buffer.append(graphId).append(" ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("'");
2491 }
2492 }
2493
2494 @Override
2495 public boolean isMatch(ExchangeSession.Contact contact) {
2496 return false;
2497 }
2498 }
2499
2500 protected static class HeaderCondition extends AttributeCondition {
2501
2502 protected HeaderCondition(String attributeName, String value) {
2503 super(attributeName, Operator.Contains, value);
2504 }
2505
2506 @Override
2507 protected GraphField getField() {
2508 return new GraphField(attributeName, GraphField.DistinguishedPropertySetType.InternetHeaders, attributeName);
2509 }
2510
2511
2512
2513
2514
2515 public void appendTo(StringBuilder buffer) {
2516 buffer.append("singleValueExtendedProperties/any(ep:ep/id eq 'String 0x007D' and contains(ep/value, '")
2517 .append(attributeName).append(": ").append(StringUtil.escapeQuotes(value)).append("'))");
2518 }
2519 }
2520
2521 protected static class IsNullCondition implements ExchangeSession.Condition, SearchExpression {
2522 protected final String attributeName;
2523
2524 protected IsNullCondition(String attributeName) {
2525 this.attributeName = attributeName;
2526 }
2527
2528 public void appendTo(StringBuilder buffer) {
2529 GraphField graphField = GraphField.get(attributeName);
2530 if (graphField.isExtended()) {
2531 if (graphField.isNumber()) {
2532 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphField.getGraphId())
2533 .append("' and cast(ep/value, Edm.Int32) eq null)");
2534 } else if (graphField.isBoolean()) {
2535 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphField.getGraphId())
2536 .append("' and cast(ep/value, Edm.Boolean) eq null)");
2537 } else {
2538 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphField.getGraphId())
2539 .append("' and ep/value eq null)");
2540 }
2541 } else {
2542 buffer.append(graphField.getGraphId()).append(" eq null");
2543 }
2544 }
2545
2546 public boolean isEmpty() {
2547 return false;
2548 }
2549
2550 public boolean isMatch(ExchangeSession.Contact contact) {
2551 String actualValue = contact.get(attributeName);
2552 return actualValue == null;
2553 }
2554
2555 }
2556
2557 protected static class ExistsCondition implements ExchangeSession.Condition, SearchExpression {
2558 protected final String attributeName;
2559
2560 protected ExistsCondition(String attributeName) {
2561 this.attributeName = attributeName;
2562 }
2563
2564 public void appendTo(StringBuilder buffer) {
2565 buffer.append(GraphField.get(attributeName).getGraphId()).append(" ne null");
2566 }
2567
2568 public boolean isEmpty() {
2569 return false;
2570 }
2571
2572 public boolean isMatch(ExchangeSession.Contact contact) {
2573 String actualValue = contact.get(attributeName);
2574 return actualValue != null;
2575 }
2576
2577 }
2578
2579
2580 static class MultiCondition extends ExchangeSession.MultiCondition {
2581
2582 protected MultiCondition(Operator operator, Condition... conditions) {
2583 super(operator, conditions);
2584 }
2585
2586 @Override
2587 public void appendTo(StringBuilder buffer) {
2588 int actualConditionCount = 0;
2589 for (Condition condition : conditions) {
2590 if (!condition.isEmpty()) {
2591 actualConditionCount++;
2592 }
2593 }
2594 if (actualConditionCount > 0) {
2595 boolean isFirst = true;
2596
2597 for (Condition condition : conditions) {
2598 if (isFirst) {
2599 isFirst = false;
2600
2601 } else {
2602 buffer.append(" ").append(operator.toString()).append(" ");
2603 }
2604 if (condition instanceof MultiCondition) {
2605 buffer.append("(");
2606 condition.appendTo(buffer);
2607 buffer.append(")");
2608 } else {
2609 condition.appendTo(buffer);
2610 }
2611 }
2612 }
2613 }
2614 }
2615
2616 static class NotCondition extends ExchangeSession.NotCondition {
2617
2618 protected NotCondition(Condition condition) {
2619 super(condition);
2620 }
2621
2622 @Override
2623 public void appendTo(StringBuilder buffer) {
2624 buffer.append("not (");
2625 condition.appendTo(buffer);
2626 buffer.append(")");
2627 }
2628 }
2629
2630 @Override
2631 public ExchangeSession.MultiCondition and(Condition... conditions) {
2632 return new MultiCondition(Operator.And, conditions);
2633 }
2634
2635 @Override
2636 public ExchangeSession.MultiCondition or(Condition... conditions) {
2637 return new MultiCondition(Operator.Or, conditions);
2638 }
2639
2640 @Override
2641 public Condition not(Condition condition) {
2642 return new NotCondition(condition);
2643 }
2644
2645 @Override
2646 public Condition isEqualTo(String attributeName, String value) {
2647 return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
2648 }
2649
2650 @Override
2651 public Condition isEqualTo(String attributeName, int value) {
2652 return new AttributeCondition(attributeName, Operator.IsEqualTo, String.valueOf(value));
2653 }
2654
2655 @Override
2656 public Condition headerIsEqualTo(String headerName, String value) {
2657 return new HeaderCondition(headerName, value);
2658 }
2659
2660 @Override
2661 public Condition gte(String attributeName, String value) {
2662 return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value);
2663 }
2664
2665 @Override
2666 public Condition gt(String attributeName, String value) {
2667 return new AttributeCondition(attributeName, Operator.IsGreaterThan, value);
2668 }
2669
2670 @Override
2671 public Condition lt(String attributeName, String value) {
2672 return new AttributeCondition(attributeName, Operator.IsLessThan, value);
2673 }
2674
2675 @Override
2676 public Condition lte(String attributeName, String value) {
2677 return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value);
2678 }
2679
2680 @Override
2681 public Condition contains(String attributeName, String value) {
2682 return new AttributeCondition(attributeName, Operator.Contains, value);
2683 }
2684
2685 @Override
2686 public Condition startsWith(String attributeName, String value) {
2687 return new AttributeCondition(attributeName, Operator.StartsWith, value);
2688 }
2689
2690 @Override
2691 public Condition isNull(String attributeName) {
2692 return new IsNullCondition(attributeName);
2693 }
2694
2695 @Override
2696 public Condition exists(String attributeName) {
2697 return new ExistsCondition(attributeName);
2698 }
2699
2700 @Override
2701 public Condition isTrue(String attributeName) {
2702 return new AttributeCondition(attributeName, Operator.IsEqualTo, "true");
2703 }
2704
2705 @Override
2706 public Condition isFalse(String attributeName) {
2707 return new AttributeCondition(attributeName, Operator.IsEqualTo, "false");
2708 }
2709
2710 @Override
2711 public List<ExchangeSession.Folder> getSubCalendarFolders(String folderName, boolean recursive) throws IOException {
2712 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder();
2713
2714 httpRequestBuilder.setMethod(HttpGet.METHOD_NAME)
2715 .setObjectType("calendars")
2716 .setSelectFields(FOLDER_PROPERTIES);
2717
2718 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
2719 List<ExchangeSession.Folder> folders = new ArrayList<>();
2720 while (graphIterator.hasNext()) {
2721 Folder folder = buildFolder(graphIterator.next());
2722 folder.folderPath = folder.displayName;
2723 if (!folder.isDefaultCalendar) {
2724 folders.add(folder);
2725 }
2726 }
2727 return folders;
2728 }
2729
2730 @Override
2731 public List<ExchangeSession.Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
2732
2733 List<ExchangeSession.Folder> folders = new ArrayList<>();
2734
2735 appendSubFolders(folders, getSubfolderPath(folderPath), getFolderId(folderPath), condition, recursive);
2736 return folders;
2737 }
2738
2739 protected void appendSubFolders(List<ExchangeSession.Folder> folders,
2740 String parentFolderPath, FolderId parentFolderId,
2741 Condition condition, boolean recursive) throws IOException {
2742 LOGGER.debug("appendSubFolders " + (parentFolderId.mailbox != null ? parentFolderId.mailbox : "me") + " " + parentFolderPath);
2743
2744 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
2745 .setMethod(HttpGet.METHOD_NAME)
2746 .setObjectType("mailFolders")
2747 .setMailbox(parentFolderId.mailbox)
2748 .setObjectId(parentFolderId.id)
2749 .setChildType("childFolders")
2750 .setSelectFields(FOLDER_PROPERTIES)
2751 .setFilter(condition);
2752
2753 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
2754
2755 while (graphIterator.hasNext()) {
2756 Folder folder = buildFolder(graphIterator.next());
2757 folder.folderId.mailbox = parentFolderId.mailbox;
2758
2759 if (parentFolderId.id.equals(folder.folderId.parentFolderId)) {
2760 if (!parentFolderPath.isEmpty()) {
2761 if (parentFolderPath.endsWith("/")) {
2762 folder.folderPath = parentFolderPath + folder.displayName;
2763 } else {
2764 folder.folderPath = parentFolderPath + '/' + folder.displayName;
2765 }
2766
2767 } else {
2768 folder.folderPath = folder.displayName;
2769 }
2770 folders.add(folder);
2771 if (recursive && folder.hasChildren) {
2772 appendSubFolders(folders, folder.folderPath, folder.folderId, condition, true);
2773 }
2774 } else {
2775 LOGGER.debug("appendSubFolders skip " + folder.folderId.mailbox + " " + folder.folderId.id + " " + folder.displayName + " not a child of " + parentFolderPath);
2776 }
2777 }
2778
2779 }
2780
2781
2782 @Override
2783 public void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException {
2784
2785 executeJsonRequest(new GraphRequestBuilder()
2786 .setMethod(HttpPost.METHOD_NAME)
2787 .setObjectType("sendMail")
2788 .setContentType("text/plain")
2789 .setMimeContent(IOUtil.encodeBase64(mimeMessage)));
2790 }
2791
2792 public void sendMessage(byte[] byteArray) throws IOException {
2793
2794 executeJsonRequest(new GraphRequestBuilder()
2795 .setMethod(HttpPost.METHOD_NAME)
2796 .setObjectType("sendMail")
2797 .setContentType("text/plain")
2798 .setMimeContent(IOUtil.encodeBase64(byteArray)));
2799 }
2800
2801 @Override
2802 protected Folder internalGetFolder(String folderPath) throws IOException {
2803 FolderId folderId = getFolderId(folderPath);
2804
2805
2806 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
2807 .setMethod(HttpGet.METHOD_NAME)
2808 .setMailbox(folderId.mailbox)
2809 .setObjectId(folderId.id);
2810 if (folderId.isCalendar()) {
2811 httpRequestBuilder
2812 .setSelectFields(FOLDER_PROPERTIES)
2813 .setObjectType("calendars");
2814 } else if (folderId.isTask()) {
2815 httpRequestBuilder.setObjectType("todo/lists");
2816 } else if (folderId.isContact()) {
2817 httpRequestBuilder
2818 .setSelectFields(FOLDER_PROPERTIES)
2819 .setObjectType("contactFolders");
2820 } else {
2821 httpRequestBuilder
2822 .setSelectFields(FOLDER_PROPERTIES)
2823 .setObjectType("mailFolders");
2824 }
2825
2826 JSONObject jsonResponse = executeJsonRequest(httpRequestBuilder);
2827
2828 Folder folder = buildFolder(jsonResponse);
2829 folder.folderPath = folderPath;
2830
2831 return folder;
2832 }
2833
2834 private Folder buildFolder(JSONObject jsonResponse) throws IOException {
2835 try {
2836 Folder folder = new Folder();
2837 folder.folderId = new FolderId();
2838 folder.folderId.id = jsonResponse.getString("id");
2839 folder.folderId.parentFolderId = jsonResponse.optString("parentFolderId", null);
2840 if (folder.folderId.parentFolderId == null) {
2841
2842 folder.displayName = StringUtil.encodeFolderName(jsonResponse.optString("name"));
2843 folder.isDefaultCalendar = jsonResponse.optBoolean("isDefaultCalendar");
2844 } else {
2845 String wellKnownName = wellKnownFolderMap.get(jsonResponse.optString("wellKnownName"));
2846 if (ExchangeSession.INBOX.equals(wellKnownName)) {
2847 folder.displayName = wellKnownName;
2848 } else {
2849 if (wellKnownName != null) {
2850 folder.setSpecialFlag(wellKnownName);
2851 }
2852
2853 folder.displayName = StringUtil.encodeFolderName(jsonResponse.getString("displayName"));
2854 }
2855
2856 folder.messageCount = jsonResponse.optInt("totalItemCount");
2857 folder.unreadCount = jsonResponse.optInt("unreadItemCount");
2858
2859 folder.recent = folder.unreadCount;
2860
2861 folder.hasChildren = jsonResponse.optInt("childFolderCount") > 0;
2862 }
2863
2864
2865 JSONArray singleValueExtendedProperties = jsonResponse.optJSONArray("singleValueExtendedProperties");
2866 if (singleValueExtendedProperties != null) {
2867 for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
2868 JSONObject singleValueProperty = singleValueExtendedProperties.getJSONObject(i);
2869 String singleValueId = singleValueProperty.getString("id");
2870 String singleValue = singleValueProperty.getString("value");
2871 if (GraphField.get("folderlastmodified").getGraphId().equals(singleValueId)) {
2872 folder.etag = singleValue;
2873 } else if (GraphField.get("folderclass").getGraphId().equals(singleValueId)) {
2874 folder.folderClass = singleValue;
2875 folder.folderId.folderClass = folder.folderClass;
2876 } else if (GraphField.get("uidNext").getGraphId().equals(singleValueId)) {
2877 folder.uidNext = Long.parseLong(singleValue);
2878 } else if (GraphField.get("ctag").getGraphId().equals(singleValueId)) {
2879 folder.ctag = singleValue;
2880 }
2881
2882 }
2883 }
2884
2885 return folder;
2886 } catch (JSONException e) {
2887 throw new IOException(e.getMessage(), e);
2888 }
2889 }
2890
2891
2892
2893
2894
2895
2896 private FolderId getFolderId(String folderPath) throws IOException {
2897 FolderId folderId = getFolderIdIfExists(folderPath);
2898 if (folderId == null) {
2899 throw new HttpNotFoundException("Folder '" + folderPath + "' not found");
2900 }
2901 return folderId;
2902 }
2903
2904 protected static final String USERS_ROOT = "/users/";
2905 protected static final String ARCHIVE_ROOT = "/archive/";
2906
2907
2908 private FolderId getFolderIdIfExists(String folderPath) throws IOException {
2909 String lowerCaseFolderPath = folderPath.toLowerCase();
2910 if (lowerCaseFolderPath.equals(currentMailboxPath)) {
2911 return getSubFolderIdIfExists(null, "");
2912 } else if (lowerCaseFolderPath.startsWith(currentMailboxPath + '/')) {
2913 return getSubFolderIdIfExists(null, folderPath.substring(currentMailboxPath.length() + 1));
2914 } else if (folderPath.startsWith(USERS_ROOT)) {
2915 int slashIndex = folderPath.indexOf('/', USERS_ROOT.length());
2916 String mailbox;
2917 String subFolderPath;
2918 if (slashIndex >= 0) {
2919 mailbox = folderPath.substring(USERS_ROOT.length(), slashIndex);
2920 subFolderPath = folderPath.substring(slashIndex + 1);
2921 } else {
2922 mailbox = folderPath.substring(USERS_ROOT.length());
2923 subFolderPath = "";
2924 }
2925 return getSubFolderIdIfExists(mailbox, subFolderPath);
2926 } else {
2927 return getSubFolderIdIfExists(null, folderPath);
2928 }
2929 }
2930
2931 private FolderId getSubFolderIdIfExists(String mailbox, String folderPath) throws IOException {
2932 String[] folderNames;
2933 FolderId currentFolderId;
2934
2935
2936 if ("/public".equals(folderPath)) {
2937 throw new UnsupportedOperationException("public folders not supported on Graph");
2938 } else if ("/archive".equals(folderPath)) {
2939 return getWellKnownFolderId(mailbox, WellKnownFolderName.archive);
2940 } else if (isSubFolderOf(folderPath, PUBLIC_ROOT)) {
2941 throw new UnsupportedOperationException("public folders not supported on Graph");
2942 } else if (isSubFolderOf(folderPath, ARCHIVE_ROOT)) {
2943 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.archive);
2944 folderNames = folderPath.substring(ARCHIVE_ROOT.length()).split("/");
2945 } else if (isSubFolderOf(folderPath, INBOX) ||
2946 isSubFolderOf(folderPath, LOWER_CASE_INBOX) ||
2947 isSubFolderOf(folderPath, MIXED_CASE_INBOX)) {
2948 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.inbox);
2949 folderNames = folderPath.substring(INBOX.length()).split("/");
2950 } else if (isSubFolderOf(folderPath, CALENDAR)) {
2951 currentFolderId = new FolderId(mailbox, WellKnownFolderName.calendar, FolderId.IPF_APPOINTMENT);
2952
2953 folderNames = folderPath.substring(CALENDAR.length()).split("/");
2954 } else if (isSubFolderOf(folderPath, TASKS)) {
2955 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.tasks);
2956 folderNames = folderPath.substring(TASKS.length()).split("/");
2957 } else if (isSubFolderOf(folderPath, CONTACTS)) {
2958 currentFolderId = new FolderId(mailbox, WellKnownFolderName.contacts, FolderId.IPF_CONTACT);
2959 folderNames = folderPath.substring(CONTACTS.length()).split("/");
2960 } else if (isSubFolderOf(folderPath, SENT)) {
2961 currentFolderId = new FolderId(mailbox, WellKnownFolderName.sentitems);
2962 folderNames = folderPath.substring(SENT.length()).split("/");
2963 } else if (isSubFolderOf(folderPath, DRAFTS)) {
2964 currentFolderId = new FolderId(mailbox, WellKnownFolderName.drafts);
2965 folderNames = folderPath.substring(DRAFTS.length()).split("/");
2966 } else if (isSubFolderOf(folderPath, TRASH)) {
2967 currentFolderId = new FolderId(mailbox, WellKnownFolderName.deleteditems);
2968 folderNames = folderPath.substring(TRASH.length()).split("/");
2969 } else if (isSubFolderOf(folderPath, JUNK)) {
2970 currentFolderId = new FolderId(mailbox, WellKnownFolderName.junkemail);
2971 folderNames = folderPath.substring(JUNK.length()).split("/");
2972 } else if (isSubFolderOf(folderPath, UNSENT)) {
2973 currentFolderId = new FolderId(mailbox, WellKnownFolderName.outbox);
2974 folderNames = folderPath.substring(UNSENT.length()).split("/");
2975 } else {
2976 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.msgfolderroot);
2977 folderNames = folderPath.split("/");
2978 }
2979 String folderClass = currentFolderId.folderClass;
2980 for (String folderName : folderNames) {
2981 if (!folderName.isEmpty()) {
2982 currentFolderId = getSubFolderByName(currentFolderId, folderName);
2983 if (currentFolderId == null) {
2984 break;
2985 }
2986 currentFolderId.folderClass = folderClass;
2987 }
2988 }
2989 return currentFolderId;
2990 }
2991
2992 protected HashMap<String, FolderId> folderIdCache = new HashMap<>();
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002 private FolderId getWellKnownFolderId(String mailbox, WellKnownFolderName wellKnownFolderName) throws IOException {
3003 FolderId wellKnownFolderId = null;
3004 if (mailbox == null && folderIdCache.containsKey(wellKnownFolderName.name())) {
3005
3006 wellKnownFolderId = folderIdCache.get(wellKnownFolderName.name());
3007 } else if (wellKnownFolderName == WellKnownFolderName.tasks) {
3008
3009 GraphIterator graphIterator = executeSearchRequest(new GraphRequestBuilder()
3010 .setMethod(HttpGet.METHOD_NAME)
3011 .setMailbox(mailbox)
3012 .setObjectType("todo/lists"));
3013 while (graphIterator.hasNext()) {
3014 JSONObject jsonResponse = graphIterator.next();
3015 if (jsonResponse.optString("wellknownListName").equals("defaultList")) {
3016 wellKnownFolderId = new FolderId(mailbox, jsonResponse.optString("id"), FolderId.IPF_TASK);
3017 }
3018 }
3019
3020 if (wellKnownFolderId == null) {
3021 throw new HttpNotFoundException("Folder '" + wellKnownFolderName.name() + "' not found");
3022 }
3023
3024 } else {
3025 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
3026 .setMethod(HttpGet.METHOD_NAME)
3027 .setMailbox(mailbox)
3028 .setObjectType("mailFolders")
3029 .setObjectId(wellKnownFolderName.name())
3030 .setSelect("id"));
3031 String id = jsonResponse.optString("id");
3032 if (id == null) {
3033 LOGGER.warn("Missing id on folder '" + wellKnownFolderName.name() + "'");
3034
3035 id = wellKnownFolderName.name();
3036 }
3037 wellKnownFolderId = new FolderId(mailbox, id, FolderId.IPF_NOTE);
3038 }
3039
3040 if (mailbox == null && !folderIdCache.containsKey(wellKnownFolderName.name())) {
3041 folderIdCache.put(wellKnownFolderName.name(), wellKnownFolderId);
3042 }
3043
3044 return wellKnownFolderId;
3045 }
3046
3047
3048
3049
3050
3051
3052
3053
3054 protected FolderId getSubFolderByName(FolderId currentFolderId, String folderName) throws IOException {
3055 LOGGER.debug("getSubFolderByName " + currentFolderId.id + " " + folderName);
3056 GraphRequestBuilder httpRequestBuilder;
3057 if (currentFolderId.isCalendar()) {
3058 httpRequestBuilder = new GraphRequestBuilder()
3059 .setMethod(HttpGet.METHOD_NAME)
3060 .setMailbox(currentFolderId.mailbox)
3061 .setObjectType("calendars")
3062 .setSelect("id")
3063 .setFilter("name eq '" + StringUtil.escapeQuotes(StringUtil.decodeFolderName(folderName)) + "'");
3064 } else if (currentFolderId.isTask()) {
3065 httpRequestBuilder = new GraphRequestBuilder()
3066 .setMethod(HttpGet.METHOD_NAME)
3067 .setMailbox(currentFolderId.mailbox)
3068 .setObjectType("todo/lists")
3069 .setSelect("id")
3070 .setFilter("displayName eq '" + StringUtil.escapeQuotes(StringUtil.decodeFolderName(folderName)) + "'");
3071 } else {
3072 String objectType = "mailFolders";
3073 if (currentFolderId.isContact()) {
3074 objectType = "contactFolders";
3075 }
3076 httpRequestBuilder = new GraphRequestBuilder()
3077 .setMethod(HttpGet.METHOD_NAME)
3078 .setMailbox(currentFolderId.mailbox)
3079 .setObjectType(objectType)
3080 .setObjectId(currentFolderId.id)
3081 .setChildType("childFolders")
3082 .setSelect("id")
3083 .setFilter("displayName eq '" + StringUtil.escapeQuotes(StringUtil.decodeFolderName(folderName)) + "'");
3084 }
3085
3086 JSONObject jsonResponse = executeJsonRequest(httpRequestBuilder);
3087
3088 FolderId folderId = null;
3089 try {
3090 JSONArray values = jsonResponse.getJSONArray("value");
3091 if (values.length() > 0) {
3092 folderId = new FolderId(currentFolderId.mailbox, values.getJSONObject(0).getString("id"), currentFolderId.folderClass);
3093 folderId.parentFolderId = currentFolderId.id;
3094 }
3095 } catch (JSONException e) {
3096 throw new IOException(e.getMessage(), e);
3097 }
3098
3099 return folderId;
3100 }
3101
3102 private boolean isSubFolderOf(String folderPath, String baseFolder) {
3103 if (PUBLIC_ROOT.equals(baseFolder) || ARCHIVE_ROOT.equals(baseFolder)) {
3104 return folderPath.startsWith(baseFolder);
3105 } else {
3106 return folderPath.startsWith(baseFolder)
3107 && (folderPath.length() == baseFolder.length() || folderPath.charAt(baseFolder.length()) == '/');
3108 }
3109 }
3110
3111 @Override
3112 public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException {
3113 if (FolderId.IPF_APPOINTMENT.equals(folderClass) && folderPath.startsWith("calendar/")) {
3114
3115 String calendarName = folderPath.substring(folderPath.indexOf('/') + 1);
3116
3117 try {
3118 executeJsonRequest(new GraphRequestBuilder()
3119 .setMethod(HttpPost.METHOD_NAME)
3120
3121
3122 .setObjectType("calendars")
3123 .setJsonBody(new JSONObject().put("name", calendarName)));
3124
3125 } catch (JSONException e) {
3126 throw new IOException(e);
3127 }
3128 } else {
3129 FolderId parentFolderId;
3130 String folderName;
3131 if (folderPath.contains("/")) {
3132 String parentFolderPath = folderPath.substring(0, folderPath.lastIndexOf('/'));
3133 parentFolderId = getFolderId(parentFolderPath);
3134 folderName = StringUtil.decodeFolderName(folderPath.substring(folderPath.lastIndexOf('/') + 1));
3135 } else {
3136 parentFolderId = getFolderId("");
3137 folderName = StringUtil.decodeFolderName(folderPath);
3138 }
3139
3140 try {
3141 String objectType = "mailFolders";
3142 if (FolderId.IPF_CONTACT.equals(folderClass)) {
3143 objectType = "contactFolders";
3144 }
3145 executeJsonRequest(new GraphRequestBuilder()
3146 .setMethod(HttpPost.METHOD_NAME)
3147 .setMailbox(parentFolderId.mailbox)
3148 .setObjectType(objectType)
3149 .setObjectId(parentFolderId.id)
3150 .setChildType("childFolders")
3151 .setJsonBody(new JSONObject().put("displayName", folderName)));
3152
3153 } catch (JSONException e) {
3154 throw new IOException(e);
3155 }
3156 }
3157
3158 return HttpStatus.SC_CREATED;
3159
3160 }
3161
3162 @Override
3163 public int updateFolder(String folderName, Map<String, String> properties) throws IOException {
3164 return 0;
3165 }
3166
3167 @Override
3168 public void deleteFolder(String folderPath) throws IOException {
3169 FolderId folderId = getFolderIdIfExists(folderPath);
3170 if (folderPath.startsWith("calendar/")) {
3171
3172 if (folderId != null) {
3173 executeJsonRequest(new GraphRequestBuilder()
3174 .setMethod(HttpDelete.METHOD_NAME)
3175
3176 .setObjectType("calendars")
3177 .setObjectId(folderId.id));
3178 }
3179 } else {
3180 if (folderId != null) {
3181 String objectType = "mailFolders";
3182 if (folderId.isContact()) {
3183 objectType = "contactFolders";
3184 }
3185 executeJsonRequest(new GraphRequestBuilder()
3186 .setMethod(HttpDelete.METHOD_NAME)
3187 .setMailbox(folderId.mailbox)
3188 .setObjectType(objectType)
3189 .setObjectId(folderId.id));
3190 }
3191 }
3192
3193 }
3194
3195 @Override
3196 public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
3197 try {
3198 FolderId targetFolderId = getFolderId(targetFolder);
3199
3200 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
3201 .setMailbox(((Message) message).folderId.mailbox)
3202 .setObjectType("messages")
3203 .setObjectId(((Message) message).id)
3204 .setChildType("copy")
3205 .setJsonBody(new JSONObject().put("destinationId", targetFolderId.id)));
3206
3207 } catch (JSONException e) {
3208 throw new IOException(e);
3209 }
3210 }
3211
3212 @Override
3213 public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
3214 try {
3215 FolderId targetFolderId = getFolderId(targetFolder);
3216
3217 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
3218 .setMailbox(((Message) message).folderId.mailbox)
3219 .setObjectType("messages")
3220 .setObjectId(((Message) message).id)
3221 .setChildType("move")
3222 .setJsonBody(new JSONObject().put("destinationId", targetFolderId.id)));
3223 } catch (JSONException e) {
3224 throw new IOException(e);
3225 }
3226 }
3227
3228 @Override
3229 public void moveFolder(String folderPath, String targetFolderPath) throws IOException {
3230 FolderId folderId = getFolderId(folderPath);
3231 String targetFolderName;
3232 String targetFolderParentPath;
3233 if (targetFolderPath.contains("/")) {
3234 targetFolderParentPath = targetFolderPath.substring(0, targetFolderPath.lastIndexOf('/'));
3235 targetFolderName = StringUtil.decodeFolderName(targetFolderPath.substring(targetFolderPath.lastIndexOf('/') + 1));
3236 } else {
3237 targetFolderParentPath = "";
3238 targetFolderName = StringUtil.decodeFolderName(targetFolderPath);
3239 }
3240 FolderId targetFolderId = getFolderId(targetFolderParentPath);
3241
3242
3243 try {
3244 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPatch.METHOD_NAME)
3245 .setMailbox(folderId.mailbox)
3246 .setObjectType("mailFolders")
3247 .setObjectId(folderId.id)
3248 .setJsonBody(new JSONObject().put("displayName", targetFolderName)));
3249 } catch (JSONException e) {
3250 throw new IOException(e);
3251 }
3252
3253 try {
3254 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
3255 .setMailbox(folderId.mailbox)
3256 .setObjectType("mailFolders")
3257 .setObjectId(folderId.id)
3258 .setChildType("move")
3259 .setJsonBody(new JSONObject().put("destinationId", targetFolderId.id)));
3260 } catch (JSONException e) {
3261 throw new IOException(e);
3262 }
3263 }
3264
3265 @Override
3266 public void moveItem(String sourcePath, String targetPath) throws IOException {
3267
3268 }
3269
3270 @Override
3271 protected void moveToTrash(ExchangeSession.Message message) throws IOException {
3272 moveMessage(message, WellKnownFolderName.deleteditems.name());
3273 }
3274
3275
3276
3277
3278
3279 protected static final Set<String> ITEM_PROPERTIES = new HashSet<>();
3280
3281 protected static final HashSet<String> EVENT_REQUEST_PROPERTIES = new HashSet<>();
3282
3283 static {
3284 EVENT_REQUEST_PROPERTIES.add("permanenturl");
3285 EVENT_REQUEST_PROPERTIES.add("etag");
3286 EVENT_REQUEST_PROPERTIES.add("displayname");
3287 EVENT_REQUEST_PROPERTIES.add("subject");
3288 EVENT_REQUEST_PROPERTIES.add("urlcompname");
3289 EVENT_REQUEST_PROPERTIES.add("displayto");
3290 EVENT_REQUEST_PROPERTIES.add("displaycc");
3291
3292 EVENT_REQUEST_PROPERTIES.add("xmozlastack");
3293 EVENT_REQUEST_PROPERTIES.add("xmozsnoozetime");
3294 }
3295
3296 protected static final HashSet<String> CALENDAR_ITEM_REQUEST_PROPERTIES = new HashSet<>();
3297
3298 static {
3299 CALENDAR_ITEM_REQUEST_PROPERTIES.addAll(EVENT_REQUEST_PROPERTIES);
3300 CALENDAR_ITEM_REQUEST_PROPERTIES.add("ismeeting");
3301 CALENDAR_ITEM_REQUEST_PROPERTIES.add("myresponsetype");
3302 }
3303
3304 @Override
3305 protected Set<String> getItemProperties() {
3306 return ITEM_PROPERTIES;
3307 }
3308
3309 @Override
3310 public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
3311 ArrayList<ExchangeSession.Contact> contactList = new ArrayList<>();
3312 FolderId folderId = getFolderId(folderPath);
3313
3314 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3315 .setMethod(HttpGet.METHOD_NAME)
3316 .setMailbox(folderId.mailbox)
3317 .setObjectType("contactFolders")
3318 .setObjectId(folderId.id)
3319 .setChildType("contacts")
3320 .setSelectFields(CONTACT_ATTRIBUTES)
3321 .setFilter(condition);
3322 LOGGER.debug("searchContacts " + folderId.getMailboxName() + "/" + folderPath + " " + httpRequestBuilder.select);
3323
3324 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3325
3326 while (graphIterator.hasNext() && (maxCount == 0 || contactList.size() < maxCount)) {
3327 Contact contact = new Contact(new GraphObject(graphIterator.next()));
3328 contact.folderPath = folderPath;
3329 contact.folderId = folderId;
3330 contactList.add(contact);
3331 }
3332
3333 return contactList;
3334 }
3335
3336 @Override
3337 public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
3338 return searchEvents(folderPath, ITEM_PROPERTIES,
3339 and(startsWith("outlookmessageclass", "IPM.Schedule.Meeting."),
3340 or(isNull("processed"), isFalse("processed"))));
3341 }
3342
3343 @Override
3344 protected Condition getCalendarItemCondition(Condition dateCondition) {
3345 return or(isTrue("isrecurring"),
3346 and(isFalse("isrecurring"), dateCondition));
3347 }
3348
3349
3350
3351
3352
3353
3354
3355 @Override
3356 public List<ExchangeSession.Event> searchTasksOnly(String folderPath) throws IOException {
3357 ArrayList<ExchangeSession.Event> eventList = new ArrayList<>();
3358 FolderId folderId = getFolderId(folderPath);
3359
3360
3361 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3362 .setMethod(HttpGet.METHOD_NAME)
3363 .setMailbox(folderId.mailbox)
3364 .setObjectType("todo/lists")
3365 .setObjectId(folderId.id)
3366 .setChildType("tasks")
3367
3368 ;
3369 LOGGER.debug("searchTasksOnly " + folderId.getMailboxName() + " " + folderPath);
3370
3371 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3372
3373 while (graphIterator.hasNext()) {
3374 Event event = new Event(folderPath, folderId, new GraphObject(graphIterator.next()));
3375 eventList.add(event);
3376 }
3377
3378 return eventList;
3379 }
3380
3381 @Override
3382 public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
3383 ArrayList<ExchangeSession.Event> eventList = new ArrayList<>();
3384 FolderId folderId = getFolderId(folderPath);
3385
3386 if (folderId.isCalendar()) {
3387
3388 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3389 .setMethod(HttpGet.METHOD_NAME)
3390 .setMailbox(folderId.mailbox)
3391 .setObjectType("calendars")
3392 .setObjectId(folderId.id)
3393 .setChildType("events")
3394 .setSelectFields(EVENT_LIST_ATTRIBUTES)
3395 .setTimezone(getVTimezone().getPropertyValue("TZID"))
3396 .setFilter(condition);
3397 LOGGER.debug("searchEvents " + folderId.getMailboxName() + " " + folderPath);
3398
3399 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3400
3401 while (graphIterator.hasNext()) {
3402 Event event = new Event(folderPath, folderId, new GraphObject(graphIterator.next()));
3403 eventList.add(event);
3404 }
3405 } else {
3406
3407 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3408 .setMethod(HttpGet.METHOD_NAME)
3409 .setMailbox(folderId.mailbox)
3410 .setObjectType("mailFolders")
3411 .setObjectId(folderId.id)
3412 .setChildType("messages")
3413 .setSelectFields(IMAP_MESSAGE_ATTRIBUTES)
3414 .setFilter(condition);
3415 LOGGER.debug("searchEventMessages " + folderId.getMailboxName() + " " + folderPath);
3416
3417 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3418
3419 while (graphIterator.hasNext()) {
3420 JSONObject jsonResponse = graphIterator.next();
3421 GraphExchangeSession.Message message = buildMessage(jsonResponse);
3422 message.folderId = folderId;
3423 LOGGER.debug("searchEventMessages " + message.contentClass + " " + message.id);
3424 try {
3425 byte[] content = getContent(message);
3426 if (content == null) {
3427 throw new IOException("empty event body");
3428 }
3429 content = getICS(new SharedByteArrayInputStream(content));
3430
3431 Event event = new Event(folderId, content);
3432 eventList.add(event);
3433
3434 } catch (IOException | MessagingException e) {
3435 LOGGER.warn("searchEventMessages " + message.id, e);
3436 }
3437 }
3438 }
3439
3440 return eventList;
3441
3442 }
3443
3444
3445
3446 protected static final String TEXT_CALENDAR = "text/calendar";
3447 protected static final String APPLICATION_ICS = "application/ics";
3448
3449 protected boolean isCalendarContentType(String contentType) {
3450 return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
3451 APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
3452 }
3453
3454 protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
3455 MimePart bodyPart = null;
3456 for (int i = 0; i < multiPart.getCount(); i++) {
3457 String contentType = multiPart.getBodyPart(i).getContentType();
3458 if (isCalendarContentType(contentType)) {
3459 bodyPart = (MimePart) multiPart.getBodyPart(i);
3460 break;
3461 } else if (contentType.startsWith("multipart")) {
3462 Object content = multiPart.getBodyPart(i).getContent();
3463 if (content instanceof MimeMultipart) {
3464 bodyPart = getCalendarMimePart((MimeMultipart) content);
3465 }
3466 }
3467 }
3468
3469 return bodyPart;
3470 }
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480 protected byte[] getICS(InputStream mimeInputStream) throws IOException, MessagingException {
3481 byte[] result;
3482 MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
3483 String[] contentClassHeader = mimeMessage.getHeader("Content-class");
3484
3485 if (contentClassHeader != null && contentClassHeader.length > 0 && "urn:content-classes:task".equals(contentClassHeader[0])) {
3486 return null;
3487 }
3488 Object mimeBody = mimeMessage.getContent();
3489 MimePart bodyPart = null;
3490 if (mimeBody instanceof MimeMultipart) {
3491 bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
3492 } else if (isCalendarContentType(mimeMessage.getContentType())) {
3493
3494 bodyPart = mimeMessage;
3495 }
3496
3497
3498 if (bodyPart != null) {
3499 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
3500 bodyPart.getDataHandler().writeTo(baos);
3501 result = baos.toByteArray();
3502 }
3503 } else {
3504 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
3505 mimeMessage.writeTo(baos);
3506 throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), StandardCharsets.UTF_8));
3507 }
3508 }
3509 return result;
3510 }
3511
3512 @Override
3513 public Item getItem(String folderPath, String itemName) throws IOException {
3514 FolderId folderId = getFolderId(folderPath);
3515
3516 if (folderId.isContact()) {
3517 JSONObject jsonResponse = getContactIfExists(folderId, itemName);
3518 if (jsonResponse != null) {
3519 Contact contact = new Contact(new GraphObject(jsonResponse));
3520 contact.folderPath = folderPath;
3521 contact.folderId = folderId;
3522 return contact;
3523 } else {
3524 throw new IOException("Item " + folderPath + " " + itemName + " not found");
3525 }
3526 } else if (folderId.isCalendar()) {
3527 JSONObject jsonResponse = getEventIfExists(folderId, itemName);
3528 if (jsonResponse != null) {
3529 return new Event(folderPath, folderId, new GraphObject(jsonResponse));
3530 } else {
3531 throw new IOException("Item " + folderPath + " " + itemName + " not found");
3532 }
3533 } else {
3534 throw new UnsupportedOperationException("Item type " + folderId.folderClass + " not supported");
3535 }
3536 }
3537
3538 @Override
3539 protected String convertItemNameToEML(String itemName) {
3540 if (itemName.endsWith(".vcf") || itemName.endsWith(".ics")) {
3541 return itemName.substring(0, itemName.length() - 3) + "EML";
3542 } else {
3543 return itemName;
3544 }
3545 }
3546
3547 protected String convertItemNameToItemId(String itemName) {
3548 return itemName.substring(0, itemName.length() - 4);
3549 }
3550
3551
3552 private JSONObject getEventIfExists(FolderId folderId, String itemName) throws IOException {
3553 String urlcompname = convertItemNameToEML(itemName);
3554 String itemId = null;
3555 if (isItemId(urlcompname)) {
3556 itemId = convertItemNameToItemId(urlcompname);
3557 } else {
3558
3559 try {
3560 if (urlcompnameToIdMap.containsKey(urlcompname)) {
3561
3562 itemId = urlcompnameToIdMap.get(urlcompname);
3563 } else if (folderId.isCalendar()) {
3564 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
3565 .setMethod(HttpGet.METHOD_NAME)
3566 .setMailbox(folderId.mailbox)
3567 .setObjectType("calendars")
3568 .setObjectId(folderId.id)
3569 .setChildType("events")
3570 .setFilter(isEqualTo("urlcompname", urlcompname))
3571 .setSelect("id")
3572 );
3573
3574 JSONArray values = jsonResponse.optJSONArray("value");
3575 if (values != null && values.length() > 0) {
3576 if (LOGGER.isDebugEnabled()) {
3577 LOGGER.debug("Found event " + values.optJSONObject(0));
3578 }
3579 itemId = values.optJSONObject(0).optString("id");
3580 }
3581 }
3582
3583 } catch (HttpNotFoundException e) {
3584 LOGGER.debug("No event found for urlcompname " + urlcompname);
3585 }
3586 }
3587
3588 if (itemId != null) {
3589 try {
3590 return executeJsonRequest(new GraphRequestBuilder()
3591 .setMethod(HttpGet.METHOD_NAME)
3592 .setMailbox(folderId.mailbox)
3593 .setObjectType("events")
3594 .setObjectId(itemId)
3595 .setSelectFields(EVENT_ATTRIBUTES)
3596 .setTimezone(getTimezoneId())
3597 );
3598 } catch (HttpNotFoundException e) {
3599
3600 FolderId taskFolderId = getFolderId(TASKS);
3601 try {
3602 return executeJsonRequest(new GraphRequestBuilder()
3603 .setMethod(HttpGet.METHOD_NAME)
3604 .setMailbox(folderId.mailbox)
3605 .setObjectType("todo/lists")
3606 .setObjectId(taskFolderId.id)
3607 .setChildType("tasks")
3608 .setChildId(itemId)
3609
3610 ).put("objecttype", FolderId.IPF_TASK);
3611 } catch (JSONException jsonException) {
3612 throw new IOException(jsonException.getMessage(), jsonException);
3613 }
3614 }
3615 }
3616 return null;
3617 }
3618
3619 private JSONObject getContactIfExists(FolderId folderId, String itemName) throws IOException {
3620 String urlcompname = convertItemNameToEML(itemName);
3621 if (isItemId(urlcompname)) {
3622
3623 return executeJsonRequest(new GraphRequestBuilder()
3624 .setMethod(HttpGet.METHOD_NAME)
3625 .setMailbox(folderId.mailbox)
3626 .setObjectType("contactFolders")
3627 .setObjectId(folderId.id)
3628 .setChildType("contacts")
3629 .setChildId(convertItemNameToItemId(itemName))
3630 .setSelectFields(CONTACT_ATTRIBUTES)
3631 );
3632
3633 } else {
3634 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
3635 .setMethod(HttpGet.METHOD_NAME)
3636 .setMailbox(folderId.mailbox)
3637 .setObjectType("contactFolders")
3638 .setObjectId(folderId.id)
3639 .setChildType("contacts")
3640 .setFilter(isEqualTo("urlcompname", urlcompname))
3641 .setSelectFields(CONTACT_ATTRIBUTES)
3642 );
3643
3644 JSONArray values = jsonResponse.optJSONArray("value");
3645 if (values != null && values.length() > 0) {
3646 if (LOGGER.isDebugEnabled()) {
3647 LOGGER.debug("Found contact " + values.optJSONObject(0));
3648 }
3649 return values.optJSONObject(0);
3650 }
3651 }
3652 return null;
3653 }
3654
3655 @Override
3656 public ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
3657
3658 if ("false".equals(contact.get("haspicture"))) {
3659 return null;
3660 }
3661 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder()
3662 .setMethod(HttpGet.METHOD_NAME)
3663 .setMailbox(((Contact) contact).folderId.mailbox)
3664 .setObjectType("contactFolders")
3665 .setObjectId(((Contact) contact).folderId.id)
3666 .setChildType("contacts")
3667 .setChildId(((Contact) contact).id)
3668 .setChildSuffix("photo/$value")
3669 .setAccessToken(token.getAccessToken());
3670
3671 byte[] contactPhotoBytes;
3672 try (
3673 CloseableHttpResponse response = httpClient.execute(graphRequestBuilder.build());
3674 InputStream inputStream = response.getEntity().getContent()
3675 ) {
3676 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
3677 throw new IOException("Unable to fetch photo" + response.getStatusLine().getReasonPhrase());
3678 }
3679 if (HttpClientAdapter.isGzipEncoded(response)) {
3680 contactPhotoBytes = IOUtil.readFully(new GZIPInputStream(inputStream));
3681 } else {
3682 contactPhotoBytes = IOUtil.readFully(inputStream);
3683 }
3684 }
3685 ContactPhoto contactPhoto = new ContactPhoto();
3686 contactPhoto.contentType = "image/jpeg";
3687 contactPhoto.content = IOUtil.encodeBase64AsString(contactPhotoBytes);
3688
3689 return contactPhoto;
3690 }
3691
3692 @Override
3693 public void deleteItem(String folderPath, String itemName) throws IOException {
3694 Item item = getItem(folderPath, itemName);
3695 if (item instanceof GraphExchangeSession.Contact) {
3696 FolderId folderId = ((Contact) item).folderId;
3697 executeJsonRequest(new GraphRequestBuilder()
3698 .setMethod(HttpDelete.METHOD_NAME)
3699 .setMailbox(folderId.mailbox)
3700 .setObjectType("contactFolders")
3701 .setObjectId(folderId.id)
3702 .setChildType("contacts")
3703 .setChildId(((Contact) item).id)
3704 );
3705 } else if (item instanceof GraphExchangeSession.Event) {
3706 FolderId folderId = ((Event) item).folderId;
3707
3708 if (folderId.isCalendar()) {
3709 executeJsonRequest(new GraphRequestBuilder()
3710 .setMethod(HttpDelete.METHOD_NAME)
3711 .setMailbox(folderId.mailbox)
3712 .setObjectType("events")
3713 .setObjectId(((Event) item).id));
3714 } else {
3715 executeJsonRequest(new GraphRequestBuilder()
3716 .setMethod(HttpDelete.METHOD_NAME)
3717 .setMailbox(folderId.mailbox)
3718 .setObjectType("todo/lists")
3719 .setObjectId(folderId.id)
3720 .setChildType("tasks")
3721 .setChildId(((Event) item).id)
3722 );
3723 }
3724 }
3725 }
3726
3727 @Override
3728 public void processItem(String folderPath, String itemName) throws IOException {
3729
3730 }
3731
3732 @Override
3733 public int sendEvent(String icsBody) throws IOException {
3734 String itemName = UUID.randomUUID() + ".EML";
3735 byte[] mimeContent = new GraphExchangeSession.Event(DRAFTS, itemName, "urn:content-classes:calendarmessage", icsBody, null, null).createMimeContent();
3736 if (mimeContent == null) {
3737
3738 return HttpStatus.SC_NO_CONTENT;
3739 } else {
3740 sendMessage(mimeContent);
3741 return HttpStatus.SC_OK;
3742 }
3743 }
3744
3745 @Override
3746 protected Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException {
3747 return new Contact(folderPath, itemName, properties, StringUtil.removeQuotes(etag), noneMatch);
3748 }
3749
3750 @Override
3751 protected ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
3752 return new Event(folderPath, itemName, contentClass, icsBody, StringUtil.removeQuotes(etag), noneMatch).createOrUpdate();
3753 }
3754
3755 @Override
3756 public boolean isSharedFolder(String folderPath) {
3757 return folderPath.startsWith("/") && !folderPath.toLowerCase().startsWith(currentMailboxPath);
3758 }
3759
3760 @Override
3761 public boolean isMainCalendar(String folderPath) throws IOException {
3762 FolderId folderId = getFolderIdIfExists(folderPath);
3763 return folderId.parentFolderId == null && WellKnownFolderName.calendar.name().equals(folderId.id);
3764 }
3765
3766 @Override
3767 protected String getCalendarEmail(String folderPath) throws IOException {
3768 FolderId folderId = getFolderId(folderPath);
3769 if (folderId.mailbox == null) {
3770 return email;
3771 } else {
3772 return folderId.mailbox;
3773 }
3774 }
3775
3776
3777
3778
3779 public static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<>();
3780
3781 static {
3782 GALFIND_ATTRIBUTE_MAP.put("id", "uid");
3783
3784 GALFIND_ATTRIBUTE_MAP.put("displayName", "cn");
3785 GALFIND_ATTRIBUTE_MAP.put("surname", "sn");
3786 GALFIND_ATTRIBUTE_MAP.put("givenName", "givenname");
3787 GALFIND_ATTRIBUTE_MAP.put("personNotes", "description");
3788
3789 GALFIND_ATTRIBUTE_MAP.put("companyName", "company");
3790 GALFIND_ATTRIBUTE_MAP.put("profession", "profession");
3791 GALFIND_ATTRIBUTE_MAP.put("title", "title");
3792 GALFIND_ATTRIBUTE_MAP.put("department", "department");
3793 GALFIND_ATTRIBUTE_MAP.put("officeLocation", "location");
3794
3795 GALFIND_ATTRIBUTE_MAP.put("birthday", "birthday");
3796
3797 GALFIND_ATTRIBUTE_MAP.put("yomiCompany", "yomicompany");
3798
3799 GALFIND_ATTRIBUTE_MAP.put("mailboxType", "mailboxtype");
3800 GALFIND_ATTRIBUTE_MAP.put("personType", "persontype");
3801 GALFIND_ATTRIBUTE_MAP.put("userPrincipalName", "userprincipalname");
3802 GALFIND_ATTRIBUTE_MAP.put("isFavorite", "isfavorite");
3803 }
3804
3805
3806 protected GraphExchangeSession.Contact buildGalfindContact(JSONObject response) {
3807 GraphExchangeSession.Contact contact = new GraphExchangeSession.Contact();
3808 contact.setName(response.optString("id"));
3809 contact.put("imapUid", response.optString("id"));
3810 contact.put("uid", response.optString("id"));
3811 Iterator keysIterator = response.keys();
3812 while (keysIterator.hasNext()) {
3813 String key = (String) keysIterator.next();
3814 String attributeName = key;
3815
3816 if ("emailAddresses".equals(key)) {
3817 JSONArray emailAddresses = response.optJSONArray("emailAddresses");
3818 if (emailAddresses != null) {
3819 for (int i = 0; i < 3; i++) {
3820 if (emailAddresses.length() > i) {
3821 contact.put("smtpemail" + (i + 1), emailAddresses.optJSONObject(i).optString("address"));
3822 }
3823 }
3824 }
3825
3826 } else if ("phones".equals(key)) {
3827 JSONArray phones = response.optJSONArray("phones");
3828 if (phones != null) {
3829 for (int i = 0; i < phones.length(); i++) {
3830 String phoneType = phones.optJSONObject(i).optString("type");
3831 String phoneNumber = phones.optJSONObject(i).optString("number");
3832 if ("business".equals(phoneType)) {
3833 contact.put("telephoneNumber", phoneNumber);
3834 } else if ("mobile".equals(phoneType)) {
3835 contact.put("mobile", phoneNumber);
3836 } else if ("home".equals(phoneType)) {
3837 contact.put("homePhone", phoneNumber);
3838 } else {
3839 LOGGER.debug("Unknown phoneType " + phoneType);
3840 contact.put(phoneType + "Phone", phoneNumber);
3841 }
3842 }
3843 }
3844 } else if ("sources".equals(key)) {
3845 JSONArray sources = response.optJSONArray("sources");
3846 if (sources != null && sources.length() > 0) {
3847 String sourceType = sources.optJSONObject(0).optString("type");
3848 contact.put("sourceType", sourceType);
3849 }
3850 } else {
3851 if (GALFIND_ATTRIBUTE_MAP.get(key) != null) {
3852 attributeName = GALFIND_ATTRIBUTE_MAP.get(key);
3853 } else {
3854 LOGGER.debug("Unknown attribute " + attributeName);
3855 }
3856
3857 String attributeValue = response.optString(key);
3858 if (attributeValue != null) {
3859 contact.put(attributeName, attributeValue);
3860 }
3861 }
3862 }
3863 return contact;
3864 }
3865
3866 @Override
3867 public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
3868 Map<String, ExchangeSession.Contact> contacts = new HashMap<>();
3869
3870
3871 String search = null;
3872 String id = null;
3873 if (condition instanceof AttributeCondition) {
3874 if ("imapUid".equals(((AttributeCondition) condition).getAttributeName())) {
3875 id = ((AttributeCondition) condition).getValue();
3876 } else {
3877 search = ((AttributeCondition) condition).getValue();
3878 }
3879 }
3880
3881 if (id != null) {
3882
3883
3884 if (id.length() == 36) {
3885 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3886 .setMethod(HttpGet.METHOD_NAME)
3887 .addHeader("X-PeopleQuery-QuerySources", "Mailbox,Directory")
3888 .setObjectType("people")
3889 .setObjectId(id);
3890 JSONObject peopleObject = null;
3891
3892 try {
3893 peopleObject = executeJsonRequest(httpRequestBuilder);
3894 } catch (HttpNotFoundException e) {
3895 LOGGER.warn("No person found for id " + id);
3896 }
3897
3898 if (peopleObject != null) {
3899 Contact contact = buildGalfindContact(peopleObject);
3900
3901 contacts.put(contact.getName().toLowerCase(), contact);
3902 LOGGER.debug("found user " + contact.getName());
3903 }
3904 }
3905
3906 } else {
3907 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3908 .setMethod(HttpGet.METHOD_NAME)
3909 .addHeader("X-PeopleQuery-QuerySources", "Mailbox,Directory")
3910 .setObjectType("people")
3911 .setSearch(search);
3912 LOGGER.debug("search users");
3913 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3914
3915 while (graphIterator.hasNext() && contacts.size() < sizeLimit) {
3916 Contact contact = buildGalfindContact(graphIterator.next());
3917 contacts.put(contact.getName().toLowerCase(), contact);
3918 LOGGER.debug("found user " + contact.getName());
3919 }
3920 }
3921
3922 return contacts;
3923 }
3924
3925 @Override
3926 protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException {
3927
3928
3929 String fbdata = null;
3930 JSONObject jsonBody = new JSONObject();
3931 try {
3932 String timeZone = getVTimezone().getPropertyValue("TZID");
3933 jsonBody.put("Schedules", new JSONArray().put(attendee));
3934 jsonBody.put("StartTime", new JSONObject().put("dateTime", start).put("timeZone", timeZone));
3935 jsonBody.put("EndTime", new JSONObject().put("dateTime", end).put("timeZone", timeZone));
3936 jsonBody.put("availabilityViewInterval", interval);
3937
3938 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
3939 .setMethod(HttpPost.METHOD_NAME)
3940 .setObjectType("calendar")
3941 .setAction("getschedule")
3942 .setJsonBody(jsonBody));
3943 JSONArray value = graphResponse.optJSONArray("value");
3944 if (value != null && value.length() > 0) {
3945 fbdata = value.getJSONObject(0).optString("availabilityView", null);
3946 }
3947 } catch (JSONException e) {
3948 throw new IOException(e.getMessage(), e);
3949 }
3950 return fbdata;
3951 }
3952
3953 @Override
3954 protected void loadVtimezone() {
3955 try {
3956
3957 String timezoneId = Settings.getProperty("davmail.timezoneId", null);
3958
3959 if (timezoneId == null) {
3960 try {
3961 timezoneId = getMailboxSettings().optString("timeZone", null);
3962 } catch (HttpForbiddenException e) {
3963 LOGGER.warn("token does not grant MailboxSettings.Read");
3964 }
3965 }
3966
3967 if (timezoneId == null) {
3968 LOGGER.warn("Unable to get user timezone, using GMT Standard Time. Set davmail.timezoneId setting to override this.");
3969 timezoneId = "GMT Standard Time";
3970 }
3971 this.vTimezone = getVTimezone(timezoneId);
3972
3973 } catch (IOException | MissingResourceException e) {
3974 LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
3975 }
3976 }
3977
3978 private VObject getVTimezone(String timezoneId) {
3979 String vTimeZone = DateUtil.getVTimeZone(timezoneId);
3980 if (vTimeZone != null) {
3981 try {
3982 return new VObject(vTimeZone);
3983 } catch (IOException e) {
3984 LOGGER.warn("Unable to get VTIMEZONE: " + e, e);
3985 }
3986 }
3987
3988 return getVTimezone();
3989 }
3990
3991 private JSONObject getMailboxSettings() throws IOException {
3992 return executeJsonRequest(new GraphRequestBuilder()
3993 .setMethod(HttpGet.METHOD_NAME)
3994 .setObjectType("mailboxSettings"));
3995 }
3996
3997 class GraphIterator {
3998
3999 private JSONObject jsonObject;
4000 private JSONArray values;
4001 private String nextLink;
4002 private int index;
4003
4004 public GraphIterator(JSONObject jsonObject) throws JSONException {
4005 this.jsonObject = jsonObject;
4006 nextLink = jsonObject.optString("@odata.nextLink", null);
4007 values = jsonObject.optJSONArray("value");
4008 }
4009
4010 public boolean hasNext() throws IOException {
4011 if (values != null && index < values.length()) {
4012 return true;
4013 } else if (nextLink != null) {
4014 fetchNextPage();
4015 return values != null && values.length() > 0;
4016 } else {
4017 return false;
4018 }
4019 }
4020
4021 public JSONObject next() throws IOException {
4022 if (values == null || !hasNext()) {
4023 throw new NoSuchElementException();
4024 }
4025 try {
4026 if (index >= values.length() && nextLink != null) {
4027 fetchNextPage();
4028 }
4029 return values.getJSONObject(index++);
4030 } catch (JSONException e) {
4031 throw new IOException(e.getMessage(), e);
4032 }
4033 }
4034
4035 private void fetchNextPage() throws IOException {
4036 HttpGet request = new HttpGet(nextLink);
4037 request.setHeader("Authorization", "Bearer " + token.getAccessToken());
4038 try (
4039 CloseableHttpResponse response = httpClient.execute(request)
4040 ) {
4041 jsonObject = new JsonResponseHandler().handleResponse(response);
4042 nextLink = jsonObject.optString("@odata.nextLink", null);
4043
4044 if (nextLink != null && nextLink.endsWith("skip=0")) {
4045 nextLink = null;
4046 }
4047 values = jsonObject.optJSONArray("value");
4048 index = 0;
4049 }
4050 }
4051 }
4052
4053 private GraphIterator executeSearchRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
4054 try {
4055 return new GraphIterator(executeJsonRequest(httpRequestBuilder));
4056 } catch (JSONException e) {
4057 throw new IOException(e.getMessage(), e);
4058 }
4059 }
4060
4061 private JSONObject executeJsonRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
4062 JSONObject jsonResponse = null;
4063 boolean isThrottled;
4064 do {
4065 HttpRequestBase request = httpRequestBuilder
4066 .setAccessToken(token.getAccessToken())
4067 .build();
4068
4069
4070
4071 try (
4072 CloseableHttpResponse response = httpClient.execute(request)
4073 ) {
4074 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
4075 LOGGER.warn(response.getStatusLine());
4076 }
4077 isThrottled = handleThrottling(response);
4078 if (!isThrottled) {
4079 jsonResponse = new JsonResponseHandler().handleResponse(response);
4080 }
4081 }
4082 } while (isThrottled);
4083 return jsonResponse;
4084 }
4085
4086
4087
4088
4089
4090
4091
4092
4093 private GraphObject executeGraphRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
4094 HttpRequestBase request = httpRequestBuilder
4095 .setAccessToken(token.getAccessToken())
4096 .build();
4097
4098 GraphObject graphObject = null;
4099 boolean isThrottled;
4100 do {
4101
4102
4103 try (
4104 CloseableHttpResponse response = httpClient.execute(request)
4105 ) {
4106 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
4107 LOGGER.warn("Request returned " + response.getStatusLine());
4108 }
4109 isThrottled = handleThrottling(response);
4110 if (!isThrottled) {
4111 graphObject = new GraphObject(new JsonResponseHandler().handleResponse(response));
4112 graphObject.statusCode = response.getStatusLine().getStatusCode();
4113 }
4114 }
4115 } while (isThrottled);
4116 return graphObject;
4117 }
4118
4119
4120
4121
4122
4123
4124
4125 private boolean handleThrottling(CloseableHttpResponse response) {
4126 long retryDelay = 0;
4127 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_TOO_MANY_REQUESTS) {
4128 LOGGER.info("Detected throttling " + response.getStatusLine());
4129 Header retryAfter = response.getFirstHeader("Retry-After");
4130 if (retryAfter != null) {
4131 retryDelay = Long.parseLong(retryAfter.getValue()) + 1;
4132 waitRetryDelay(retryDelay);
4133 }
4134 } else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_SERVICE_UNAVAILABLE) {
4135 LOGGER.info("Detected graph request error, waiting to retry " + response.getStatusLine());
4136 retryDelay = 5;
4137 waitRetryDelay(retryDelay);
4138 }
4139 return retryDelay > 0;
4140 }
4141
4142 private void waitRetryDelay(long retryDelay) {
4143 LOGGER.debug("Waiting " + retryDelay + " seconds to retry request");
4144 try {
4145 Thread.sleep(retryDelay * 1000L);
4146 } catch (InterruptedException e) {
4147 Thread.currentThread().interrupt();
4148 }
4149 }
4150
4151
4152
4153
4154
4155
4156
4157
4158 protected static boolean isItemId(String itemName) {
4159
4160 return (itemName.length() >= 140 || itemName.length() == 72)
4161
4162 && itemName.matches("^([A-Za-z0-9-_]{4})*([A-Za-z0-9-_]{4}|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{2}==)\\.EML$")
4163 && itemName.indexOf(' ') < 0;
4164 }
4165
4166 }