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