View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  
20  package davmail.exchange.graph;
21  
22  import davmail.Settings;
23  import davmail.exception.DavMailException;
24  import davmail.exchange.VObject;
25  import davmail.exchange.VProperty;
26  import davmail.util.DateUtil;
27  import davmail.util.StringUtil;
28  import org.apache.log4j.Logger;
29  import org.codehaus.jettison.json.JSONArray;
30  import org.codehaus.jettison.json.JSONException;
31  import org.codehaus.jettison.json.JSONObject;
32  
33  import java.text.ParseException;
34  import java.text.SimpleDateFormat;
35  import java.util.Date;
36  import java.util.HashMap;
37  import java.util.HashSet;
38  import java.util.Map;
39  import java.util.TimeZone;
40  
41  /**
42   * Wrapper for Graph API JsonObject
43   */
44  public class GraphObject {
45      protected static final Logger LOGGER = Logger.getLogger(GraphObject.class);
46  
47      protected final JSONObject jsonObject;
48      protected int statusCode;
49  
50      public GraphObject() {
51          this.jsonObject = new JSONObject();
52      }
53  
54      public GraphObject(JSONObject jsonObject) {
55          this.jsonObject = jsonObject;
56      }
57  
58      public String optString(String key) {
59          // use field mapping to get value
60          return optString(GraphField.get(key));
61      }
62  
63      public String optString(GraphField field) {
64          String key = field.getGraphId();
65          if (key == null) {
66              return null;
67          }
68          String value = null;
69          // special case for keywords/categories
70          if ("keywords".equals(key) || "categories".equals(key)) {
71              JSONArray categoriesArray = jsonObject.optJSONArray("categories");
72              HashSet<String> keywords = new HashSet<>();
73              // Collects keywords from the categories array, joins into comma‑separated string
74              if (categoriesArray != null) {
75                  for (int j = 0; j < categoriesArray.length(); j++) {
76                      keywords.add(categoriesArray.optString(j));
77                  }
78                  value = StringUtil.join(keywords, ",");
79              }
80          } else if ("from".equals(key)) {
81              value = formatEmailAddress(jsonObject.optJSONObject(key));
82          } else if ("@odata.etag".equals(key)) {
83              // tasks don't have an etag field, use @odata.etag
84              String odataEtag = jsonObject.optString("@odata.etag");
85              if (odataEtag != null && odataEtag.startsWith("W/\"") && odataEtag.endsWith("\"")) {
86                  value = odataEtag.substring(3, odataEtag.length() - 1);
87              }
88          } else if (!field.isExtended()) {
89              // grab value by key
90              value = jsonObject.optString(key, null);
91          } else {
92              JSONArray singleValueExtendedProperties = jsonObject.optJSONArray("singleValueExtendedProperties");
93              if (singleValueExtendedProperties != null) {
94                  // Iterates extended properties to find a matching value
95                  for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
96                      JSONObject singleValueObject = singleValueExtendedProperties.optJSONObject(i);
97                      if (singleValueObject != null && key.equals(singleValueObject.optString("id"))) {
98                          value = singleValueObject.optString("value");
99                      }
100                 }
101             }
102         }
103         if (field.isDate()) {
104             try {
105                 value = GraphExchangeSession.convertDateFromExchange(value);
106             } catch (DavMailException e) {
107                 LOGGER.warn("Invalid date " + value + " on field " + key);
108             }
109         }
110         return value;
111     }
112 
113     protected String formatEmailAddress(JSONObject jsonObject) {
114         String value = null;
115         if (jsonObject != null) {
116             JSONObject jsonEmailAddress = jsonObject.optJSONObject("emailAddress");
117             if (jsonEmailAddress != null) {
118                 value = jsonEmailAddress.optString("name", "") + " <" + jsonEmailAddress.optString("address", "") + ">";
119             }
120         }
121         return value;
122     }
123 
124     public String getCalendarUid() {
125         String iCalUId = optString("calendaruid");
126         if (iCalUId == null && Settings.getBooleanProperty("davmail.caldavUIDFromTransactionId",false)) {
127             // restore pre 7.0.0 behavior, use transaction as UID
128             iCalUId = optString("transactionId");
129         }
130         if (iCalUId == null) {
131             // default to O365 iCalUid
132             iCalUId = optString("iCalUId");
133         }
134         return iCalUId;
135     }
136 
137     public JSONArray optJSONArray(String key) {
138         return jsonObject.optJSONArray(key);
139     }
140 
141     /**
142      * Set value for alias.
143      * First map property alias to a field to determine graph id, then set graph property or singleValueExtendedProperty
144      * @param alias field alias
145      * @param value property value
146      * @throws JSONException on error
147      */
148     public GraphObject put(String alias, String value) throws JSONException {
149         GraphField field = GraphField.get(alias);
150         String key = field.getGraphId();
151         // handle MAPI extended fields
152         if (field.isExtended()) {
153             // force number attributes value
154             if (field.isNumber() && value == null) {
155                 value = "0";
156             }
157             if (field.isBoolean() && value == null) {
158                 value = "false";
159             }
160             getSingleValueExtendedProperties().put(new JSONObject().put("id", key).put("value", value == null ? JSONObject.NULL : value));
161         } else {
162             jsonObject.put(key, value == null ? JSONObject.NULL : value);
163         }
164         return this;
165     }
166 
167     /**
168      * Set the boolean value on the field defined by alias.
169      * First map property alias to a field to determine graph id, then set graph property or singleValueExtendedProperty
170      * @param alias field alias
171      * @param value property value
172      * @throws JSONException on error
173      */
174     public GraphObject put(String alias, boolean value) throws JSONException {
175         GraphField field = GraphField.get(alias);
176         String key = field.getGraphId();
177         // extended field values go under singleValueExtendedProperties
178         if (field.isExtended()) {
179             getSingleValueExtendedProperties().put(new JSONObject().put("id", key).put("value", value));
180         } else {
181             jsonObject.put(key, value);
182         }
183         return this;
184     }
185 
186      public GraphObject put(String key, JSONArray value) throws JSONException {
187         jsonObject.put(key, value);
188         return this;
189     }
190 
191     public GraphObject put(String key, JSONObject value) throws JSONException {
192         jsonObject.put(key, value);
193         return this;
194     }
195 
196     /**
197      * Set categories from comma separated values.
198      * @param values comma separated values
199      * @throws JSONException on error
200      */
201     public void setCategories(String values) throws JSONException {
202         if (values != null) {
203             setCategories(values.split(","));
204         } else {
205             jsonObject.put("categories", new JSONArray());
206         }
207     }
208 
209     public void setCategories(String[] values) throws JSONException {
210         // assume all expanded properties have a space
211         JSONArray jsonValues = new JSONArray();
212         for (String singleValue : values) {
213             jsonValues.put(singleValue);
214         }
215         jsonObject.put("categories", jsonValues);
216     }
217 
218 
219     /**
220      * Get a singleValueExtendedProperties JSON array.
221      * Create an empty JSON array on the first call.
222      * @return singleValueExtendedProperties array
223      * @throws JSONException on error
224      */
225     protected JSONArray getSingleValueExtendedProperties() throws JSONException {
226         JSONArray singleValueExtendedProperties = jsonObject.optJSONArray("singleValueExtendedProperties");
227         if (singleValueExtendedProperties == null) {
228             singleValueExtendedProperties = new JSONArray();
229             jsonObject.put("singleValueExtendedProperties", singleValueExtendedProperties);
230         }
231         return singleValueExtendedProperties;
232     }
233 
234 
235     /**
236      * Get mandatory property value.
237      * @param key property name
238      * @return value
239      * @throws JSONException on missing property
240      */
241     public String getString(String key) throws JSONException {
242         String value = optString(key);
243         if (value == null) {
244             throw new JSONException("JSONObject[" + key + "] not found.");
245         }
246         return value;
247     }
248 
249     /**
250      * Get optional parameter from JSON property.
251      * @param section JSON property name
252      * @param key internal property name
253      * @return value or null
254      */
255     public String optSectionString(String section, String key) {
256         JSONObject sectionObject = jsonObject.optJSONObject(section);
257         if (sectionObject != null) {
258             return sectionObject.optString(key, null);
259         }
260         return null;
261     }
262 
263     public boolean getBoolean(String key) throws JSONException {
264         return getBoolean(GraphField.get(key));
265     }
266 
267     public boolean optBoolean(String key) {
268         return optBoolean(GraphField.get(key));
269     }
270 
271     public boolean getBoolean(GraphField field) throws JSONException {
272         String key = field.getGraphId();
273         if (key == null) {
274             return false;
275         }
276         return jsonObject.getBoolean(key);
277     }
278 
279     public boolean optBoolean(GraphField field) {
280         String key = field.getGraphId();
281         if (key == null) {
282             return false;
283         }
284         return jsonObject.optBoolean(key);
285     }
286 
287     public JSONObject optJSONObject(String key) {
288         return jsonObject.optJSONObject(key);
289     }
290 
291     /**
292      * Convert datetimetimezone to java date.
293      * Graph API returns some dates as json object with dateTime and timeZone, see
294      * <a href="https://learn.microsoft.com/en-us/graph/api/resources/datetimetimezone">datetimetimezone</a>
295      * @param key property key, e.g. dueDateTime
296      * @return java date
297      * @throws DavMailException on error
298      */
299     public Date optDateTimeTimeZone(String key) throws DavMailException {
300         JSONObject sectionObject = jsonObject.optJSONObject(key);
301         if (sectionObject != null) {
302             String timeZone = sectionObject.optString("timeZone");
303             String dateTime = sectionObject.optString("dateTime");
304             if (timeZone != null && dateTime != null) {
305                 try {
306                     SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS");
307                     parser.setTimeZone(DateUtil.getTimeZone(timeZone));
308                     return parser.parse(dateTime);
309                 } catch (ParseException e) {
310                     throw new DavMailException("EXCEPTION_INVALID_DATE", dateTime);
311                 }
312             }
313         }
314         return null;
315     }
316 
317     /**
318      * Compute recurrenceId property based on the original start and timezone.
319      * @return recurrenceId property
320      * @throws DavMailException on error
321      */
322     public VProperty getRecurrenceId() throws DavMailException {
323         // get the unmodified start date and timezone of occurrence
324         String originalStartTimeZone = optString("originalStartTimeZone");
325         // originalStart is always in UTC, see https://learn.microsoft.com/en-us/graph/api/resources/event
326         String originalStart = optString("originalStart");
327 
328         if (originalStartTimeZone != null && originalStart != null && originalStart.length() >= 19) {
329             String convertedOriginalStart = originalStart;
330             // Per https://learn.microsoft.com/en-us/graph/api/resources/recurrencerange?view=graph-rest-1.0,
331             // originalStart is always in ISO8601 format with offset or Zulu
332             SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
333             parser.setTimeZone(TimeZone.getTimeZone("UTC"));
334 
335             String standardTimeZoneId = DateUtil.getStandardTimeZone(originalStartTimeZone);
336             SimpleDateFormat formatter;
337             if (standardTimeZoneId == null) {
338                 // format to zulu
339                 formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
340                 formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
341             } else {
342                 // format to original timezone
343                 formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
344                 formatter.setTimeZone(DateUtil.getTimeZone(standardTimeZoneId));
345             }
346             try {
347                 convertedOriginalStart = formatter.format(parser.parse(originalStart));
348             } catch (ParseException e) {
349                 LOGGER.warn("Unable to convert to original timezone: " + originalStart + ", " + originalStartTimeZone);
350             }
351 
352             // Convert date from graph to caldav format, keep timezone information
353             VProperty recurrenceId = new VProperty("RECURRENCE-ID", convertedOriginalStart);
354             if (standardTimeZoneId != null) {
355                 recurrenceId.setParam("TZID", originalStartTimeZone);
356             }
357             return recurrenceId;
358         } else {
359             throw new DavMailException("LOG_MESSAGE", "Missing original start date and timezone");
360         }
361     }
362 
363 
364 
365     public int optInt(String key) {
366         return jsonObject.optInt(key);
367     }
368 
369     public String toString(int indentFactor) throws JSONException {
370         return jsonObject.toString(indentFactor);
371     }
372 
373     protected static final Map<String, String> vTodoToTaskStatusMap = new HashMap<>();
374     protected static final Map<String, String> taskTovTodoStatusMap = new HashMap<>();
375 
376     static {
377         // The possible values are: notStarted, inProgress, completed, waitingOnOthers, deferred
378         //taskTovTodoStatusMap.put("notStarted", null);
379         taskTovTodoStatusMap.put("inProgress", "IN-PROCESS");
380         taskTovTodoStatusMap.put("completed", "COMPLETED");
381         taskTovTodoStatusMap.put("waitingOnOthers", "NEEDS-ACTION");
382         taskTovTodoStatusMap.put("deferred", "CANCELLED");
383 
384         //vTodoToTaskStatusMap.put(null, "NotStarted");
385         vTodoToTaskStatusMap.put("IN-PROCESS", "inProgress");
386         vTodoToTaskStatusMap.put("COMPLETED", "completed");
387         vTodoToTaskStatusMap.put("NEEDS-ACTION", "waitingOnOthers");
388         vTodoToTaskStatusMap.put("CANCELLED", "deferred");
389     }
390 
391     public void setTaskStatusFromVTodo(VObject vEvent) throws JSONException {
392         String taskStatus = vTodoToTaskStatusMap.get(vEvent.getPropertyValue("STATUS"));
393         if (taskStatus == null) {
394             taskStatus = "notStarted";
395         }
396         put("status", taskStatus);
397     }
398 
399     public String getVTodoStatusFromTask() {
400         return taskTovTodoStatusMap.get(optString("status"));
401     }
402 
403     protected static final Map<String, String> importanceToPriorityMap = new HashMap<>();
404 
405     static {
406         importanceToPriorityMap.put("high", "1");
407         importanceToPriorityMap.put("normal", "5");
408         importanceToPriorityMap.put("low", "9");
409     }
410 
411     protected static final Map<String, String> priorityToImportanceMap = new HashMap<>();
412 
413     static {
414         // 0 means undefined, map it to normal
415         priorityToImportanceMap.put("0", "normal");
416 
417         priorityToImportanceMap.put("1", "high");
418         priorityToImportanceMap.put("2", "high");
419         priorityToImportanceMap.put("3", "high");
420         priorityToImportanceMap.put("4", "normal");
421         priorityToImportanceMap.put("5", "normal");
422         priorityToImportanceMap.put("6", "normal");
423         priorityToImportanceMap.put("7", "low");
424         priorityToImportanceMap.put("8", "low");
425         priorityToImportanceMap.put("9", "low");
426     }
427 
428     protected String getTaskPriority() {
429         String taskImportance = optString("importance");
430         String taskPriority = null;
431         if (taskImportance != null) {
432             taskPriority = importanceToPriorityMap.get(taskImportance);
433         }
434         return taskPriority;
435     }
436 
437     public void setTaskImportanceFromVTodo(VObject vEvent) throws JSONException {
438         String taskImportance = priorityToImportanceMap.get(vEvent.getPropertyValue("PRIORITY"));
439         if (taskImportance == null) {
440             taskImportance = "normal";
441         }
442         put("importance", taskImportance);
443     }
444 
445 }
446