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  package davmail.exchange.ews;
20  
21  import davmail.BundleMessage;
22  import davmail.Settings;
23  import davmail.exchange.XMLStreamUtil;
24  import davmail.http.HttpClientAdapter;
25  import davmail.ui.tray.DavGatewayTray;
26  import davmail.util.StringUtil;
27  import org.apache.commons.codec.binary.Base64;
28  import org.apache.http.HttpResponse;
29  import org.apache.http.HttpStatus;
30  import org.apache.http.client.ResponseHandler;
31  import org.apache.http.client.methods.HttpPost;
32  import org.apache.http.entity.AbstractHttpEntity;
33  import org.apache.http.entity.ContentType;
34  import org.apache.log4j.Level;
35  import org.apache.log4j.Logger;
36  import org.codehaus.stax2.typed.TypedXMLStreamReader;
37  
38  import javax.xml.stream.XMLStreamConstants;
39  import javax.xml.stream.XMLStreamException;
40  import javax.xml.stream.XMLStreamReader;
41  import java.io.ByteArrayInputStream;
42  import java.io.ByteArrayOutputStream;
43  import java.io.FilterInputStream;
44  import java.io.IOException;
45  import java.io.InputStream;
46  import java.io.OutputStream;
47  import java.io.OutputStreamWriter;
48  import java.io.Writer;
49  import java.net.URI;
50  import java.nio.charset.StandardCharsets;
51  import java.util.ArrayList;
52  import java.util.HashMap;
53  import java.util.HashSet;
54  import java.util.List;
55  import java.util.Set;
56  import java.util.zip.GZIPInputStream;
57  
58  /**
59   * EWS SOAP method.
60   */
61  public abstract class EWSMethod extends HttpPost implements ResponseHandler<EWSMethod> {
62      protected static final String CONTENT_TYPE = ContentType.create("text/xml", StandardCharsets.UTF_8).toString();
63      protected static final Logger LOGGER = Logger.getLogger(EWSMethod.class);
64      protected static final int CHUNK_LENGTH = 131072;
65  
66      protected FolderQueryTraversal traversal;
67      protected BaseShape baseShape;
68      protected boolean includeMimeContent;
69      protected FolderId folderId;
70      protected FolderId savedItemFolderId;
71      protected FolderId toFolderId;
72      protected FolderId parentFolderId;
73      protected ItemId itemId;
74      protected List<ItemId> itemIds;
75      protected ItemId parentItemId;
76      protected Set<FieldURI> additionalProperties;
77      protected Disposal deleteType;
78      protected Set<AttributeOption> methodOptions;
79      protected ElementOption unresolvedEntry;
80  
81      // paging request
82      protected int maxCount;
83      protected int offset;
84      // paging response
85      protected boolean includesLastItemInRange;
86  
87      protected List<FieldUpdate> updates;
88  
89      protected FileAttachment attachment;
90  
91      protected String attachmentId;
92  
93      protected final String itemType;
94      protected final String methodName;
95      protected final String responseCollectionName;
96  
97      protected List<Item> responseItems;
98      protected String errorDetail;
99      protected String errorDescription;
100     protected String errorValue;
101     protected long backOffMilliseconds;
102     protected Item item;
103 
104     protected SearchExpression searchExpression;
105     protected FieldOrder fieldOrder;
106 
107     protected String serverVersion;
108     protected String timezoneContext;
109     private HttpResponse response;
110 
111     /**
112      * Build EWS method
113      *
114      * @param itemType   item type
115      * @param methodName method name
116      */
117     public EWSMethod(String itemType, String methodName) {
118         this(itemType, methodName, itemType + 's');
119     }
120 
121     /**
122      * Build EWS method
123      *
124      * @param itemType               item type
125      * @param methodName             method name
126      * @param responseCollectionName item response collection name
127      */
128     public EWSMethod(String itemType, String methodName, String responseCollectionName) {
129         super(URI.create("/ews/exchange.asmx"));
130         this.itemType = itemType;
131         this.methodName = methodName;
132         this.responseCollectionName = responseCollectionName;
133         if (Settings.getBooleanProperty("davmail.acceptEncodingGzip", true) &&
134                 !Level.DEBUG.toString().equals(Settings.getProperty("log4j.logger.httpclient.wire"))) {
135             setHeader("Accept-Encoding", "gzip");
136         }
137 
138         AbstractHttpEntity httpEntity = new AbstractHttpEntity() {
139             byte[] content;
140 
141             @Override
142             public boolean isRepeatable() {
143                 return true;
144             }
145 
146             @Override
147             public long getContentLength() {
148                 if (content == null) {
149                     content = generateSoapEnvelope();
150                 }
151                 return content.length;
152             }
153 
154             @Override
155             public InputStream getContent() throws UnsupportedOperationException {
156                 if (content == null) {
157                     content = generateSoapEnvelope();
158                 }
159                 return new ByteArrayInputStream(content);
160             }
161 
162             @Override
163             public void writeTo(OutputStream outputStream) throws IOException {
164                 boolean firstPass = content == null;
165                 if (content == null) {
166                     content = generateSoapEnvelope();
167                 }
168                 if (content.length < CHUNK_LENGTH) {
169                     outputStream.write(content);
170                 } else {
171                     int i = 0;
172                     while (i < content.length) {
173                         int length = CHUNK_LENGTH;
174                         if (i + CHUNK_LENGTH > content.length) {
175                             length = content.length - i;
176                         }
177                         outputStream.write(content, i, length);
178                         if (!firstPass) {
179                             DavGatewayTray.debug(new BundleMessage("LOG_UPLOAD_PROGRESS", String.valueOf((i + length) / 1024), (i + length) * 100 / content.length));
180                             DavGatewayTray.switchIcon();
181                         }
182                         i += CHUNK_LENGTH;
183                     }
184                 }
185             }
186 
187             @Override
188             public boolean isStreaming() {
189                 return false;
190             }
191         };
192 
193         httpEntity.setContentType(CONTENT_TYPE);
194         setEntity(httpEntity);
195     }
196 
197     protected void addAdditionalProperty(FieldURI additionalProperty) {
198         if (additionalProperties == null) {
199             additionalProperties = new HashSet<>();
200         }
201         additionalProperties.add(additionalProperty);
202     }
203 
204     protected void addMethodOption(AttributeOption attributeOption) {
205         if (methodOptions == null) {
206             methodOptions = new HashSet<>();
207         }
208         methodOptions.add(attributeOption);
209     }
210 
211     protected void setSearchExpression(SearchExpression searchExpression) {
212         this.searchExpression = searchExpression;
213     }
214 
215     protected void setFieldOrder(FieldOrder fieldOrder) {
216         this.fieldOrder = fieldOrder;
217     }
218 
219     protected void writeShape(Writer writer) throws IOException {
220         if (baseShape != null) {
221             writer.write("<m:");
222             writer.write(itemType);
223             writer.write("Shape>");
224             baseShape.write(writer);
225             if (includeMimeContent) {
226                 writer.write("<t:IncludeMimeContent>true</t:IncludeMimeContent>");
227             }
228             if (additionalProperties != null) {
229                 writer.write("<t:AdditionalProperties>");
230                 StringBuilder buffer = new StringBuilder();
231                 for (FieldURI fieldURI : additionalProperties) {
232                     fieldURI.appendTo(buffer);
233                 }
234                 writer.write(buffer.toString());
235                 writer.write("</t:AdditionalProperties>");
236             }
237             writer.write("</m:");
238             writer.write(itemType);
239             writer.write("Shape>");
240         }
241     }
242 
243     protected void writeItemId(Writer writer) throws IOException {
244         if (itemId != null || itemIds != null) {
245             if (updates == null) {
246                 writer.write("<m:ItemIds>");
247             }
248             if (itemId != null) {
249                 itemId.write(writer);
250             }
251             if (itemIds != null) {
252                 for (ItemId localItemId : itemIds) {
253                     localItemId.write(writer);
254                 }
255             }
256             if (updates == null) {
257                 writer.write("</m:ItemIds>");
258             }
259         }
260     }
261 
262     protected void writeParentItemId(Writer writer) throws IOException {
263         if (parentItemId != null) {
264             writer.write("<m:ParentItemId Id=\"");
265             writer.write(parentItemId.id);
266             if (parentItemId.changeKey != null) {
267                 writer.write("\" ChangeKey=\"");
268                 writer.write(parentItemId.changeKey);
269             }
270             writer.write("\"/>");
271         }
272     }
273 
274     protected void writeFolderId(Writer writer) throws IOException {
275         if (folderId != null) {
276             if (updates == null) {
277                 writer.write("<m:FolderIds>");
278             }
279             folderId.write(writer);
280             if (updates == null) {
281                 writer.write("</m:FolderIds>");
282             }
283         }
284     }
285 
286     protected void writeSavedItemFolderId(Writer writer) throws IOException {
287         if (savedItemFolderId != null) {
288             writer.write("<m:SavedItemFolderId>");
289             savedItemFolderId.write(writer);
290             writer.write("</m:SavedItemFolderId>");
291         }
292     }
293 
294     protected void writeToFolderId(Writer writer) throws IOException {
295         if (toFolderId != null) {
296             writer.write("<m:ToFolderId>");
297             toFolderId.write(writer);
298             writer.write("</m:ToFolderId>");
299         }
300     }
301 
302     protected void writeParentFolderId(Writer writer) throws IOException {
303         if (parentFolderId != null) {
304             writer.write("<m:ParentFolderId");
305             if (item == null) {
306                 writer.write("s");
307             }
308             writer.write(">");
309             parentFolderId.write(writer);
310             writer.write("</m:ParentFolderId");
311             if (item == null) {
312                 writer.write("s");
313             }
314             writer.write(">");
315         }
316     }
317 
318     protected void writeItem(Writer writer) throws IOException {
319         if (item != null) {
320             writer.write("<m:");
321             writer.write(itemType);
322             writer.write("s>");
323             item.write(writer);
324             writer.write("</m:");
325             writer.write(itemType);
326             writer.write("s>");
327         }
328     }
329 
330     protected void writeRestriction(Writer writer) throws IOException {
331         if (searchExpression != null) {
332             writer.write("<m:Restriction>");
333             StringBuilder buffer = new StringBuilder();
334             searchExpression.appendTo(buffer);
335             writer.write(buffer.toString());
336             writer.write("</m:Restriction>");
337         }
338     }
339 
340     protected void writeSortOrder(Writer writer) throws IOException {
341         if (fieldOrder != null) {
342             writer.write("<m:SortOrder>");
343             StringBuilder buffer = new StringBuilder();
344             fieldOrder.appendTo(buffer);
345             writer.write(buffer.toString());
346             writer.write("</m:SortOrder>");
347         }
348     }
349 
350     protected void startChanges(Writer writer) throws IOException {
351         //noinspection VariableNotUsedInsideIf
352         if (updates != null) {
353             writer.write("<m:");
354             writer.write(itemType);
355             writer.write("Changes>");
356             writer.write("<t:");
357             writer.write(itemType);
358             writer.write("Change>");
359         }
360     }
361 
362     protected void writeUpdates(Writer writer) throws IOException {
363         if (updates != null) {
364             writer.write("<t:Updates>");
365             for (FieldUpdate fieldUpdate : updates) {
366                 fieldUpdate.write(itemType, writer);
367             }
368             writer.write("</t:Updates>");
369         }
370     }
371 
372     protected void writeUnresolvedEntry(Writer writer) throws IOException {
373         if (unresolvedEntry != null) {
374             unresolvedEntry.write(writer);
375         }
376     }
377 
378     protected void endChanges(Writer writer) throws IOException {
379         //noinspection VariableNotUsedInsideIf
380         if (updates != null) {
381             writer.write("</t:");
382             writer.write(itemType);
383             writer.write("Change>");
384             writer.write("</m:");
385             writer.write(itemType);
386             writer.write("Changes>");
387         }
388     }
389 
390     protected byte[] generateSoapEnvelope() {
391         ByteArrayOutputStream baos = new ByteArrayOutputStream();
392         try {
393             OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
394             writer.write("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" " +
395                     "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\" " +
396                     "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\">");
397             writer.write("<soap:Header>");
398             if (serverVersion != null) {
399                 writer.write("<t:RequestServerVersion Version=\"");
400                 writer.write(serverVersion);
401                 writer.write("\"/>");
402             }
403             if (timezoneContext != null) {
404                 writer.write("<t:TimeZoneContext><t:TimeZoneDefinition Id=\"");
405                 writer.write(timezoneContext);
406                 writer.write("\"/></t:TimeZoneContext>");
407             }
408             writer.write("</soap:Header>");
409 
410             writer.write("<soap:Body>");
411             writer.write("<m:");
412             writer.write(methodName);
413             if (traversal != null) {
414                 traversal.write(writer);
415             }
416             if (deleteType != null) {
417                 deleteType.write(writer);
418             }
419             if (methodOptions != null) {
420                 for (AttributeOption attributeOption : methodOptions) {
421                     attributeOption.write(writer);
422                 }
423             }
424             writer.write(">");
425             writeSoapBody(writer);
426             writer.write("</m:");
427             writer.write(methodName);
428             writer.write(">");
429             writer.write("</soap:Body>" +
430                     "</soap:Envelope>");
431             writer.flush();
432         } catch (IOException e) {
433             throw new RuntimeException(e);
434         }
435         return baos.toByteArray();
436     }
437 
438     protected void writeSoapBody(Writer writer) throws IOException {
439         startChanges(writer);
440         writeShape(writer);
441         writeIndexedPageView(writer);
442         writeRestriction(writer);
443         writeSortOrder(writer);
444         writeParentFolderId(writer);
445         writeToFolderId(writer);
446         writeItemId(writer);
447         writeParentItemId(writer);
448         writeAttachments(writer);
449         writeAttachmentId(writer);
450         writeFolderId(writer);
451         writeSavedItemFolderId(writer);
452         writeItem(writer);
453         writeUpdates(writer);
454         writeUnresolvedEntry(writer);
455         endChanges(writer);
456     }
457 
458 
459     protected void writeIndexedPageView(Writer writer) throws IOException {
460         if (maxCount > 0) {
461             writer.write("<m:IndexedPage" + itemType + "View MaxEntriesReturned=\"");
462             writer.write(String.valueOf(maxCount));
463             writer.write("\" Offset=\"");
464             writer.write(String.valueOf(offset));
465             writer.write("\" BasePoint=\"Beginning\"/>");
466 
467         }
468     }
469 
470     protected void writeAttachmentId(Writer writer) throws IOException {
471         if (attachmentId != null) {
472             if ("CreateAttachment".equals(methodName)) {
473                 writer.write("<m:AttachmentShape>");
474                 writer.write("<t:IncludeMimeContent>true</t:IncludeMimeContent>");
475                 writer.write("</m:AttachmentShape>");
476             }
477             writer.write("<m:AttachmentIds>");
478             writer.write("<t:AttachmentId Id=\"");
479             writer.write(attachmentId);
480             writer.write("\"/>");
481             writer.write("</m:AttachmentIds>");
482         }
483     }
484 
485     protected void writeAttachments(Writer writer) throws IOException {
486         if (attachment != null) {
487             writer.write("<m:Attachments>");
488             attachment.write(writer);
489             writer.write("</m:Attachments>");
490         }
491     }
492 
493     /**
494      * Get Exchange server version, Exchange2013, Exchange2010 or Exchange2007_SP1
495      *
496      * @return server version
497      */
498     public String getServerVersion() {
499         return serverVersion;
500     }
501 
502     /**
503      * Set Exchange server version, Exchange2010 or Exchange2007_SP1
504      *
505      * @param serverVersion server version
506      */
507     public void setServerVersion(String serverVersion) {
508         this.serverVersion = serverVersion;
509     }
510 
511     /**
512      * Set Exchange timezone context
513      *
514      * @param timezoneContext user timezone context
515      */
516     public void setTimezoneContext(String timezoneContext) {
517         this.timezoneContext = timezoneContext;
518     }
519 
520     /**
521      * Meeting attendee object
522      */
523     public static class Attendee {
524         /**
525          * attendee role
526          */
527         public String role;
528         /**
529          * attendee email address
530          */
531         public String email;
532         /**
533          * attendee participation status
534          */
535         public String partstat;
536         /**
537          * attendee fullname
538          */
539         public String name;
540     }
541 
542     /**
543      * Recurring event occurrence
544      */
545     public static class Occurrence {
546         /**
547          * Original occurence start date
548          */
549         public String originalStart;
550 
551         /**
552          * Occurence itemid
553          */
554         public ItemId itemId;
555     }
556 
557     /**
558      * Item
559      */
560     public static class Item extends HashMap<String, String> {
561         /**
562          * Item type.
563          */
564         public String type;
565         protected byte[] mimeContent;
566         protected List<FieldUpdate> fieldUpdates;
567         protected List<FileAttachment> attachments;
568         protected List<Attendee> attendees;
569         protected final List<String> fieldNames = new ArrayList<>();
570         protected List<Occurrence> occurrences;
571         protected List<String> members;
572         protected ItemId referenceItemId;
573 
574         @Override
575         public String toString() {
576             return "type: " + type + ' ' + super.toString();
577         }
578 
579         @Override
580         public String put(String key, String value) {
581             if (value != null) {
582                 if (get(key) == null) {
583                     fieldNames.add(key);
584                 }
585                 return super.put(key, value);
586             } else {
587                 return null;
588             }
589         }
590 
591         /**
592          * Write XML content to writer.
593          *
594          * @param writer writer
595          * @throws IOException on error
596          */
597         public void write(Writer writer) throws IOException {
598             writer.write("<t:");
599             writer.write(type);
600             writer.write(">");
601             if (mimeContent != null) {
602                 writer.write("<t:MimeContent>");
603                 for (byte c : mimeContent) {
604                     writer.write(c);
605                 }
606                 writer.write("</t:MimeContent>");
607             }
608             // write ordered fields
609             for (String key : fieldNames) {
610                 if ("MeetingTimeZone".equals(key)) {
611                     writer.write("<t:MeetingTimeZone TimeZoneName=\"");
612                     writer.write(StringUtil.xmlEncodeAttribute(get(key)));
613                     writer.write("\"></t:MeetingTimeZone>");
614                 } else if ("StartTimeZone".equals(key)) {
615                     writer.write("<t:StartTimeZone Id=\"");
616                     writer.write(StringUtil.xmlEncodeAttribute(get(key)));
617                     writer.write("\"></t:StartTimeZone>");
618                 } else if ("Body".equals(key)) {
619                     writer.write("<t:Body BodyType=\"Text\">");
620                     writer.write(StringUtil.xmlEncode(get(key)));
621                     writer.write("</t:Body>");
622                 } else {
623                     writer.write("<t:");
624                     writer.write(key);
625                     writer.write(">");
626                     writer.write(StringUtil.xmlEncode(get(key)));
627                     writer.write("</t:");
628                     writer.write(key);
629                     writer.write(">");
630                 }
631             }
632             if (fieldUpdates != null) {
633                 for (FieldUpdate fieldUpdate : fieldUpdates) {
634                     fieldUpdate.write(null, writer);
635                 }
636             }
637             if (referenceItemId != null) {
638                 referenceItemId.write(writer);
639             }
640             writer.write("</t:");
641             writer.write(type);
642             writer.write(">");
643         }
644 
645         /**
646          * Field updates.
647          *
648          * @param fieldUpdates field updates
649          */
650         public void setFieldUpdates(List<FieldUpdate> fieldUpdates) {
651             this.fieldUpdates = fieldUpdates;
652         }
653 
654         /**
655          * Get property value as int
656          *
657          * @param key property response name
658          * @return property value
659          */
660         public int getInt(String key) {
661             int result = 0;
662             String value = get(key);
663             if (value != null && !value.isEmpty()) {
664                 result = Integer.parseInt(value);
665             }
666             return result;
667         }
668 
669         /**
670          * Get property value as long
671          *
672          * @param key property response name
673          * @return property value
674          */
675         public long getLong(String key) {
676             long result = 0;
677             String value = get(key);
678             if (value != null && !value.isEmpty()) {
679                 result = Long.parseLong(value);
680             }
681             return result;
682         }
683 
684 
685         /**
686          * Get property value as boolean
687          *
688          * @param key property response name
689          * @return property value
690          */
691         public boolean getBoolean(String key) {
692             boolean result = false;
693             String value = get(key);
694             if (value != null && !value.isEmpty()) {
695                 result = Boolean.parseBoolean(value);
696             }
697             return result;
698         }
699 
700         /**
701          * Get file attachment by file name
702          *
703          * @param attachmentName attachment name
704          * @return attachment
705          */
706         public FileAttachment getAttachmentByName(String attachmentName) {
707             FileAttachment result = null;
708             if (attachments != null) {
709                 for (FileAttachment fileAttachment : attachments) {
710                     if (attachmentName.equals(fileAttachment.name)) {
711                         result = fileAttachment;
712                         break;
713                     }
714                 }
715             }
716             return result;
717         }
718 
719         /**
720          * Get all attendees.
721          *
722          * @return all attendees
723          */
724         public List<Attendee> getAttendees() {
725             return attendees;
726         }
727 
728         /**
729          * Add attendee.
730          *
731          * @param attendee attendee object
732          */
733         public void addAttendee(Attendee attendee) {
734             if (attendees == null) {
735                 attendees = new ArrayList<>();
736             }
737             attendees.add(attendee);
738         }
739 
740         /**
741          * Add occurrence.
742          *
743          * @param occurrence event occurence
744          */
745         public void addOccurrence(Occurrence occurrence) {
746             if (occurrences == null) {
747                 occurrences = new ArrayList<>();
748             }
749             occurrences.add(occurrence);
750         }
751 
752         /**
753          * Get occurences.
754          *
755          * @return event occurences
756          */
757         public List<Occurrence> getOccurrences() {
758             return occurrences;
759         }
760 
761         /**
762          * Add member.
763          *
764          * @param member list member
765          */
766         public void addMember(String member) {
767             if (members == null) {
768                 members = new ArrayList<>();
769             }
770             members.add(member);
771         }
772 
773         /**
774          * Get members.
775          *
776          * @return event members
777          */
778         public List<String> getMembers() {
779             return members;
780         }
781     }
782 
783     /**
784      * Check method success.
785      *
786      * @throws EWSException on error
787      */
788     public void checkSuccess() throws EWSException {
789         if ("The server cannot service this request right now. Try again later.".equals(errorDetail)) {
790             throw new EWSThrottlingException(errorDetail);
791         }
792         if (errorDetail != null && (!"ErrorAccessDenied".equals(errorDetail)
793                     && !"ErrorMailRecipientNotFound".equals(errorDetail)
794                     && !"ErrorItemNotFound".equals(errorDetail)
795                     && !"ErrorCalendarOccurrenceIsDeletedFromRecurrence".equals(errorDetail)
796             )) {
797                 throw new EWSException(errorDetail
798                         + ' ' + ((errorDescription != null) ? errorDescription : "")
799                         + ' ' + ((errorValue != null) ? errorValue : "")
800                         + "\n request: " + new String(generateSoapEnvelope(), StandardCharsets.UTF_8));
801 
802         }
803         if (getStatusCode() == HttpStatus.SC_BAD_REQUEST || getStatusCode() == HttpStatus.SC_INSUFFICIENT_STORAGE) {
804             throw new EWSException(response.getStatusLine().getReasonPhrase());
805         }
806     }
807 
808     public int getStatusCode() {
809         if ("ErrorAccessDenied".equals(errorDetail)) {
810             return HttpStatus.SC_FORBIDDEN;
811         } else if ("ErrorItemNotFound".equals(errorDetail)) {
812             return HttpStatus.SC_NOT_FOUND;
813         } else {
814             return response.getStatusLine().getStatusCode();
815         }
816     }
817 
818     /**
819      * Get response items.
820      *
821      * @return response items
822      * @throws EWSException on error
823      */
824     public List<Item> getResponseItems() throws EWSException {
825         checkSuccess();
826         if (responseItems != null) {
827             return responseItems;
828         } else {
829             return new ArrayList<>();
830         }
831     }
832 
833     /**
834      * Get single response item.
835      *
836      * @return response item
837      * @throws EWSException on error
838      */
839     public Item getResponseItem() throws EWSException {
840         checkSuccess();
841         if (responseItems != null && !responseItems.isEmpty()) {
842             return responseItems.get(0);
843         } else {
844             return null;
845         }
846     }
847 
848     /**
849      * Get response mime content.
850      *
851      * @return mime content
852      * @throws EWSException on error
853      */
854     public byte[] getMimeContent() throws EWSException {
855         checkSuccess();
856         Item responseItem = getResponseItem();
857         if (responseItem != null) {
858             return responseItem.mimeContent;
859         } else {
860             return null;
861         }
862     }
863 
864     protected String handleTag(XMLStreamReader reader, String localName) throws XMLStreamException {
865         StringBuilder result = null;
866         int event = reader.getEventType();
867         if (event == XMLStreamConstants.START_ELEMENT && localName.equals(reader.getLocalName())) {
868             result = new StringBuilder();
869             while (reader.hasNext() &&
870                     !((event == XMLStreamConstants.END_ELEMENT && localName.equals(reader.getLocalName())))) {
871                 event = reader.next();
872                 if (event == XMLStreamConstants.CHARACTERS) {
873                     result.append(reader.getText());
874                 } else if ("MessageXml".equals(localName) && event == XMLStreamConstants.START_ELEMENT) {
875                     String attributeValue = null;
876                     for (int i = 0; i < reader.getAttributeCount(); i++) {
877                         if (result.length() > 0) {
878                             result.append(", ");
879                         }
880                         attributeValue = reader.getAttributeValue(i);
881                         result.append(reader.getAttributeLocalName(i)).append(": ").append(reader.getAttributeValue(i));
882                     }
883                     // catch BackOffMilliseconds value
884                     if ("BackOffMilliseconds".equals(attributeValue)) {
885                         try {
886                             backOffMilliseconds = Long.parseLong(reader.getElementText());
887                         } catch (NumberFormatException e) {
888                             LOGGER.error("Unable to parse BackOffMilliseconds");
889                         }
890                     }
891                 }
892             }
893         }
894         if (result != null && result.length() > 0) {
895             return result.toString();
896         } else {
897             return null;
898         }
899     }
900 
901     protected void handleErrors(XMLStreamReader reader) throws XMLStreamException {
902         String result = handleTag(reader, "ResponseCode");
903         // store error description
904         String messageText = handleTag(reader, "MessageText");
905         if (messageText != null) {
906             errorDescription = messageText;
907         }
908         String messageXml = handleTag(reader, "MessageXml");
909         if (messageXml != null) {
910             // contains BackOffMilliseconds on ErrorServerBusy
911             errorValue = messageXml;
912         }
913         if (errorDetail == null && result != null
914                 && !"NoError".equals(result)
915                 && !"ErrorNameResolutionMultipleResults".equals(result)
916                 && !"ErrorNameResolutionNoResults".equals(result)
917                 && !"ErrorFolderExists".equals(result)
918         ) {
919             errorDetail = result;
920         }
921         if (XMLStreamUtil.isStartTag(reader, "faultstring")) {
922             errorDetail = XMLStreamUtil.getElementText(reader);
923         }
924     }
925 
926     protected Item handleItem(XMLStreamReader reader) throws XMLStreamException {
927         Item responseItem = new Item();
928         responseItem.type = reader.getLocalName();
929         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, responseItem.type)) {
930             reader.next();
931             if (XMLStreamUtil.isStartTag(reader)) {
932                 String tagLocalName = reader.getLocalName();
933                 String value = null;
934                 if ("ExtendedProperty".equals(tagLocalName)) {
935                     addExtendedPropertyValue(reader, responseItem);
936                 } else if ("Members".equals(tagLocalName)) {
937                     handleMembers(reader, responseItem);
938                 } else if (tagLocalName.endsWith("MimeContent")) {
939                     handleMimeContent(reader, responseItem);
940                 } else if ("Attachments".equals(tagLocalName)) {
941                     responseItem.attachments = handleAttachments(reader);
942                 } else if ("EmailAddresses".equals(tagLocalName)) {
943                     handleEmailAddresses(reader, responseItem);
944                 } else if ("RequiredAttendees".equals(tagLocalName) || "OptionalAttendees".equals(tagLocalName)) {
945                     handleAttendees(reader, responseItem, tagLocalName);
946                 } else if ("ModifiedOccurrences".equals(tagLocalName)) {
947                     handleModifiedOccurrences(reader, responseItem);
948                 } else {
949                     if (tagLocalName.endsWith("Id")) {
950                         value = getAttributeValue(reader, "Id");
951                         // get change key
952                         responseItem.put("ChangeKey", getAttributeValue(reader, "ChangeKey"));
953                     }
954                     if (value == null) {
955                         value = getTagContent(reader);
956                     }
957                     if (value != null) {
958                         responseItem.put(tagLocalName, value);
959                     }
960                 }
961             }
962         }
963         return responseItem;
964     }
965 
966     protected void handleEmailAddresses(XMLStreamReader reader, Item item) throws XMLStreamException {
967         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "EmailAddresses"))) {
968             reader.next();
969             if (XMLStreamUtil.isStartTag(reader)) {
970                 String tagLocalName = reader.getLocalName();
971                 if ("Entry".equals(tagLocalName)) {
972                     item.put(reader.getAttributeValue(null, "Key"), XMLStreamUtil.getElementText(reader));
973                 }
974             }
975         }
976     }
977 
978     protected void handleAttendees(XMLStreamReader reader, Item item, String attendeeType) throws XMLStreamException {
979         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, attendeeType))) {
980             reader.next();
981             if (XMLStreamUtil.isStartTag(reader)) {
982                 String tagLocalName = reader.getLocalName();
983                 if ("Attendee".equals(tagLocalName)) {
984                     handleAttendee(reader, item, attendeeType);
985                 }
986             }
987         }
988     }
989 
990     protected void handleModifiedOccurrences(XMLStreamReader reader, Item item) throws XMLStreamException {
991         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "ModifiedOccurrences"))) {
992             reader.next();
993             if (XMLStreamUtil.isStartTag(reader)) {
994                 String tagLocalName = reader.getLocalName();
995                 if ("Occurrence".equals(tagLocalName)) {
996                     handleOccurrence(reader, item);
997                 }
998             }
999         }
1000     }
1001 
1002     protected void handleOccurrence(XMLStreamReader reader, Item item) throws XMLStreamException {
1003         Occurrence occurrence = new Occurrence();
1004         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Occurrence"))) {
1005             reader.next();
1006             if (XMLStreamUtil.isStartTag(reader)) {
1007                 String tagLocalName = reader.getLocalName();
1008                 if ("ItemId".equals(tagLocalName)) {
1009                     occurrence.itemId = new ItemId("ItemId", getAttributeValue(reader, "Id"), getAttributeValue(reader, "ChangeKey"));
1010                 }
1011                 if ("OriginalStart".equals(tagLocalName)) {
1012                     occurrence.originalStart = XMLStreamUtil.getElementText(reader);
1013                 }
1014             }
1015         }
1016         item.addOccurrence(occurrence);
1017     }
1018 
1019     protected void handleMembers(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
1020         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "Members")) {
1021             reader.next();
1022             if (XMLStreamUtil.isStartTag(reader)) {
1023                 String tagLocalName = reader.getLocalName();
1024                 if ("Member".equals(tagLocalName)) {
1025                     handleMember(reader, responseItem);
1026                 }
1027             }
1028         }
1029     }
1030 
1031     protected void handleMember(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
1032         String member = null;
1033         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "Member")) {
1034             reader.next();
1035             if (XMLStreamUtil.isStartTag(reader)) {
1036                 String tagLocalName = reader.getLocalName();
1037                 if ("EmailAddress".equals(tagLocalName) && member == null) {
1038                     member = "mailto:" + XMLStreamUtil.getElementText(reader);
1039                 }
1040             }
1041         }
1042         if (member != null) {
1043             responseItem.addMember(member);
1044         }
1045     }
1046 
1047     /**
1048      * Convert response type to partstat value
1049      *
1050      * @param responseType response type
1051      * @return partstat value
1052      */
1053     public static String responseTypeToPartstat(String responseType) {
1054         if ("Accept".equals(responseType) || "Organizer".equals(responseType)) {
1055             return "ACCEPTED";
1056         } else if ("Tentative".equals(responseType)) {
1057             return "TENTATIVE";
1058         } else if ("Decline".equals(responseType)) {
1059             return "DECLINED";
1060         } else {
1061             return "NEEDS-ACTION";
1062         }
1063     }
1064 
1065     protected void handleAttendee(XMLStreamReader reader, Item item, String attendeeType) throws XMLStreamException {
1066         Attendee attendee = new Attendee();
1067         if ("RequiredAttendees".equals(attendeeType)) {
1068             attendee.role = "REQ-PARTICIPANT";
1069         } else {
1070             attendee.role = "OPT-PARTICIPANT";
1071         }
1072         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Attendee"))) {
1073             reader.next();
1074             if (XMLStreamUtil.isStartTag(reader)) {
1075                 String tagLocalName = reader.getLocalName();
1076                 if ("EmailAddress".equals(tagLocalName)) {
1077                     attendee.email = reader.getElementText();
1078                 } else if ("Name".equals(tagLocalName)) {
1079                     attendee.name = XMLStreamUtil.getElementText(reader);
1080                 } else if ("ResponseType".equals(tagLocalName)) {
1081                     String responseType = XMLStreamUtil.getElementText(reader);
1082                     attendee.partstat = responseTypeToPartstat(responseType);
1083                 }
1084             }
1085         }
1086         item.addAttendee(attendee);
1087     }
1088 
1089     protected List<FileAttachment> handleAttachments(XMLStreamReader reader) throws XMLStreamException {
1090         List<FileAttachment> attachments = new ArrayList<>();
1091         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Attachments"))) {
1092             reader.next();
1093             if (XMLStreamUtil.isStartTag(reader)) {
1094                 String tagLocalName = reader.getLocalName();
1095                 if ("FileAttachment".equals(tagLocalName)) {
1096                     attachments.add(handleFileAttachment(reader));
1097                 }
1098             }
1099         }
1100         return attachments;
1101     }
1102 
1103     protected FileAttachment handleFileAttachment(XMLStreamReader reader) throws XMLStreamException {
1104         FileAttachment fileAttachment = new FileAttachment();
1105         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "FileAttachment"))) {
1106             reader.next();
1107             if (XMLStreamUtil.isStartTag(reader)) {
1108                 String tagLocalName = reader.getLocalName();
1109                 if ("AttachmentId".equals(tagLocalName)) {
1110                     fileAttachment.attachmentId = getAttributeValue(reader, "Id");
1111                 } else if ("Name".equals(tagLocalName)) {
1112                     fileAttachment.name = getTagContent(reader);
1113                 } else if ("ContentType".equals(tagLocalName)) {
1114                     fileAttachment.contentType = getTagContent(reader);
1115                 }
1116             }
1117         }
1118         return fileAttachment;
1119     }
1120 
1121 
1122     protected void handleMimeContent(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
1123         if (reader instanceof TypedXMLStreamReader) {
1124             // Stax2 parser: use enhanced base64 conversion
1125             responseItem.mimeContent = ((TypedXMLStreamReader) reader).getElementAsBinary();
1126         } else {
1127             // failover: slow and memory consuming conversion
1128             responseItem.mimeContent = Base64.decodeBase64(reader.getElementText().getBytes(StandardCharsets.US_ASCII));
1129         }
1130     }
1131 
1132     protected void addExtendedPropertyValue(XMLStreamReader reader, Item item) throws XMLStreamException {
1133         String propertyTag = null;
1134         String propertyValue = null;
1135         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "ExtendedProperty"))) {
1136             reader.next();
1137             if (XMLStreamUtil.isStartTag(reader)) {
1138                 String tagLocalName = reader.getLocalName();
1139                 if ("ExtendedFieldURI".equals(tagLocalName)) {
1140                     propertyTag = getAttributeValue(reader, "PropertyTag");
1141                     // property name is in PropertyId or PropertyName with DistinguishedPropertySetId
1142                     if (propertyTag == null) {
1143                         propertyTag = getAttributeValue(reader, "PropertyId");
1144                     }
1145                     if (propertyTag == null) {
1146                         propertyTag = getAttributeValue(reader, "PropertyName");
1147                     }
1148                 } else if ("Value".equals(tagLocalName)) {
1149                     propertyValue = XMLStreamUtil.getElementText(reader);
1150                 } else if ("Values".equals(tagLocalName)) {
1151                     StringBuilder buffer = new StringBuilder();
1152                     while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Values"))) {
1153                         reader.next();
1154                         if (XMLStreamUtil.isStartTag(reader)) {
1155 
1156                             if (buffer.length() > 0) {
1157                                 buffer.append(',');
1158                             }
1159                             String singleValue = XMLStreamUtil.getElementText(reader);
1160                             if (singleValue != null) {
1161                                 buffer.append(singleValue);
1162                             }
1163                         }
1164                     }
1165                     propertyValue = buffer.toString();
1166                 }
1167             }
1168         }
1169         if ((propertyTag != null) && (propertyValue != null)) {
1170             item.put(propertyTag, propertyValue);
1171         }
1172     }
1173 
1174     protected String getTagContent(XMLStreamReader reader) throws XMLStreamException {
1175         String tagLocalName = reader.getLocalName();
1176         while (reader.hasNext() && !(reader.getEventType() == XMLStreamConstants.END_ELEMENT)) {
1177             reader.next();
1178             if (reader.getEventType() == XMLStreamConstants.CHARACTERS) {
1179                 return reader.getText();
1180             }
1181         }
1182         // empty tag
1183         if (reader.hasNext()) {
1184             return null;
1185         } else {
1186             throw new XMLStreamException("End element for " + tagLocalName + " not found");
1187         }
1188     }
1189 
1190     protected String getAttributeValue(XMLStreamReader reader, String attributeName) {
1191         for (int i = 0; i < reader.getAttributeCount(); i++) {
1192             if (attributeName.equals(reader.getAttributeLocalName(i))) {
1193                 return reader.getAttributeValue(i);
1194             }
1195         }
1196         return null;
1197     }
1198 
1199     @Override
1200     public EWSMethod handleResponse(HttpResponse response) {
1201         this.response = response;
1202         org.apache.http.Header contentTypeHeader = response.getFirstHeader("Content-Type");
1203         if (contentTypeHeader != null && "text/xml; charset=utf-8".equals(contentTypeHeader.getValue())) {
1204             try (
1205                     InputStream inputStream = response.getEntity().getContent()
1206             ) {
1207                 if (HttpClientAdapter.isGzipEncoded(response)) {
1208                     processResponseStream(new GZIPInputStream(inputStream));
1209                 } else {
1210                     processResponseStream(inputStream);
1211                 }
1212             } catch (IOException e) {
1213                 LOGGER.error("Error while parsing soap response: " + e, e);
1214             }
1215         }
1216         return this;
1217     }
1218 
1219     protected void processResponseStream(InputStream inputStream) {
1220         responseItems = new ArrayList<>();
1221         XMLStreamReader reader = null;
1222         try {
1223             inputStream = new FilterInputStream(inputStream) {
1224                 int totalCount;
1225                 int lastLogCount;
1226 
1227                 @Override
1228                 public int read(byte[] buffer, int offset, int length) throws IOException {
1229                     int count = super.read(buffer, offset, length);
1230                     totalCount += count;
1231                     if (totalCount - lastLogCount > 1024 * 128) {
1232                         DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), EWSMethod.this.getURI()));
1233                         DavGatewayTray.switchIcon();
1234                         lastLogCount = totalCount;
1235                     }
1236                     /*if (count > 0 && LOGGER.isDebugEnabled()) {
1237                         LOGGER.debug(new String(buffer, offset, count, "UTF-8"));
1238                     }*/
1239                     return count;
1240                 }
1241             };
1242             reader = XMLStreamUtil.createXMLStreamReader(inputStream);
1243             while (reader.hasNext()) {
1244                 reader.next();
1245                 handleErrors(reader);
1246                 if (serverVersion == null && XMLStreamUtil.isStartTag(reader, "ServerVersionInfo")) {
1247                     String majorVersion = getAttributeValue(reader, "MajorVersion");
1248                     String minorVersion = getAttributeValue(reader, "MinorVersion");
1249                     if ("15".equals(majorVersion)) {
1250                         if ("0".equals(minorVersion)) {
1251                             serverVersion = "Exchange2013";
1252                         } else {
1253                             serverVersion = "Exchange2013_SP1";
1254                         }
1255                     } else if ("14".equals(majorVersion)) {
1256                         if ("0".equals(minorVersion)) {
1257                             serverVersion = "Exchange2010";
1258                         } else {
1259                             serverVersion = "Exchange2010_SP1";
1260                         }
1261                     } else {
1262                         serverVersion = "Exchange2007_SP1";
1263                     }
1264                 } else if (XMLStreamUtil.isStartTag(reader, "RootFolder")) {
1265                     includesLastItemInRange = "true".equals(reader.getAttributeValue(null, "IncludesLastItemInRange"));
1266                 } else if (XMLStreamUtil.isStartTag(reader, responseCollectionName)) {
1267                     handleItems(reader);
1268                 } else {
1269                     handleCustom(reader);
1270                 }
1271             }
1272         } catch (XMLStreamException e) {
1273             errorDetail = e.getMessage();
1274             LOGGER.error("Error while parsing soap response: " + e, e);
1275             if (reader != null) {
1276                 try {
1277                     String content = reader.getText();
1278                     if (content != null && content.length() > 4096) {
1279                         content = content.substring(0, 4096)+" ...";
1280                     }
1281                     LOGGER.debug("Current text: " + content);
1282                 } catch (IllegalStateException ise) {
1283                     LOGGER.error(e + " " + e.getMessage());
1284                 }
1285             }
1286         }
1287         if (errorDetail != null) {
1288             LOGGER.debug(errorDetail);
1289         }
1290     }
1291 
1292     @SuppressWarnings({"NoopMethodInAbstractClass"})
1293     protected void handleCustom(XMLStreamReader reader) throws XMLStreamException {
1294         // override to handle custom content
1295     }
1296 
1297     private void handleItems(XMLStreamReader reader) throws XMLStreamException {
1298         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, responseCollectionName)) {
1299             reader.next();
1300             if (XMLStreamUtil.isStartTag(reader)) {
1301                 responseItems.add(handleItem(reader));
1302             }
1303         }
1304 
1305     }
1306 
1307 }