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