Codebase list davmail / fcdce49
New upstream version 6.0.0.3375 Alexandre Rossi 2 years ago
61 changed file(s) with 1940 addition(s) and 9947 deletion(s). Raw diff Collapse all Expand all
77 [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=mguessan_davmail&metric=alert_status)](https://sonarcloud.io/dashboard/index/mguessan_davmail)
88 [![SonarCloud Bugs](https://sonarcloud.io/api/project_badges/measure?project=mguessan_davmail&metric=bugs)](https://sonarcloud.io/dashboard/index/mguessan_davmail)
99 [![SonarCloud Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=mguessan_davmail&metric=vulnerabilities)](https://sonarcloud.io/dashboard/index/mguessan_davmail)
10
11 :warning: **HttpClient 4 migration in progress**: Trunk will not be as stable as usual
1012
1113 Ever wanted to get rid of Outlook ? DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP gateway allowing users to use any mail client with Exchange, even from the internet through Outlook Web Access on any platform, tested on MacOSX, Linux and Windows
1214
2729 * Windows setup [davmail-5.5.1-trunk-setup.exe](https://ci.appveyor.com/api/projects/mguessan/davmail/artifacts/dist%2Fdavmail-5.5.1-trunk-setup.exe?job=Environment%3A%20JAVA_HOME%3DC%3A%5CProgram%20Files%5CJava%5Cjdk1.8.0)
2830 * Windows 64 bits setup [davmail-5.5.1-trunk-setup64.exe](https://ci.appveyor.com/api/projects/mguessan/davmail/artifacts/dist%2Fdavmail-5.5.1-trunk-setup64.exe?job=Environment%3A%20JAVA_HOME%3DC%3A%5CProgram%20Files%5CJava%5Cjdk1.8.0)
2931 * Windows noinstall package [davmail-5.5.1-trunk-windows-noinstall.zip](https://ci.appveyor.com/api/projects/mguessan/davmail/artifacts/dist%2Fdavmail-5.5.1-trunk-windows-noinstall.zip?job=Environment%3A%20JAVA_HOME%3DC%3A%5CProgram%20Files%5CJava%5Cjdk1.8.0)
32 * Windows standalone (with embedded Azul JRE-FX) package [davmail-5.5.1-trunk-windows-standalone.zip](https://ci.appveyor.com/api/projects/mguessan/davmail/artifacts/dist%2Fdavmail-5.5.1-trunk-windows-standalone.zip?job=Environment%3A%20JAVA_HOME%3DC%3A%5CProgram%20Files%5CJava%5Cjdk1.8.0)
3033
3134 * Platform independent package [davmail-5.5.1-trunk.zip](https://ci.appveyor.com/api/projects/mguessan/davmail/artifacts/dist%2Fdavmail-5.5.1-trunk.zip?job=Environment%3A%20JAVA_HOME%3DC%3A%5CProgram%20Files%5CJava%5Cjdk1.8.0)
3235
0 ## DavMail 6.0.0 2021-07-05
1 First major release in a long time, main change is switch from HttpClient 3 to 4, please report any regression related to this major rewrite.
2 DavMail now supports more O365 configurations, including access to client certificate to validate device trust.
3 O365 refresh tokens can now be stored securely in a separate (writable) file.
4 On Linux, in order to ensure the right java version is used, a command line option to download latest Azul JRE with OpenJFX support was added,
5 on windows a standalone package contains Azul JRE FX 15, on OSX updated universalJavaApplicationStub to latest version.
6
7 ### OSX:
8 - OSX: completely drop Growl support
9 - OSX: prepare possible path for an embedded jre mode
10 - OSX: update universalJavaApplicationStub to latest version from https://github.com/tofi86/universalJavaApplicationStub/blob/master/src/universalJavaApplicationStub
11
12 ### Documentation:
13 - Doc: merge Clarify the usage of imapIdleDelay https://github.com/mguessan/davmail/pull/116
14 - Doc: add comment on IDLE and timeout setting
15 - Doc: link to standalone windows package
16 - Doc: fix Zulu link
17 - Doc: remove references to Java 6 in documentation
18
19 ### Build:
20 - Appveyor: update ant
21 - Appveyor: build with jdk15
22 - Appveyor: purge artifacts for all builds except jdk 8
23 - Build: run Sonar with JDK 11
24 - Update junit to 4.13.1 in Maven
25 - Update junit to 4.13.1
26
27 ### Linux:
28 - Linux: Experimental: download Azul JRE FX with command 'davmail azul'
29 - Linux: merge https://github.com/mguessan/davmail/pull/133 Linux Gnome Desktop: fix systray support
30 - Linux: Update service file to allow 0-1023 ports binding (https://github.com/mguessan/davmail/pull/117)
31
32
33 ### Windows:
34 - Windows: switch standalone jre to Azul FX 15
35 - Windows: create a standalone package with Azul JRE FX in order to have a working O365InteractiveAuthenticator
36 - Winrun4J: prefer embedded VM for standalone package and export sun.net.www.protocol.https
37 - Winrun4J: update binaries
38 - Winrun4J: prepare standalone configuration
39 - Windows: update winrun4j config to require Java >= 8
40
41 ### IMAP:
42 - IMAP: fix thread handling from audit
43 - IMAP: Compute body part size with failover
44
45 ### O365:
46 - O365: log token file creation
47 - O365: cleanup from audit
48 - O365: Add davmail.oauth.tokenFilePath to sample properties file
49 - O365: disable HTTP/2 loader on Java 14 and later to enable custom socket factory
50 - O365: allow user agent override in O365InteractiveAuthenticator, switch default user agent to latest Edge
51 - O365: with Java 15 url with code returns as CANCELLED
52 - O365: MSCAPI and Java version 13 or higher required to access TPM protected client certificate on Windows
53 - O365: merge first commit from https://github.com/mguessan/davmail/pull/134/ OAuth via ADFS with MFA support
54 - O365: fix store refreshToken call
55 - O365: introduce davmail.oauth.tokenFilePath setting to store Oauth tokens in a separate file
56 - O365: switch to try with resource style
57 - Drop explicit dependency to netscape.javascript package in O365InteractiveJSLogger
58 - O365: follow redirects on ADFS authentication
59
60 ### HC4:
61 - Refactor ExchangeSessionFactory, create HttpClientAdapter in session
62 - HC4: update winrun4j binaries
63 - HC4: drop HttpClient 3 dependency in Maven, winrun4j binaries and nsi setup
64 - HC4: drop remaining HttpClient 3 classes
65 - HC4: drop DavMailCookieSpec and DavGatewaySSLProtocolSocketFactory (merge in SSLProtocolSocketFactory)
66 - HC4: drop DavGatewayHttpClientFacade and RestMethod
67 - HC4: default to Edge user agent
68 - HC4: Do not enable NTLM in Kerberos mode
69 - HC4: switch checkConfig to HttpClient 4
70 - HC4: merge HC4DavExchangeSession to DavExchangeSession
71 - HC4: cleanup HC4ExchangeFormAuthenticator references
72 - HC4: merge HC4ExchangeFormAuthenticator to ExchangeFormAuthenticator, extend authenticator interface to return HttpClientAdapter, switch to DavExchangeSession
73 - HC4: switch O365 authenticators test code to HttpClient 4
74 - HC4: adjust CreateItemMethod chunked call
75 - HC4: switch ExchangeSessionFactory to HttpClient 4
76 - HC4: add a warning about HttpClient 4 migration
77 - HC4: Enable ssl logging in addition to wire with HttpClient 4
78 - HC4: switch EWS implementation to HttpClient 4
79
80 ### EWS:
81 - EWS: improve isItemId detection to match base 64 encoded id
82 - EWS: drop NTLM as a failover logic
83 - EWS: cleanup unused code now that we have a reliable way to retrieve email address with ConvertId
84 - EWS: drop property davmail.caldavRealUpdate, no longer used
85 - EWS: Improved uid handling from audit
86 - EWS: Enable Woodstox workaround for malformed xml with latest Woodstox version
87
88 ### Enhancements:
89 - Clear session pool on DavMail restart
90 - Upgrade to Woodstox 6.2.0 as it's now available on debian, drop Woodstox patched StreamScanner
91
92 ### Caldav:
93 - Caldav: merge https://github.com/mguessan/davmail/pull/139 Fix missing XML encode
94 - Caldav: use Exchange timezone to compute event time in test case
95 - Caldav: create test cases for recurring events
96
97
098 ## DavMail 5.5.1 2019-04-19
199 Fix regression on domain\username authentication over IMAP and some cleanup
2100
55 matrix:
66 - JAVA_HOME: C:\Program Files\Java\jdk9
77 - JAVA_HOME: C:\Program Files\Java\jdk1.8.0
8 - JAVA_HOME: C:\Program Files\Java\jdk10
98 - JAVA_HOME: C:\Program Files\Java\jdk11
10 - JAVA_HOME: C:\Program Files\Java\jdk12
11 - JAVA_HOME: C:\Program Files\Java\jdk13
9 - JAVA_HOME: C:\Program Files\Java\jdk15
1210
1311 install:
1412 - ps: |
1513 Add-Type -AssemblyName System.IO.Compression.FileSystem
16 if (!(Test-Path -Path "C:\ant\apache-ant-1.10.7" )) {
14 if (!(Test-Path -Path "C:\ant\apache-ant-1.10.10" )) {
1715 (new-object System.Net.WebClient).DownloadFile(
18 'https://www-eu.apache.org/dist//ant/binaries/apache-ant-1.10.7-bin.zip',
16 'https://www-eu.apache.org/dist//ant/binaries/apache-ant-1.10.10-bin.zip',
1917 'C:\ant-bin.zip'
2018 )
2119 [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\ant-bin.zip", "C:\ant")
2220 }
23 - cmd: SET PATH=C:\ant\apache-ant-1.10.7\bin;%JAVA_HOME%\bin;%PATH%
21 - cmd: SET PATH=C:\ant\apache-ant-1.10.10\bin;%JAVA_HOME%\bin;%PATH%
2422 - cmd: set ANT_OPTS=-Dfile.encoding=UTF-8
2523 - cmd: java -version
2624 - cmd: ant -version
2725 - cmd: copy /y C:\projects\davmail\nsis\* "C:\Program Files (x86)\NSIS\Plugins\x86-ansi"
2826 build_script:
2927 - ant dist
30 - IF "%JAVA_HOME%" == "C:\Program Files\Java\jdk1.8.0" ant sonar -Dsonar.login=%SONAR_LOGIN%
28 - IF "%JAVA_HOME%" == "C:\Program Files\Java\jdk11" ant sonar -Dsonar.login=%SONAR_LOGIN%
29 - IF not "%JAVA_HOME%" == "C:\Program Files\Java\jdk1.8.0" echo remove artifacts && del /s /q dist
3130 test: false
3231 cache:
3332 - C:\ant
00 <project name="DavMail" default="dist" basedir=".">
11 <property file="user.properties"/>
2 <property name="version" value="5.5.1"/>
2 <property name="version" value="6.0.0"/>
33
44 <path id="classpath">
55 <pathelement location="classes"/>
233233 <exclude name="winrun4j-*.jar"/>
234234 </fileset>
235235 </copy>
236 <!-- move libgrowl to library path -->
237 <copy todir="dist/DavMail.app/Contents/MacOS">
238 <fileset file="lib/libgrowl.jnilib"/>
239 </copy>
240236 <copy file="src/osx/tray.icns" todir="dist/DavMail.app/Contents/Resources" overwrite="true"/>
241237 <!-- use generic app launcher -->
242238 <copy file="src/osx/davmail" todir="dist/DavMail.app/Contents/MacOS" overwrite="true"/>
293289 <include name="*.jar"/>
294290 <!-- exclude swt jars from debian package -->
295291 <exclude name="lib/swt*.jar"/>
296 <exclude name="lib/libgrowl-*.jar"/>
297292 <exclude name="lib/winrun4j-*.jar"/>
298293 </tarfileset>
299294 <tarfileset dir="src/bin" prefix="usr/bin" filemode="755">
329324 <exclude name="ant-deb*.jar"/>
330325 <exclude name="junit-*.jar"/>
331326 <exclude name="hamcrest-core-*.jar"/>
332 <exclude name="libgrowl-*.jar"/>
333327 <exclude name="nsisant-*.jar"/>
334328 <exclude name="servlet-api-*.jar"/>
335329 <exclude name="swt-*.jar"/>
382376 <include name="*.jar"/>
383377 <exclude name="nsisant*.jar"/>
384378 <exclude name="swt*.jar"/>
385 <exclude name="libgrowl-*.jar"/>
386379 <exclude name="winrun4j-*.jar"/>
387380 </fileset>
388381 </copy>
397390 <include name="*.jar"/>
398391 <!-- exclude swt jars from platform independent package -->
399392 <exclude name="lib/swt*.jar"/>
400 <exclude name="lib/libgrowl-*.jar"/>
401393 <exclude name="lib/junit-*.jar"/>
402394 <exclude name="lib/hamcrest-core-*.jar"/>
403395 <exclude name="lib/winrun4j-*.jar"/>
415407 <include name="*.jar"/>
416408 <include name="davmail*.exe"/>
417409 <exclude name="lib/swt-*-gtk-*.jar"/>
418 <exclude name="lib/libgrowl-*.jar"/>
410 <exclude name="lib/junit-*.jar"/>
411 <exclude name="lib/hamcrest-core-*.jar"/>
412 </fileset>
413 </zip>
414 <antcall target="download-jre"/>
415 <zip file="dist/davmail-${release-name}-windows-standalone.zip">
416 <fileset dir="dist">
417 <include name="jre/**"/>
418 <include name="lib/*.jar"/>
419 <include name="*.jar"/>
420 <include name="davmail64.exe"/>
421 <include name="davmailservice64.exe"/>
422 <exclude name="lib/swt-*-gtk-*.jar"/>
419423 <exclude name="lib/junit-*.jar"/>
420424 <exclude name="lib/hamcrest-core-*.jar"/>
421425 </fileset>
447451 <exclude name="target/**"/>
448452 <exclude name="archive/**"/>
449453 <exclude name="lib/**"/>
450 <exclude name="libgrowl/**"/>
451454 <exclude name="nsis/**"/>
452455 <exclude name="svnant/**"/>
453456 <exclude name="user.properties"/>
543546 <sonar/>
544547 </target>
545548
549 <target name="download-jre">
550 <get src="https://api.azul.com/zulu/download/community/v1.0/bundles/latest/binary/?jdk_version=15&amp;ext=zip&amp;os=windows&amp;arch=x86&amp;hw_bitness=64&amp;bundle_type=jre&amp;features=fx"
551 dest="dist/jrefx.zip"
552 />
553 <unzip src="dist/jrefx.zip" dest="dist/jre">
554 <cutdirsmapper dirs="1"/>
555 </unzip>
556 <delete file="dist/jrefx.zip"/>
557 </target>
558
546559 </project>
0 %{?!davrel: %define davrel 5.5.1}
1 %{?!davsvn: %define davsvn 3300}
0 %{?!davrel: %define davrel 6.0.0}
1 %{?!davsvn: %define davsvn 3376}
22 %define davver %{davrel}-%{davsvn}
33
44 Summary: DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP gateway for Microsoft Exchange
7777 [ -f %{_libdir}/java/swt.jar ] && ln -s %{_libdir}/java/swt.jar lib/swt.jar || ln -s /usr/lib/java/swt.jar lib/swt.jar
7878 %endif
7979
80 # we have java 1.6
80 # we have java 8
8181 ant -Dant.java.version=1.8 prepare-dist
8282
8383 %install
44 <groupId>davmail</groupId>
55 <artifactId>davmail</artifactId>
66 <packaging>jar</packaging>
7 <version>5.5.1</version>
7 <version>6.0.0</version>
88 <name>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange and Office 365 Gateway</name>
99 <organization>
1010 <name>Mickaël Guessant</name>
163163 <dependency>
164164 <groupId>junit</groupId>
165165 <artifactId>junit</artifactId>
166 <version>4.12</version>
166 <version>4.13.1</version>
167167 <scope>test</scope>
168168 </dependency>
169169 <dependency>
191191 <artifactId>jcl-over-slf4j</artifactId>
192192 </exclusion>
193193 </exclusions>
194 </dependency>
195 <dependency>
196 <groupId>commons-httpclient</groupId>
197 <artifactId>commons-httpclient</artifactId>
198 <version>3.1</version>
199194 </dependency>
200195 <dependency>
201196 <groupId>commons-codec</groupId>
253248 <dependency>
254249 <groupId>com.fasterxml.woodstox</groupId>
255250 <artifactId>woodstox-core</artifactId>
256 <version>5.1.0</version>
251 <version>6.2.0</version>
257252 </dependency>
258253 <dependency>
259254 <groupId>org.samba.jcifs</groupId>
273268 <version>0.4.5</version>
274269 <scope>system</scope>
275270 <systemPath>${project.basedir}/lib/winrun4j-0.4.5.jar</systemPath>
276 </dependency>
277 <dependency>
278 <groupId>info.growl</groupId>
279 <artifactId>growl</artifactId>
280 <version>0.2</version>
281 <scope>system</scope>
282 <systemPath>${project.basedir}/lib/libgrowl-0.2.jar</systemPath>
283271 </dependency>
284272 <dependency>
285273 <groupId>org.codehaus.jettison</groupId>
3838 <developer_name>Mickaël Guessant</developer_name>
3939 <content_rating type="oars-1.1" />
4040 <releases>
41 <release version="6.0.0" date="2021-07-05">
42 <description>
43 <p>
44 First major release in a long time, main change is switch from HttpClient 3 to 4, please report any regression related to this major rewrite.
45 DavMail now supports more O365 configurations, including access to client certificate to validate device trust.
46 O365 refresh tokens can now be stored securely in a separate (writable) file.
47 On Linux, in order to ensure the right java version is used, a command line option to download latest Azul JRE with OpenJFX support was added,
48 on windows a standalone package contains Azul JRE FX 15, on OSX updated universalJavaApplicationStub to latest version.
49
50 ### OSX:
51 - OSX: completely drop Growl support
52 - OSX: prepare possible path for an embedded jre mode
53 - OSX: update universalJavaApplicationStub to latest version from https://github.com/tofi86/universalJavaApplicationStub/blob/master/src/universalJavaApplicationStub
54
55 ### Documentation:
56 - Doc: merge Clarify the usage of imapIdleDelay https://github.com/mguessan/davmail/pull/116
57 - Doc: add comment on IDLE and timeout setting
58 - Doc: link to standalone windows package
59 - Doc: fix Zulu link
60 - Doc: remove references to Java 6 in documentation
61
62 ### Build:
63 - Appveyor: update ant
64 - Appveyor: build with jdk15
65 - Appveyor: purge artifacts for all builds except jdk 8
66 - Build: run Sonar with JDK 11
67 - Update junit to 4.13.1 in Maven
68 - Update junit to 4.13.1
69
70 ### Linux:
71 - Linux: Experimental: download Azul JRE FX with command 'davmail azul'
72 - Linux: merge https://github.com/mguessan/davmail/pull/133 Linux Gnome Desktop: fix systray support
73 - Linux: Update service file to allow 0-1023 ports binding (https://github.com/mguessan/davmail/pull/117)
74
75
76 ### Windows:
77 - Windows: switch standalone jre to Azul FX 15
78 - Windows: create a standalone package with Azul JRE FX in order to have a working O365InteractiveAuthenticator
79 - Winrun4J: prefer embedded VM for standalone package and export sun.net.www.protocol.https
80 - Winrun4J: update binaries
81 - Winrun4J: prepare standalone configuration
82 - Windows: update winrun4j config to require Java >= 8
83
84 ### IMAP:
85 - IMAP: fix thread handling from audit
86 - IMAP: Compute body part size with failover
87
88 ### O365:
89 - O365: log token file creation
90 - O365: cleanup from audit
91 - O365: Add davmail.oauth.tokenFilePath to sample properties file
92 - O365: disable HTTP/2 loader on Java 14 and later to enable custom socket factory
93 - O365: allow user agent override in O365InteractiveAuthenticator, switch default user agent to latest Edge
94 - O365: with Java 15 url with code returns as CANCELLED
95 - O365: MSCAPI and Java version 13 or higher required to access TPM protected client certificate on Windows
96 - O365: merge first commit from https://github.com/mguessan/davmail/pull/134/ OAuth via ADFS with MFA support
97 - O365: fix store refreshToken call
98 - O365: introduce davmail.oauth.tokenFilePath setting to store Oauth tokens in a separate file
99 - O365: switch to try with resource style
100 - Drop explicit dependency to netscape.javascript package in O365InteractiveJSLogger
101 - O365: follow redirects on ADFS authentication
102
103 ### HC4:
104 - Refactor ExchangeSessionFactory, create HttpClientAdapter in session
105 - HC4: update winrun4j binaries
106 - HC4: drop HttpClient 3 dependency in Maven, winrun4j binaries and nsi setup
107 - HC4: drop remaining HttpClient 3 classes
108 - HC4: drop DavMailCookieSpec and DavGatewaySSLProtocolSocketFactory (merge in SSLProtocolSocketFactory)
109 - HC4: drop DavGatewayHttpClientFacade and RestMethod
110 - HC4: default to Edge user agent
111 - HC4: Do not enable NTLM in Kerberos mode
112 - HC4: switch checkConfig to HttpClient 4
113 - HC4: merge HC4DavExchangeSession to DavExchangeSession
114 - HC4: cleanup HC4ExchangeFormAuthenticator references
115 - HC4: merge HC4ExchangeFormAuthenticator to ExchangeFormAuthenticator, extend authenticator interface to return HttpClientAdapter, switch to DavExchangeSession
116 - HC4: switch O365 authenticators test code to HttpClient 4
117 - HC4: adjust CreateItemMethod chunked call
118 - HC4: switch ExchangeSessionFactory to HttpClient 4
119 - HC4: add a warning about HttpClient 4 migration
120 - HC4: Enable ssl logging in addition to wire with HttpClient 4
121 - HC4: switch EWS implementation to HttpClient 4
122
123 ### EWS:
124 - EWS: improve isItemId detection to match base 64 encoded id
125 - EWS: drop NTLM as a failover logic
126 - EWS: cleanup unused code now that we have a reliable way to retrieve email address with ConvertId
127 - EWS: drop property davmail.caldavRealUpdate, no longer used
128 - EWS: Improved uid handling from audit
129 - EWS: Enable Woodstox workaround for malformed xml with latest Woodstox version
130
131 ### Enhancements:
132 - Clear session pool on DavMail restart
133 - Upgrade to Woodstox 6.2.0 as it's now available on debian, drop Woodstox patched StreamScanner
134
135 ### Caldav:
136 - Caldav: merge https://github.com/mguessan/davmail/pull/139 Fix missing XML encode
137 - Caldav: use Exchange timezone to compute event time in test case
138 - Caldav: create test cases for recurring events
139 </p>
140 </description>
141 </release>
41142 <release version="5.5.1" date="2019-04-19">
42143 <description>
43144 <p>
77 # force GTK2 to avoid crash with OpenJDK 11
88 JAVA_OPTS="-Xmx512M -Dsun.net.inetaddr.ttl=60 -Djdk.gtk.version=2.2"
99 JAVA=java
10
11 # Experimental: download Azul JRE FX with command 'davmail azul'
12 if [ "x$1" = 'xazul' ]; then
13 curl -L -o $BASE/jre.tgz "https://api.azul.com/zulu/download/community/v1.0/bundles/latest/binary/?jdk_version=15&ext=tar.gz&os=linux&arch=x86&hw_bitness=64&bundle_type=jre&features=fx"
14 rm -Rf $BASE/jre
15 mkdir $BASE/jre
16 tar xvzf jre.tgz -C $BASE/jre --strip 1
17 exit
18 fi
19
20 # check for embedded jre
21 if [ -e jre/bin/java ]; then
22 JAVA=jre/bin/java
23 fi
24
1025 # uncomment this to force JDK 8
1126 #JAVA=/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java
1227 # add JFX to classpath with OpenJDK 11
2222 davmail.ldapPort=1389
2323 davmail.popPort=1110
2424 davmail.smtpPort=1025
25
26 # Optional: separate file to store Oauth tokens
27 #davmail.oauth.tokenFilePath=
2528
2629 #############################################################
2730 # Network settings
8790
8891 # Delete messages immediately on IMAP STORE \Deleted flag
8992 davmail.imapAutoExpunge=true
90 # Enable IDLE support, set polling delay in minutes
93 # To enable IDLE support, set a maximum client polling delay in minutes
94 # Clients using IDLE should poll more frequently than this delay
9195 davmail.imapIdleDelay=
9296 # Always reply to IMAP RFC822.SIZE requests with Exchange approximate message size for performance reasons
9397 davmail.imapAlwaysApproxMsgSize=
118122 # log levels
119123 log4j.logger.davmail=WARN
120124 log4j.logger.httpclient.wire=WARN
121 log4j.logger.org.apache.commons.httpclient=WARN
125 log4j.logger.httpclient=WARN
122126 log4j.rootLogger=WARN
123127
124128 #############################################################
88 Type=simple
99 User=davmail
1010 PermissionsStartOnly=true
11 AmbientCapabilities=CAP_NET_BIND_SERVICE
1112 ExecStartPre=/usr/bin/touch /var/log/davmail.log
1213 ExecStartPre=/bin/chown davmail:davmail /var/log/davmail.log
1314 ExecStart=/usr/bin/davmail -server /etc/davmail.properties
+0
-2479
src/java/com/ctc/wstx/sr/StreamScanner.java less more
0 /* Woodstox XML processor
1 *
2 * Copyright (c) 2004- Tatu Saloranta, tatu.saloranta@iki.fi
3 *
4 * Licensed under the License specified in file LICENSE, included with
5 * the source code.
6 * You may not use this file except in compliance with the License.
7 *
8 * Unless required by applicable law or agreed to in writing, software
9 * distributed under the License is distributed on an "AS IS" BASIS,
10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 * See the License for the specific language governing permissions and
12 * limitations under the License.
13 */
14
15 package com.ctc.wstx.sr;
16
17 import java.io.FileNotFoundException;
18 import java.io.IOException;
19 import java.net.URL;
20 import java.text.MessageFormat;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.Map;
24
25 import javax.xml.stream.Location;
26 import javax.xml.stream.XMLInputFactory;
27 import javax.xml.stream.XMLReporter;
28 import javax.xml.stream.XMLResolver;
29 import javax.xml.stream.XMLStreamException;
30
31 import org.codehaus.stax2.XMLReporter2;
32 import org.codehaus.stax2.XMLStreamLocation2;
33 import org.codehaus.stax2.validation.XMLValidationProblem;
34
35 import com.ctc.wstx.api.ReaderConfig;
36 import com.ctc.wstx.cfg.ErrorConsts;
37 import com.ctc.wstx.cfg.InputConfigFlags;
38 import com.ctc.wstx.cfg.ParsingErrorMsgs;
39 import com.ctc.wstx.cfg.XmlConsts;
40 import com.ctc.wstx.dtd.MinimalDTDReader;
41 import com.ctc.wstx.ent.EntityDecl;
42 import com.ctc.wstx.ent.IntEntity;
43 import com.ctc.wstx.exc.*;
44 import com.ctc.wstx.io.DefaultInputResolver;
45 import com.ctc.wstx.io.WstxInputData;
46 import com.ctc.wstx.io.WstxInputLocation;
47 import com.ctc.wstx.io.WstxInputSource;
48 import com.ctc.wstx.util.ExceptionUtil;
49 import com.ctc.wstx.util.SymbolTable;
50 import com.ctc.wstx.util.TextBuffer;
51
52 /**
53 * Abstract base class that defines some basic functionality that all
54 * Woodstox reader classes (main XML reader, DTD reader) extend from.
55 */
56 public abstract class StreamScanner
57 extends WstxInputData
58 implements InputProblemReporter,
59 InputConfigFlags, ParsingErrorMsgs
60 {
61
62 // // // Some well-known chars:
63
64 /**
65 * Last (highest) char code of the three, LF, CR and NULL
66 */
67 public final static char CHAR_CR_LF_OR_NULL = (char) 13;
68
69 public final static int INT_CR_LF_OR_NULL = 13;
70
71 /**
72 * Character that allows quick check of whether a char can potentially
73 * be some kind of markup, WRT input stream processing;
74 * has to contain linefeeds, &, < and > (">" only matters when
75 * quoting text, as part of "]]>")
76 */
77 protected final static char CHAR_FIRST_PURE_TEXT = (char) ('>' + 1);
78
79
80 /**
81 * First character in Unicode (ie one with lowest id) that is legal
82 * as part of a local name (all valid name chars minus ':'). Used
83 * for doing quick check for local name end; usually name ends in
84 * a whitespace or equals sign.
85 */
86 protected final static char CHAR_LOWEST_LEGAL_LOCALNAME_CHAR = '-';
87
88 /*
89 ///////////////////////////////////////////////////////////////////////
90 // Character validity constants, structs
91 ///////////////////////////////////////////////////////////////////////
92 */
93
94 /**
95 * We will only use validity array for first 256 characters, mostly
96 * because after those characters it's easier to do fairly simple
97 * block checks.
98 */
99 private final static int VALID_CHAR_COUNT = 0x100;
100
101 private final static byte NAME_CHAR_INVALID_B = (byte) 0;
102 private final static byte NAME_CHAR_ALL_VALID_B = (byte) 1;
103 private final static byte NAME_CHAR_VALID_NONFIRST_B = (byte) -1;
104
105 private final static byte[] sCharValidity = new byte[VALID_CHAR_COUNT];
106
107 static {
108 // First, since all valid-as-first chars are also valid-as-other chars,
109 // we'll initialize common chars:
110 sCharValidity['_'] = NAME_CHAR_ALL_VALID_B;
111 for (int i = 0, last = ('z' - 'a'); i <= last; ++i) {
112 sCharValidity['A' + i] = NAME_CHAR_ALL_VALID_B;
113 sCharValidity['a' + i] = NAME_CHAR_ALL_VALID_B;
114 }
115 for (int i = 0xC0; i < 0xF6; ++i) { // not all are fully valid, but
116 sCharValidity[i] = NAME_CHAR_ALL_VALID_B;
117 }
118 // ... now we can 'revert' ones not fully valid:
119 sCharValidity[0xD7] = NAME_CHAR_INVALID_B;
120 sCharValidity[0xF7] = NAME_CHAR_INVALID_B;
121
122 // And then we can proceed with ones only valid-as-other.
123 sCharValidity['-'] = NAME_CHAR_VALID_NONFIRST_B;
124 sCharValidity['.'] = NAME_CHAR_VALID_NONFIRST_B;
125 sCharValidity[0xB7] = NAME_CHAR_VALID_NONFIRST_B;
126 for (int i = '0'; i <= '9'; ++i) {
127 sCharValidity[i] = NAME_CHAR_VALID_NONFIRST_B;
128 }
129 }
130
131 /**
132 * Public identifiers only use 7-bit ascii range.
133 */
134 private final static int VALID_PUBID_CHAR_COUNT = 0x80;
135 private final static byte[] sPubidValidity = new byte[VALID_PUBID_CHAR_COUNT];
136 // private final static byte PUBID_CHAR_INVALID_B = (byte) 0;
137 private final static byte PUBID_CHAR_VALID_B = (byte) 1;
138 static {
139 for (int i = 0, last = ('z' - 'a'); i <= last; ++i) {
140 sPubidValidity['A' + i] = PUBID_CHAR_VALID_B;
141 sPubidValidity['a' + i] = PUBID_CHAR_VALID_B;
142 }
143 for (int i = '0'; i <= '9'; ++i) {
144 sPubidValidity[i] = PUBID_CHAR_VALID_B;
145 }
146
147 // 3 main white space types are valid
148 sPubidValidity[0x0A] = PUBID_CHAR_VALID_B;
149 sPubidValidity[0x0D] = PUBID_CHAR_VALID_B;
150 sPubidValidity[0x20] = PUBID_CHAR_VALID_B;
151
152 // And many of punctuation/separator ascii chars too:
153 sPubidValidity['-'] = PUBID_CHAR_VALID_B;
154 sPubidValidity['\''] = PUBID_CHAR_VALID_B;
155 sPubidValidity['('] = PUBID_CHAR_VALID_B;
156 sPubidValidity[')'] = PUBID_CHAR_VALID_B;
157 sPubidValidity['+'] = PUBID_CHAR_VALID_B;
158 sPubidValidity[','] = PUBID_CHAR_VALID_B;
159 sPubidValidity['.'] = PUBID_CHAR_VALID_B;
160 sPubidValidity['/'] = PUBID_CHAR_VALID_B;
161 sPubidValidity[':'] = PUBID_CHAR_VALID_B;
162 sPubidValidity['='] = PUBID_CHAR_VALID_B;
163 sPubidValidity['?'] = PUBID_CHAR_VALID_B;
164 sPubidValidity[';'] = PUBID_CHAR_VALID_B;
165 sPubidValidity['!'] = PUBID_CHAR_VALID_B;
166 sPubidValidity['*'] = PUBID_CHAR_VALID_B;
167 sPubidValidity['#'] = PUBID_CHAR_VALID_B;
168 sPubidValidity['@'] = PUBID_CHAR_VALID_B;
169 sPubidValidity['$'] = PUBID_CHAR_VALID_B;
170 sPubidValidity['_'] = PUBID_CHAR_VALID_B;
171 sPubidValidity['%'] = PUBID_CHAR_VALID_B;
172 }
173
174 /*
175 ///////////////////////////////////////////////////////////////////////
176 // Basic configuration
177 ///////////////////////////////////////////////////////////////////////
178 */
179
180 /**
181 * Copy of the configuration object passed by the factory.
182 * Contains immutable settings for this reader (or in case
183 * of DTD parsers, reader that uses it)
184 */
185 protected final ReaderConfig mConfig;
186
187 // // // Various extracted settings:
188
189 /**
190 * If true, Reader is namespace aware, and should do basic checks
191 * (usually enforcing limitations on having colons in names)
192 */
193 protected final boolean mCfgNsEnabled;
194
195 // Extracted standard on/off settings:
196
197 /**
198 * note: left non-final on purpose: sub-class may need to modify
199 * the default value after construction.
200 */
201 protected boolean mCfgReplaceEntities;
202
203 /*
204 ///////////////////////////////////////////////////////////////////////
205 // Symbol handling, if applicable
206 ///////////////////////////////////////////////////////////////////////
207 */
208
209 final SymbolTable mSymbols;
210
211 /**
212 * Local full name for the event, if it has one (note: element events
213 * do NOT use this variable; those names are stored in element stack):
214 * target for processing instructions.
215 *<p>
216 * Currently used for proc. instr. target, and entity name (at least
217 * when current entity reference is null).
218 *<p>
219 * Note: this variable is generally not cleared, since it comes from
220 * a symbol table, ie. this won't be the only reference.
221 */
222 protected String mCurrName;
223
224 /*
225 ///////////////////////////////////////////////////////////////////////
226 // Input handling
227 ///////////////////////////////////////////////////////////////////////
228 */
229
230 /**
231 * Currently active input source; contains link to parent (nesting) input
232 * sources, if any.
233 */
234 protected WstxInputSource mInput;
235
236 /**
237 * Top-most input source this reader can use; due to input source
238 * chaining, this is not necessarily the root of all input; for example,
239 * external DTD subset reader's root input still has original document
240 * input as its parent.
241 */
242 protected final WstxInputSource mRootInput;
243
244 /**
245 * Custom resolver used to handle external entities that are to be expanded
246 * by this reader (external param/general entity expander)
247 */
248 protected XMLResolver mEntityResolver = null;
249
250 /**
251 * This is the current depth of the input stack (same as what input
252 * element stack would return as its depth).
253 * It is used to enforce input scope constraints for nesting of
254 * elements (for xml reader) and dtd declaration (for dtd reader)
255 * with regards to input block (entity expansion) boundaries.
256 *<p>
257 * Basically this value is compared to {@link #mInputTopDepth}, which
258 * indicates what was the depth at the point where the currently active
259 * input scope/block was started.
260 */
261 protected int mCurrDepth;
262
263 protected int mInputTopDepth;
264
265 /**
266 * Number of times a parsed general entity has been expanded; used for
267 * (optionally) limiting number of expansion to guard against
268 * denial-of-service attacks like "Billion Laughs".
269 *
270 * @since 4.3
271 */
272 protected int mEntityExpansionCount;
273
274 /**
275 * Flag that indicates whether linefeeds in the input data are to
276 * be normalized or not.
277 * Xml specs mandate that the line feeds are only normalized
278 * when they are from the external entities (main doc, external
279 * general/parsed entities), so normalization has to be
280 * suppressed when expanding internal general/parsed entities.
281 */
282 protected boolean mNormalizeLFs;
283
284 /**
285 * Flag that indicates whether all escaped chars are accepted in XML 1.0.
286 */
287 protected boolean mXml10AllowAllEscapedChars;
288
289 /*
290 ///////////////////////////////////////////////////////////////////////
291 // Buffer(s) for local name(s) and text content
292 ///////////////////////////////////////////////////////////////////////
293 */
294
295 /**
296 * Temporary buffer used if local name can not be just directly
297 * constructed from input buffer (name is on a boundary or such).
298 */
299 protected char[] mNameBuffer = null;
300
301 /*
302 ///////////////////////////////////////////////////////////////////////
303 // Information about starting location of event
304 // Reader is pointing to; updated on-demand
305 ///////////////////////////////////////////////////////////////////////
306 */
307
308 // // // Location info at point when current token was started
309
310 /**
311 * Total number of characters read before start of current token.
312 * For big (gigabyte-sized) sizes are possible, needs to be long,
313 * unlike pointers and sizes related to in-memory buffers.
314 */
315 protected long mTokenInputTotal = 0;
316
317 /**
318 * Input row on which current token starts, 1-based
319 */
320 protected int mTokenInputRow = 1;
321
322 /**
323 * Column on input row that current token starts; 0-based (although
324 * in the end it'll be converted to 1-based)
325 */
326 protected int mTokenInputCol = 0;
327
328 /*
329 ///////////////////////////////////////////////////////////////////////
330 // XML document information (from doc decl if one was found) common to
331 // all entities (main xml document, external DTD subset)
332 ///////////////////////////////////////////////////////////////////////
333 */
334
335 /**
336 * Input stream encoding, if known (passed in, or determined by
337 * auto-detection); null if not.
338 */
339 protected String mDocInputEncoding = null;
340
341 /**
342 * Character encoding from xml declaration, if any; null if no
343 * declaration, or it didn't specify encoding.
344 */
345 protected String mDocXmlEncoding = null;
346
347 /**
348 * XML version as declared by the document; one of constants
349 * from {@link XmlConsts} (like {@link XmlConsts#XML_V_10}).
350 */
351 protected int mDocXmlVersion = XmlConsts.XML_V_UNKNOWN;
352
353 /**
354 * Cache of internal character entities;
355 */
356 protected Map<String,IntEntity> mCachedEntities;
357
358 /**
359 * Flag for whether or not character references should be treated as entities
360 */
361 protected boolean mCfgTreatCharRefsAsEntities;
362
363 /**
364 * Entity reference stream currently points to.
365 */
366 protected EntityDecl mCurrEntity;
367
368 /*
369 ///////////////////////////////////////////////////////////////////////
370 // Life-cycle
371 ///////////////////////////////////////////////////////////////////////
372 */
373
374 /**
375 * Constructor used when creating a complete new (main-level) reader that
376 * does not share its input buffers or state with another reader.
377 */
378 protected StreamScanner(WstxInputSource input, ReaderConfig cfg,
379 XMLResolver res)
380 {
381 super();
382 mInput = input;
383 // 17-Jun-2004, TSa: Need to know root-level input source
384 mRootInput = input;
385
386 mConfig = cfg;
387 mSymbols = cfg.getSymbols();
388 int cf = cfg.getConfigFlags();
389 mCfgNsEnabled = (cf & CFG_NAMESPACE_AWARE) != 0;
390 mCfgReplaceEntities = (cf & CFG_REPLACE_ENTITY_REFS) != 0;
391
392 // waiting for pull request, see https://github.com/FasterXML/woodstox/pull/56
393 mXml10AllowAllEscapedChars = true;//mConfig.willXml10AllowAllEscapedChars();
394
395 mNormalizeLFs = mConfig.willNormalizeLFs();
396 mInputBuffer = null;
397 mInputPtr = mInputEnd = 0;
398 mEntityResolver = res;
399
400 mCfgTreatCharRefsAsEntities = mConfig.willTreatCharRefsAsEnts();
401 if (mCfgTreatCharRefsAsEntities) {
402 mCachedEntities = new HashMap<String,IntEntity>();
403 } else {
404 mCachedEntities = Collections.emptyMap();
405 }
406 }
407
408 /*
409 ///////////////////////////////////////////////////////////////////////
410 // Package API
411 ///////////////////////////////////////////////////////////////////////
412 */
413
414 /**
415 * Method that returns location of the last character returned by this
416 * reader; that is, location "one less" than the currently pointed to
417 * location.
418 */
419 protected WstxInputLocation getLastCharLocation()
420 {
421 return mInput.getLocation(mCurrInputProcessed + mInputPtr - 1,
422 mCurrInputRow, mInputPtr - mCurrInputRowStart);
423 }
424
425 protected URL getSource() throws IOException {
426 return mInput.getSource();
427 }
428
429 protected String getSystemId() {
430 return mInput.getSystemId();
431 }
432
433 /*
434 ///////////////////////////////////////////////////////////////////////
435 // Partial `LocationInfo` implementation (not implemented
436 // by this base class, but is by some sub-classes)
437 ///////////////////////////////////////////////////////////////////////
438 */
439
440 /**
441 * Returns location of last properly parsed token; as per StAX specs,
442 * apparently needs to be the end of current event, which is the same
443 * as the start of the following event (or EOF if that's next).
444 */
445 @Override
446 public abstract Location getLocation();
447
448 public XMLStreamLocation2 getStartLocation()
449 {
450 // note: +1 is used as columns are 1-based...
451 return mInput.getLocation(mTokenInputTotal,
452 mTokenInputRow, mTokenInputCol + 1);
453 }
454
455 public XMLStreamLocation2 getCurrentLocation()
456 {
457 return mInput.getLocation(mCurrInputProcessed + mInputPtr,
458 mCurrInputRow, mInputPtr - mCurrInputRowStart + 1);
459 }
460
461 /*
462 ///////////////////////////////////////////////////////////////////////
463 // InputProblemReporter implementation
464 ///////////////////////////////////////////////////////////////////////
465 */
466
467 public WstxException throwWfcException(String msg, boolean deferErrors)
468 throws WstxException
469 {
470 WstxException ex = constructWfcException(msg);
471 if (!deferErrors) {
472 throw ex;
473 }
474 return ex;
475 }
476
477 @Override
478 public void throwParseError(String msg) throws XMLStreamException {
479 throwParseError(msg, null, null);
480 }
481
482 /**
483 * Throws generic parse error with specified message and current parsing
484 * location.
485 *<p>
486 * Note: public access only because core code in other packages needs
487 * to access it.
488 */
489 @Override
490 public void throwParseError(String format, Object arg, Object arg2)
491 throws XMLStreamException
492 {
493 String msg = (arg != null || arg2 != null) ?
494 MessageFormat.format(format, new Object[] { arg, arg2 }) : format;
495 throw constructWfcException(msg);
496 }
497
498 public void reportProblem(String probType, String format, Object arg, Object arg2)
499 throws XMLStreamException
500 {
501 XMLReporter rep = mConfig.getXMLReporter();
502 if (rep != null) {
503 _reportProblem(rep, probType,
504 MessageFormat.format(format, new Object[] { arg, arg2 }), null);
505 }
506 }
507
508 @Override
509 public void reportProblem(Location loc, String probType,
510 String format, Object arg, Object arg2)
511 throws XMLStreamException
512 {
513 XMLReporter rep = mConfig.getXMLReporter();
514 if (rep != null) {
515 String msg = (arg != null || arg2 != null) ?
516 MessageFormat.format(format, new Object[] { arg, arg2 }) : format;
517 _reportProblem(rep, probType, msg, loc);
518 }
519 }
520
521 protected void _reportProblem(XMLReporter rep, String probType, String msg, Location loc)
522 throws XMLStreamException
523 {
524 if (loc == null) {
525 loc = getLastCharLocation();
526 }
527 _reportProblem(rep, new XMLValidationProblem(loc, msg, XMLValidationProblem.SEVERITY_ERROR, probType));
528 }
529
530 protected void _reportProblem(XMLReporter rep, XMLValidationProblem prob)
531 throws XMLStreamException
532 {
533 if (rep != null) {
534 Location loc = prob.getLocation();
535 if (loc == null) {
536 loc = getLastCharLocation();
537 prob.setLocation(loc);
538 }
539 // Backwards-compatibility fix: add non-null type, if missing:
540 if (prob.getType() == null) {
541 prob.setType(ErrorConsts.WT_VALIDATION);
542 }
543 // [WSTX-154]: was catching and dropping thrown exception: shouldn't.
544 // [WTSX-157]: need to support XMLReporter2
545 if (rep instanceof XMLReporter2) {
546 ((XMLReporter2) rep).report(prob);
547 } else {
548 rep.report(prob.getMessage(), prob.getType(), prob, loc);
549 }
550 }
551 }
552
553 /**
554 *<p>
555 * Note: this is the base implementation used for implementing
556 * <code>ValidationContext</code>
557 */
558 @Override
559 public void reportValidationProblem(XMLValidationProblem prob)
560 throws XMLStreamException
561 {
562 // !!! TBI: Fail-fast vs. deferred modes?
563 /* For now let's implement basic functionality: warnings get
564 * reported via XMLReporter, errors and fatal errors result in
565 * immediate exceptions.
566 */
567 /* 27-May-2008, TSa: [WSTX-153] Above is incorrect: as per Stax
568 * javadocs for XMLReporter, both warnings and non-fatal errors
569 * (which includes all validation errors) should be reported via
570 * XMLReporter interface, and only fatals should cause an
571 * immediate stream exception (by-passing reporter)
572 */
573 if (prob.getSeverity() > XMLValidationProblem.SEVERITY_ERROR) {
574 throw WstxValidationException.create(prob);
575 }
576 XMLReporter rep = mConfig.getXMLReporter();
577 if (rep != null) {
578 _reportProblem(rep, prob);
579 } else {
580 /* If no reporter, regular non-fatal errors are to be reported
581 * as exceptions as well, for backwards compatibility
582 */
583 if (prob.getSeverity() >= XMLValidationProblem.SEVERITY_ERROR) {
584 throw WstxValidationException.create(prob);
585 }
586 }
587 }
588
589 public void reportValidationProblem(String msg, int severity)
590 throws XMLStreamException
591 {
592 reportValidationProblem(new XMLValidationProblem(getLastCharLocation(),
593 msg, severity));
594 }
595
596 @Override
597 public void reportValidationProblem(String msg)
598 throws XMLStreamException
599 {
600 reportValidationProblem(new XMLValidationProblem(getLastCharLocation(), msg,
601 XMLValidationProblem.SEVERITY_ERROR));
602 }
603
604 public void reportValidationProblem(Location loc, String msg)
605 throws XMLStreamException
606 {
607 reportValidationProblem(new XMLValidationProblem(loc, msg));
608 }
609
610 @Override
611 public void reportValidationProblem(String format, Object arg, Object arg2)
612 throws XMLStreamException
613 {
614 reportValidationProblem(MessageFormat.format(format, new Object[] { arg, arg2 }));
615 }
616
617 /*
618 ///////////////////////////////////////////////////////////////////////
619 // Other error reporting methods
620 ///////////////////////////////////////////////////////////////////////
621 */
622
623 protected WstxException constructWfcException(String msg)
624 {
625 return new WstxParsingException(msg, getLastCharLocation());
626 }
627
628 /**
629 * Construct and return a {@link XMLStreamException} to throw
630 * as a result of a failed Typed Access operation (but one not
631 * caused by a Well-Formedness Constraint or Validation Constraint
632 * problem)
633 */
634 /*
635 protected WstxException _constructTypeException(String msg)
636 {
637 // Hmmh. Should there be a distinct sub-type?
638 return new WstxParsingException(msg, getLastCharLocation());
639 }
640 */
641
642 protected WstxException constructFromIOE(IOException ioe)
643 {
644 return new WstxIOException(ioe);
645 }
646
647 protected WstxException constructNullCharException()
648 {
649 return new WstxUnexpectedCharException("Illegal character (NULL, unicode 0) encountered: not valid in any content",
650 getLastCharLocation(), CHAR_NULL);
651 }
652
653 protected void throwUnexpectedChar(int i, String msg) throws WstxException
654 {
655 char c = (char) i;
656 String excMsg = "Unexpected character "+getCharDesc(c)+msg;
657 throw new WstxUnexpectedCharException(excMsg, getLastCharLocation(), c);
658 }
659
660 protected void throwNullChar() throws WstxException {
661 throw constructNullCharException();
662 }
663
664 protected void throwInvalidSpace(int i) throws WstxException {
665 throwInvalidSpace(i, false);
666 }
667
668 protected WstxException throwInvalidSpace(int i, boolean deferErrors)
669 throws WstxException
670 {
671 char c = (char) i;
672 WstxException ex;
673 if (c == CHAR_NULL) {
674 ex = constructNullCharException();
675 } else {
676 String msg = "Illegal character ("+getCharDesc(c)+")";
677 if (mXml11) {
678 msg += " [note: in XML 1.1, it could be included via entity expansion]";
679 }
680 ex = new WstxUnexpectedCharException(msg, getLastCharLocation(), c);
681 }
682 if (!deferErrors) {
683 throw ex;
684 }
685 return ex;
686 }
687
688 protected void throwUnexpectedEOF(String msg)
689 throws WstxException
690 {
691 throw new WstxEOFException("Unexpected EOF"+(msg == null ? "" : msg),
692 getLastCharLocation());
693 }
694
695 /**
696 * Similar to {@link #throwUnexpectedEOF}, but only indicates ending
697 * of an input block. Used when reading a token that can not span
698 * input block boundaries (ie. can not continue past end of an
699 * entity expansion).
700 */
701 protected void throwUnexpectedEOB(String msg)
702 throws WstxException
703 {
704 throw new WstxEOFException("Unexpected end of input block"+(msg == null ? "" : msg),
705 getLastCharLocation());
706 }
707
708 protected void throwFromIOE(IOException ioe) throws WstxException {
709 throw new WstxIOException(ioe);
710 }
711
712 protected void throwFromStrE(XMLStreamException strex)
713 throws WstxException
714 {
715 if (strex instanceof WstxException) {
716 throw (WstxException) strex;
717 }
718 throw new WstxException(strex);
719 }
720
721 /**
722 * Method called to report an error, when caller's signature only
723 * allows runtime exceptions to be thrown.
724 */
725 protected void throwLazyError(Exception e)
726 {
727 if (e instanceof XMLStreamException) {
728 WstxLazyException.throwLazily((XMLStreamException) e);
729 }
730 ExceptionUtil.throwRuntimeException(e);
731 }
732
733 protected String tokenTypeDesc(int type) {
734 return ErrorConsts.tokenTypeDesc(type);
735 }
736
737 /*
738 ///////////////////////////////////////////////////////////////////////
739 // Input buffer handling
740 ///////////////////////////////////////////////////////////////////////
741 */
742
743 /**
744 * Returns current input source this source uses.
745 *<p>
746 * Note: public only because some implementations are on different
747 * package.
748 */
749 public final WstxInputSource getCurrentInput() {
750 return mInput;
751 }
752
753 protected final int inputInBuffer() {
754 return mInputEnd - mInputPtr;
755 }
756
757 @SuppressWarnings("cast")
758 protected final int getNext() throws XMLStreamException
759 {
760 if (mInputPtr >= mInputEnd) {
761 if (!loadMore()) {
762 return -1;
763 }
764 }
765 return (int) mInputBuffer[mInputPtr++];
766 }
767
768 /**
769 * Similar to {@link #getNext}, but does not advance pointer
770 * in input buffer.
771 *<p>
772 * Note: this method only peeks within current input source;
773 * it does not close it and check nested input source (if any).
774 * This is necessary when checking keywords, since they can never
775 * cross input block boundary.
776 */
777 @SuppressWarnings("cast")
778 protected final int peekNext()
779 throws XMLStreamException
780 {
781 if (mInputPtr >= mInputEnd) {
782 if (!loadMoreFromCurrent()) {
783 return -1;
784 }
785 }
786 return (int) mInputBuffer[mInputPtr];
787 }
788
789 protected final char getNextChar(String errorMsg)
790 throws XMLStreamException
791 {
792 if (mInputPtr >= mInputEnd) {
793 loadMore(errorMsg);
794 }
795 return mInputBuffer[mInputPtr++];
796 }
797
798 /**
799 * Similar to {@link #getNextChar}, but will not read more characters
800 * from parent input source(s) if the current input source doesn't
801 * have more content. This is often needed to prevent "runaway" content,
802 * such as comments that start in an entity but do not have matching
803 * close marker inside entity; XML specification specifically states
804 * such markup is not legal.
805 */
806 protected final char getNextCharFromCurrent(String errorMsg)
807 throws XMLStreamException
808 {
809 if (mInputPtr >= mInputEnd) {
810 loadMoreFromCurrent(errorMsg);
811 }
812 return mInputBuffer[mInputPtr++];
813 }
814
815 /**
816 * Method that will skip through zero or more white space characters,
817 * and return either the character following white space, or -1 to
818 * indicate EOF (end of the outermost input source)/
819 */
820 @SuppressWarnings("cast")
821 protected final int getNextAfterWS()
822 throws XMLStreamException
823 {
824 if (mInputPtr >= mInputEnd) {
825 if (!loadMore()) {
826 return -1;
827 }
828 }
829 char c = mInputBuffer[mInputPtr++];
830 while (c <= CHAR_SPACE) {
831 // Linefeed?
832 if (c == '\n' || c == '\r') {
833 skipCRLF(c);
834 } else if (c != CHAR_SPACE && c != '\t') {
835 throwInvalidSpace(c);
836 }
837 // Still a white space?
838 if (mInputPtr >= mInputEnd) {
839 if (!loadMore()) {
840 return -1;
841 }
842 }
843 c = mInputBuffer[mInputPtr++];
844 }
845 return (int) c;
846 }
847
848 protected final char getNextCharAfterWS(String errorMsg)
849 throws XMLStreamException
850 {
851 if (mInputPtr >= mInputEnd) {
852 loadMore(errorMsg);
853 }
854
855 char c = mInputBuffer[mInputPtr++];
856 while (c <= CHAR_SPACE) {
857 // Linefeed?
858 if (c == '\n' || c == '\r') {
859 skipCRLF(c);
860 } else if (c != CHAR_SPACE && c != '\t') {
861 throwInvalidSpace(c);
862 }
863
864 // Still a white space?
865 if (mInputPtr >= mInputEnd) {
866 loadMore(errorMsg);
867 }
868 c = mInputBuffer[mInputPtr++];
869 }
870 return c;
871 }
872
873 protected final char getNextInCurrAfterWS(String errorMsg)
874 throws XMLStreamException
875 {
876 return getNextInCurrAfterWS(errorMsg, getNextCharFromCurrent(errorMsg));
877 }
878
879 protected final char getNextInCurrAfterWS(String errorMsg, char c)
880 throws XMLStreamException
881 {
882 while (c <= CHAR_SPACE) {
883 // Linefeed?
884 if (c == '\n' || c == '\r') {
885 skipCRLF(c);
886 } else if (c != CHAR_SPACE && c != '\t') {
887 throwInvalidSpace(c);
888 }
889
890 // Still a white space?
891 if (mInputPtr >= mInputEnd) {
892 loadMoreFromCurrent(errorMsg);
893 }
894 c = mInputBuffer[mInputPtr++];
895 }
896 return c;
897 }
898
899 /**
900 * Method called when a CR has been spotted in input; checks if next
901 * char is LF, and if so, skips it. Note that next character has to
902 * come from the current input source, to qualify; it can never come
903 * from another (nested) input source.
904 *
905 * @return True, if passed in char is '\r' and next one is '\n'.
906 */
907 protected final boolean skipCRLF(char c)
908 throws XMLStreamException
909 {
910 boolean result;
911
912 if (c == '\r' && peekNext() == '\n') {
913 ++mInputPtr;
914 result = true;
915 } else {
916 result = false;
917 }
918 ++mCurrInputRow;
919 mCurrInputRowStart = mInputPtr;
920 return result;
921 }
922
923 protected final void markLF() {
924 ++mCurrInputRow;
925 mCurrInputRowStart = mInputPtr;
926 }
927
928 protected final void markLF(int inputPtr) {
929 ++mCurrInputRow;
930 mCurrInputRowStart = inputPtr;
931 }
932
933 /**
934 * Method to push back last character read; can only be called once,
935 * that is, no more than one char can be guaranteed to be succesfully
936 * returned.
937 */
938 protected final void pushback() { --mInputPtr; }
939
940 /*
941 ///////////////////////////////////////////////////////////////////////
942 // Sub-class overridable input handling methods
943 ///////////////////////////////////////////////////////////////////////
944 */
945
946 /**
947 * Method called when an entity has been expanded (new input source
948 * has been created). Needs to initialize location information and change
949 * active input source.
950 *
951 * @param entityId Name of the entity being expanded
952 */
953 protected void initInputSource(WstxInputSource newInput, boolean isExt,
954 String entityId)
955 throws XMLStreamException
956 {
957 // Let's make sure new input will be read next time input is needed:
958 mInputPtr = 0;
959 mInputEnd = 0;
960 /* Plus, reset the input location so that'll be accurate for
961 * error reporting etc.
962 */
963 mInputTopDepth = mCurrDepth;
964
965 // [WSTX-296]: Check for entity expansion depth against configurable limit
966 int entityDepth = mInput.getEntityDepth() + 1;
967 verifyLimit("Maximum entity expansion depth", mConfig.getMaxEntityDepth(), entityDepth);
968 mInput = newInput;
969 mInput.initInputLocation(this, mCurrDepth, entityDepth);
970
971 /* 21-Feb-2006, TSa: Linefeeds are NOT normalized when expanding
972 * internal entities (XML, 2.11)
973 */
974 if (isExt) {
975 mNormalizeLFs = true;
976 } else {
977 mNormalizeLFs = false;
978 }
979 }
980
981 /**
982 * Method that will try to read one or more characters from currently
983 * open input sources; closing input sources if necessary.
984 *
985 * @return true if reading succeeded (or may succeed), false if
986 * we reached EOF.
987 */
988 protected boolean loadMore()
989 throws XMLStreamException
990 {
991 WstxInputSource input = mInput;
992 do {
993 /* Need to make sure offsets are properly updated for error
994 * reporting purposes, and do this now while previous amounts
995 * are still known.
996 */
997 mCurrInputProcessed += mInputEnd;
998 verifyLimit("Maximum document characters", mConfig.getMaxCharacters(), mCurrInputProcessed);
999 mCurrInputRowStart -= mInputEnd;
1000 int count;
1001 try {
1002 count = input.readInto(this);
1003 if (count > 0) {
1004 return true;
1005 }
1006 input.close();
1007 } catch (IOException ioe) {
1008 throw constructFromIOE(ioe);
1009 }
1010 if (input == mRootInput) {
1011 /* Note: no need to check entity/input nesting in this
1012 * particular case, since it will be handled by higher level
1013 * parsing code (results in an unexpected EOF)
1014 */
1015 return false;
1016 }
1017 WstxInputSource parent = input.getParent();
1018 if (parent == null) { // sanity check!
1019 throwNullParent(input);
1020 }
1021 /* 13-Feb-2006, TSa: Ok, do we violate a proper nesting constraints
1022 * with this input block closure?
1023 */
1024 if (mCurrDepth != input.getScopeId()) {
1025 handleIncompleteEntityProblem(input);
1026 }
1027
1028 mInput = input = parent;
1029 input.restoreContext(this);
1030 mInputTopDepth = input.getScopeId();
1031 /* 21-Feb-2006, TSa: Since linefeed normalization needs to be
1032 * suppressed for internal entity expansion, we may need to
1033 * change the state...
1034 */
1035 if (!mNormalizeLFs) {
1036 mNormalizeLFs = !input.fromInternalEntity();
1037 }
1038 // Maybe there are leftovers from that input in buffer now?
1039 } while (mInputPtr >= mInputEnd);
1040
1041 return true;
1042 }
1043
1044 protected final boolean loadMore(String errorMsg)
1045 throws XMLStreamException
1046 {
1047 if (!loadMore()) {
1048 throwUnexpectedEOF(errorMsg);
1049 }
1050 return true;
1051 }
1052
1053 protected boolean loadMoreFromCurrent()
1054 throws XMLStreamException
1055 {
1056 // Need to update offsets properly
1057 mCurrInputProcessed += mInputEnd;
1058 mCurrInputRowStart -= mInputEnd;
1059 verifyLimit("Maximum document characters", mConfig.getMaxCharacters(), mCurrInputProcessed);
1060 try {
1061 int count = mInput.readInto(this);
1062 return (count > 0);
1063 } catch (IOException ie) {
1064 throw constructFromIOE(ie);
1065 }
1066 }
1067
1068 protected final boolean loadMoreFromCurrent(String errorMsg)
1069 throws XMLStreamException
1070 {
1071 if (!loadMoreFromCurrent()) {
1072 throwUnexpectedEOB(errorMsg);
1073 }
1074 return true;
1075 }
1076
1077 /**
1078 * Method called to make sure current main-level input buffer has at
1079 * least specified number of characters available consequtively,
1080 * without having to call {@link #loadMore}. It can only be called
1081 * when input comes from main-level buffer; further, call can shift
1082 * content in input buffer, so caller has to flush any data still
1083 * pending. In short, caller has to know exactly what it's doing. :-)
1084 *<p>
1085 * Note: method does not check for any other input sources than the
1086 * current one -- if current source can not fulfill the request, a
1087 * failure is indicated.
1088 *
1089 * @return true if there's now enough data; false if not (EOF)
1090 */
1091 protected boolean ensureInput(int minAmount)
1092 throws XMLStreamException
1093 {
1094 int currAmount = mInputEnd - mInputPtr;
1095 if (currAmount >= minAmount) {
1096 return true;
1097 }
1098 try {
1099 return mInput.readMore(this, minAmount);
1100 } catch (IOException ie) {
1101 throw constructFromIOE(ie);
1102 }
1103 }
1104
1105 protected void closeAllInput(boolean force)
1106 throws XMLStreamException
1107 {
1108 WstxInputSource input = mInput;
1109 while (true) {
1110 try {
1111 if (force) {
1112 input.closeCompletely();
1113 } else {
1114 input.close();
1115 }
1116 } catch (IOException ie) {
1117 throw constructFromIOE(ie);
1118 }
1119 if (input == mRootInput) {
1120 break;
1121 }
1122 WstxInputSource parent = input.getParent();
1123 if (parent == null) { // sanity check!
1124 throwNullParent(input);
1125 }
1126 mInput = input = parent;
1127 }
1128 }
1129
1130 /**
1131 * @param curr Input source currently in use
1132 */
1133 protected void throwNullParent(WstxInputSource curr)
1134 {
1135 throw new IllegalStateException(ErrorConsts.ERR_INTERNAL);
1136 //throw new IllegalStateException("Internal error: null parent for input source '"+curr+"'; should never occur (should have stopped at root input '"+mRootInput+"').");
1137 }
1138
1139 /*
1140 ///////////////////////////////////////////////////////////////////////
1141 // Entity resolution
1142 ///////////////////////////////////////////////////////////////////////
1143 */
1144
1145 /**
1146 * Method that tries to resolve a character entity, or (if caller so
1147 * specifies), a pre-defined internal entity (lt, gt, amp, apos, quot).
1148 * It will succeed iff:
1149 * <ol>
1150 * <li>Entity in question is a simple character entity (either one of
1151 * 5 pre-defined ones, or using decimal/hex notation), AND
1152 * <li>
1153 * <li>Entity fits completely inside current input buffer.
1154 * <li>
1155 * </ol>
1156 * If so, character value of entity is returned. Character 0 is returned
1157 * otherwise; if so, caller needs to do full resolution.
1158 *<p>
1159 * Note: On entry we are guaranteed there are at least 3 more characters
1160 * in this buffer; otherwise we shouldn't be called.
1161 *
1162 * @param checkStd If true, will check pre-defined internal entities
1163 * (gt, lt, amp, apos, quot); if false, will only check actual
1164 * character entities.
1165 *
1166 * @return (Valid) character value, if entity is a character reference,
1167 * and could be resolved from current input buffer (does not span
1168 * buffer boundary); null char (code 0) if not (either non-char
1169 * entity, or spans input buffer boundary).
1170 */
1171 protected int resolveSimpleEntity(boolean checkStd)
1172 throws XMLStreamException
1173 {
1174 char[] buf = mInputBuffer;
1175 int ptr = mInputPtr;
1176 char c = buf[ptr++];
1177
1178 // Numeric reference?
1179 if (c == '#') {
1180 c = buf[ptr++];
1181 int value = 0;
1182 int inputLen = mInputEnd;
1183 if (c == 'x') { // hex
1184 while (ptr < inputLen) {
1185 c = buf[ptr++];
1186 if (c == ';') {
1187 break;
1188 }
1189 value = value << 4;
1190 if (c <= '9' && c >= '0') {
1191 value += (c - '0');
1192 } else if (c >= 'a' && c <= 'f') {
1193 value += (10 + (c - 'a'));
1194 } else if (c >= 'A' && c <= 'F') {
1195 value += (10 + (c - 'A'));
1196 } else {
1197 mInputPtr = ptr; // so error points to correct char
1198 throwUnexpectedChar(c, "; expected a hex digit (0-9a-fA-F).");
1199 }
1200 /* Need to check for overflow; easiest to do right as
1201 * it happens...
1202 */
1203 if (value > MAX_UNICODE_CHAR) {
1204 reportUnicodeOverflow();
1205 }
1206 }
1207 } else { // numeric (decimal)
1208 while (c != ';') {
1209 if (c <= '9' && c >= '0') {
1210 value = (value * 10) + (c - '0');
1211 // Overflow?
1212 if (value > MAX_UNICODE_CHAR) {
1213 reportUnicodeOverflow();
1214 }
1215 } else {
1216 mInputPtr = ptr; // so error points to correct char
1217 throwUnexpectedChar(c, "; expected a decimal number.");
1218 }
1219 if (ptr >= inputLen) {
1220 break;
1221 }
1222 c = buf[ptr++];
1223 }
1224 }
1225 /* We get here either if we got it all, OR if we ran out of
1226 * input in current buffer.
1227 */
1228 if (c == ';') { // got the full thing
1229 mInputPtr = ptr;
1230 validateChar(value);
1231 return value;
1232 }
1233
1234 /* If we ran out of input, need to just fall back, gets
1235 * resolved via 'full' resolution mechanism.
1236 */
1237 } else if (checkStd) {
1238 /* Caller may not want to resolve these quite yet...
1239 * (when it wants separate events for non-char entities)
1240 */
1241 if (c == 'a') { // amp or apos?
1242 c = buf[ptr++];
1243
1244 if (c == 'm') { // amp?
1245 if (buf[ptr++] == 'p') {
1246 if (ptr < mInputEnd && buf[ptr++] == ';') {
1247 mInputPtr = ptr;
1248 return '&';
1249 }
1250 }
1251 } else if (c == 'p') { // apos?
1252 if (buf[ptr++] == 'o') {
1253 int len = mInputEnd;
1254 if (ptr < len && buf[ptr++] == 's') {
1255 if (ptr < len && buf[ptr++] == ';') {
1256 mInputPtr = ptr;
1257 return '\'';
1258 }
1259 }
1260 }
1261 }
1262 } else if (c == 'g') { // gt?
1263 if (buf[ptr++] == 't' && buf[ptr++] == ';') {
1264 mInputPtr = ptr;
1265 return '>';
1266 }
1267 } else if (c == 'l') { // lt?
1268 if (buf[ptr++] == 't' && buf[ptr++] == ';') {
1269 mInputPtr = ptr;
1270 return '<';
1271 }
1272 } else if (c == 'q') { // quot?
1273 if (buf[ptr++] == 'u' && buf[ptr++] == 'o') {
1274 int len = mInputEnd;
1275 if (ptr < len && buf[ptr++] == 't') {
1276 if (ptr < len && buf[ptr++] == ';') {
1277 mInputPtr = ptr;
1278 return '"';
1279 }
1280 }
1281 }
1282 }
1283 }
1284 return 0;
1285 }
1286
1287 /**
1288 * Method called to resolve character entities, and only character
1289 * entities (except that pre-defined char entities -- amp, apos, lt,
1290 * gt, quote -- MAY be "char entities" in this sense, depending on
1291 * arguments).
1292 * Otherwise it is to return the null char; if so,
1293 * the input pointer will point to the same point as when method
1294 * entered (char after ampersand), plus the ampersand itself is
1295 * guaranteed to be in the input buffer (so caller can just push it
1296 * back if necessary).
1297 *<p>
1298 * Most often this method is called when reader is not to expand
1299 * non-char entities automatically, but to return them as separate
1300 * events.
1301 *<p>
1302 * Main complication here is that we need to do 5-char lookahead. This
1303 * is problematic if chars are on input buffer boundary. This is ok
1304 * for the root level input buffer, but not for some nested buffers.
1305 * However, according to XML specs, such split entities are actually
1306 * illegal... so we can throw an exception in those cases.
1307 *
1308 * @param checkStd If true, will check pre-defined internal entities
1309 * (gt, lt, amp, apos, quot) as character entities; if false, will only
1310 * check actual 'real' character entities.
1311 *
1312 * @return (Valid) character value, if entity is a character reference,
1313 * and could be resolved from current input buffer (does not span
1314 * buffer boundary); null char (code 0) if not (either non-char
1315 * entity, or spans input buffer boundary).
1316 */
1317 protected int resolveCharOnlyEntity(boolean checkStd)
1318 throws XMLStreamException
1319 {
1320 //int avail = inputInBuffer();
1321 int avail = mInputEnd - mInputPtr;
1322 if (avail < 6) {
1323 // split entity, or buffer boundary
1324 /* Don't want to lose leading '&' (in case we can not expand
1325 * the entity), so let's push it back first
1326 */
1327 --mInputPtr;
1328 /* Shortest valid reference would be 3 chars ('&a;'); which
1329 * would only be legal from an expanded entity...
1330 */
1331 if (!ensureInput(6)) {
1332 avail = inputInBuffer();
1333 if (avail < 3) {
1334 throwUnexpectedEOF(SUFFIX_IN_ENTITY_REF);
1335 }
1336 } else {
1337 avail = 6;
1338 }
1339 // ... and now we can move pointer back as well:
1340 ++mInputPtr;
1341 }
1342
1343 /* Ok, now we have one more character to check, and that's enough
1344 * to determine type decisively.
1345 */
1346 char c = mInputBuffer[mInputPtr];
1347
1348 // A char reference?
1349 if (c == '#') { // yup
1350 ++mInputPtr;
1351 return resolveCharEnt(null);
1352 }
1353
1354 // nope... except may be a pre-def?
1355 if (checkStd) {
1356 if (c == 'a') {
1357 char d = mInputBuffer[mInputPtr+1];
1358 if (d == 'm') {
1359 if (avail >= 4
1360 && mInputBuffer[mInputPtr+2] == 'p'
1361 && mInputBuffer[mInputPtr+3] == ';') {
1362 mInputPtr += 4;
1363 return '&';
1364 }
1365 } else if (d == 'p') {
1366 if (avail >= 5
1367 && mInputBuffer[mInputPtr+2] == 'o'
1368 && mInputBuffer[mInputPtr+3] == 's'
1369 && mInputBuffer[mInputPtr+4] == ';') {
1370 mInputPtr += 5;
1371 return '\'';
1372 }
1373 }
1374 } else if (c == 'l') {
1375 if (avail >= 3
1376 && mInputBuffer[mInputPtr+1] == 't'
1377 && mInputBuffer[mInputPtr+2] == ';') {
1378 mInputPtr += 3;
1379 return '<';
1380 }
1381 } else if (c == 'g') {
1382 if (avail >= 3
1383 && mInputBuffer[mInputPtr+1] == 't'
1384 && mInputBuffer[mInputPtr+2] == ';') {
1385 mInputPtr += 3;
1386 return '>';
1387 }
1388 } else if (c == 'q') {
1389 if (avail >= 5
1390 && mInputBuffer[mInputPtr+1] == 'u'
1391 && mInputBuffer[mInputPtr+2] == 'o'
1392 && mInputBuffer[mInputPtr+3] == 't'
1393 && mInputBuffer[mInputPtr+4] == ';') {
1394 mInputPtr += 5;
1395 return '"';
1396 }
1397 }
1398 }
1399 return 0;
1400 }
1401
1402 /**
1403 * Reverse of {@link #resolveCharOnlyEntity}; will only resolve entity
1404 * if it is NOT a character entity (or pre-defined 'generic' entity;
1405 * amp, apos, lt, gt or quot). Only used in cases where entities
1406 * are to be separately returned unexpanded (in non-entity-replacing
1407 * mode); which means it's never called from dtd handler.
1408 */
1409 protected EntityDecl resolveNonCharEntity()
1410 throws XMLStreamException
1411 {
1412 //int avail = inputInBuffer();
1413 int avail = mInputEnd - mInputPtr;
1414 if (avail < 6) {
1415 // split entity, or buffer boundary
1416 /* Don't want to lose leading '&' (in case we can not expand
1417 * the entity), so let's push it back first
1418 */
1419 --mInputPtr;
1420
1421 /* Shortest valid reference would be 3 chars ('&a;'); which
1422 * would only be legal from an expanded entity...
1423 */
1424 if (!ensureInput(6)) {
1425 avail = inputInBuffer();
1426 if (avail < 3) {
1427 throwUnexpectedEOF(SUFFIX_IN_ENTITY_REF);
1428 }
1429 } else {
1430 avail = 6;
1431 }
1432 // ... and now we can move pointer back as well:
1433 ++mInputPtr;
1434 }
1435
1436 // We don't care about char entities:
1437 char c = mInputBuffer[mInputPtr];
1438 if (c == '#') {
1439 return null;
1440 }
1441
1442 /* 19-Aug-2004, TSa: Need special handling for pre-defined
1443 * entities; they are not counted as 'real' general parsed
1444 * entities, but more as character entities...
1445 */
1446
1447 // have chars at least up to mInputPtr+4 by now
1448 if (c == 'a') {
1449 char d = mInputBuffer[mInputPtr+1];
1450 if (d == 'm') {
1451 if (avail >= 4
1452 && mInputBuffer[mInputPtr+2] == 'p'
1453 && mInputBuffer[mInputPtr+3] == ';') {
1454 // If not automatically expanding:
1455 //return sEntityAmp;
1456 // mInputPtr += 4;
1457 return null;
1458 }
1459 } else if (d == 'p') {
1460 if (avail >= 5
1461 && mInputBuffer[mInputPtr+2] == 'o'
1462 && mInputBuffer[mInputPtr+3] == 's'
1463 && mInputBuffer[mInputPtr+4] == ';') {
1464 return null;
1465 }
1466 }
1467 } else if (c == 'l') {
1468 if (avail >= 3
1469 && mInputBuffer[mInputPtr+1] == 't'
1470 && mInputBuffer[mInputPtr+2] == ';') {
1471 return null;
1472 }
1473 } else if (c == 'g') {
1474 if (avail >= 3
1475 && mInputBuffer[mInputPtr+1] == 't'
1476 && mInputBuffer[mInputPtr+2] == ';') {
1477 return null;
1478 }
1479 } else if (c == 'q') {
1480 if (avail >= 5
1481 && mInputBuffer[mInputPtr+1] == 'u'
1482 && mInputBuffer[mInputPtr+2] == 'o'
1483 && mInputBuffer[mInputPtr+3] == 't'
1484 && mInputBuffer[mInputPtr+4] == ';') {
1485 return null;
1486 }
1487 }
1488
1489 // Otherwise, let's just parse in generic way:
1490 ++mInputPtr; // since we already read the first letter
1491 String id = parseEntityName(c);
1492 mCurrName = id;
1493
1494 return findEntity(id, null);
1495 }
1496
1497 /**
1498 * Method that does full resolution of an entity reference, be it
1499 * character entity, internal entity or external entity, including
1500 * updating of input buffers, and depending on whether result is
1501 * a character entity (or one of 5 pre-defined entities), returns
1502 * char in question, or null character (code 0) to indicate it had
1503 * to change input source.
1504 *
1505 * @param allowExt If true, is allowed to expand external entities
1506 * (expanding text); if false, is not (expanding attribute value).
1507 *
1508 * @return Either single-character replacement (which is NOT to be
1509 * reparsed), or null char (0) to indicate expansion is done via
1510 * input source.
1511 */
1512 protected int fullyResolveEntity(boolean allowExt)
1513 throws XMLStreamException
1514 {
1515 char c = getNextCharFromCurrent(SUFFIX_IN_ENTITY_REF);
1516 // Do we have a (numeric) character entity reference?
1517 if (c == '#') { // numeric
1518 final StringBuffer originalSurface = new StringBuffer("#");
1519 int ch = resolveCharEnt(originalSurface);
1520 if (mCfgTreatCharRefsAsEntities) {
1521 final char[] originalChars = new char[originalSurface.length()];
1522 originalSurface.getChars(0, originalSurface.length(), originalChars, 0);
1523 mCurrEntity = getIntEntity(ch, originalChars);
1524 return 0;
1525 }
1526 return ch;
1527 }
1528
1529 String id = parseEntityName(c);
1530
1531 // Perhaps we have a pre-defined char reference?
1532 c = id.charAt(0);
1533 /*
1534 * 16-May-2004, TSa: Should custom entities (or ones defined in int/ext subset) override
1535 * pre-defined settings for these?
1536 */
1537 char d = CHAR_NULL;
1538 if (c == 'a') { // amp or apos?
1539 if (id.equals("amp")) {
1540 d = '&';
1541 } else if (id.equals("apos")) {
1542 d = '\'';
1543 }
1544 } else if (c == 'g') { // gt?
1545 if (id.length() == 2 && id.charAt(1) == 't') {
1546 d = '>';
1547 }
1548 } else if (c == 'l') { // lt?
1549 if (id.length() == 2 && id.charAt(1) == 't') {
1550 d = '<';
1551 }
1552 } else if (c == 'q') { // quot?
1553 if (id.equals("quot")) {
1554 d = '"';
1555 }
1556 }
1557
1558 if (d != CHAR_NULL) {
1559 if (mCfgTreatCharRefsAsEntities) {
1560 final char[] originalChars = new char[id.length()];
1561 id.getChars(0, id.length(), originalChars, 0);
1562 mCurrEntity = getIntEntity(d, originalChars);
1563 return 0;
1564 }
1565 return d;
1566 }
1567
1568 final EntityDecl e = expandEntity(id, allowExt, null);
1569 if (mCfgTreatCharRefsAsEntities) {
1570 mCurrEntity = e;
1571 }
1572 return 0;
1573 }
1574
1575 /**
1576 * Returns an entity (possibly from cache) for the argument character using the encoded
1577 * representation in mInputBuffer[entityStartPos ... mInputPtr-1].
1578 */
1579 protected EntityDecl getIntEntity(int ch, final char[] originalChars)
1580 {
1581 String cacheKey = new String(originalChars);
1582
1583 IntEntity entity = mCachedEntities.get(cacheKey);
1584 if (entity == null) {
1585 String repl;
1586 if (ch <= 0xFFFF) {
1587 repl = Character.toString((char) ch);
1588 } else {
1589 StringBuffer sb = new StringBuffer(2);
1590 ch -= 0x10000;
1591 sb.append((char) ((ch >> 10) + 0xD800));
1592 sb.append((char) ((ch & 0x3FF) + 0xDC00));
1593 repl = sb.toString();
1594 }
1595 entity = IntEntity.create(new String(originalChars), repl);
1596 mCachedEntities.put(cacheKey, entity);
1597 }
1598 return entity;
1599 }
1600
1601
1602 /**
1603 * Helper method that will try to expand a parsed entity (parameter or
1604 * generic entity).
1605 *<p>
1606 * note: called by sub-classes (dtd parser), needs to be protected.
1607 *
1608 * @param id Name of the entity being expanded
1609 * @param allowExt Whether external entities can be expanded or not; if
1610 * not, and the entity to expand would be external one, an exception
1611 * will be thrown
1612 */
1613 protected EntityDecl expandEntity(String id, boolean allowExt,
1614 Object extraArg)
1615 throws XMLStreamException
1616 {
1617 mCurrName = id;
1618
1619 EntityDecl ed = findEntity(id, extraArg);
1620
1621 if (ed == null) {
1622 /* 30-Sep-2005, TSa: As per [WSTX-5], let's only throw exception
1623 * if we have to resolve it (otherwise it's just best-effort,
1624 * and null is ok)
1625 */
1626 /* 02-Oct-2005, TSa: Plus, [WSTX-4] adds "undeclared entity
1627 * resolver"
1628 */
1629 if (mCfgReplaceEntities) {
1630 mCurrEntity = expandUnresolvedEntity(id);
1631 }
1632 return null;
1633 }
1634
1635 if (!mCfgTreatCharRefsAsEntities || this instanceof MinimalDTDReader) {
1636 expandEntity(ed, allowExt);
1637 }
1638
1639 return ed;
1640 }
1641
1642 /**
1643 *<p>
1644 * note: defined as private for documentation, ie. it's just called
1645 * from within this class (not sub-classes), from one specific method
1646 * (see above)
1647 *
1648 * @param ed Entity to be expanded
1649 * @param allowExt Whether external entities are allowed or not.
1650 */
1651 private void expandEntity(EntityDecl ed, boolean allowExt)
1652 throws XMLStreamException
1653 {
1654 String id = ed.getName();
1655
1656 /* Very first thing; we can immediately check if expanding
1657 * this entity would result in infinite recursion:
1658 */
1659 if (mInput.isOrIsExpandedFrom(id)) {
1660 throwRecursionError(id);
1661 }
1662
1663 /* Should not refer unparsed entities from attribute values
1664 * or text content (except via notation mechanism, but that's
1665 * not parsed here)
1666 */
1667 if (!ed.isParsed()) {
1668 throwParseError("Illegal reference to unparsed external entity \"{0}\"", id, null);
1669 }
1670
1671 // 28-Jun-2004, TSa: Do we support external entity expansion?
1672 boolean isExt = ed.isExternal();
1673 if (isExt) {
1674 if (!allowExt) { // never ok in attribute value...
1675 throwParseError("Encountered a reference to external parsed entity \"{0}\" when expanding attribute value: not legal as per XML 1.0/1.1 #3.1", id, null);
1676 }
1677 if (!mConfig.willSupportExternalEntities()) {
1678 throwParseError("Encountered a reference to external entity \"{0}\", but stream reader has feature \"{1}\" disabled",
1679 id, XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES);
1680 }
1681 }
1682 verifyLimit("Maximum entity expansion count", mConfig.getMaxEntityCount(), ++mEntityExpansionCount);
1683 // First, let's give current context chance to save its stuff
1684 WstxInputSource oldInput = mInput;
1685 oldInput.saveContext(this);
1686 WstxInputSource newInput = null;
1687 try {
1688 newInput = ed.expand(oldInput, mEntityResolver, mConfig, mDocXmlVersion);
1689 } catch (FileNotFoundException fex) {
1690 /* Let's catch and rethrow this just so we get more meaningful
1691 * description (with input source position etc)
1692 */
1693 throwParseError("(was {0}) {1}", fex.getClass().getName(), fex.getMessage());
1694 } catch (IOException ioe) {
1695 throw constructFromIOE(ioe);
1696 }
1697 /* And then we'll need to make sure new input comes from the new
1698 * input source
1699 */
1700 initInputSource(newInput, isExt, id);
1701 }
1702
1703 /**
1704 *<p>
1705 * note: only called from the local expandEntity() method
1706 */
1707 private EntityDecl expandUnresolvedEntity(String id)
1708 throws XMLStreamException
1709 {
1710 XMLResolver resolver = mConfig.getUndeclaredEntityResolver();
1711 if (resolver != null) {
1712 /* Ok, we can check for recursion here; but let's only do that
1713 * if there is any chance that it might get resolved by
1714 * the special resolver (it must have been resolved this way
1715 * earlier, too...)
1716 */
1717 if (mInput.isOrIsExpandedFrom(id)) {
1718 throwRecursionError(id);
1719 }
1720
1721 WstxInputSource oldInput = mInput;
1722 oldInput.saveContext(this);
1723 // null, null -> no public or system ids
1724 int xmlVersion = mDocXmlVersion;
1725 // 05-Feb-2006, TSa: If xmlVersion not explicitly known, defaults to 1.0
1726 if (xmlVersion == XmlConsts.XML_V_UNKNOWN) {
1727 xmlVersion = XmlConsts.XML_V_10;
1728 }
1729 WstxInputSource newInput;
1730 try {
1731 newInput = DefaultInputResolver.resolveEntityUsing
1732 (oldInput, id, null, null, resolver, mConfig, xmlVersion);
1733 if (mCfgTreatCharRefsAsEntities) {
1734 return new IntEntity(WstxInputLocation.getEmptyLocation(), newInput.getEntityId(),
1735 newInput.getSource(), new char[]{}, WstxInputLocation.getEmptyLocation());
1736 }
1737 } catch (IOException ioe) {
1738 throw constructFromIOE(ioe);
1739 }
1740 if (newInput != null) {
1741 // true -> is external
1742 initInputSource(newInput, true, id);
1743 return null;
1744 }
1745 }
1746 handleUndeclaredEntity(id);
1747 return null;
1748 }
1749
1750 /*
1751 ///////////////////////////////////////////////////////////////////////
1752 // Abstract methods for sub-classes to implement
1753 ///////////////////////////////////////////////////////////////////////
1754 */
1755
1756 /**
1757 * Abstract method for sub-classes to implement, for finding
1758 * a declared general or parsed entity.
1759 *
1760 * @param id Identifier of the entity to find
1761 * @param arg Optional argument passed from caller; needed by DTD
1762 * reader.
1763 */
1764 protected abstract EntityDecl findEntity(String id, Object arg)
1765 throws XMLStreamException;
1766
1767 /**
1768 * This method gets called if a declaration for an entity was not
1769 * found in entity expanding mode (enabled by default for xml reader,
1770 * always enabled for dtd reader).
1771 */
1772 protected abstract void handleUndeclaredEntity(String id)
1773 throws XMLStreamException;
1774
1775 protected abstract void handleIncompleteEntityProblem(WstxInputSource closing)
1776 throws XMLStreamException;
1777
1778 /*
1779 ///////////////////////////////////////////////////////////////////////
1780 // Basic tokenization
1781 ///////////////////////////////////////////////////////////////////////
1782 */
1783
1784 /**
1785 * Method that will parse name token (roughly equivalent to XML specs;
1786 * although bit lenier for more efficient handling); either uri prefix,
1787 * or local name.
1788 *<p>
1789 * Much of complexity in this method has to do with the intention to
1790 * try to avoid any character copies. In this optimal case algorithm
1791 * would be fairly simple. However, this only works if all data is
1792 * already in input buffer... if not, copy has to be made halfway
1793 * through parsing, and that complicates things.
1794 *<p>
1795 * One thing to note is that String returned has been canonicalized
1796 * and (if necessary) added to symbol table. It can thus be compared
1797 * against other such (usually id) Strings, with simple equality operator.
1798 *
1799 * @param c First character of the name; not yet checked for validity
1800 *
1801 * @return Canonicalized name String (which may have length 0, if
1802 * EOF or non-name-start char encountered)
1803 */
1804 protected String parseLocalName(char c)
1805 throws XMLStreamException
1806 {
1807 /* Has to start with letter, or '_' (etc); we won't allow ':' as that
1808 * is taken as namespace separator; no use trying to optimize
1809 * heavily as it's 98% likely it is a valid char...
1810 */
1811 if (!isNameStartChar(c)) {
1812 if (c == ':') {
1813 throwUnexpectedChar(c, " (missing namespace prefix?)");
1814 }
1815 throwUnexpectedChar(c, " (expected a name start character)");
1816 }
1817
1818 int ptr = mInputPtr;
1819 int hash = c;
1820 final int inputLen = mInputEnd;
1821 int startPtr = ptr-1; // already read previous char
1822 final char[] inputBuf = mInputBuffer;
1823
1824 /* After which there may be zero or more name chars
1825 * we have to consider
1826 */
1827 while (true) {
1828 if (ptr >= inputLen) {
1829 /* Ok, identifier may continue past buffer end, need
1830 * to continue with part 2 (separate method, as this is
1831 * not as common as having it all in buffer)
1832 */
1833 mInputPtr = ptr;
1834 return parseLocalName2(startPtr, hash);
1835 }
1836 // Ok, we have the char... is it a name char?
1837 c = inputBuf[ptr];
1838 if (c < CHAR_LOWEST_LEGAL_LOCALNAME_CHAR) {
1839 break;
1840 }
1841 if (!isNameChar(c)) {
1842 break;
1843 }
1844 hash = (hash * 31) + c;
1845 ++ptr;
1846 }
1847 mInputPtr = ptr;
1848 return mSymbols.findSymbol(mInputBuffer, startPtr, ptr - startPtr, hash);
1849 }
1850
1851 /**
1852 * Second part of name token parsing; called when name can continue
1853 * past input buffer end (so only part was read before calling this
1854 * method to read the rest).
1855 *<p>
1856 * Note that this isn't heavily optimized, on assumption it's not
1857 * called very often.
1858 */
1859 protected String parseLocalName2(int start, int hash)
1860 throws XMLStreamException
1861 {
1862 int ptr = mInputEnd - start;
1863 // Let's assume fairly short names
1864 char[] outBuf = getNameBuffer(ptr+8);
1865
1866 if (ptr > 0) {
1867 System.arraycopy(mInputBuffer, start, outBuf, 0, ptr);
1868 }
1869
1870 int outLen = outBuf.length;
1871 while (true) {
1872 // note: names can not cross input block (entity) boundaries...
1873 if (mInputPtr >= mInputEnd) {
1874 if (!loadMoreFromCurrent()) {
1875 break;
1876 }
1877 }
1878 char c = mInputBuffer[mInputPtr];
1879 if (c < CHAR_LOWEST_LEGAL_LOCALNAME_CHAR) {
1880 break;
1881 }
1882 if (!isNameChar(c)) {
1883 break;
1884 }
1885 ++mInputPtr;
1886 if (ptr >= outLen) {
1887 mNameBuffer = outBuf = expandBy50Pct(outBuf);
1888 outLen = outBuf.length;
1889 }
1890 outBuf[ptr++] = c;
1891 hash = (hash * 31) + c;
1892 }
1893 // Still need to canonicalize the name:
1894 return mSymbols.findSymbol(outBuf, 0, ptr, hash);
1895 }
1896
1897 /**
1898 * Method that will parse 'full' name token; what full means depends on
1899 * whether reader is namespace aware or not. If it is, full name means
1900 * local name with no namespace prefix (PI target, entity/notation name);
1901 * if not, name can contain arbitrary number of colons. Note that
1902 * element and attribute names are NOT parsed here, so actual namespace
1903 * prefix separation can be handled properly there.
1904 *<p>
1905 * Similar to {@link #parseLocalName}, much of complexity stems from
1906 * trying to avoid copying name characters from input buffer.
1907 *<p>
1908 * Note that returned String will be canonicalized, similar to
1909 * {@link #parseLocalName}, but without separating prefix/local name.
1910 *
1911 * @return Canonicalized name String (which may have length 0, if
1912 * EOF or non-name-start char encountered)
1913 */
1914 protected String parseFullName()
1915 throws XMLStreamException
1916 {
1917 if (mInputPtr >= mInputEnd) {
1918 loadMoreFromCurrent();
1919 }
1920 return parseFullName(mInputBuffer[mInputPtr++]);
1921 }
1922
1923 protected String parseFullName(char c)
1924 throws XMLStreamException
1925 {
1926 // First char has special handling:
1927 if (!isNameStartChar(c)) {
1928 if (c == ':') { // no name.... generally an error:
1929 if (mCfgNsEnabled) {
1930 throwNsColonException(parseFNameForError());
1931 }
1932 // Ok, that's fine actually
1933 } else {
1934 if (c <= CHAR_SPACE) {
1935 throwUnexpectedChar(c, " (missing name?)");
1936 }
1937 throwUnexpectedChar(c, " (expected a name start character)");
1938 }
1939 }
1940
1941 int ptr = mInputPtr;
1942 int hash = c;
1943 int inputLen = mInputEnd;
1944 int startPtr = ptr-1; // to account for the first char
1945
1946 /* After which there may be zero or more name chars
1947 * we have to consider
1948 */
1949 while (true) {
1950 if (ptr >= inputLen) {
1951 /* Ok, identifier may continue past buffer end, need
1952 * to continue with part 2 (separate method, as this is
1953 * not as common as having it all in buffer)
1954 */
1955 mInputPtr = ptr;
1956 return parseFullName2(startPtr, hash);
1957 }
1958 c = mInputBuffer[ptr];
1959 if (c == ':') { // colon only allowed in non-NS mode
1960 if (mCfgNsEnabled) {
1961 mInputPtr = ptr;
1962 throwNsColonException(new String(mInputBuffer, startPtr, ptr - startPtr) + parseFNameForError());
1963 }
1964 } else {
1965 if (c < CHAR_LOWEST_LEGAL_LOCALNAME_CHAR) {
1966 break;
1967 }
1968 if (!isNameChar(c)) {
1969 break;
1970 }
1971 }
1972 hash = (hash * 31) + c;
1973 ++ptr;
1974 }
1975 mInputPtr = ptr;
1976 return mSymbols.findSymbol(mInputBuffer, startPtr, ptr - startPtr, hash);
1977 }
1978
1979 @SuppressWarnings("cast")
1980 protected String parseFullName2(int start, int hash)
1981 throws XMLStreamException
1982 {
1983 int ptr = mInputEnd - start;
1984 // Let's assume fairly short names
1985 char[] outBuf = getNameBuffer(ptr+8);
1986
1987 if (ptr > 0) {
1988 System.arraycopy(mInputBuffer, start, outBuf, 0, ptr);
1989 }
1990
1991 int outLen = outBuf.length;
1992 while (true) {
1993 /* 06-Sep-2004, TSa: Name tokens are not allowed to continue
1994 * past entity expansion ranges... that is, all characters
1995 * have to come from the same input source. Thus, let's only
1996 * load things from same input level
1997 */
1998 if (mInputPtr >= mInputEnd) {
1999 if (!loadMoreFromCurrent()) {
2000 break;
2001 }
2002 }
2003 char c = mInputBuffer[mInputPtr];
2004 if (c == ':') { // colon only allowed in non-NS mode
2005 if (mCfgNsEnabled) {
2006 throwNsColonException(new String(outBuf, 0, ptr) + c + parseFNameForError());
2007 }
2008 } else if (c < CHAR_LOWEST_LEGAL_LOCALNAME_CHAR) {
2009 break;
2010 } else if (!isNameChar(c)) {
2011 break;
2012 }
2013 ++mInputPtr;
2014
2015 if (ptr >= outLen) {
2016 mNameBuffer = outBuf = expandBy50Pct(outBuf);
2017 outLen = outBuf.length;
2018 }
2019 outBuf[ptr++] = c;
2020 hash = (hash * 31) + (int) c;
2021 }
2022
2023 // Still need to canonicalize the name:
2024 return mSymbols.findSymbol(outBuf, 0, ptr, hash);
2025 }
2026
2027 /**
2028 * Method called to read in full name, including unlimited number of
2029 * namespace separators (':'), for the purpose of displaying name in
2030 * an error message. Won't do any further validations, and parsing
2031 * is not optimized: main need is just to get more meaningful error
2032 * messages.
2033 */
2034 protected String parseFNameForError()
2035 throws XMLStreamException
2036 {
2037 StringBuilder sb = new StringBuilder(100);
2038 while (true) {
2039 char c;
2040
2041 if (mInputPtr < mInputEnd) {
2042 c = mInputBuffer[mInputPtr++];
2043 } else { // can't error here, so let's accept EOF for now:
2044 int i = getNext();
2045 if (i < 0) {
2046 break;
2047 }
2048 c = (char) i;
2049 }
2050 if (c != ':' && !isNameChar(c)) {
2051 --mInputPtr;
2052 break;
2053 }
2054 sb.append(c);
2055 }
2056 return sb.toString();
2057 }
2058
2059 protected final String parseEntityName(char c)
2060 throws XMLStreamException
2061 {
2062 String id = parseFullName(c);
2063 // Needs to be followed by a semi-colon, too.. from same input source:
2064 if (mInputPtr >= mInputEnd) {
2065 if (!loadMoreFromCurrent()) {
2066 throwParseError("Missing semicolon after reference for entity \"{0}\"", id, null);
2067 }
2068 }
2069 c = mInputBuffer[mInputPtr++];
2070 if (c != ';') {
2071 throwUnexpectedChar(c, "; expected a semi-colon after the reference for entity '"+id+"'");
2072 }
2073 return id;
2074 }
2075
2076 /**
2077 * Note: does not check for number of colons, amongst other things.
2078 * Main idea is to skip through what superficially seems like a valid
2079 * id, nothing more. This is only done when really skipping through
2080 * something we do not care about at all: not even whether names/ids
2081 * would be valid (for example, when ignoring internal DTD subset).
2082 *
2083 * @return Length of skipped name.
2084 */
2085 protected int skipFullName(char c)
2086 throws XMLStreamException
2087 {
2088 if (!isNameStartChar(c)) {
2089 --mInputPtr;
2090 return 0;
2091 }
2092
2093 /* After which there may be zero or more name chars
2094 * we have to consider
2095 */
2096 int count = 1;
2097 while (true) {
2098 c = (mInputPtr < mInputEnd) ?
2099 mInputBuffer[mInputPtr++] : getNextChar(SUFFIX_EOF_EXP_NAME);
2100 if (c != ':' && !isNameChar(c)) {
2101 break;
2102 }
2103 ++count;
2104 }
2105 return count;
2106 }
2107
2108 /**
2109 * Simple parsing method that parses system ids, which are generally
2110 * used in entities (from DOCTYPE declaration to internal/external
2111 * subsets).
2112 *<p>
2113 * NOTE: returned String is not canonicalized, on assumption that
2114 * external ids may be longish, and are not shared all that often, as
2115 * they are generally just used for resolving paths, if anything.
2116 *<br />
2117 * Also note that this method is not heavily optimized, as it's not
2118 * likely to be a bottleneck for parsing.
2119 */
2120 protected final String parseSystemId(char quoteChar, boolean convertLFs,
2121 String errorMsg)
2122 throws XMLStreamException
2123 {
2124 char[] buf = getNameBuffer(-1);
2125 int ptr = 0;
2126
2127 while (true) {
2128 char c = (mInputPtr < mInputEnd) ?
2129 mInputBuffer[mInputPtr++] : getNextChar(errorMsg);
2130 if (c == quoteChar) {
2131 break;
2132 }
2133 /* ??? 14-Jun-2004, TSa: Should we normalize linefeeds or not?
2134 * It seems like we should, for all input... so that's the way it
2135 * works.
2136 */
2137 if (c == '\n') {
2138 markLF();
2139 } else if (c == '\r') {
2140 if (peekNext() == '\n') {
2141 ++mInputPtr;
2142 if (!convertLFs) {
2143 /* The only tricky thing; need to preserve 2-char LF; need to
2144 * output one char from here, then can fall back to default:
2145 */
2146 if (ptr >= buf.length) {
2147 buf = expandBy50Pct(buf);
2148 }
2149 buf[ptr++] = '\r';
2150 }
2151 c = '\n';
2152 } else if (convertLFs) {
2153 c = '\n';
2154 }
2155 }
2156
2157 // Other than that, let's just append it:
2158 if (ptr >= buf.length) {
2159 buf = expandBy50Pct(buf);
2160 }
2161 buf[ptr++] = c;
2162 }
2163
2164 return (ptr == 0) ? "" : new String(buf, 0, ptr);
2165 }
2166
2167 /**
2168 * Simple parsing method that parses system ids, which are generally
2169 * used in entities (from DOCTYPE declaration to internal/external
2170 * subsets).
2171 *<p>
2172 * As per xml specs, the contents are actually normalized.
2173 *<p>
2174 * NOTE: returned String is not canonicalized, on assumption that
2175 * external ids may be longish, and are not shared all that often, as
2176 * they are generally just used for resolving paths, if anything.
2177 *<br />
2178 * Also note that this method is not heavily optimized, as it's not
2179 * likely to be a bottleneck for parsing.
2180 */
2181 protected final String parsePublicId(char quoteChar, String errorMsg)
2182 throws XMLStreamException
2183 {
2184 char[] buf = getNameBuffer(-1);
2185 int ptr = 0;
2186 boolean spaceToAdd = false;
2187
2188 while (true) {
2189 char c = (mInputPtr < mInputEnd) ?
2190 mInputBuffer[mInputPtr++] : getNextChar(errorMsg);
2191 if (c == quoteChar) {
2192 break;
2193 }
2194 if (c == '\n') {
2195 markLF();
2196 spaceToAdd = true;
2197 continue;
2198 } else if (c == '\r') {
2199 if (peekNext() == '\n') {
2200 ++mInputPtr;
2201 }
2202 spaceToAdd = true;
2203 continue;
2204 } else if (c == CHAR_SPACE) {
2205 spaceToAdd = true;
2206 continue;
2207 } else {
2208 // Verify it's a legal pubid char (see XML spec, #13, from 2.3)
2209 if ((c >= VALID_PUBID_CHAR_COUNT)
2210 || sPubidValidity[c] != PUBID_CHAR_VALID_B) {
2211 throwUnexpectedChar(c, " in public identifier");
2212 }
2213 }
2214
2215 // Other than that, let's just append it:
2216 if (ptr >= buf.length) {
2217 buf = expandBy50Pct(buf);
2218 }
2219 /* Space-normalization means scrapping leading and trailing
2220 * white space, and coalescing remaining ws into single spaces.
2221 */
2222 if (spaceToAdd) { // pending white space to add?
2223 if (c == CHAR_SPACE) { // still a space; let's skip
2224 continue;
2225 }
2226 /* ok: if we have non-space, we'll either forget about
2227 * space(s) (if nothing has been output, ie. leading space),
2228 * or output a single space (in-between non-white space)
2229 */
2230 spaceToAdd = false;
2231 if (ptr > 0) {
2232 buf[ptr++] = CHAR_SPACE;
2233 if (ptr >= buf.length) {
2234 buf = expandBy50Pct(buf);
2235 }
2236 }
2237 }
2238 buf[ptr++] = c;
2239 }
2240
2241 return (ptr == 0) ? "" : new String(buf, 0, ptr);
2242 }
2243
2244 protected final void parseUntil(TextBuffer tb, char endChar, boolean convertLFs,
2245 String errorMsg)
2246 throws XMLStreamException
2247 {
2248 // Let's first ensure we have some data in there...
2249 if (mInputPtr >= mInputEnd) {
2250 loadMore(errorMsg);
2251 }
2252 while (true) {
2253 // Let's loop consequtive 'easy' spans:
2254 char[] inputBuf = mInputBuffer;
2255 int inputLen = mInputEnd;
2256 int ptr = mInputPtr;
2257 int startPtr = ptr;
2258 while (ptr < inputLen) {
2259 char c = inputBuf[ptr++];
2260 if (c == endChar) {
2261 int thisLen = ptr - startPtr - 1;
2262 if (thisLen > 0) {
2263 tb.append(inputBuf, startPtr, thisLen);
2264 }
2265 mInputPtr = ptr;
2266 return;
2267 }
2268 if (c == '\n') {
2269 mInputPtr = ptr; // markLF() requires this
2270 markLF();
2271 } else if (c == '\r') {
2272 if (!convertLFs && ptr < inputLen) {
2273 if (inputBuf[ptr] == '\n') {
2274 ++ptr;
2275 }
2276 mInputPtr = ptr;
2277 markLF();
2278 } else {
2279 int thisLen = ptr - startPtr - 1;
2280 if (thisLen > 0) {
2281 tb.append(inputBuf, startPtr, thisLen);
2282 }
2283 mInputPtr = ptr;
2284 c = getNextChar(errorMsg);
2285 if (c != '\n') {
2286 --mInputPtr; // pusback
2287 tb.append(convertLFs ? '\n' : '\r');
2288 } else {
2289 if (convertLFs) {
2290 tb.append('\n');
2291 } else {
2292 tb.append('\r');
2293 tb.append('\n');
2294 }
2295 }
2296 startPtr = ptr = mInputPtr;
2297 markLF();
2298 }
2299 }
2300 }
2301 int thisLen = ptr - startPtr;
2302 if (thisLen > 0) {
2303 tb.append(inputBuf, startPtr, thisLen);
2304 }
2305 loadMore(errorMsg);
2306 startPtr = ptr = mInputPtr;
2307 inputBuf = mInputBuffer;
2308 inputLen = mInputEnd;
2309 }
2310 }
2311
2312 /*
2313 ///////////////////////////////////////////////////////////////////////
2314 // Internal methods
2315 ///////////////////////////////////////////////////////////////////////
2316 */
2317
2318 private int resolveCharEnt(StringBuffer originalCharacters)
2319 throws XMLStreamException
2320 {
2321 int value = 0;
2322 char c = getNextChar(SUFFIX_IN_ENTITY_REF);
2323
2324 if (originalCharacters != null) {
2325 originalCharacters.append(c);
2326 }
2327
2328 if (c == 'x') { // hex
2329 while (true) {
2330 c = (mInputPtr < mInputEnd) ? mInputBuffer[mInputPtr++]
2331 : getNextCharFromCurrent(SUFFIX_IN_ENTITY_REF);
2332 if (c == ';') {
2333 break;
2334 }
2335
2336 if (originalCharacters != null) {
2337 originalCharacters.append(c);
2338 }
2339 value = value << 4;
2340 if (c <= '9' && c >= '0') {
2341 value += (c - '0');
2342 } else if (c >= 'a' && c <= 'f') {
2343 value += 10 + (c - 'a');
2344 } else if (c >= 'A' && c <= 'F') {
2345 value += 10 + (c - 'A');
2346 } else {
2347 throwUnexpectedChar(c, "; expected a hex digit (0-9a-fA-F).");
2348 }
2349 // Overflow?
2350 if (value > MAX_UNICODE_CHAR) {
2351 reportUnicodeOverflow();
2352 }
2353 }
2354 } else { // numeric (decimal)
2355 while (c != ';') {
2356 if (c <= '9' && c >= '0') {
2357 value = (value * 10) + (c - '0');
2358 // Overflow?
2359 if (value > MAX_UNICODE_CHAR) {
2360 reportUnicodeOverflow();
2361 }
2362 } else {
2363 throwUnexpectedChar(c, "; expected a decimal number.");
2364 }
2365 c = (mInputPtr < mInputEnd) ? mInputBuffer[mInputPtr++]
2366 : getNextCharFromCurrent(SUFFIX_IN_ENTITY_REF);
2367
2368 if (originalCharacters != null && c != ';') {
2369 originalCharacters.append(c);
2370 }
2371 }
2372 }
2373 validateChar(value);
2374 return value;
2375 }
2376
2377 /**
2378 * Method that will verify that expanded Unicode codepoint is a valid
2379 * XML content character.
2380 */
2381 private final void validateChar(int value)
2382 throws XMLStreamException
2383 {
2384 /* 24-Jan-2006, TSa: Ok, "high" Unicode chars are problematic,
2385 * need to be reported by a surrogate pair..
2386 */
2387 if (value >= 0xD800) {
2388 if (value < 0xE000) { // no surrogates via entity expansion
2389 reportIllegalChar(value);
2390 }
2391 if (value > 0xFFFF) {
2392 // Within valid range at all?
2393 if (value > MAX_UNICODE_CHAR) {
2394 reportUnicodeOverflow();
2395 }
2396 } else if (value >= 0xFFFE) { // 0xFFFE and 0xFFFF are illegal too
2397 reportIllegalChar(value);
2398 }
2399 // Ok, fine as is
2400 } else if (value < 32) {
2401 if (value == 0) {
2402 throwParseError("Invalid character reference: null character not allowed in XML content.");
2403 }
2404 // XML 1.1 allows most other chars; 1.0 does not:
2405 if (!mXml10AllowAllEscapedChars) {
2406 if (!mXml11 &&
2407 (value != 0x9 && value != 0xA && value != 0xD)) {
2408 reportIllegalChar(value);
2409 }
2410 }
2411 }
2412 }
2413
2414 protected final char[] getNameBuffer(int minSize)
2415 {
2416 char[] buf = mNameBuffer;
2417
2418 if (buf == null) {
2419 mNameBuffer = buf = new char[(minSize > 48) ? (minSize+16) : 64];
2420 } else if (minSize >= buf.length) { // let's allow one char extra...
2421 int len = buf.length;
2422 len += (len >> 1); // grow by 50%
2423 mNameBuffer = buf = new char[(minSize >= len) ? (minSize+16) : len];
2424 }
2425 return buf;
2426 }
2427
2428 protected final char[] expandBy50Pct(char[] buf)
2429 {
2430 int len = buf.length;
2431 char[] newBuf = new char[len + (len >> 1)];
2432 System.arraycopy(buf, 0, newBuf, 0, len);
2433 return newBuf;
2434 }
2435
2436 /**
2437 * Method called to throw an exception indicating that a name that
2438 * should not be namespace-qualified (PI target, entity/notation name)
2439 * is one, and reader is namespace aware.
2440 */
2441 private void throwNsColonException(String name)
2442 throws XMLStreamException
2443 {
2444 throwParseError("Illegal name \"{0}\" (PI target, entity/notation name): can not contain a colon (XML Namespaces 1.0#6)", name, null);
2445 }
2446
2447 private void throwRecursionError(String entityName)
2448 throws XMLStreamException
2449 {
2450 throwParseError("Illegal entity expansion: entity \"{0}\" expands itself recursively.", entityName, null);
2451 }
2452
2453 private void reportUnicodeOverflow()
2454 throws XMLStreamException
2455 {
2456 throwParseError("Illegal character entity: value higher than max allowed (0x{0})", Integer.toHexString(MAX_UNICODE_CHAR), null);
2457 }
2458
2459 private void reportIllegalChar(int value)
2460 throws XMLStreamException
2461 {
2462 throwParseError("Illegal character entity: expansion character (code 0x{0}", Integer.toHexString(value), null);
2463 }
2464
2465 protected void verifyLimit(String type, long maxValue, long currentValue)
2466 throws XMLStreamException
2467 {
2468 if (currentValue > maxValue) {
2469 throw constructLimitViolation(type, maxValue);
2470 }
2471 }
2472
2473 protected XMLStreamException constructLimitViolation(String type, long limit)
2474 throws XMLStreamException
2475 {
2476 return new XMLStreamException(type+" limit ("+limit+") exceeded");
2477 }
2478 }
2020 import davmail.caldav.CaldavServer;
2121 import davmail.exception.DavMailException;
2222 import davmail.exchange.ExchangeSessionFactory;
23 import davmail.http.DavGatewayHttpClientFacade;
24 import davmail.http.DavGatewaySSLProtocolSocketFactory;
2523 import davmail.http.HttpClientAdapter;
2624 import davmail.http.request.GetRequest;
2725 import davmail.imap.ImapServer;
119117 * Start DavMail listeners.
120118 */
121119 public static void start() {
122 // register custom SSL Socket factory
123 DavGatewaySSLProtocolSocketFactory.register();
124
125 // prepare HTTP connection pool
126 DavGatewayHttpClientFacade.start();
127
128120 SERVER_LIST.clear();
129121
130122 int smtpPort = Settings.getIntProperty("davmail.smtpPort");
189181 public static void stop() {
190182 DavGateway.stopServers();
191183 // close pooled connections
192 DavGatewayHttpClientFacade.stop();
193 // clear session cache
194 ExchangeSessionFactory.reset();
184 ExchangeSessionFactory.shutdown();
195185 DavGatewayTray.info(new BundleMessage("LOG_GATEWAY_STOP"));
196186 DavGatewayTray.dispose();
197187 }
202192 public static void restart() {
203193 DavGateway.stopServers();
204194 // clear session cache
205 ExchangeSessionFactory.reset();
195 ExchangeSessionFactory.shutdown();
206196 DavGateway.start();
207197 }
208198
4242 import java.util.Properties;
4343 import java.util.TreeSet;
4444
45 import static org.apache.http.util.TextUtils.isEmpty;
46
4547 /**
4648 * Settings facade.
4749 * DavMail settings are stored in the .davmail.properties file in current
4850 * user home directory or in the file specified on the command line.
4951 */
5052 public final class Settings {
53
54 protected static final Logger LOGGER = Logger.getLogger(Settings.class);
5155
5256 public static final String O365_URL = "https://outlook.office365.com/EWS/Exchange.asmx";
5357 public static final String O365 = "O365";
5761 public static final String WEBDAV = "WebDav";
5862 public static final String EWS = "EWS";
5963 public static final String AUTO = "Auto";
64
65 public static final String EDGE_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Edg/90.0.818.49";
6066
6167 private Settings() {
6268 }
212218 SETTINGS.put("log4j.rootLogger", Level.WARN.toString());
213219 SETTINGS.put("log4j.logger.davmail", Level.DEBUG.toString());
214220 SETTINGS.put("log4j.logger.httpclient.wire", Level.WARN.toString());
215 SETTINGS.put("log4j.logger.org.apache.commons.httpclient", Level.WARN.toString());
221 SETTINGS.put("log4j.logger.httpclient", Level.WARN.toString());
216222 SETTINGS.put("davmail.logFilePath", "");
217223 }
218224
325331 // update logging levels
326332 Settings.setLoggingLevel("rootLogger", Settings.getLoggingLevel("rootLogger"));
327333 Settings.setLoggingLevel("davmail", Settings.getLoggingLevel("davmail"));
328 Settings.setLoggingLevel("httpclient.wire", Settings.getLoggingLevel("httpclient.wire"));
329 Settings.setLoggingLevel("org.apache.commons.httpclient", Settings.getLoggingLevel("org.apache.commons.httpclient"));
330334 // set logging levels for HttpClient 4
331335 Settings.setLoggingLevel("org.apache.http.wire", Settings.getLoggingLevel("httpclient.wire"));
332 Settings.setLoggingLevel("org.apache.http", Settings.getLoggingLevel("org.apache.commons.httpclient"));
336 Settings.setLoggingLevel("org.apache.http.conn.ssl", Settings.getLoggingLevel("httpclient.wire"));
337 Settings.setLoggingLevel("org.apache.http", Settings.getLoggingLevel("httpclient"));
333338 }
334339
335340 /**
356361 Enumeration<?> propertyEnumeration = properties.propertyNames();
357362 while (propertyEnumeration.hasMoreElements()) {
358363 String propertyName = (String) propertyEnumeration.nextElement();
359 writer.write(propertyName+"="+ escapeValue(properties.getProperty(propertyName)));
364 writer.write(propertyName + "=" + escapeValue(properties.getProperty(propertyName)));
360365 writer.newLine();
361366 }
362367 } catch (IOException e) {
386391 * Convert input property line to new line with value from properties.
387392 * Preserve comments
388393 *
389 * @param line input line
394 * @param line input line
390395 * @param properties new property values
391396 * @return new line
392397 */
403408 String value = properties.getProperty(key);
404409 if (value != null) {
405410 // build property with new value
406 line = key+"="+ escapeValue(value);
411 line = key + "=" + escapeValue(value);
407412 // remove property from source
408413 properties.remove(key);
409414 }
410415 }
411 return line+comment;
416 return line + comment;
412417 }
413418
414419 /**
415420 * Escape backslash in value.
421 *
416422 * @param value value
417423 * @return escaped value
418424 */
419425 private static String escapeValue(String value) {
420426 StringBuilder buffer = new StringBuilder();
421 for (char c:value.toCharArray()) {
427 for (char c : value.toCharArray()) {
422428 if (c == '\\') {
423429 buffer.append('\\');
424430 }
546552 }
547553
548554 public static synchronized String loadRefreshToken(String username) {
549 return Settings.getProperty("davmail.oauth."+username.toLowerCase()+".refreshToken");
550 }
551
552 public static synchronized void storeRefreshToken(String refreshToken, String username) {
553 Settings.setProperty("davmail.oauth."+username.toLowerCase()+".refreshToken", refreshToken);
554 Settings.save();
555 }
555 String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
556 if (isEmpty(tokenFilePath)) {
557 return Settings.getProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken");
558 } else {
559 return loadtokenFromFile(tokenFilePath, username.toLowerCase());
560 }
561 }
562
563
564 public static synchronized void storeRefreshToken(String username, String refreshToken) {
565 String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
566 if (isEmpty(tokenFilePath)) {
567 Settings.setProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken", refreshToken);
568 Settings.save();
569 } else {
570 savetokentoFile(tokenFilePath, username.toLowerCase(), refreshToken);
571 }
572 }
573
574 /**
575 * Persist token in davmail.oauth.tokenFilePath.
576 *
577 * @param tokenFilePath token file path
578 * @param username username
579 * @param refreshToken Oauth refresh token
580 */
581 private static void savetokentoFile(String tokenFilePath, String username, String refreshToken) {
582 try {
583 checkCreateTokenFilePath(tokenFilePath);
584 Properties properties = new Properties();
585 try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
586 properties.load(fis);
587 }
588 properties.setProperty(username, refreshToken);
589 try (FileOutputStream fos = new FileOutputStream(tokenFilePath)) {
590 properties.store(fos, "Oauth tokens");
591 }
592 } catch (IOException e) {
593 Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
594 }
595 }
596
597 /**
598 * Load token from davmail.oauth.tokenFilePath.
599 *
600 * @param tokenFilePath token file path
601 * @param username username
602 * @return encrypted token value
603 */
604 private static String loadtokenFromFile(String tokenFilePath, String username) {
605 try {
606 checkCreateTokenFilePath(tokenFilePath);
607 Properties properties = new Properties();
608 try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
609 properties.load(fis);
610 }
611 return properties.getProperty(username);
612 } catch (IOException e) {
613 Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
614 }
615 return null;
616 }
617
618 private static void checkCreateTokenFilePath(String tokenFilePath) throws IOException {
619 File file = new File(tokenFilePath);
620 File parentFile = file.getParentFile();
621 if (parentFile != null) {
622 if (parentFile.mkdirs()) {
623 LOGGER.info("Created token file directory "+parentFile.getAbsolutePath());
624 }
625 }
626 if (file.createNewFile()) {
627 LOGGER.info("Created token file "+tokenFilePath);
628 }
629 }
630
556631 /**
557632 * Build logging properties prefix.
558633 *
667742 System.getProperty("os.name").toLowerCase().startsWith("freebsd");
668743 }
669744
745 public static String getUserAgent() {
746 return getProperty("davmail.userAgent", Settings.EDGE_USER_AGENT);
747 }
670748 }
471471 response.appendProperty("D:resourcetype");
472472 }
473473 if (request.hasProperty("displayname")) {
474 response.appendProperty("D:displayname", item.getName());
474 response.appendProperty("D:displayname", StringUtil.xmlEncode(item.getName()));
475475 }
476476 response.endPropStatOK();
477477 response.endResponse();
2424 import davmail.exception.WebdavNotAvailableException;
2525 import davmail.exchange.auth.ExchangeAuthenticator;
2626 import davmail.exchange.auth.ExchangeFormAuthenticator;
27 import davmail.exchange.auth.HC4ExchangeFormAuthenticator;
2827 import davmail.exchange.dav.DavExchangeSession;
29 import davmail.exchange.dav.HC4DavExchangeSession;
3028 import davmail.exchange.ews.EwsExchangeSession;
31 import davmail.http.DavGatewayHttpClientFacade;
32 import org.apache.commons.httpclient.HttpClient;
33 import org.apache.commons.httpclient.HttpStatus;
34 import org.apache.commons.httpclient.methods.GetMethod;
29 import davmail.http.HttpClientAdapter;
30 import davmail.http.request.GetRequest;
31 import org.apache.http.HttpStatus;
32 import org.apache.http.client.methods.CloseableHttpResponse;
3533
3634 import java.awt.*;
3735 import java.io.IOException;
124122 try {
125123 String mode = Settings.getProperty("davmail.mode");
126124 if (Settings.O365.equals(mode)) {
127 // force url wit O365
125 // force url with O365
128126 baseUrl = Settings.O365_URL;
129127 }
130128
176174 }
177175
178176 if (authenticatorClass != null) {
179 ExchangeAuthenticator authenticator = (ExchangeAuthenticator) Class.forName(authenticatorClass).newInstance();
177 ExchangeAuthenticator authenticator = (ExchangeAuthenticator) Class.forName(authenticatorClass)
178 .getDeclaredConstructor().newInstance();
180179 authenticator.setUsername(poolKey.userName);
181180 authenticator.setPassword(poolKey.password);
182181 authenticator.authenticate();
183 HttpClient httpClient = DavGatewayHttpClientFacade.getInstance(authenticator.getExchangeUri().toString());
184 DavGatewayHttpClientFacade.createMultiThreadedHttpConnectionManager(httpClient);
185 session = new EwsExchangeSession(httpClient, authenticator.getToken(), poolKey.userName);
182 session = new EwsExchangeSession(authenticator.getExchangeUri(), authenticator.getToken(), poolKey.userName);
186183
187184 } else if (Settings.EWS.equals(mode) || Settings.O365.equals(mode)
188 // direct EWS even if mode is different
189 || poolKey.url.toLowerCase().endsWith("/ews/exchange.asmx")) {
185 // direct EWS even if mode is different
186 || poolKey.url.toLowerCase().endsWith("/ews/exchange.asmx")) {
190187 if (poolKey.url.toLowerCase().endsWith("/ews/exchange.asmx")) {
191188 ExchangeSession.LOGGER.debug("Direct EWS authentication");
192 HttpClient httpClient = DavGatewayHttpClientFacade.getInstance(poolKey.url);
193 DavGatewayHttpClientFacade.createMultiThreadedHttpConnectionManager(httpClient);
194 DavGatewayHttpClientFacade.setCredentials(httpClient, poolKey.userName, poolKey.password);
195 session = new EwsExchangeSession(httpClient, poolKey.userName);
189 session = new EwsExchangeSession(poolKey.url, poolKey.userName, poolKey.password);
196190 } else {
197191 ExchangeSession.LOGGER.debug("OWA authentication in EWS mode");
198192 ExchangeFormAuthenticator exchangeFormAuthenticator = new ExchangeFormAuthenticator();
200194 exchangeFormAuthenticator.setUsername(poolKey.userName);
201195 exchangeFormAuthenticator.setPassword(poolKey.password);
202196 exchangeFormAuthenticator.authenticate();
203 session = new EwsExchangeSession(exchangeFormAuthenticator.getHttpClient(),
197 session = new EwsExchangeSession(exchangeFormAuthenticator.getHttpClientAdapter(),
204198 exchangeFormAuthenticator.getExchangeUri(), exchangeFormAuthenticator.getUsername());
205199 }
206 } else if ("HC4WebDav".equals(mode)) {
207 HC4ExchangeFormAuthenticator exchangeFormAuthenticator = new HC4ExchangeFormAuthenticator();
208 exchangeFormAuthenticator.setUrl(poolKey.url);
209 exchangeFormAuthenticator.setUsername(poolKey.userName);
210 exchangeFormAuthenticator.setPassword(poolKey.password);
211 exchangeFormAuthenticator.authenticate();
212 session = new HC4DavExchangeSession(exchangeFormAuthenticator.getHttpClientAdapter(),
213 exchangeFormAuthenticator.getExchangeUri(),
214 exchangeFormAuthenticator.getUsername());
215200 } else {
216201 ExchangeFormAuthenticator exchangeFormAuthenticator = new ExchangeFormAuthenticator();
217202 exchangeFormAuthenticator.setUrl(poolKey.url);
219204 exchangeFormAuthenticator.setPassword(poolKey.password);
220205 exchangeFormAuthenticator.authenticate();
221206 try {
222 session = new DavExchangeSession(exchangeFormAuthenticator.getHttpClient(),
207 session = new DavExchangeSession(exchangeFormAuthenticator.getHttpClientAdapter(),
223208 exchangeFormAuthenticator.getExchangeUri(),
224209 exchangeFormAuthenticator.getUsername());
225210 } catch (WebdavNotAvailableException e) {
226211 if (Settings.AUTO.equals(mode)) {
227212 ExchangeSession.LOGGER.debug(e.getMessage() + ", retry with EWS");
228 session = new EwsExchangeSession(exchangeFormAuthenticator.getHttpClient(),
229 exchangeFormAuthenticator.getExchangeUri(),
230 exchangeFormAuthenticator.getUsername());
213 session = new EwsExchangeSession(poolKey.url, poolKey.userName, poolKey.password);
231214 } else {
232215 throw e;
233216 }
256239 * Check if whitelist is empty or email is allowed.
257240 * userWhiteList is a comma separated list of values.
258241 * \@company.com means all domain users are allowed
242 *
259243 * @param email user email
260244 */
261245 private static void checkWhiteList(String email) throws DavMailAuthenticationException {
318302 if (url == null || (!url.startsWith("http://") && !url.startsWith("https://"))) {
319303 throw new DavMailException("LOG_INVALID_URL", url);
320304 }
321 HttpClient httpClient = DavGatewayHttpClientFacade.getInstance(url);
322 GetMethod testMethod = new GetMethod(url);
323 try {
305 try (
306 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(url);
307 CloseableHttpResponse response = httpClientAdapter.execute(new GetRequest(url))
308 ) {
324309 // get webMail root url (will not follow redirects)
325 int status = DavGatewayHttpClientFacade.executeTestMethod(httpClient, testMethod);
310 int status = response.getStatusLine().getStatusCode();
326311 ExchangeSession.LOGGER.debug("Test configuration status: " + status);
327312 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED
328 && !DavGatewayHttpClientFacade.isRedirect(status)) {
313 && !HttpClientAdapter.isRedirect(status)) {
329314 throw new DavMailException("EXCEPTION_CONNECTION_FAILED", url, status);
330315 }
331316 // session opened, future failure will mean network down
334319 errorSent = false;
335320 } catch (Exception exc) {
336321 handleNetworkDown(exc);
337 } finally {
338 testMethod.releaseConnection();
339322 }
340323
341324 }
408391 /**
409392 * Reset config check status and clear session pool.
410393 */
411 public static void reset() {
394 public static void shutdown() {
412395 configChecked = false;
413396 errorSent = false;
414 POOL_MAP.clear();
397 synchronized (LOCK) {
398 for (ExchangeSession session:POOL_MAP.values()) {
399 session.close();
400 }
401 POOL_MAP.clear();
402 }
415403 }
416404 }
6161 * @throws InterruptedException on error
6262 * @throws IOException on error
6363 */
64 public static void loadFolder(ExchangeSession.Folder folder, OutputStream outputStream) throws InterruptedException, IOException {
64 public static void loadFolder(ExchangeSession.Folder folder, OutputStream outputStream) throws IOException {
6565 FolderLoadThread folderLoadThread = new FolderLoadThread(currentThread().getName(), folder);
6666 folderLoadThread.start();
6767 while (!folderLoadThread.isComplete) {
68 folderLoadThread.join(20000);
68 try {
69 folderLoadThread.join(20000);
70 } catch (InterruptedException e) {
71 LOGGER.warn("Thread interrupted", e);
72 Thread.currentThread().interrupt();
73 }
6974 LOGGER.debug("Still loading " + folder.folderPath + " (" + folder.count() + " messages)");
7075 if (Settings.getBooleanProperty("davmail.enableKeepAlive", false)) {
7176 try {
4646 /**
4747 * Create message in a separate thread.
4848 *
49 * @param session Exchange session
50 * @param folderPath folder path
51 * @param messageName message name
52 * @param properties message properties
53 * @param mimeMessage message content
49 * @param session Exchange session
50 * @param folderPath folder path
51 * @param messageName message name
52 * @param properties message properties
53 * @param mimeMessage message content
5454 * @param outputStream output stream
5555 * @param capabilities IMAP capabilities
5656 * @throws InterruptedException on error
57 * @throws IOException on error
57 * @throws IOException on error
5858 */
59 public static void createMessage(ExchangeSession session, String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage, OutputStream outputStream, String capabilities) throws InterruptedException, IOException {
59 public static void createMessage(ExchangeSession session, String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage, OutputStream outputStream, String capabilities) throws IOException {
6060 MessageCreateThread messageCreateThread = new MessageCreateThread(currentThread().getName(), session, folderPath, messageName, properties, mimeMessage);
6161 messageCreateThread.start();
6262 while (!messageCreateThread.isComplete) {
63 messageCreateThread.join(20000);
63 try {
64 messageCreateThread.join(20000);
65 } catch (InterruptedException e) {
66 LOGGER.warn("Thread interrupted", e);
67 Thread.currentThread().interrupt();
68 }
6469 if (!messageCreateThread.isComplete) {
6570 if (Settings.getBooleanProperty("davmail.enableKeepAlive", false)) {
6671 LOGGER.debug("Still loading message, send capabilities untagged response to avoid timeout");
6772 try {
68 LOGGER.debug("* "+capabilities);
69 outputStream.write(("* "+capabilities).getBytes(StandardCharsets.US_ASCII));
73 LOGGER.debug("* " + capabilities);
74 outputStream.write(("* " + capabilities).getBytes(StandardCharsets.US_ASCII));
7075 outputStream.write((char) 13);
7176 outputStream.write((char) 10);
7277 outputStream.flush();
4646 * @throws IOException on error
4747 * @throws MessagingException on error
4848 */
49 public static void loadMimeMessage(ExchangeSession.Message message, OutputStream outputStream) throws IOException, MessagingException, InterruptedException {
49 public static void loadMimeMessage(ExchangeSession.Message message, OutputStream outputStream) throws IOException, MessagingException {
5050 if (message.size < 1024 * 1024) {
5151 message.loadMimeMessage();
5252 } else {
5454 MessageLoadThread messageLoadThread = new MessageLoadThread(currentThread().getName(), message);
5555 messageLoadThread.start();
5656 while (!messageLoadThread.isComplete) {
57 messageLoadThread.join(10000);
57 try {
58 messageLoadThread.join(10000);
59 } catch (InterruptedException e) {
60 LOGGER.warn("Thread interrupted", e);
61 Thread.currentThread().interrupt();
62 }
5863 LOGGER.debug("Still loading uid " + message.getUid() + " imapUid " + message.getImapUid());
5964 if (Settings.getBooleanProperty("davmail.enableKeepAlive", false)) {
6065 try {
4949 inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
5050 inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE);
5151 inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
52 // Woodstox 5.2.0
53 //if (inputFactory.isPropertySupported("com.ctc.wstx.allowXml11EscapedCharsInXml10")) {
54 // inputFactory.setProperty("com.ctc.wstx.allowXml11EscapedCharsInXml10", Boolean.TRUE);
55 //}
52 // Woodstox 5.2.0 or later
53 if (inputFactory.isPropertySupported("com.ctc.wstx.allowXml11EscapedCharsInXml10")) {
54 inputFactory.setProperty("com.ctc.wstx.allowXml11EscapedCharsInXml10", Boolean.TRUE);
55 }
5656 return inputFactory;
5757 }
5858
1818
1919 package davmail.exchange.auth;
2020
21 import davmail.http.HttpClientAdapter;
22
2123 import java.io.IOException;
2224 import java.net.URI;
2325
26 /**
27 * Common interface for all Exchange and O365 authenticators.
28 * Implement this interface to build custom authenticators for unsupported Exchange architecture
29 */
2430 public interface ExchangeAuthenticator {
2531 void setUsername(String username);
2632
2733 void setPassword(String password);
2834
35 /**
36 * Authenticate against Exchange or O365
37 * @throws IOException on error
38 */
2939 void authenticate() throws IOException;
3040
3141 O365Token getToken() throws IOException;
3242
43 /**
44 * Return default or computed Exchange or O365 url
45 * @return target url
46 */
3347 URI getExchangeUri();
48
49 /**
50 * Return a new HttpClientAdapter instance with pooling enabled for ExchangeSession
51 * @return HttpClientAdapter instance
52 */
53 HttpClientAdapter getHttpClientAdapter();
3454 }
2222 import davmail.exception.DavMailAuthenticationException;
2323 import davmail.exception.DavMailException;
2424 import davmail.exception.WebdavNotAvailableException;
25 import davmail.http.DavGatewayHttpClientFacade;
2625 import davmail.http.DavGatewayOTPPrompt;
26 import davmail.http.HttpClientAdapter;
27 import davmail.http.URIUtil;
28 import davmail.http.request.GetRequest;
29 import davmail.http.request.PostRequest;
30 import davmail.http.request.ResponseWrapper;
2731 import davmail.util.StringUtil;
28 import org.apache.commons.httpclient.Cookie;
29 import org.apache.commons.httpclient.HttpClient;
30 import org.apache.commons.httpclient.HttpMethod;
31 import org.apache.commons.httpclient.URI;
32 import org.apache.commons.httpclient.URIException;
33 import org.apache.commons.httpclient.methods.GetMethod;
34 import org.apache.commons.httpclient.methods.PostMethod;
35 import org.apache.commons.httpclient.params.HttpClientParams;
3632 import org.apache.http.HttpStatus;
33 import org.apache.http.client.methods.CloseableHttpResponse;
34 import org.apache.http.client.methods.HttpGet;
35 import org.apache.http.client.methods.HttpRequestBase;
36 import org.apache.http.client.protocol.HttpClientContext;
37 import org.apache.http.client.utils.URIBuilder;
38 import org.apache.http.cookie.Cookie;
39 import org.apache.http.impl.client.BasicCookieStore;
40 import org.apache.http.impl.cookie.BasicClientCookie;
3741 import org.apache.log4j.Logger;
42 import org.htmlcleaner.BaseToken;
3843 import org.htmlcleaner.CommentNode;
3944 import org.htmlcleaner.ContentNode;
4045 import org.htmlcleaner.HtmlCleaner;
4550 import java.io.ByteArrayInputStream;
4651 import java.io.IOException;
4752 import java.net.ConnectException;
53 import java.net.URI;
54 import java.net.URISyntaxException;
4855 import java.net.UnknownHostException;
4956 import java.nio.charset.StandardCharsets;
5057 import java.util.ArrayList;
5360 import java.util.Set;
5461
5562 /**
56 * Form based Exchange authentication.
63 * New Exchange form authenticator based on HttpClient 4.
5764 */
5865 public class ExchangeFormAuthenticator implements ExchangeAuthenticator {
59 protected static final Logger LOGGER = Logger.getLogger(ExchangeFormAuthenticator.class);
66 protected static final Logger LOGGER = Logger.getLogger("davmail.exchange.ExchangeSession");
6067
6168 /**
6269 * Various username fields found on custom Exchange authentication forms
6370 */
6471 protected static final Set<String> USER_NAME_FIELDS = new HashSet<>();
72
6573 static {
6674 USER_NAME_FIELDS.add("username");
6775 USER_NAME_FIELDS.add("txtusername");
7684 * Various password fields found on custom Exchange authentication forms
7785 */
7886 protected static final Set<String> PASSWORD_FIELDS = new HashSet<>();
87
7988 static {
8089 PASSWORD_FIELDS.add("password");
8190 PASSWORD_FIELDS.add("txtUserPass");
9099 * Used to open OTP dialog
91100 */
92101 protected static final Set<String> TOKEN_FIELDS = new HashSet<>();
102
93103 static {
94104 TOKEN_FIELDS.add("SafeWordPassword");
95105 TOKEN_FIELDS.add("passcode");
112122 */
113123 private String url;
114124 /**
115 * HttpClient 3 instance
116 */
117 private HttpClient httpClient;
125 * HttpClient 4 adapter
126 */
127 private HttpClientAdapter httpClientAdapter;
118128 /**
119129 * A OTP pre-auth page may require a different username.
120130 */
140150 * Maximum number of times the user can try to input again the OTP pre-auth key before giving up.
141151 */
142152 private static final int MAX_OTP_RETRIES = 3;
143 // base Exchange URI after authentication
153
154 /**
155 * base Exchange URI after authentication
156 */
144157 private java.net.URI exchangeUri;
145158
146159
161174 @Override
162175 public void authenticate() throws DavMailException {
163176 try {
164 httpClient = DavGatewayHttpClientFacade.getInstance(url);
165 // set private connection pool
166 DavGatewayHttpClientFacade.createMultiThreadedHttpConnectionManager(httpClient);
167 boolean isHttpAuthentication = isHttpAuthentication(httpClient, url);
168 if (isHttpAuthentication) {
169 DavGatewayHttpClientFacade.addNTLM(httpClient);
170 }
171 // clear cookies created by authentication test
172 httpClient.getState().clearCookies();
177 // create HttpClient adapter, enable pooling as this instance will be passed to ExchangeSession
178 httpClientAdapter = new HttpClientAdapter(url, true);
179 boolean isHttpAuthentication = isHttpAuthentication(httpClientAdapter, url);
173180
174181 // The user may have configured an OTP pre-auth username. It is processed
175182 // so early because OTP pre-auth may disappear in the Exchange LAN and this
187194 }
188195 }
189196
190 DavGatewayHttpClientFacade.setCredentials(httpClient, username, password);
197 // set real credentials on http client
198 httpClientAdapter.setCredentials(username, password);
191199
192200 // get webmail root url
193201 // providing credentials
194202 // manually follow redirect
195 HttpMethod method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, url);
196
197 if (!this.isAuthenticated(method)) {
203 GetRequest getRequest = httpClientAdapter.executeFollowRedirect(new GetRequest(url));
204
205 if (!this.isAuthenticated(getRequest)) {
198206 if (isHttpAuthentication) {
199 int status = method.getStatusCode();
207 int status = getRequest.getStatusCode();
200208
201209 if (status == HttpStatus.SC_UNAUTHORIZED) {
202 method.releaseConnection();
203210 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
204211 } else if (status != HttpStatus.SC_OK) {
205 method.releaseConnection();
206 throw DavGatewayHttpClientFacade.buildHttpResponseException(method);
212 throw HttpClientAdapter.buildHttpResponseException(getRequest, getRequest.getHttpResponse());
207213 }
208214 // workaround for basic authentication on /exchange and form based authentication at /owa
209 if ("/owa/auth/logon.aspx".equals(method.getPath())) {
210 method = formLogin(httpClient, method, password);
215 if ("/owa/auth/logon.aspx".equals(getRequest.getURI().getPath())) {
216 formLogin(httpClientAdapter, getRequest, password);
211217 }
212218 } else {
213 method = formLogin(httpClient, method, password);
214 }
215 }
216
217 // avoid 401 roundtrips, only if NTLM is disabled and basic authentication enabled
218 if (isHttpAuthentication && !DavGatewayHttpClientFacade.hasNTLMorNegotiate(httpClient)) {
219 httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, true);
220 }
221
222 exchangeUri = java.net.URI.create(method.getURI().getURI());
223 method.releaseConnection();
219 formLogin(httpClientAdapter, getRequest, password);
220 }
221 }
224222
225223 } catch (DavMailAuthenticationException exc) {
226224 close();
239237 LOGGER.error(BundleMessage.formatLog("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc));
240238 throw new DavMailException("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc);
241239 }
242 LOGGER.debug("Authenticated with "+url);
240 LOGGER.debug("Successfully authenticated to " + exchangeUri);
243241 }
244242
245243 /**
246244 * Test authentication mode : form based or basic.
247245 *
248246 * @param url exchange base URL
249 * @param httpClient httpClient instance
247 * @param httpClient httpClientAdapter instance
250248 * @return true if basic authentication detected
251249 */
252 protected boolean isHttpAuthentication(HttpClient httpClient, String url) {
253 return DavGatewayHttpClientFacade.getHttpStatus(httpClient, url) == HttpStatus.SC_UNAUTHORIZED;
250 protected boolean isHttpAuthentication(HttpClientAdapter httpClient, String url) {
251 boolean isHttpAuthentication = false;
252 HttpGet httpGet = new HttpGet(url);
253 // Create a local context to avoid cookies in main httpClient
254 HttpClientContext context = HttpClientContext.create();
255 context.setCookieStore(new BasicCookieStore());
256 try (CloseableHttpResponse response = httpClient.execute(httpGet, context)) {
257 isHttpAuthentication = response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED;
258 } catch (IOException e) {
259 // ignore
260 }
261 return isHttpAuthentication;
254262 }
255263
256264 /**
258266 *
259267 * @return true if session cookies are available
260268 */
261 protected boolean isAuthenticated(HttpMethod method) {
269 protected boolean isAuthenticated(ResponseWrapper getRequest) {
262270 boolean authenticated = false;
263 if (method.getStatusCode() == HttpStatus.SC_OK
264 && "/ews/services.wsdl".equalsIgnoreCase(method.getPath())) {
271 if (getRequest.getStatusCode() == HttpStatus.SC_OK
272 && "/ews/services.wsdl".equalsIgnoreCase(getRequest.getURI().getPath())) {
265273 // direct EWS access returned wsdl
266274 authenticated = true;
267275 } else {
268276 // check cookies
269 for (Cookie cookie : httpClient.getState().getCookies()) {
277 for (Cookie cookie : httpClientAdapter.getCookies()) {
270278 // Exchange 2003 cookies
271279 if (cookie.getName().startsWith("cadata") || "sessionid".equals(cookie.getName())
272280 // Exchange 2007 cookie
280288 return authenticated;
281289 }
282290
283 protected HttpMethod formLogin(HttpClient httpClient, HttpMethod initmethod, String password) throws IOException {
291 protected void formLogin(HttpClientAdapter httpClient, ResponseWrapper initRequest, String password) throws IOException {
284292 LOGGER.debug("Form based authentication detected");
285293
286 HttpMethod logonMethod = buildLogonMethod(httpClient, initmethod);
287 if (logonMethod == null) {
288 LOGGER.debug("Authentication form not found at " + initmethod.getURI() + ", trying default url");
289 logonMethod = new PostMethod("/owa/auth/owaauth.dll");
290 }
291 logonMethod = postLogonMethod(httpClient, logonMethod, password);
292
293 return logonMethod;
294 PostRequest postRequest = buildLogonMethod(httpClient, initRequest);
295 if (postRequest == null) {
296 LOGGER.debug("Authentication form not found at " + initRequest.getURI() + ", trying default url");
297 postRequest = new PostRequest("/owa/auth/owaauth.dll");
298 }
299
300 exchangeUri = postLogonMethod(httpClient, postRequest, password).getURI();
294301 }
295302
296303 /**
297304 * Try to find logon method path from logon form body.
298305 *
299 * @param httpClient httpClient instance
300 * @param initmethod form body http method
306 * @param httpClient httpClientAdapter instance
307 * @param responseWrapper init request response wrapper
301308 * @return logon method
302 * @throws IOException on error
303 */
304 protected HttpMethod buildLogonMethod(HttpClient httpClient, HttpMethod initmethod) throws IOException {
305
306 HttpMethod logonMethod = null;
309 */
310 protected PostRequest buildLogonMethod(HttpClientAdapter httpClient, ResponseWrapper responseWrapper) {
311 PostRequest logonMethod = null;
307312
308313 // create an instance of HtmlCleaner
309314 HtmlCleaner cleaner = new HtmlCleaner();
312317 usernameInputs.clear();
313318
314319 try {
315 TagNode node = cleaner.clean(initmethod.getResponseBodyAsStream());
316 List forms = node.getElementListByName("form", true);
320 URI uri = responseWrapper.getURI();
321 String responseBody = responseWrapper.getResponseBodyAsString();
322 TagNode node = cleaner.clean(new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)));
323 List<? extends TagNode> forms = node.getElementListByName("form", true);
317324 TagNode logonForm = null;
318325 // select form
319326 if (forms.size() == 1) {
320 logonForm = (TagNode) forms.get(0);
327 logonForm = forms.get(0);
321328 } else if (forms.size() > 1) {
322329 for (Object form : forms) {
323330 if ("logonForm".equals(((TagNode) form).getAttributeByName("name"))) {
333340 logonMethodPath = "/owa/auth.owa";
334341 }
335342
336 logonMethod = new PostMethod(getAbsoluteUri(initmethod, logonMethodPath));
343 logonMethod = new PostRequest(getAbsoluteUri(uri, logonMethodPath));
337344
338345 // retrieve lost inputs attached to body
339 List inputList = node.getElementListByName("input", true);
346 List<? extends TagNode> inputList = node.getElementListByName("input", true);
340347
341348 for (Object input : inputList) {
342349 String type = ((TagNode) input).getAttributeByName("type");
343350 String name = ((TagNode) input).getAttributeByName("name");
344351 String value = ((TagNode) input).getAttributeByName("value");
345352 if ("hidden".equalsIgnoreCase(type) && name != null && value != null) {
346 ((PostMethod) logonMethod).addParameter(name, value);
353 logonMethod.setParameter(name, value);
347354 }
348355 // custom login form
349 if (name == null) {
350 LOGGER.debug("Skip invalid input with empty name");
351 } else if (USER_NAME_FIELDS.contains(name)) {
356 if (USER_NAME_FIELDS.contains(name)) {
352357 usernameInputs.add(name);
353358 } else if (PASSWORD_FIELDS.contains(name)) {
354359 passwordInput = name;
355360 } else if ("addr".equals(name)) {
356361 // this is not a logon form but a redirect form
357 HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
358 logonMethod = buildLogonMethod(httpClient, newInitMethod);
362 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(logonMethod));
359363 } else if (TOKEN_FIELDS.contains(name)) {
360364 // one time password, ask it to the user
361 ((PostMethod) logonMethod).addParameter(name, DavGatewayOTPPrompt.getOneTimePassword());
365 logonMethod.setParameter(name, DavGatewayOTPPrompt.getOneTimePassword());
362366 } else if ("otc".equals(name)) {
363367 // captcha image, get image and ask user
364368 String pinsafeUser = getAliasFromLogin();
365369 if (pinsafeUser == null) {
366370 pinsafeUser = username;
367371 }
368 GetMethod getMethod = new GetMethod("/PINsafeISAFilter.dll?username=" + pinsafeUser);
369 try {
370 int status = httpClient.executeMethod(getMethod);
372 HttpGet pinRequest = new HttpGet("/PINsafeISAFilter.dll?username=" + pinsafeUser);
373 try (CloseableHttpResponse pinResponse = httpClient.execute(pinRequest)) {
374 int status = pinResponse.getStatusLine().getStatusCode();
371375 if (status != HttpStatus.SC_OK) {
372 throw DavGatewayHttpClientFacade.buildHttpResponseException(getMethod);
376 throw HttpClientAdapter.buildHttpResponseException(pinRequest, pinResponse.getStatusLine());
373377 }
374 BufferedImage captchaImage = ImageIO.read(getMethod.getResponseBodyAsStream());
375 ((PostMethod) logonMethod).addParameter(name, DavGatewayOTPPrompt.getCaptchaValue(captchaImage));
376
377 } finally {
378 getMethod.releaseConnection();
378 BufferedImage captchaImage = ImageIO.read(pinResponse.getEntity().getContent());
379 logonMethod.setParameter(name, DavGatewayOTPPrompt.getCaptchaValue(captchaImage));
379380 }
380381 }
381382 }
382383 } else {
383 List frameList = node.getElementListByName("frame", true);
384 List<? extends TagNode> frameList = node.getElementListByName("frame", true);
384385 if (frameList.size() == 1) {
385 String src = ((TagNode) frameList.get(0)).getAttributeByName("src");
386 String src = frameList.get(0).getAttributeByName("src");
386387 if (src != null) {
387388 LOGGER.debug("Frames detected in form page, try frame content");
388 initmethod.releaseConnection();
389 HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, src);
390 logonMethod = buildLogonMethod(httpClient, newInitMethod);
389 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(src)));
391390 }
392391 } else {
393392 // another failover for script based logon forms (Exchange 2007)
394 List scriptList = node.getElementListByName("script", true);
393 List<? extends TagNode> scriptList = node.getElementListByName("script", true);
395394 for (Object script : scriptList) {
396 List contents = ((TagNode) script).getAllChildren();
395 List<? extends BaseToken> contents = ((TagNode) script).getAllChildren();
397396 for (Object content : contents) {
398397 if (content instanceof CommentNode) {
399398 String scriptValue = ((CommentNode) content).getCommentedContent();
403402 sLgn = StringUtil.getToken(scriptValue, "var a_sLgn = \"", "\"");
404403 }
405404 if (sUrl != null && sLgn != null) {
406 String src = getScriptBasedFormURL(initmethod, sLgn + sUrl);
405 URI src = getScriptBasedFormURL(uri, sLgn + sUrl);
407406 LOGGER.debug("Detected script based logon, redirect to form at " + src);
408 HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, src);
409 logonMethod = buildLogonMethod(httpClient, newInitMethod);
407 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(src)));
410408 }
411409
412410 } else if (content instanceof ContentNode) {
415413 String location = StringUtil.getToken(scriptValue, "window.location.replace(\"", "\"");
416414 if (location != null) {
417415 LOGGER.debug("Post logon redirect to: " + location);
418 logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, location);
416 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(location)));
419417 }
420418 }
421419 }
422420 }
423421 }
424422 }
425 } catch (IOException e) {
426 LOGGER.error("Error parsing login form at " + initmethod.getURI());
427 } finally {
428 initmethod.releaseConnection();
423 } catch (IOException | URISyntaxException e) {
424 LOGGER.error("Error parsing login form at " + responseWrapper.getURI());
429425 }
430426
431427 return logonMethod;
432428 }
433429
434430
435 protected HttpMethod postLogonMethod(HttpClient httpClient, HttpMethod logonMethod, String password) throws IOException {
431 protected ResponseWrapper postLogonMethod(HttpClientAdapter httpClient, PostRequest logonMethod, String password) throws IOException {
436432
437433 setAuthFormFields(logonMethod, httpClient, password);
438434
439435 // add exchange 2010 PBack cookie in compatibility mode
440 httpClient.getState().addCookie(new Cookie(httpClient.getHostConfiguration().getHost(), "PBack", "0", "/", null, false));
441
442 logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
436 BasicClientCookie pBackCookie = new BasicClientCookie("PBack", "0");
437 pBackCookie.setPath("/");
438 pBackCookie.setDomain(httpClientAdapter.getHost());
439 httpClient.addCookie(pBackCookie);
440
441 ResponseWrapper resultRequest = httpClient.executeFollowRedirect(logonMethod);
443442
444443 // test form based authentication
445 checkFormLoginQueryString(logonMethod);
444 checkFormLoginQueryString(resultRequest);
446445
447446 // workaround for post logon script redirect
448 if (!isAuthenticated(logonMethod)) {
447 if (!isAuthenticated(resultRequest)) {
449448 // try to get new method from script based redirection
450 logonMethod = buildLogonMethod(httpClient, logonMethod);
449 logonMethod = buildLogonMethod(httpClient, resultRequest);
451450
452451 if (logonMethod != null) {
453452 if (otpPreAuthFound && otpPreAuthRetries < MAX_OTP_RETRIES) {
459458 }
460459
461460 // if logonMethod is not null, try to follow redirection
462 logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
463 checkFormLoginQueryString(logonMethod);
461 resultRequest = httpClient.executeFollowRedirect(logonMethod);
462
463 checkFormLoginQueryString(resultRequest);
464464 // also check cookies
465 if (!isAuthenticated(logonMethod)) {
465 if (!isAuthenticated(resultRequest)) {
466466 throwAuthenticationFailed();
467467 }
468468 } else {
472472 }
473473
474474 // check for language selection form
475 if (logonMethod != null && "/owa/languageselection.aspx".equals(logonMethod.getPath())) {
475 if ("/owa/languageselection.aspx".equals(resultRequest.getURI().getPath())) {
476476 // need to submit form
477 logonMethod = submitLanguageSelectionForm(logonMethod);
478 }
479 return logonMethod;
480 }
481
482 protected HttpMethod submitLanguageSelectionForm(HttpMethod logonMethod) throws IOException {
483 PostMethod postLanguageFormMethod;
477 resultRequest = submitLanguageSelectionForm(resultRequest.getURI(), resultRequest.getResponseBodyAsString());
478 }
479 return resultRequest;
480 }
481
482 protected ResponseWrapper submitLanguageSelectionForm(URI uri, String responseBodyAsString) throws IOException {
483 PostRequest postLanguageFormMethod;
484484 // create an instance of HtmlCleaner
485485 HtmlCleaner cleaner = new HtmlCleaner();
486486
487487 try {
488 TagNode node = cleaner.clean(logonMethod.getResponseBodyAsStream());
489 List forms = node.getElementListByName("form", true);
488 TagNode node = cleaner.clean(responseBodyAsString);
489 List<? extends TagNode> forms = node.getElementListByName("form", true);
490490 TagNode languageForm;
491491 // select form
492492 if (forms.size() == 1) {
493 languageForm = (TagNode) forms.get(0);
493 languageForm = forms.get(0);
494494 } else {
495495 throw new IOException("Form not found");
496496 }
497497 String languageMethodPath = languageForm.getAttributeByName("action");
498498
499 postLanguageFormMethod = new PostMethod(getAbsoluteUri(logonMethod, languageMethodPath));
500
501 List inputList = languageForm.getElementListByName("input", true);
499 postLanguageFormMethod = new PostRequest(getAbsoluteUri(uri, languageMethodPath));
500
501 List<? extends TagNode> inputList = languageForm.getElementListByName("input", true);
502502 for (Object input : inputList) {
503503 String name = ((TagNode) input).getAttributeByName("name");
504504 String value = ((TagNode) input).getAttributeByName("value");
505505 if (name != null && value != null) {
506 postLanguageFormMethod.addParameter(name, value);
507 }
508 }
509 List selectList = languageForm.getElementListByName("select", true);
506 postLanguageFormMethod.setParameter(name, value);
507 }
508 }
509 List<? extends TagNode> selectList = languageForm.getElementListByName("select", true);
510510 for (Object select : selectList) {
511511 String name = ((TagNode) select).getAttributeByName("name");
512 List optionList = ((TagNode) select).getElementListByName("option", true);
512 List<? extends TagNode> optionList = ((TagNode) select).getElementListByName("option", true);
513513 String value = null;
514514 for (Object option : optionList) {
515515 if (((TagNode) option).getAttributeByName("selected") != null) {
518518 }
519519 }
520520 if (name != null && value != null) {
521 postLanguageFormMethod.addParameter(name, value);
522 }
523 }
524 } catch (IOException e) {
525 String errorMessage = "Error parsing language selection form at " + logonMethod.getURI();
521 postLanguageFormMethod.setParameter(name, value);
522 }
523 }
524 } catch (IOException | URISyntaxException e) {
525 String errorMessage = "Error parsing language selection form at " + uri;
526526 LOGGER.error(errorMessage);
527527 throw new IOException(errorMessage);
528 } finally {
529 logonMethod.releaseConnection();
530 }
531
532 return DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, postLanguageFormMethod);
533 }
534
535 protected void setAuthFormFields(HttpMethod logonMethod, HttpClient httpClient, String password) throws IllegalArgumentException {
528 }
529
530 return httpClientAdapter.executeFollowRedirect(postLanguageFormMethod);
531 }
532
533 protected void setAuthFormFields(HttpRequestBase logonMethod, HttpClientAdapter httpClient, String password) throws IllegalArgumentException {
536534 String usernameInput;
537535 if (usernameInputs.size() == 2) {
538536 String userid;
545543 userid = username.substring(0, pipeIndex);
546544 username = username.substring(pipeIndex + 1);
547545 // adjust credentials
548 DavGatewayHttpClientFacade.setCredentials(httpClient, username, password);
549 }
550 ((PostMethod) logonMethod).removeParameter("userid");
551 ((PostMethod) logonMethod).addParameter("userid", userid);
546 httpClient.setCredentials(username, password);
547 }
548 ((PostRequest) logonMethod).removeParameter("userid");
549 ((PostRequest) logonMethod).setParameter("userid", userid);
552550
553551 usernameInput = "username";
554552 } else if (usernameInputs.size() == 1) {
559557 usernameInput = "username";
560558 }
561559 // make sure username and password fields are empty
562 ((PostMethod) logonMethod).removeParameter(usernameInput);
560 ((PostRequest) logonMethod).removeParameter(usernameInput);
563561 if (passwordInput != null) {
564 ((PostMethod) logonMethod).removeParameter(passwordInput);
565 }
566 ((PostMethod) logonMethod).removeParameter("trusted");
567 ((PostMethod) logonMethod).removeParameter("flags");
562 ((PostRequest) logonMethod).removeParameter(passwordInput);
563 }
564 ((PostRequest) logonMethod).removeParameter("trusted");
565 ((PostRequest) logonMethod).removeParameter("flags");
568566
569567 if (passwordInput == null) {
570568 // This is a OTP pre-auth page. A different username may be required.
571569 otpPreAuthFound = true;
572570 otpPreAuthRetries++;
573 ((PostMethod) logonMethod).addParameter(usernameInput, preAuthusername);
571 ((PostRequest) logonMethod).setParameter(usernameInput, preAuthusername);
574572 } else {
575573 otpPreAuthFound = false;
576574 otpPreAuthRetries = 0;
577575 // This is a regular Exchange login page
578 ((PostMethod) logonMethod).addParameter(usernameInput, username);
579 ((PostMethod) logonMethod).addParameter(passwordInput, password);
580 ((PostMethod) logonMethod).addParameter("trusted", "4");
581 ((PostMethod) logonMethod).addParameter("flags", "4");
582 }
583 }
584
585 protected String getAbsoluteUri(HttpMethod method, String path) throws URIException {
586 URI uri = method.getURI();
576 ((PostRequest) logonMethod).setParameter(usernameInput, username);
577 ((PostRequest) logonMethod).setParameter(passwordInput, password);
578 ((PostRequest) logonMethod).setParameter("trusted", "4");
579 ((PostRequest) logonMethod).setParameter("flags", "4");
580 }
581 }
582
583 protected URI getAbsoluteUri(URI uri, String path) throws URISyntaxException {
584 URIBuilder uriBuilder = new URIBuilder(uri);
587585 if (path != null) {
588586 // reset query string
589 uri.setQuery(null);
587 uriBuilder.clearParameters();
590588 if (path.startsWith("/")) {
591589 // path is absolute, replace method path
592 uri.setPath(path);
590 uriBuilder.setPath(path);
593591 } else if (path.startsWith("http://") || path.startsWith("https://")) {
594 return path;
592 return URI.create(path);
595593 } else {
596594 // relative path, build new path
597 String currentPath = method.getPath();
595 String currentPath = uri.getPath();
598596 int end = currentPath.lastIndexOf('/');
599597 if (end >= 0) {
600 uri.setPath(currentPath.substring(0, end + 1) + path);
598 uriBuilder.setPath(currentPath.substring(0, end + 1) + path);
601599 } else {
602 throw new URIException(uri.getURI());
603 }
604 }
605 }
606 return uri.getURI();
607 }
608
609 protected String getScriptBasedFormURL(HttpMethod initmethod, String pathQuery) throws URIException {
610 URI initmethodURI = initmethod.getURI();
600 throw new URISyntaxException(uriBuilder.build().toString(), "Invalid path");
601 }
602 }
603 }
604 return uriBuilder.build();
605 }
606
607 protected URI getScriptBasedFormURL(URI uri, String pathQuery) throws URISyntaxException, IOException {
608 URIBuilder uriBuilder = new URIBuilder(uri);
611609 int queryIndex = pathQuery.indexOf('?');
612610 if (queryIndex >= 0) {
613611 if (queryIndex > 0) {
615613 String newPath = pathQuery.substring(0, queryIndex);
616614 if (newPath.startsWith("/")) {
617615 // absolute path
618 initmethodURI.setPath(newPath);
616 uriBuilder.setPath(newPath);
619617 } else {
620 String currentPath = initmethodURI.getPath();
618 String currentPath = uriBuilder.getPath();
621619 int folderIndex = currentPath.lastIndexOf('/');
622620 if (folderIndex >= 0) {
623621 // replace relative path
624 initmethodURI.setPath(currentPath.substring(0, folderIndex + 1) + newPath);
622 uriBuilder.setPath(currentPath.substring(0, folderIndex + 1) + newPath);
625623 } else {
626624 // should not happen
627 initmethodURI.setPath('/' + newPath);
628 }
629 }
630 }
631 initmethodURI.setQuery(pathQuery.substring(queryIndex + 1));
632 }
633 return initmethodURI.getURI();
634 }
635
636 protected void checkFormLoginQueryString(HttpMethod logonMethod) throws DavMailAuthenticationException {
637 String queryString = logonMethod.getQueryString();
625 uriBuilder.setPath('/' + newPath);
626 }
627 }
628 }
629 uriBuilder.setCustomQuery(URIUtil.decode(pathQuery.substring(queryIndex + 1)));
630 }
631 return uriBuilder.build();
632 }
633
634 protected void checkFormLoginQueryString(ResponseWrapper logonMethod) throws DavMailAuthenticationException {
635 String queryString = logonMethod.getURI().getRawQuery();
638636 if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=4"))) {
639 logonMethod.releaseConnection();
640637 throwAuthenticationFailed();
641638 }
642639 }
673670 * Shutdown http client connection manager
674671 */
675672 public void close() {
676 DavGatewayHttpClientFacade.close(httpClient);
673 httpClientAdapter.close();
677674 }
678675
679676 /**
680677 * Oauth token.
681678 * Only for Office 365 authenticators
679 *
682680 * @return unsupported
683681 */
684682 @Override
698696 }
699697
700698 /**
701 * Authenticated httpClient (with cookies).
699 * Return authenticated HttpClient 4 HttpClientAdapter
702700 *
703 * @return http client
704 */
705 public HttpClient getHttpClient() {
706 return httpClient;
701 * @return HttpClientAdapter instance
702 */
703 public HttpClientAdapter getHttpClientAdapter() {
704 return httpClientAdapter;
707705 }
708706
709707 /**
716714 return username;
717715 }
718716 }
717
+0
-745
src/java/davmail/exchange/auth/HC4ExchangeFormAuthenticator.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2010 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18
19 package davmail.exchange.auth;
20
21 import davmail.BundleMessage;
22 import davmail.exception.DavMailAuthenticationException;
23 import davmail.exception.DavMailException;
24 import davmail.exception.WebdavNotAvailableException;
25 import davmail.http.DavGatewayHttpClientFacade;
26 import davmail.http.DavGatewayOTPPrompt;
27 import davmail.http.HttpClientAdapter;
28 import davmail.http.URIUtil;
29 import davmail.http.request.GetRequest;
30 import davmail.http.request.PostRequest;
31 import davmail.http.request.ResponseWrapper;
32 import davmail.util.StringUtil;
33 import org.apache.http.HttpStatus;
34 import org.apache.http.client.methods.CloseableHttpResponse;
35 import org.apache.http.client.methods.HttpGet;
36 import org.apache.http.client.methods.HttpRequestBase;
37 import org.apache.http.client.protocol.HttpClientContext;
38 import org.apache.http.client.utils.URIBuilder;
39 import org.apache.http.cookie.Cookie;
40 import org.apache.http.impl.client.BasicCookieStore;
41 import org.apache.http.impl.cookie.BasicClientCookie;
42 import org.apache.log4j.Logger;
43 import org.htmlcleaner.BaseToken;
44 import org.htmlcleaner.CommentNode;
45 import org.htmlcleaner.ContentNode;
46 import org.htmlcleaner.HtmlCleaner;
47 import org.htmlcleaner.TagNode;
48
49 import javax.imageio.ImageIO;
50 import java.awt.image.BufferedImage;
51 import java.io.ByteArrayInputStream;
52 import java.io.IOException;
53 import java.net.ConnectException;
54 import java.net.URI;
55 import java.net.URISyntaxException;
56 import java.net.UnknownHostException;
57 import java.nio.charset.StandardCharsets;
58 import java.util.ArrayList;
59 import java.util.HashSet;
60 import java.util.List;
61 import java.util.Set;
62
63 /**
64 * New Exchange form authenticator based on HttpClient 4.
65 */
66 public class HC4ExchangeFormAuthenticator implements ExchangeAuthenticator {
67 protected static final Logger LOGGER = Logger.getLogger("davmail.exchange.ExchangeSession");
68
69 /**
70 * Various username fields found on custom Exchange authentication forms
71 */
72 protected static final Set<String> USER_NAME_FIELDS = new HashSet<>();
73
74 static {
75 USER_NAME_FIELDS.add("username");
76 USER_NAME_FIELDS.add("txtusername");
77 USER_NAME_FIELDS.add("userid");
78 USER_NAME_FIELDS.add("SafeWordUser");
79 USER_NAME_FIELDS.add("user_name");
80 USER_NAME_FIELDS.add("login");
81 USER_NAME_FIELDS.add("UserName");
82 }
83
84 /**
85 * Various password fields found on custom Exchange authentication forms
86 */
87 protected static final Set<String> PASSWORD_FIELDS = new HashSet<>();
88
89 static {
90 PASSWORD_FIELDS.add("password");
91 PASSWORD_FIELDS.add("txtUserPass");
92 PASSWORD_FIELDS.add("pw");
93 PASSWORD_FIELDS.add("basicPassword");
94 PASSWORD_FIELDS.add("passwd");
95 PASSWORD_FIELDS.add("Password");
96 }
97
98 /**
99 * Various OTP (one time password) fields found on custom Exchange authentication forms.
100 * Used to open OTP dialog
101 */
102 protected static final Set<String> TOKEN_FIELDS = new HashSet<>();
103
104 static {
105 TOKEN_FIELDS.add("SafeWordPassword");
106 TOKEN_FIELDS.add("passcode");
107 }
108
109
110 /**
111 * User provided username.
112 * Old preauth syntax: preauthusername"username
113 * Windows authentication with domain: domain\\username
114 * Note that OSX Mail.app does not support backslash in username, set default domain in DavMail settings instead
115 */
116 private String username;
117 /**
118 * User provided password
119 */
120 private String password;
121 /**
122 * OWA or EWS url
123 */
124 private String url;
125 /**
126 * HttpClient 4 adapter
127 */
128 private HttpClientAdapter httpClientAdapter;
129 /**
130 * A OTP pre-auth page may require a different username.
131 */
132 private String preAuthusername;
133
134 /**
135 * Logon form user name fields.
136 */
137 private final List<String> usernameInputs = new ArrayList<>();
138 /**
139 * Logon form password field, default is password.
140 */
141 private String passwordInput = null;
142 /**
143 * Tells if, during the login navigation, an OTP pre-auth page has been found.
144 */
145 private boolean otpPreAuthFound = false;
146 /**
147 * Lets the user try again a couple of times to enter the OTP pre-auth key before giving up.
148 */
149 private int otpPreAuthRetries = 0;
150 /**
151 * Maximum number of times the user can try to input again the OTP pre-auth key before giving up.
152 */
153 private static final int MAX_OTP_RETRIES = 3;
154
155 /**
156 * base Exchange URI after authentication
157 */
158 private java.net.URI exchangeUri;
159
160
161 @Override
162 public void setUsername(String username) {
163 this.username = username;
164 }
165
166 @Override
167 public void setPassword(String password) {
168 this.password = password;
169 }
170
171 public void setUrl(String url) {
172 this.url = url;
173 }
174
175 @Override
176 public void authenticate() throws DavMailException {
177 try {
178 // create HttpClient adapter, enable pooling as this instance will be passed to ExchangeSession
179 httpClientAdapter = new HttpClientAdapter(url, true);
180 boolean isHttpAuthentication = isHttpAuthentication(httpClientAdapter, url);
181
182 // The user may have configured an OTP pre-auth username. It is processed
183 // so early because OTP pre-auth may disappear in the Exchange LAN and this
184 // helps the user to not change is account settings in mail client at each network change.
185 if (preAuthusername == null) {
186 // Searches for the delimiter in configured username for the pre-auth user.
187 // The double-quote is not allowed inside email addresses anyway.
188 int doubleQuoteIndex = this.username.indexOf('"');
189 if (doubleQuoteIndex > 0) {
190 preAuthusername = this.username.substring(0, doubleQuoteIndex);
191 this.username = this.username.substring(doubleQuoteIndex + 1);
192 } else {
193 // No doublequote: the pre-auth user is the full username, or it is not used at all.
194 preAuthusername = this.username;
195 }
196 }
197
198 // set real credentials on http client
199 httpClientAdapter.setCredentials(username, password);
200
201 // get webmail root url
202 // providing credentials
203 // manually follow redirect
204 GetRequest getRequest = httpClientAdapter.executeFollowRedirect(new GetRequest(url));
205
206 if (!this.isAuthenticated(getRequest)) {
207 if (isHttpAuthentication) {
208 int status = getRequest.getStatusCode();
209
210 if (status == HttpStatus.SC_UNAUTHORIZED) {
211 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
212 } else if (status != HttpStatus.SC_OK) {
213 throw HttpClientAdapter.buildHttpResponseException(getRequest, getRequest.getHttpResponse());
214 }
215 // workaround for basic authentication on /exchange and form based authentication at /owa
216 if ("/owa/auth/logon.aspx".equals(getRequest.getURI().getPath())) {
217 formLogin(httpClientAdapter, getRequest, password);
218 }
219 } else {
220 formLogin(httpClientAdapter, getRequest, password);
221 }
222 }
223
224 } catch (DavMailAuthenticationException exc) {
225 close();
226 LOGGER.error(exc.getMessage());
227 throw exc;
228 } catch (ConnectException | UnknownHostException exc) {
229 close();
230 BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
231 LOGGER.error(message);
232 throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
233 } catch (WebdavNotAvailableException exc) {
234 close();
235 throw exc;
236 } catch (IOException exc) {
237 close();
238 LOGGER.error(BundleMessage.formatLog("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc));
239 throw new DavMailException("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc);
240 }
241 LOGGER.debug("Successfully authenticated to " + exchangeUri);
242 }
243
244 /**
245 * Test authentication mode : form based or basic.
246 *
247 * @param url exchange base URL
248 * @param httpClient httpClientAdapter instance
249 * @return true if basic authentication detected
250 */
251 protected boolean isHttpAuthentication(HttpClientAdapter httpClient, String url) {
252 boolean isHttpAuthentication = false;
253 HttpGet httpGet = new HttpGet(url);
254 // Create a local context to avoid cookies in main httpClient
255 HttpClientContext context = HttpClientContext.create();
256 context.setCookieStore(new BasicCookieStore());
257 try (CloseableHttpResponse response = httpClient.execute(httpGet, context)) {
258 isHttpAuthentication = response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED;
259 } catch (IOException e) {
260 // ignore
261 }
262 return isHttpAuthentication;
263 }
264
265 /**
266 * Look for session cookies.
267 *
268 * @return true if session cookies are available
269 */
270 protected boolean isAuthenticated(ResponseWrapper getRequest) {
271 boolean authenticated = false;
272 if (getRequest.getStatusCode() == HttpStatus.SC_OK
273 && "/ews/services.wsdl".equalsIgnoreCase(getRequest.getURI().getPath())) {
274 // direct EWS access returned wsdl
275 authenticated = true;
276 } else {
277 // check cookies
278 for (Cookie cookie : httpClientAdapter.getCookies()) {
279 // Exchange 2003 cookies
280 if (cookie.getName().startsWith("cadata") || "sessionid".equals(cookie.getName())
281 // Exchange 2007 cookie
282 || "UserContext".equals(cookie.getName())
283 ) {
284 authenticated = true;
285 break;
286 }
287 }
288 }
289 return authenticated;
290 }
291
292 protected void formLogin(HttpClientAdapter httpClient, ResponseWrapper initRequest, String password) throws IOException {
293 LOGGER.debug("Form based authentication detected");
294
295 PostRequest postRequest = buildLogonMethod(httpClient, initRequest);
296 if (postRequest == null) {
297 LOGGER.debug("Authentication form not found at " + initRequest.getURI() + ", trying default url");
298 postRequest = new PostRequest("/owa/auth/owaauth.dll");
299 }
300
301 exchangeUri = postLogonMethod(httpClient, postRequest, password).getURI();
302 }
303
304 /**
305 * Try to find logon method path from logon form body.
306 *
307 * @param httpClient httpClientAdapter instance
308 * @param responseWrapper init request response wrapper
309 * @return logon method
310 */
311 protected PostRequest buildLogonMethod(HttpClientAdapter httpClient, ResponseWrapper responseWrapper) {
312 PostRequest logonMethod = null;
313
314 // create an instance of HtmlCleaner
315 HtmlCleaner cleaner = new HtmlCleaner();
316
317 // A OTP token authentication form in a previous page could have username fields with different names
318 usernameInputs.clear();
319
320 try {
321 URI uri = responseWrapper.getURI();
322 String responseBody = responseWrapper.getResponseBodyAsString();
323 TagNode node = cleaner.clean(new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)));
324 List<? extends TagNode> forms = node.getElementListByName("form", true);
325 TagNode logonForm = null;
326 // select form
327 if (forms.size() == 1) {
328 logonForm = forms.get(0);
329 } else if (forms.size() > 1) {
330 for (Object form : forms) {
331 if ("logonForm".equals(((TagNode) form).getAttributeByName("name"))) {
332 logonForm = ((TagNode) form);
333 }
334 }
335 }
336 if (logonForm != null) {
337 String logonMethodPath = logonForm.getAttributeByName("action");
338
339 // workaround for broken form with empty action
340 if (logonMethodPath != null && logonMethodPath.length() == 0) {
341 logonMethodPath = "/owa/auth.owa";
342 }
343
344 logonMethod = new PostRequest(getAbsoluteUri(uri, logonMethodPath));
345
346 // retrieve lost inputs attached to body
347 List<? extends TagNode> inputList = node.getElementListByName("input", true);
348
349 for (Object input : inputList) {
350 String type = ((TagNode) input).getAttributeByName("type");
351 String name = ((TagNode) input).getAttributeByName("name");
352 String value = ((TagNode) input).getAttributeByName("value");
353 if ("hidden".equalsIgnoreCase(type) && name != null && value != null) {
354 logonMethod.setParameter(name, value);
355 }
356 // custom login form
357 if (USER_NAME_FIELDS.contains(name)) {
358 usernameInputs.add(name);
359 } else if (PASSWORD_FIELDS.contains(name)) {
360 passwordInput = name;
361 } else if ("addr".equals(name)) {
362 // this is not a logon form but a redirect form
363 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(logonMethod));
364 } else if (TOKEN_FIELDS.contains(name)) {
365 // one time password, ask it to the user
366 logonMethod.setParameter(name, DavGatewayOTPPrompt.getOneTimePassword());
367 } else if ("otc".equals(name)) {
368 // captcha image, get image and ask user
369 String pinsafeUser = getAliasFromLogin();
370 if (pinsafeUser == null) {
371 pinsafeUser = username;
372 }
373 HttpGet pinRequest = new HttpGet("/PINsafeISAFilter.dll?username=" + pinsafeUser);
374 try (CloseableHttpResponse pinResponse = httpClient.execute(pinRequest)) {
375 int status = pinResponse.getStatusLine().getStatusCode();
376 if (status != HttpStatus.SC_OK) {
377 throw HttpClientAdapter.buildHttpResponseException(pinRequest, pinResponse.getStatusLine());
378 }
379 BufferedImage captchaImage = ImageIO.read(pinResponse.getEntity().getContent());
380 logonMethod.setParameter(name, DavGatewayOTPPrompt.getCaptchaValue(captchaImage));
381 }
382 }
383 }
384 } else {
385 List<? extends TagNode> frameList = node.getElementListByName("frame", true);
386 if (frameList.size() == 1) {
387 String src = frameList.get(0).getAttributeByName("src");
388 if (src != null) {
389 LOGGER.debug("Frames detected in form page, try frame content");
390 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(src)));
391 }
392 } else {
393 // another failover for script based logon forms (Exchange 2007)
394 List<? extends TagNode> scriptList = node.getElementListByName("script", true);
395 for (Object script : scriptList) {
396 List<? extends BaseToken> contents = ((TagNode) script).getAllChildren();
397 for (Object content : contents) {
398 if (content instanceof CommentNode) {
399 String scriptValue = ((CommentNode) content).getCommentedContent();
400 String sUrl = StringUtil.getToken(scriptValue, "var a_sUrl = \"", "\"");
401 String sLgn = StringUtil.getToken(scriptValue, "var a_sLgnQS = \"", "\"");
402 if (sLgn == null) {
403 sLgn = StringUtil.getToken(scriptValue, "var a_sLgn = \"", "\"");
404 }
405 if (sUrl != null && sLgn != null) {
406 URI src = getScriptBasedFormURL(uri, sLgn + sUrl);
407 LOGGER.debug("Detected script based logon, redirect to form at " + src);
408 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(src)));
409 }
410
411 } else if (content instanceof ContentNode) {
412 // Microsoft Forefront Unified Access Gateway redirect
413 String scriptValue = ((ContentNode) content).getContent();
414 String location = StringUtil.getToken(scriptValue, "window.location.replace(\"", "\"");
415 if (location != null) {
416 LOGGER.debug("Post logon redirect to: " + location);
417 logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(location)));
418 }
419 }
420 }
421 }
422 }
423 }
424 } catch (IOException | URISyntaxException e) {
425 LOGGER.error("Error parsing login form at " + responseWrapper.getURI());
426 }
427
428 return logonMethod;
429 }
430
431
432 protected ResponseWrapper postLogonMethod(HttpClientAdapter httpClient, PostRequest logonMethod, String password) throws IOException {
433
434 setAuthFormFields(logonMethod, httpClient, password);
435
436 // add exchange 2010 PBack cookie in compatibility mode
437 BasicClientCookie pBackCookie = new BasicClientCookie("PBack", "0");
438 pBackCookie.setPath("/");
439 pBackCookie.setDomain(httpClientAdapter.getHost());
440 httpClient.addCookie(pBackCookie);
441
442 ResponseWrapper resultRequest = httpClient.executeFollowRedirect(logonMethod);
443
444 // test form based authentication
445 checkFormLoginQueryString(resultRequest);
446
447 // workaround for post logon script redirect
448 if (!isAuthenticated(resultRequest)) {
449 // try to get new method from script based redirection
450 logonMethod = buildLogonMethod(httpClient, resultRequest);
451
452 if (logonMethod != null) {
453 if (otpPreAuthFound && otpPreAuthRetries < MAX_OTP_RETRIES) {
454 // A OTP pre-auth page has been found, it is needed to restart the login process.
455 // This applies to both the case the user entered a good OTP code (the usual login process
456 // takes place) and the case the user entered a wrong OTP code (another code will be asked to him).
457 // The user has up to MAX_OTP_RETRIES chances to input a valid OTP key.
458 return postLogonMethod(httpClient, logonMethod, password);
459 }
460
461 // if logonMethod is not null, try to follow redirection
462 resultRequest = httpClient.executeFollowRedirect(logonMethod);
463
464 checkFormLoginQueryString(resultRequest);
465 // also check cookies
466 if (!isAuthenticated(resultRequest)) {
467 throwAuthenticationFailed();
468 }
469 } else {
470 // authentication failed
471 throwAuthenticationFailed();
472 }
473 }
474
475 // check for language selection form
476 if ("/owa/languageselection.aspx".equals(resultRequest.getURI().getPath())) {
477 // need to submit form
478 resultRequest = submitLanguageSelectionForm(resultRequest.getURI(), resultRequest.getResponseBodyAsString());
479 }
480 return resultRequest;
481 }
482
483 protected ResponseWrapper submitLanguageSelectionForm(URI uri, String responseBodyAsString) throws IOException {
484 PostRequest postLanguageFormMethod;
485 // create an instance of HtmlCleaner
486 HtmlCleaner cleaner = new HtmlCleaner();
487
488 try {
489 TagNode node = cleaner.clean(responseBodyAsString);
490 List<? extends TagNode> forms = node.getElementListByName("form", true);
491 TagNode languageForm;
492 // select form
493 if (forms.size() == 1) {
494 languageForm = forms.get(0);
495 } else {
496 throw new IOException("Form not found");
497 }
498 String languageMethodPath = languageForm.getAttributeByName("action");
499
500 postLanguageFormMethod = new PostRequest(getAbsoluteUri(uri, languageMethodPath));
501
502 List<? extends TagNode> inputList = languageForm.getElementListByName("input", true);
503 for (Object input : inputList) {
504 String name = ((TagNode) input).getAttributeByName("name");
505 String value = ((TagNode) input).getAttributeByName("value");
506 if (name != null && value != null) {
507 postLanguageFormMethod.setParameter(name, value);
508 }
509 }
510 List<? extends TagNode> selectList = languageForm.getElementListByName("select", true);
511 for (Object select : selectList) {
512 String name = ((TagNode) select).getAttributeByName("name");
513 List<? extends TagNode> optionList = ((TagNode) select).getElementListByName("option", true);
514 String value = null;
515 for (Object option : optionList) {
516 if (((TagNode) option).getAttributeByName("selected") != null) {
517 value = ((TagNode) option).getAttributeByName("value");
518 break;
519 }
520 }
521 if (name != null && value != null) {
522 postLanguageFormMethod.setParameter(name, value);
523 }
524 }
525 } catch (IOException | URISyntaxException e) {
526 String errorMessage = "Error parsing language selection form at " + uri;
527 LOGGER.error(errorMessage);
528 throw new IOException(errorMessage);
529 }
530
531 return httpClientAdapter.executeFollowRedirect(postLanguageFormMethod);
532 }
533
534 protected void setAuthFormFields(HttpRequestBase logonMethod, HttpClientAdapter httpClient, String password) throws IllegalArgumentException {
535 String usernameInput;
536 if (usernameInputs.size() == 2) {
537 String userid;
538 // multiple username fields, split userid|username on |
539 int pipeIndex = username.indexOf('|');
540 if (pipeIndex < 0) {
541 LOGGER.debug("Multiple user fields detected, please use userid|username as user name in client, except when userid is username");
542 userid = username;
543 } else {
544 userid = username.substring(0, pipeIndex);
545 username = username.substring(pipeIndex + 1);
546 // adjust credentials
547 httpClient.setCredentials(username, password);
548 }
549 ((PostRequest) logonMethod).removeParameter("userid");
550 ((PostRequest) logonMethod).setParameter("userid", userid);
551
552 usernameInput = "username";
553 } else if (usernameInputs.size() == 1) {
554 // simple username field
555 usernameInput = usernameInputs.get(0);
556 } else {
557 // should not happen
558 usernameInput = "username";
559 }
560 // make sure username and password fields are empty
561 ((PostRequest) logonMethod).removeParameter(usernameInput);
562 if (passwordInput != null) {
563 ((PostRequest) logonMethod).removeParameter(passwordInput);
564 }
565 ((PostRequest) logonMethod).removeParameter("trusted");
566 ((PostRequest) logonMethod).removeParameter("flags");
567
568 if (passwordInput == null) {
569 // This is a OTP pre-auth page. A different username may be required.
570 otpPreAuthFound = true;
571 otpPreAuthRetries++;
572 ((PostRequest) logonMethod).setParameter(usernameInput, preAuthusername);
573 } else {
574 otpPreAuthFound = false;
575 otpPreAuthRetries = 0;
576 // This is a regular Exchange login page
577 ((PostRequest) logonMethod).setParameter(usernameInput, username);
578 ((PostRequest) logonMethod).setParameter(passwordInput, password);
579 ((PostRequest) logonMethod).setParameter("trusted", "4");
580 ((PostRequest) logonMethod).setParameter("flags", "4");
581 }
582 }
583
584 protected URI getAbsoluteUri(URI uri, String path) throws URISyntaxException {
585 URIBuilder uriBuilder = new URIBuilder(uri);
586 if (path != null) {
587 // reset query string
588 uriBuilder.clearParameters();
589 if (path.startsWith("/")) {
590 // path is absolute, replace method path
591 uriBuilder.setPath(path);
592 } else if (path.startsWith("http://") || path.startsWith("https://")) {
593 return URI.create(path);
594 } else {
595 // relative path, build new path
596 String currentPath = uri.getPath();
597 int end = currentPath.lastIndexOf('/');
598 if (end >= 0) {
599 uriBuilder.setPath(currentPath.substring(0, end + 1) + path);
600 } else {
601 throw new URISyntaxException(uriBuilder.build().toString(), "Invalid path");
602 }
603 }
604 }
605 return uriBuilder.build();
606 }
607
608 protected URI getScriptBasedFormURL(URI uri, String pathQuery) throws URISyntaxException, IOException {
609 URIBuilder uriBuilder = new URIBuilder(uri);
610 int queryIndex = pathQuery.indexOf('?');
611 if (queryIndex >= 0) {
612 if (queryIndex > 0) {
613 // update path
614 String newPath = pathQuery.substring(0, queryIndex);
615 if (newPath.startsWith("/")) {
616 // absolute path
617 uriBuilder.setPath(newPath);
618 } else {
619 String currentPath = uriBuilder.getPath();
620 int folderIndex = currentPath.lastIndexOf('/');
621 if (folderIndex >= 0) {
622 // replace relative path
623 uriBuilder.setPath(currentPath.substring(0, folderIndex + 1) + newPath);
624 } else {
625 // should not happen
626 uriBuilder.setPath('/' + newPath);
627 }
628 }
629 }
630 uriBuilder.setCustomQuery(URIUtil.decode(pathQuery.substring(queryIndex + 1)));
631 }
632 return uriBuilder.build();
633 }
634
635 protected void checkFormLoginQueryString(ResponseWrapper logonMethod) throws DavMailAuthenticationException {
636 String queryString = logonMethod.getURI().getRawQuery();
637 if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=4"))) {
638 throwAuthenticationFailed();
639 }
640 }
641
642 protected void throwAuthenticationFailed() throws DavMailAuthenticationException {
643 if (this.username != null && this.username.contains("\\")) {
644 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
645 } else {
646 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_RETRY");
647 }
648 }
649
650 /**
651 * Get current Exchange alias name from login name
652 *
653 * @return user name
654 */
655 public String getAliasFromLogin() {
656 // login is email, not alias
657 if (this.username.indexOf('@') >= 0) {
658 return null;
659 }
660 String result = this.username;
661 // remove domain name
662 int index = Math.max(result.indexOf('\\'), result.indexOf('/'));
663 if (index >= 0) {
664 result = result.substring(index + 1);
665 }
666 return result;
667 }
668
669 /**
670 * Close session.
671 * Shutdown http client connection manager
672 */
673 public void close() {
674 httpClientAdapter.close();
675 }
676
677 /**
678 * Oauth token.
679 * Only for Office 365 authenticators
680 *
681 * @return unsupported
682 */
683 @Override
684 public O365Token getToken() {
685 throw new UnsupportedOperationException();
686 }
687
688 /**
689 * Base Exchange URL.
690 * Welcome page for Exchange 2003, EWS url for Exchange 2007 and later
691 *
692 * @return Exchange url
693 */
694 @Override
695 public java.net.URI getExchangeUri() {
696 return exchangeUri;
697 }
698
699 /**
700 * Return authenticated HttpClient 4 HttpClientAdapter
701 *
702 * @return HttpClientAdapter instance
703 */
704 public HttpClientAdapter getHttpClientAdapter() {
705 return httpClientAdapter;
706 }
707
708 /**
709 * Authenticated httpClientAdapter (with cookies).
710 *
711 * @return http client
712 */
713 public org.apache.commons.httpclient.HttpClient getHttpClient() throws DavMailException {
714 org.apache.commons.httpclient.HttpClient oldHttpClient;
715 oldHttpClient = DavGatewayHttpClientFacade.getInstance(url);
716 DavGatewayHttpClientFacade.setCredentials(oldHttpClient, username, password);
717 DavGatewayHttpClientFacade.createMultiThreadedHttpConnectionManager(oldHttpClient);
718
719 for (Cookie cookie : httpClientAdapter.getCookies()) {
720 org.apache.commons.httpclient.Cookie oldCookie = new org.apache.commons.httpclient.Cookie(
721 cookie.getDomain(),
722 cookie.getName(),
723 cookie.getValue(),
724 cookie.getPath(),
725 cookie.getExpiryDate(),
726 cookie.isSecure());
727 oldCookie.setPathAttributeSpecified(cookie.getPath() != null);
728 oldHttpClient.getState().addCookie(oldCookie);
729 }
730
731 return oldHttpClient;
732 }
733
734 /**
735 * Actual username.
736 * may be different from input username with preauth
737 *
738 * @return username
739 */
740 public String getUsername() {
741 return username;
742 }
743 }
744
102102 return URI.create(RESOURCE + "/EWS/Exchange.asmx");
103103 }
104104
105 /**
106 * Return a pool enabled HttpClientAdapter instance to access O365
107 * @return HttpClientAdapter instance
108 */
109 @Override
110 public HttpClientAdapter getHttpClientAdapter() {
111 return new HttpClientAdapter(getExchangeUri(), username, password, true);
112 }
113
105114 public void authenticate() throws IOException {
106 HttpClientAdapter httpClientAdapter = null;
107 try {
108115 // common DavMail client id
109116 String clientId = Settings.getProperty("davmail.oauth.clientId", "facd6cff-a294-4415-b59f-c5b01937d7bd");
110117 // standard native app redirectUri
120127
121128 String url = O365Authenticator.buildAuthorizeUrl(tenantId, clientId, redirectUri, username);
122129
123 httpClientAdapter = new HttpClientAdapter(url, userid, password);
130 try (
131 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(url, userid, password)
132 ){
124133
125134 GetRequest getRequest = new GetRequest(url);
126135 String responseBodyAsString = executeFollowRedirect(httpClientAdapter, getRequest);
226235
227236 } catch (JSONException e) {
228237 throw new IOException(e + " " + e.getMessage());
229 } finally {
230 // do not keep login connections open
231 if (httpClientAdapter != null) {
232 httpClientAdapter.close();
233 }
234 }
235
236 }
237
238 private String authenticateRedirectADFS(HttpClientAdapter httpClientAdapter, String federationRedirectUrl, String authorizeUrl) throws IOException {
238 }
239
240 }
241
242 private String authenticateRedirectADFS(HttpClientAdapter httpClientAdapter, String federationRedirectUrl, String authorizeUrl) throws IOException, JSONException {
239243 // get ADFS login form
240244 GetRequest logonFormMethod = new GetRequest(federationRedirectUrl);
241 String responseBodyAsString = httpClientAdapter.executeGetRequest(logonFormMethod);
245 logonFormMethod = httpClientAdapter.executeFollowRedirect(logonFormMethod);
246 String responseBodyAsString = logonFormMethod.getResponseBodyAsString();
242247 return authenticateADFS(httpClientAdapter, responseBodyAsString, authorizeUrl);
243248 }
244249
245 private String authenticateADFS(HttpClientAdapter httpClientAdapter, String responseBodyAsString, String authorizeUrl) throws IOException {
250 private String authenticateADFS(HttpClientAdapter httpClientAdapter, String responseBodyAsString, String authorizeUrl) throws IOException, JSONException {
246251 URI location;
247252
248253 if (responseBodyAsString.contains("login.microsoftonline.com")) {
320325 throw new IOException("Unknown ADFS authentication failure");
321326 }
322327
323 private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throws IOException {
328 private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throws IOException, JSONException {
324329 URI result = location;
325 LOGGER.debug("Proceed to device authentication");
330 LOGGER.debug("Proceed to device authentication, must have access to a client certificate signed by MS-Organization-Access");
331 if (Settings.isWindows() &&
332 (System.getProperty("java.version").compareTo("13") < 0
333 || !"MSCAPI".equals(Settings.getProperty("davmail.ssl.clientKeystoreType")))
334 ) {
335 LOGGER.warn("MSCAPI and Java version 13 or higher required to access TPM protected client certificate on Windows");
336 }
326337 GetRequest deviceLoginMethod = new GetRequest(location);
327338
328339 String responseBodyAsString = httpClient.executeGetRequest(deviceLoginMethod);
337348 processMethod.setParameter("ctx", ctx);
338349 processMethod.setParameter("flowtoken", flowtoken);
339350
340 httpClient.executePostRequest(processMethod);
351 responseBodyAsString = httpClient.executePostRequest(processMethod);
341352 result = processMethod.getRedirectLocation();
353
354 // MFA triggered after device authentication
355 if (result == null && responseBodyAsString != null && responseBodyAsString.indexOf("arrUserProofs") > 0) {
356 result = handleMfa(httpClient, processMethod, username, null);
357 }
342358
343359 if (result == null) {
344360 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
348364 return result;
349365 }
350366
351 private URI handleMfa(HttpClientAdapter httpClientAdapter, PostRequest logonMethod, String username, String clientRequestId) throws JSONException, IOException {
367 private URI handleMfa(HttpClientAdapter httpClientAdapter, PostRequest logonMethod, String username, String clientRequestId) throws IOException, JSONException {
352368 JSONObject config = extractConfig(logonMethod.getResponseBodyAsString());
353369 LOGGER.debug("Config=" + config);
354370
355371 String urlBeginAuth = config.getString("urlBeginAuth");
356372 String urlEndAuth = config.getString("urlEndAuth");
373 // Get processAuth url from config
374 String urlProcessAuth = config.optString("urlPost", "https://login.microsoftonline.com/" + tenantId + "/SAS/ProcessAuth");
357375
358376 boolean isMFAMethodSupported = false;
359377
380398 String hpgact = config.getString("hpgact");
381399 String hpgid = config.getString("hpgid");
382400
401 // clientRequestId is null coming from device login
402 String correlationId = clientRequestId;
403 if (correlationId == null) {
404 correlationId = config.getString("correlationId");
405 }
406
383407 RestRequest beginAuthMethod = new RestRequest(urlBeginAuth);
384408 beginAuthMethod.setRequestHeader("Accept", "application/json");
385409 beginAuthMethod.setRequestHeader("canary", apiCanary);
386 beginAuthMethod.setRequestHeader("client-request-id", clientRequestId);
410 beginAuthMethod.setRequestHeader("client-request-id", correlationId);
387411 beginAuthMethod.setRequestHeader("hpgact", hpgact);
388412 beginAuthMethod.setRequestHeader("hpgid", hpgid);
389413 beginAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);
457481 flowToken = config.getString("FlowToken");
458482
459483 // process auth
460 PostRequest processAuthMethod = new PostRequest("https://login.microsoftonline.com/" + tenantId + "/SAS/ProcessAuth");
484 PostRequest processAuthMethod = new PostRequest(urlProcessAuth);
461485 processAuthMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
462486 processAuthMethod.setParameter("type", type);
463487 processAuthMethod.setParameter("request", context);
2424 import davmail.exchange.ews.DistinguishedFolderId;
2525 import davmail.exchange.ews.GetFolderMethod;
2626 import davmail.exchange.ews.GetUserConfigurationMethod;
27 import davmail.http.DavGatewayHttpClientFacade;
28 import org.apache.commons.httpclient.HttpClient;
27 import davmail.http.HttpClientAdapter;
28 import org.apache.http.client.methods.CloseableHttpResponse;
2929 import org.apache.log4j.Logger;
3030
3131 import javax.swing.*;
3333 import java.net.Authenticator;
3434 import java.net.PasswordAuthentication;
3535 import java.net.URI;
36 import java.security.Security;
3637
3738 public class O365InteractiveAuthenticator implements ExchangeAuthenticator {
3839
3940 private static final int MAX_COUNT = 300;
4041 private static final Logger LOGGER = Logger.getLogger(O365InteractiveAuthenticator.class);
42
43 static {
44 // disable HTTP/2 loader on Java 14 and later to enable custom socket factory
45 System.setProperty("com.sun.webkit.useHTTP2Loader", "false");
46 }
4147
4248 boolean isAuthenticated = false;
4349 String errorCode = null;
7379 this.password = password;
7480 }
7581
82 /**
83 * Return a pool enabled HttpClientAdapter instance to access O365
84 *
85 * @return HttpClientAdapter instance
86 */
87 @Override
88 public HttpClientAdapter getHttpClientAdapter() {
89 return new HttpClientAdapter(getExchangeUri(), username, password, true);
90 }
7691
7792 public void authenticate() throws IOException {
93
7894 // allow cross domain requests for Okta form support
7995 System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
8096 // enable NTLM for ADFS support
166182 }
167183
168184 public static void main(String[] argv) {
185
169186 try {
187 // set custom factory before loading OpenJFX
188 Security.setProperty("ssl.SocketFactory.provider", "davmail.http.DavGatewaySSLSocketFactory");
189
170190 Settings.setDefaultSettings();
171 //Settings.setConfigFilePath("davmail-interactive.properties");
172 //Settings.load();
173 //Settings.setLoggingLevel("httpclient.wire", Level.DEBUG);
191 Settings.setConfigFilePath("davmail-interactive.properties");
192 Settings.load();
174193
175194 O365InteractiveAuthenticator authenticator = new O365InteractiveAuthenticator();
176195 authenticator.setUsername("");
177196 authenticator.authenticate();
178197
198 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(authenticator.getExchangeUri(), true);
199
179200 // switch to EWS url
180 HttpClient httpClient = DavGatewayHttpClientFacade.getInstance(authenticator.ewsUrl.toString());
181
182201 GetFolderMethod checkMethod = new GetFolderMethod(BaseShape.ID_ONLY, DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null);
183 checkMethod.setRequestHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
184 try {
185 //checkMethod.setServerVersion(serverVersion);
186 httpClient.executeMethod(checkMethod);
187
202 checkMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
203 try (
204 CloseableHttpResponse response = httpClientAdapter.execute(checkMethod)
205 ) {
206 checkMethod.handleResponse(response);
188207 checkMethod.checkSuccess();
189 } finally {
190 checkMethod.releaseConnection();
191208 }
192209 System.out.println("Retrieved folder id " + checkMethod.getResponseItem().get("FolderId"));
193210
195212 int i = 0;
196213 while (i++ < 12 * 60 * 2) {
197214 GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
198 getUserConfigurationMethod.setRequestHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
199 httpClient.executeMethod(getUserConfigurationMethod);
215 getUserConfigurationMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
216 try (
217 CloseableHttpResponse response = httpClientAdapter.execute(getUserConfigurationMethod)
218 ) {
219 getUserConfigurationMethod.handleResponse(response);
220 getUserConfigurationMethod.checkSuccess();
221 }
200222 System.out.println(getUserConfigurationMethod.getResponseItem());
201223
202224 Thread.sleep(5000);
203225 }
204
226 } catch (InterruptedException e) {
227 LOGGER.warn("Thread interrupted", e);
228 Thread.currentThread().interrupt();
205229 } catch (Exception e) {
206230 LOGGER.error(e + " " + e.getMessage(), e);
207231 }
1919 package davmail.exchange.auth;
2020
2121 import davmail.BundleMessage;
22 import davmail.Settings;
2223 import davmail.ui.tray.DavGatewayTray;
2324 import davmail.util.IOUtil;
2425 import javafx.application.Platform;
5455 import java.net.URLStreamHandler;
5556 import java.net.URLStreamHandlerFactory;
5657 import java.nio.charset.StandardCharsets;
58 import java.util.logging.Level;
5759
5860 public class O365InteractiveAuthenticatorFrame extends JFrame {
5961 private static final Logger LOGGER = Logger.getLogger(O365InteractiveAuthenticatorFrame.class);
209211 Scene scene = new Scene(hBox);
210212 fxPanel.setScene(scene);
211213
214 webViewEngine.setUserAgent(Settings.getUserAgent());
215
212216 webViewEngine.setOnAlert(stringWebEvent -> SwingUtilities.invokeLater(() -> {
213217 String message = stringWebEvent.getData();
214218 JOptionPane.showMessageDialog(O365InteractiveAuthenticatorFrame.this, message);
217221
218222
219223 webViewEngine.getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> {
220 if (newState == Worker.State.SUCCEEDED) {
224 // with Java 15 url with code returns as CANCELLED
225 if (newState == Worker.State.SUCCEEDED || newState == Worker.State.CANCELLED) {
221226 loadProgress.setVisible(false);
222227 location = webViewEngine.getLocation();
223228 updateTitleAndFocus(location);
251256 handleError(e);
252257 }
253258 close();
259 } else {
260 LOGGER.debug(webViewEngine.getLoadWorker().getState()+" "+webViewEngine.getLoadWorker().getMessage()+" " + webViewEngine.getLocation()+" ");
254261 }
255262
256263 });
1919 package davmail.exchange.auth;
2020
2121 import javafx.scene.web.WebEngine;
22 import netscape.javascript.JSObject;
2322 import org.apache.log4j.Logger;
2423
2524 import java.lang.reflect.InvocationTargetException;
3837 Class jsObjectClass = Class.forName("netscape.javascript.JSObject");
3938 Method setMemberMethod = jsObjectClass.getDeclaredMethod("setMember", String.class,Object.class);
4039
41 JSObject window = (JSObject) webEngine.executeScript("window");
40 Object window = webEngine.executeScript("window");
4241 setMemberMethod.invoke(window, "davmail", new O365InteractiveJSLogger());
4342
4443 webEngine.executeScript("console.log = function(message) { davmail.log(message); }");
2626 import davmail.exchange.ews.DistinguishedFolderId;
2727 import davmail.exchange.ews.GetFolderMethod;
2828 import davmail.exchange.ews.GetUserConfigurationMethod;
29 import davmail.http.DavGatewayHttpClientFacade;
30 import org.apache.commons.httpclient.HttpClient;
29 import davmail.http.HttpClientAdapter;
30 import org.apache.http.client.methods.CloseableHttpResponse;
3131 import org.apache.log4j.Logger;
3232
3333 import javax.swing.*;
7575 this.password = password;
7676 }
7777
78 /**
79 * Return a pool enabled HttpClientAdapter instance to access O365
80 * @return HttpClientAdapter instance
81 */
82 @Override
83 public HttpClientAdapter getHttpClientAdapter() {
84 return new HttpClientAdapter(getExchangeUri(), username, password, true);
85 }
7886
7987 public void authenticate() throws IOException {
8088 // common DavMail client id
148156 authenticator.authenticate();
149157
150158 // switch to EWS url
151 HttpClient httpClient = DavGatewayHttpClientFacade.getInstance(authenticator.ewsUrl.toString());
159 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(authenticator.getExchangeUri(), true);
152160
153161 GetFolderMethod checkMethod = new GetFolderMethod(BaseShape.ID_ONLY, DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null);
154 checkMethod.setRequestHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
155 try {
156 //checkMethod.setServerVersion(serverVersion);
157 httpClient.executeMethod(checkMethod);
158
162 checkMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
163 try (
164 CloseableHttpResponse response = httpClientAdapter.execute(checkMethod)
165 ) {
166 checkMethod.handleResponse(response);
159167 checkMethod.checkSuccess();
160 } finally {
161 checkMethod.releaseConnection();
162168 }
163169 System.out.println("Retrieved folder id " + checkMethod.getResponseItem().get("FolderId"));
164170
166172 int i = 0;
167173 while (i++ < 12 * 60 * 2) {
168174 GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
169 getUserConfigurationMethod.setRequestHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
170 httpClient.executeMethod(getUserConfigurationMethod);
175 getUserConfigurationMethod.setHeader("Authorization", "Bearer " + authenticator.getToken().getAccessToken());
176 try (
177 CloseableHttpResponse response = httpClientAdapter.execute(checkMethod)
178 ) {
179 checkMethod.handleResponse(response);
180
181 checkMethod.checkSuccess();
182 }
171183 System.out.println(getUserConfigurationMethod.getResponseItem());
172184
173185 Thread.sleep(5000);
174186 }
175187
188 } catch (InterruptedException e) {
189 LOGGER.warn("Thread interrupted", e);
190 Thread.currentThread().interrupt();
176191 } catch (Exception e) {
177192 LOGGER.error(e + " " + e.getMessage(), e);
178193 }
1919 package davmail.exchange.auth;
2020
2121 import davmail.Settings;
22 import davmail.http.HttpClientAdapter;
2223 import org.apache.log4j.Logger;
2324
2425 import java.io.IOException;
3536 URI ewsUrl = URI.create(resource + "/EWS/Exchange.asmx");
3637
3738 private String username;
39 private String password;
3840 private O365Token token;
3941
4042 @Override
4446
4547 @Override
4648 public void setPassword(String password) {
47 // unused
49 this.password = password;
50 }
51
52 /**
53 * Return a pool enabled HttpClientAdapter instance to access O365
54 * @return HttpClientAdapter instance
55 */
56 @Override
57 public HttpClientAdapter getHttpClientAdapter() {
58 return new HttpClientAdapter(getExchangeUri(), username, password, true);
4859 }
4960
5061 @Override
174174 O365Token token = new O365Token(tenantId, clientId, redirectUri, code);
175175 if (Settings.getBooleanProperty("davmail.oauth.persistToken", true)) {
176176 try {
177 Settings.storeRefreshToken(encryptToken(token.getRefreshToken(), password), token.getUsername());
177 Settings.storeRefreshToken(token.getUsername(), encryptToken(token.getRefreshToken(), password));
178178 } catch (IOException e) {
179179 LOGGER.warn("Unable to store refreshToken: "+e.getMessage());
180180 }
3131 import davmail.exchange.VObject;
3232 import davmail.exchange.VProperty;
3333 import davmail.exchange.XMLStreamUtil;
34 import davmail.http.DavGatewayHttpClientFacade;
34 import davmail.http.HttpClientAdapter;
3535 import davmail.http.URIUtil;
36 import davmail.http.request.ExchangePropPatchRequest;
3637 import davmail.ui.tray.DavGatewayTray;
3738 import davmail.util.IOUtil;
3839 import davmail.util.StringUtil;
39 import org.apache.commons.httpclient.Cookie;
40 import org.apache.commons.httpclient.HttpClient;
41 import org.apache.commons.httpclient.HttpConnection;
42 import org.apache.commons.httpclient.HttpState;
43 import org.apache.commons.httpclient.HttpStatus;
44 import org.apache.commons.httpclient.URI;
45 import org.apache.commons.httpclient.URIException;
46 import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
47 import org.apache.commons.httpclient.methods.DeleteMethod;
48 import org.apache.commons.httpclient.methods.GetMethod;
49 import org.apache.commons.httpclient.methods.HeadMethod;
50 import org.apache.commons.httpclient.methods.PostMethod;
51 import org.apache.commons.httpclient.methods.PutMethod;
40 import org.apache.http.Consts;
41 import org.apache.http.HttpResponse;
42 import org.apache.http.HttpStatus;
43 import org.apache.http.NameValuePair;
5244 import org.apache.http.client.HttpResponseException;
45 import org.apache.http.client.entity.UrlEncodedFormEntity;
46 import org.apache.http.client.methods.CloseableHttpResponse;
47 import org.apache.http.client.methods.HttpDelete;
48 import org.apache.http.client.methods.HttpGet;
49 import org.apache.http.client.methods.HttpHead;
50 import org.apache.http.client.methods.HttpPost;
51 import org.apache.http.client.methods.HttpPut;
52 import org.apache.http.client.protocol.HttpClientContext;
53 import org.apache.http.client.utils.URIUtils;
54 import org.apache.http.entity.ByteArrayEntity;
55 import org.apache.http.entity.ContentType;
56 import org.apache.http.impl.client.BasicCookieStore;
57 import org.apache.http.impl.client.BasicResponseHandler;
58 import org.apache.http.message.BasicNameValuePair;
5359 import org.apache.jackrabbit.webdav.DavException;
5460 import org.apache.jackrabbit.webdav.MultiStatus;
5561 import org.apache.jackrabbit.webdav.MultiStatusResponse;
56 import org.apache.jackrabbit.webdav.client.methods.CopyMethod;
57 import org.apache.jackrabbit.webdav.client.methods.MoveMethod;
58 import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
59 import org.apache.jackrabbit.webdav.client.methods.PropPatchMethod;
62 import org.apache.jackrabbit.webdav.client.methods.HttpCopy;
63 import org.apache.jackrabbit.webdav.client.methods.HttpMove;
64 import org.apache.jackrabbit.webdav.client.methods.HttpPropfind;
65 import org.apache.jackrabbit.webdav.client.methods.HttpProppatch;
6066 import org.apache.jackrabbit.webdav.property.DavProperty;
6167 import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
6268 import org.apache.jackrabbit.webdav.property.DavPropertySet;
8187 import java.io.InputStreamReader;
8288 import java.net.NoRouteToHostException;
8389 import java.net.SocketException;
90 import java.net.URISyntaxException;
8491 import java.net.URL;
8592 import java.net.UnknownHostException;
8693 import java.nio.charset.StandardCharsets;
93100 * Webdav Exchange adapter.
94101 * Compatible with Exchange 2003 and 2007 with webdav available.
95102 */
96 @SuppressWarnings({"rawtypes", "deprecation"})
103 @SuppressWarnings("rawtypes")
97104 public class DavExchangeSession extends ExchangeSession {
98105 protected enum FolderQueryTraversal {
99106 Shallow, Deep
131138 }
132139
133140 /**
141 * HttpClient 4 adapter to replace httpClient
142 */
143 private HttpClientAdapter httpClientAdapter;
144
145 /**
134146 * Various standard mail boxes Urls
135147 */
136148 protected String inboxUrl;
155167
156168 protected static final String USERS = "/users/";
157169
158 protected HttpClient httpClient;
170 /**
171 * HttpClient4 conversion.
172 * TODO: move up to ExchangeSession
173 */
174 protected void getEmailAndAliasFromOptions() {
175 // get user mail URL from html body
176 HttpGet optionsMethod = new HttpGet("/owa/?ae=Options&t=About");
177 try (
178 CloseableHttpResponse response = httpClientAdapter.execute(optionsMethod, cloneContext());
179 InputStream inputStream = response.getEntity().getContent();
180 BufferedReader optionsPageReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
181 ) {
182 String line;
183
184 // find email and alias
185 //noinspection StatementWithEmptyBody
186 while ((line = optionsPageReader.readLine()) != null
187 && (line.indexOf('[') == -1
188 || line.indexOf('@') == -1
189 || line.indexOf(']') == -1
190 || !line.toLowerCase().contains(MAILBOX_BASE))) {
191 }
192 if (line != null) {
193 int start = line.toLowerCase().lastIndexOf(MAILBOX_BASE) + MAILBOX_BASE.length();
194 int end = line.indexOf('<', start);
195 alias = line.substring(start, end);
196 end = line.lastIndexOf(']');
197 start = line.lastIndexOf('[', end) + 1;
198 email = line.substring(start, end);
199 }
200 } catch (IOException e) {
201 LOGGER.error("Error parsing options page at " + optionsMethod.getURI());
202 }
203 }
204
205 /**
206 * Create a separate Http context to protect session cookies.
207 *
208 * @return HttpClientContext instance with cookies
209 */
210 private HttpClientContext cloneContext() {
211 // Create a local context to avoid cookie reset on error
212 BasicCookieStore cookieStore = new BasicCookieStore();
213 cookieStore.addCookies(httpClientAdapter.getCookies().toArray(new org.apache.http.cookie.Cookie[0]));
214 HttpClientContext context = HttpClientContext.create();
215 context.setCookieStore(cookieStore);
216 return context;
217 }
159218
160219 @Override
161220 public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
162221 // experimental: try to reset session timeout
163222 if ("Exchange2007".equals(serverVersion)) {
164 GetMethod getMethod = null;
165 try {
166 getMethod = new GetMethod("/owa/");
167 getMethod.setFollowRedirects(false);
168 httpClient.executeMethod(getMethod);
223 HttpGet getMethod = new HttpGet("/owa/");
224 try (CloseableHttpResponse response = httpClientAdapter.execute(getMethod)) {
225 LOGGER.debug(response.getStatusLine().getStatusCode() + " at /owa/");
169226 } catch (IOException e) {
170227 LOGGER.warn(e.getMessage());
171 } finally {
172 if (getMethod != null) {
173 getMethod.releaseConnection();
174 }
175228 }
176229 }
177230
350403 protected Map<String, Map<String, String>> galFind(String query) throws IOException {
351404 Map<String, Map<String, String>> results;
352405 String path = getCmdBasePath() + "?Cmd=galfind" + query;
353 GetMethod getMethod = new GetMethod(path);
354 try {
355 DavGatewayHttpClientFacade.executeGetMethod(httpClient, getMethod, true);
356 results = XMLStreamUtil.getElementContentsAsMap(getMethod.getResponseBodyAsStream(), "item", "AN");
406 HttpGet httpGet = new HttpGet(path);
407 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
408 results = XMLStreamUtil.getElementContentsAsMap(response.getEntity().getContent(), "item", "AN");
357409 if (LOGGER.isDebugEnabled()) {
358410 LOGGER.debug(path + ": " + results.size() + " result(s)");
359411 }
361413 LOGGER.debug("GET " + path + " failed: " + e + ' ' + e.getMessage());
362414 disableGalFind = true;
363415 throw e;
364 } finally {
365 getMethod.releaseConnection();
366416 }
367417 return results;
368418 }
473523 public void galLookup(Contact contact) {
474524 if (!disableGalLookup) {
475525 LOGGER.debug("galLookup(" + contact.get("smtpemail1") + ')');
476 GetMethod getMethod = null;
477 try {
478 getMethod = new GetMethod(URIUtil.encodePathQuery(getCmdBasePath() + "?Cmd=gallookup&ADDR=" + contact.get("smtpemail1")));
479 DavGatewayHttpClientFacade.executeGetMethod(httpClient, getMethod, true);
480 Map<String, Map<String, String>> results = XMLStreamUtil.getElementContentsAsMap(getMethod.getResponseBodyAsStream(), "person", "alias");
526 HttpGet httpGet = new HttpGet(URIUtil.encodePathQuery(getCmdBasePath() + "?Cmd=gallookup&ADDR=" + contact.get("smtpemail1")));
527 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
528 Map<String, Map<String, String>> results = XMLStreamUtil.getElementContentsAsMap(response.getEntity().getContent(), "person", "alias");
481529 // add detailed information
482530 if (!results.isEmpty()) {
483531 Map<String, String> personGalLookupDetails = results.get(contact.get("uid").toLowerCase());
488536 } catch (IOException e) {
489537 LOGGER.warn("Unable to gallookup person: " + contact + ", disable GalLookup");
490538 disableGalLookup = true;
491 } finally {
492 if (getMethod != null) {
493 getMethod.releaseConnection();
494 }
495539 }
496540 }
497541 }
512556 "&end=" + end +
513557 "&interval=" + interval +
514558 "&u=SMTP:" + attendee;
515 GetMethod getMethod = new GetMethod(freebusyUrl);
516 getMethod.setRequestHeader("Content-Type", "text/xml");
559 HttpGet httpGet = new HttpGet(freebusyUrl);
560 httpGet.setHeader("Content-Type", "text/xml");
517561 String fbdata;
518 try {
519 DavGatewayHttpClientFacade.executeGetMethod(httpClient, getMethod, true);
520 fbdata = StringUtil.getLastToken(getMethod.getResponseBodyAsString(), "<a:fbdata>", "</a:fbdata>");
521 } finally {
522 getMethod.releaseConnection();
562 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
563 fbdata = StringUtil.getLastToken(new BasicResponseHandler().handleResponse(response), "<a:fbdata>", "</a:fbdata>");
523564 }
524565 return fbdata;
525566 }
526567
527 public DavExchangeSession(HttpClient httpClient, java.net.URI uri, String userName) throws IOException {
528 this.httpClient = httpClient;
568 public DavExchangeSession(HttpClientAdapter httpClientAdapter, java.net.URI uri, String userName) throws IOException {
569 this.httpClientAdapter = httpClientAdapter;
529570 this.userName = userName;
530571 buildSessionInfo(uri);
531572 }
537578
538579 // get base http mailbox http urls
539580 getWellKnownFolders();
540 }
541
542 protected void getEmailAndAliasFromOptions() {
543 synchronized (httpClient.getState()) {
544 Cookie[] currentCookies = httpClient.getState().getCookies();
545 // get user mail URL from html body
546 BufferedReader optionsPageReader = null;
547 GetMethod optionsMethod = new GetMethod("/owa/?ae=Options&t=About");
548 try {
549 DavGatewayHttpClientFacade.executeGetMethod(httpClient, optionsMethod, false);
550 optionsPageReader = new BufferedReader(new InputStreamReader(optionsMethod.getResponseBodyAsStream(), StandardCharsets.UTF_8));
551 String line;
552
553 // find email and alias
554 //noinspection StatementWithEmptyBody
555 while ((line = optionsPageReader.readLine()) != null
556 && (line.indexOf('[') == -1
557 || line.indexOf('@') == -1
558 || line.indexOf(']') == -1
559 || !line.toLowerCase().contains(MAILBOX_BASE))) {
560 }
561 if (line != null) {
562 int start = line.toLowerCase().lastIndexOf(MAILBOX_BASE) + MAILBOX_BASE.length();
563 int end = line.indexOf('<', start);
564 alias = line.substring(start, end);
565 end = line.lastIndexOf(']');
566 start = line.lastIndexOf('[', end) + 1;
567 email = line.substring(start, end);
568 }
569 } catch (IOException e) {
570 // restore cookies on error
571 httpClient.getState().addCookies(currentCookies);
572 LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
573 } finally {
574 if (optionsPageReader != null) {
575 try {
576 optionsPageReader.close();
577 } catch (IOException e) {
578 LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
579 }
580 }
581 optionsMethod.releaseConnection();
582 }
583 }
584581 }
585582
586583 static final String BASE_HREF = "<base href=\"";
594591 protected String getMailpathFromWelcomePage(java.net.URI uri) {
595592 String welcomePageMailPath = null;
596593 // get user mail URL from html body (multi frame)
597 BufferedReader mainPageReader = null;
598 GetMethod method = null;
599 try {
600 method = new GetMethod(uri.toString());
601 httpClient.executeMethod(method);
602 mainPageReader = new BufferedReader(new InputStreamReader(method.getResponseBodyAsStream(), StandardCharsets.UTF_8));
594 HttpGet method = new HttpGet(uri.toString());
595
596 try (
597 CloseableHttpResponse response = httpClientAdapter.execute(method);
598 InputStream inputStream = response.getEntity().getContent();
599 BufferedReader mainPageReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
600 ) {
603601 String line;
604602 //noinspection StatementWithEmptyBody
605603 while ((line = mainPageReader.readLine()) != null && !line.toLowerCase().contains(BASE_HREF)) {
614612 LOGGER.debug("Base href found in body, mailPath is " + welcomePageMailPath);
615613 }
616614 } catch (IOException e) {
617 LOGGER.error("Error parsing main page at " + method.getPath(), e);
618 } finally {
619 if (mainPageReader != null) {
620 try {
621 mainPageReader.close();
622 } catch (IOException e) {
623 LOGGER.error("Error parsing main page at " + method.getPath());
624 }
625 }
626 if (method != null) {
627 method.releaseConnection();
628 }
615 LOGGER.error("Error parsing main page at " + method.getURI(), e);
629616 }
630617 return welcomePageMailPath;
631618 }
821808 protected void fixClientHost(java.net.URI currentUri) {
822809 // update client host, workaround for Exchange 2003 mailbox with an Exchange 2007 frontend
823810 if (currentUri != null && currentUri.getHost() != null && currentUri.getScheme() != null) {
824 httpClient.getHostConfiguration().setHost(currentUri.getHost(), currentUri.getPort(), currentUri.getScheme());
811 httpClientAdapter.setUri(currentUri);
825812 }
826813 }
827814
828815 protected void checkPublicFolder() {
829 synchronized (httpClient.getState()) {
830 Cookie[] currentCookies = httpClient.getState().getCookies();
831 // check public folder access
832 try {
833 publicFolderUrl = httpClient.getHostConfiguration().getHostURL() + PUBLIC_ROOT;
834 DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
835 davPropertyNameSet.add(Field.getPropertyName("displayname"));
836 PropFindMethod propFindMethod = new PropFindMethod(publicFolderUrl, davPropertyNameSet, 0);
837 try {
838 DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod);
839 } catch (IOException e) {
840 // workaround for NTLM authentication only on /public
841 if (!DavGatewayHttpClientFacade.hasNTLMorNegotiate(httpClient)) {
842 DavGatewayHttpClientFacade.addNTLM(httpClient);
843 DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod);
844 }
845 }
846 // update public folder URI
847 publicFolderUrl = propFindMethod.getURI().getURI();
848 } catch (IOException e) {
849 // restore cookies on error
850 httpClient.getState().addCookies(currentCookies);
851 LOGGER.warn("Public folders not available: " + (e.getMessage() == null ? e : e.getMessage()));
852 // default public folder path
853 publicFolderUrl = PUBLIC_ROOT;
854 }
855 }
856 }
816 // check public folder access
817 try {
818 publicFolderUrl = URIUtils.resolve(httpClientAdapter.getUri(), PUBLIC_ROOT).toString();
819 DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
820 davPropertyNameSet.add(Field.getPropertyName("displayname"));
821
822 HttpPropfind httpPropfind = new HttpPropfind(publicFolderUrl, davPropertyNameSet, 0);
823 httpClientAdapter.executeDavRequest(httpPropfind);
824 // update public folder URI
825 publicFolderUrl = httpPropfind.getURI().toString();
826
827 } catch (IOException e) {
828 LOGGER.warn("Public folders not available: " + (e.getMessage() == null ? e : e.getMessage()));
829 // default public folder path
830 publicFolderUrl = PUBLIC_ROOT;
831 }
832 }
833
857834
858835 protected void getWellKnownFolders() throws DavMailException {
859836 // Retrieve well known URLs
860 MultiStatusResponse[] responses;
861837 try {
862 responses = DavGatewayHttpClientFacade.executePropFindMethod(
863 httpClient, URIUtil.encodePath(mailPath), 0, WELL_KNOWN_FOLDERS);
838 HttpPropfind httpPropfind = new HttpPropfind(mailPath, WELL_KNOWN_FOLDERS, 0);
839 MultiStatus multiStatus;
840 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) {
841 multiStatus = httpPropfind.getResponseBodyAsMultiStatus(response);
842 }
843 MultiStatusResponse[] responses = multiStatus.getResponses();
864844 if (responses.length == 0) {
865845 throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath);
866846 }
867 DavPropertySet properties = responses[0].getProperties(HttpStatus.SC_OK);
847 DavPropertySet properties = responses[0].getProperties(org.apache.http.HttpStatus.SC_OK);
868848 inboxUrl = getURIPropertyIfExists(properties, "inbox");
869849 inboxName = getFolderName(inboxUrl);
870850 deleteditemsUrl = getURIPropertyIfExists(properties, "deleteditems");
896876 " Outbox URL: " + outboxUrl +
897877 " Public folder URL: " + publicFolderUrl
898878 );
899 } catch (IOException e) {
879 } catch (IOException | DavException e) {
900880 LOGGER.error(e.getMessage());
901881 throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath);
902882 }
12471227 return propertyValues;
12481228 }
12491229
1250 protected ExchangePropPatchMethod internalCreateOrUpdate(String encodedHref) throws IOException {
1251 ExchangePropPatchMethod propPatchMethod = new ExchangePropPatchMethod(encodedHref, buildProperties());
1252 propPatchMethod.setRequestHeader("Translate", "f");
1230 protected ExchangePropPatchRequest internalCreateOrUpdate(String encodedHref) throws IOException {
1231 ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(encodedHref, buildProperties());
1232 propPatchRequest.setHeader("Translate", "f");
12531233 if (etag != null) {
1254 propPatchMethod.setRequestHeader("If-Match", etag);
1234 propPatchRequest.setHeader("If-Match", etag);
12551235 }
12561236 if (noneMatch != null) {
1257 propPatchMethod.setRequestHeader("If-None-Match", noneMatch);
1258 }
1259 try {
1260 httpClient.executeMethod(propPatchMethod);
1261 } finally {
1262 propPatchMethod.releaseConnection();
1263 }
1264 return propPatchMethod;
1237 propPatchRequest.setHeader("If-None-Match", noneMatch);
1238 }
1239 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) {
1240 LOGGER.debug("internalCreateOrUpdate returned " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase());
1241 }
1242 return propPatchRequest;
12651243 }
12661244
12671245 /**
12731251 @Override
12741252 public ItemResult createOrUpdate() throws IOException {
12751253 String encodedHref = URIUtil.encodePath(getHref());
1276 ExchangePropPatchMethod propPatchMethod = internalCreateOrUpdate(encodedHref);
1277 int status = propPatchMethod.getStatusCode();
1254 ExchangePropPatchRequest propPatchRequest = internalCreateOrUpdate(encodedHref);
1255 int status = propPatchRequest.getStatusLine().getStatusCode();
12781256 if (status == HttpStatus.SC_MULTI_STATUS) {
1279 status = propPatchMethod.getResponseStatusCode();
1257 try {
1258 status = propPatchRequest.getResponseStatusCode();
1259 } catch (HttpResponseException e) {
1260 throw new IOException(e.getMessage(), e);
1261 }
12801262 //noinspection VariableNotUsedInsideIf
12811263 if (status == HttpStatus.SC_CREATED) {
12821264 LOGGER.debug("Created contact " + encodedHref);
12901272 if (responses.length == 1) {
12911273 encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl");
12921274 LOGGER.warn("Contact found, permanenturl is " + encodedHref);
1293 propPatchMethod = internalCreateOrUpdate(encodedHref);
1294 status = propPatchMethod.getStatusCode();
1275 propPatchRequest = internalCreateOrUpdate(encodedHref);
1276 status = propPatchRequest.getStatusLine().getStatusCode();
12951277 if (status == HttpStatus.SC_MULTI_STATUS) {
1296 status = propPatchMethod.getResponseStatusCode();
1278 try {
1279 status = propPatchRequest.getResponseStatusCode();
1280 } catch (HttpResponseException e) {
1281 throw new IOException(e.getMessage(), e);
1282 }
12971283 LOGGER.debug("Updated contact " + encodedHref);
12981284 } else {
1299 LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchMethod.getStatusLine());
1285 LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchRequest.getStatusLine());
13001286 }
13011287 }
13021288
13031289 } else {
1304 LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchMethod.getStatusLine());
1290 LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchRequest.getStatusLine().getReasonPhrase());
13051291 }
13061292 ItemResult itemResult = new ItemResult();
13071293 // 440 means forbidden on Exchange
13141300 String contactPictureUrl = URIUtil.encodePath(getHref() + "/ContactPicture.jpg");
13151301 String photo = get("photo");
13161302 if (photo != null) {
1317 final PutMethod putmethod = new PutMethod(contactPictureUrl);
13181303 try {
1304 final HttpPut httpPut = new HttpPut(contactPictureUrl);
13191305 // need to update photo
13201306 byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90);
13211307
1322 putmethod.setRequestHeader("Overwrite", "t");
1323 putmethod.setRequestHeader("Content-Type", "image/jpeg");
1324 putmethod.setRequestEntity(new ByteArrayRequestEntity(resizedImageBytes, "image/jpeg"));
1325
1326 status = httpClient.executeMethod(putmethod);
1327 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED) {
1328 throw new IOException("Unable to update contact picture: " + status + ' ' + putmethod.getStatusLine());
1308 httpPut.setHeader("Overwrite", "t");
1309 // TODO: required ?
1310 httpPut.setHeader("Content-Type", "image/jpeg");
1311 httpPut.setEntity(new ByteArrayEntity(resizedImageBytes, ContentType.IMAGE_JPEG));
1312
1313 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPut)) {
1314 status = response.getStatusLine().getStatusCode();
1315 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED) {
1316 throw new IOException("Unable to update contact picture: " + status + ' ' + response.getStatusLine().getReasonPhrase());
1317 }
13291318 }
13301319 } catch (IOException e) {
13311320 LOGGER.error("Error in contact photo create or update", e);
13321321 throw e;
1333 } finally {
1334 putmethod.releaseConnection();
13351322 }
13361323
13371324 Set<PropertyValue> picturePropertyValues = new HashSet<>();
13391326 // picturePropertyValues.add(Field.createPropertyValue("renderingPosition", "-1"));
13401327 picturePropertyValues.add(Field.createPropertyValue("attachExtension", ".jpg"));
13411328
1342 final ExchangePropPatchMethod attachmentPropPatchMethod = new ExchangePropPatchMethod(contactPictureUrl, picturePropertyValues);
1343 try {
1344 status = httpClient.executeMethod(attachmentPropPatchMethod);
1329 final ExchangePropPatchRequest attachmentPropPatchRequest = new ExchangePropPatchRequest(contactPictureUrl, picturePropertyValues);
1330 try (CloseableHttpResponse response = httpClientAdapter.execute(attachmentPropPatchRequest)) {
1331 attachmentPropPatchRequest.handleResponse(response);
1332 status = response.getStatusLine().getStatusCode();
13451333 if (status != HttpStatus.SC_MULTI_STATUS) {
1346 LOGGER.error("Error in contact photo create or update: " + attachmentPropPatchMethod.getStatusCode());
1334 LOGGER.error("Error in contact photo create or update: " + response.getStatusLine().getStatusCode());
13471335 throw new IOException("Unable to update contact picture");
13481336 }
1349 } finally {
1350 attachmentPropPatchMethod.releaseConnection();
13511337 }
13521338
13531339 } else {
13541340 // try to delete picture
1355 DeleteMethod deleteMethod = new DeleteMethod(contactPictureUrl);
1356 try {
1357 status = httpClient.executeMethod(deleteMethod);
1341 HttpDelete httpDelete = new HttpDelete(contactPictureUrl);
1342 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
1343 status = response.getStatusLine().getStatusCode();
13581344 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
13591345 LOGGER.error("Error in contact photo delete: " + status);
13601346 throw new IOException("Unable to delete contact picture");
13611347 }
1362 } finally {
1363 deleteMethod.releaseConnection();
13641348 }
13651349 }
13661350 // need to retrieve new etag
1367 HeadMethod headMethod = new HeadMethod(URIUtil.encodePath(getHref()));
1368 try {
1369 httpClient.executeMethod(headMethod);
1370 if (headMethod.getResponseHeader("ETag") != null) {
1371 itemResult.etag = headMethod.getResponseHeader("ETag").getValue();
1372 }
1373 } finally {
1374 headMethod.releaseConnection();
1351 HttpHead headMethod = new HttpHead(URIUtil.encodePath(getHref()));
1352 try (CloseableHttpResponse response = httpClientAdapter.execute(headMethod)) {
1353 if (response.getFirstHeader("ETag") != null) {
1354 itemResult.etag = response.getFirstHeader("ETag").getValue();
1355 }
13751356 }
13761357 }
13771358 return itemResult;
13901371 * Build Event instance from response info.
13911372 *
13921373 * @param multiStatusResponse response
1393 * @throws URIException on error
1374 * @throws IOException on error
13941375 */
13951376 public Event(MultiStatusResponse multiStatusResponse) throws IOException {
13961377 setHref(URIUtil.decode(multiStatusResponse.getHref()));
14061387 protected String getPermanentUrl() {
14071388 return permanentUrl;
14081389 }
1409
14101390
14111391 public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
14121392 super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
14391419 try {
14401420 result = getICSFromInternetContentProperty();
14411421 if (result == null) {
1442 GetMethod method = new GetMethod(encodeAndFixUrl(permanentUrl));
1443 method.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
1444 method.setRequestHeader("Translate", "f");
1445 try {
1446 DavGatewayHttpClientFacade.executeGetMethod(httpClient, method, true);
1447 result = getICS(method.getResponseBodyAsStream());
1448 } finally {
1449 method.releaseConnection();
1422 HttpGet httpGet = new HttpGet(encodeAndFixUrl(permanentUrl));
1423 httpGet.setHeader("Content-Type", "text/xml; charset=utf-8");
1424 httpGet.setHeader("Translate", "f");
1425 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
1426 result = getICS(response.getEntity().getContent());
14501427 }
14511428 }
14521429 } catch (DavException | IOException | MessagingException e) {
14581435 if (result == null) {
14591436 try {
14601437 result = getICSFromItemProperties();
1461 } catch (HttpResponseException e) {
1438 } catch (IOException e) {
14621439 deleteBroken();
14631440 throw e;
14641441 }
14731450 return result;
14741451 }
14751452
1476 private byte[] getICSFromItemProperties() throws IOException {
1453 private byte[] getICSFromItemProperties() throws HttpNotFoundException {
14771454 byte[] result;
14781455
14791456 // experimental: build VCALENDAR from properties
16281605 result = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
16291606 } catch (MessagingException | IOException e) {
16301607 LOGGER.warn("Unable to rebuild event content: " + e.getMessage(), e);
1631 throw buildHttpNotFoundException(e);
1608 throw new HttpNotFoundException("Unable to get event " + getName() + " subject: " + subject + " at " + permanentUrl + ": " + e.getMessage());
16321609 }
16331610
16341611 return result;
16391616 if (Settings.getBooleanProperty("davmail.deleteBroken")) {
16401617 LOGGER.warn("Deleting broken event at: " + permanentUrl);
16411618 try {
1642 DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, encodeAndFixUrl(permanentUrl));
1643 } catch (IOException ioe) {
1619 HttpDelete httpDelete = new HttpDelete(encodeAndFixUrl(permanentUrl));
1620 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
1621 LOGGER.warn("deleteBroken returned " + response.getStatusLine().getStatusCode());
1622 }
1623 } catch (IOException e) {
16441624 LOGGER.warn("Unable to delete broken event at: " + permanentUrl);
16451625 }
16461626 }
16471627 }
16481628
1649 protected PutMethod internalCreateOrUpdate(String encodedHref, byte[] mimeContent) throws IOException {
1650 PutMethod putmethod = new PutMethod(encodedHref);
1651 putmethod.setRequestHeader("Translate", "f");
1652 putmethod.setRequestHeader("Overwrite", "f");
1629 protected CloseableHttpResponse internalCreateOrUpdate(String encodedHref, byte[] mimeContent) throws IOException {
1630 HttpPut httpPut = new HttpPut(encodedHref);
1631 httpPut.setHeader("Translate", "f");
1632 httpPut.setHeader("Overwrite", "f");
16531633 if (etag != null) {
1654 putmethod.setRequestHeader("If-Match", etag);
1634 httpPut.setHeader("If-Match", etag);
16551635 }
16561636 if (noneMatch != null) {
1657 putmethod.setRequestHeader("If-None-Match", noneMatch);
1658 }
1659 putmethod.setRequestHeader("Content-Type", "message/rfc822");
1660 putmethod.setRequestEntity(new ByteArrayRequestEntity(mimeContent, "message/rfc822"));
1661 try {
1662 httpClient.executeMethod(putmethod);
1663 } finally {
1664 putmethod.releaseConnection();
1665 }
1666 return putmethod;
1637 httpPut.setHeader("If-None-Match", noneMatch);
1638 }
1639 httpPut.setHeader("Content-Type", "message/rfc822");
1640 httpPut.setEntity(new ByteArrayEntity(mimeContent, ContentType.getByMimeType("message/rfc822")));
1641 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPut)) {
1642 return response;
1643 }
16671644 }
16681645
16691646 /**
17031680 propertyValues.add(Field.createPropertyValue("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
17041681 propertyValues.add(Field.createPropertyValue("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
17051682
1706 ExchangePropPatchMethod propPatchMethod = new ExchangePropPatchMethod(encodedHref, propertyValues);
1707 propPatchMethod.setRequestHeader("Translate", "f");
1683 ExchangePropPatchRequest propPatchMethod = new ExchangePropPatchRequest(encodedHref, propertyValues);
1684 propPatchMethod.setHeader("Translate", "f");
17081685 if (etag != null) {
1709 propPatchMethod.setRequestHeader("If-Match", etag);
1686 propPatchMethod.setHeader("If-Match", etag);
17101687 }
17111688 if (noneMatch != null) {
1712 propPatchMethod.setRequestHeader("If-None-Match", noneMatch);
1713 }
1714 try {
1715 int status = httpClient.executeMethod(propPatchMethod);
1689 propPatchMethod.setHeader("If-None-Match", noneMatch);
1690 }
1691 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) {
1692 int status = response.getStatusLine().getStatusCode();
17161693
17171694 if (status == HttpStatus.SC_MULTI_STATUS) {
17181695 Item newItem = getItem(folderPath, itemName);
1719 itemResult.status = propPatchMethod.getResponseStatusCode();
1696 try {
1697 itemResult.status = propPatchMethod.getResponseStatusCode();
1698 } catch (HttpResponseException e) {
1699 throw new IOException(e.getMessage(), e);
1700 }
17201701 itemResult.etag = newItem.etag;
17211702 } else {
17221703 itemResult.status = status;
17231704 }
1724 } finally {
1725 propPatchMethod.releaseConnection();
17261705 }
17271706
17281707 } else {
17291708 String encodedHref = URIUtil.encodePath(getHref());
17301709 byte[] mimeContent = createMimeContent();
1731 PutMethod putMethod = internalCreateOrUpdate(encodedHref, mimeContent);
1732 int status = putMethod.getStatusCode();
1710 HttpResponse httpResponse = internalCreateOrUpdate(encodedHref, mimeContent);
1711 int status = httpResponse.getStatusLine().getStatusCode();
17331712
17341713 if (status == HttpStatus.SC_OK) {
17351714 LOGGER.debug("Updated event " + encodedHref);
17421721 if (responses.length == 1) {
17431722 encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl");
17441723 LOGGER.warn("Event found, permanenturl is " + encodedHref);
1745 putMethod = internalCreateOrUpdate(encodedHref, mimeContent);
1746 status = putMethod.getStatusCode();
1724 httpResponse = internalCreateOrUpdate(encodedHref, mimeContent);
1725 status = httpResponse.getStatusLine().getStatusCode();
17471726 if (status == HttpStatus.SC_OK) {
17481727 LOGGER.debug("Updated event " + encodedHref);
17491728 } else {
1750 LOGGER.warn("Unable to create or update event " + status + ' ' + putMethod.getStatusLine());
1729 LOGGER.warn("Unable to create or update event " + status + ' ' + httpResponse.getStatusLine().getReasonPhrase());
17511730 }
17521731 }
17531732 } else {
1754 LOGGER.warn("Unable to create or update event " + status + ' ' + putMethod.getStatusLine());
1733 LOGGER.warn("Unable to create or update event " + status + ' ' + httpResponse.getStatusLine().getReasonPhrase());
17551734 }
17561735
17571736 // 440 means forbidden on Exchange
17621741 status = HttpStatus.SC_OK;
17631742 }
17641743 itemResult.status = status;
1765 if (putMethod.getResponseHeader("GetETag") != null) {
1766 itemResult.etag = putMethod.getResponseHeader("GetETag").getValue();
1744 if (httpResponse.getFirstHeader("GetETag") != null) {
1745 itemResult.etag = httpResponse.getFirstHeader("GetETag").getValue();
17671746 }
17681747
17691748 // trigger activeSync push event, only if davmail.forceActiveSyncUpdate setting is true
17741753 propertyList.add(Field.createDavProperty("contentclass", contentClass));
17751754 // ... but also set PR_INTERNET_CONTENT to preserve custom properties
17761755 propertyList.add(Field.createDavProperty("internetContent", IOUtil.encodeBase64AsString(mimeContent)));
1777 PropPatchMethod propPatchMethod = new PropPatchMethod(encodedHref, propertyList);
1778 int patchStatus = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propPatchMethod);
1779 if (patchStatus != HttpStatus.SC_MULTI_STATUS) {
1780 LOGGER.warn("Unable to patch event to trigger activeSync push");
1781 } else {
1782 // need to retrieve new etag
1783 Item newItem = getItem(folderPath, itemName);
1784 itemResult.etag = newItem.etag;
1756 HttpProppatch propPatchMethod = new HttpProppatch(encodedHref, propertyList);
1757 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) {
1758 int patchStatus = response.getStatusLine().getStatusCode();
1759 if (patchStatus != HttpStatus.SC_MULTI_STATUS) {
1760 LOGGER.warn("Unable to patch event to trigger activeSync push");
1761 } else {
1762 // need to retrieve new etag
1763 Item newItem = getItem(folderPath, itemName);
1764 itemResult.etag = newItem.etag;
1765 }
17851766 }
17861767 }
17871768 }
18311812 }
18321813 } else {
18331814 try {
1834 URI folderURI = new URI(href, false);
1815 java.net.URI folderURI = new java.net.URI(href);
18351816 folder.folderPath = folderURI.getPath();
18361817 if (folder.folderPath == null) {
1837 throw new URIException();
1838 }
1839 } catch (URIException e) {
1818 throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href);
1819 }
1820 } catch (URISyntaxException e) {
18401821 throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href);
18411822 }
18421823 }
18741855 */
18751856 @Override
18761857 protected Folder internalGetFolder(String folderPath) throws IOException {
1877 MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executePropFindMethod(
1878 httpClient, URIUtil.encodePath(getFolderPath(folderPath)), 0, FOLDER_PROPERTIES_NAME_SET);
1858 MultiStatus multiStatus = httpClientAdapter.executeDavRequest(new HttpPropfind(
1859 URIUtil.encodePath(getFolderPath(folderPath)),
1860 FOLDER_PROPERTIES_NAME_SET, 0));
1861 MultiStatusResponse[] responses = multiStatus.getResponses();
1862
18791863 Folder folder = null;
18801864 if (responses.length > 0) {
18811865 folder = buildFolder(responses[0]);
19181902 }
19191903 propertyValues.add(Field.createPropertyValue("folderclass", folderClass));
19201904
1921 // standard MkColMethod does not take properties, override PropPatchMethod instead
1922 ExchangePropPatchMethod method = new ExchangePropPatchMethod(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues) {
1905 // standard MkColMethod does not take properties, override ExchangePropPatchRequest instead
1906 ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues) {
19231907 @Override
1924 public String getName() {
1908 public String getMethod() {
19251909 return "MKCOL";
19261910 }
19271911 };
1928 int status = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, method);
1929 if (status == HttpStatus.SC_MULTI_STATUS) {
1930 status = method.getResponseStatusCode();
1931 }
1912 int status;
1913 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) {
1914 propPatchRequest.handleResponse(response);
1915 status = response.getStatusLine().getStatusCode();
1916 if (status == HttpStatus.SC_MULTI_STATUS) {
1917 status = propPatchRequest.getResponseStatusCode();
1918 } else if (status == HttpStatus.SC_METHOD_NOT_ALLOWED) {
1919 LOGGER.info("Folder " + folderPath + " already exists");
1920 }
1921 } catch (HttpResponseException e) {
1922 throw new IOException(e.getMessage(), e);
1923 }
1924 LOGGER.debug("Create folder " + folderPath + " returned " + status);
19321925 return status;
19331926 }
19341927
19441937 }
19451938 }
19461939
1947 // standard MkColMethod does not take properties, override PropPatchMethod instead
1948 ExchangePropPatchMethod method = new ExchangePropPatchMethod(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues);
1949 int status = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, method);
1950 if (status == HttpStatus.SC_MULTI_STATUS) {
1951 status = method.getResponseStatusCode();
1952 }
1953 return status;
1940 ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues);
1941 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) {
1942 propPatchRequest.handleResponse(response);
1943 int status = response.getStatusLine().getStatusCode();
1944 if (status == HttpStatus.SC_MULTI_STATUS) {
1945 try {
1946 status = propPatchRequest.getResponseStatusCode();
1947 } catch (HttpResponseException e) {
1948 throw new IOException(e.getMessage(), e);
1949 }
1950 }
1951
1952 return status;
1953 }
19541954 }
19551955
19561956 /**
19581958 */
19591959 @Override
19601960 public void deleteFolder(String folderPath) throws IOException {
1961 DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, URIUtil.encodePath(getFolderPath(folderPath)));
1961 HttpDelete httpDelete = new HttpDelete(URIUtil.encodePath(getFolderPath(folderPath)));
1962 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
1963 int status = response.getStatusLine().getStatusCode();
1964 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
1965 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response);
1966 }
1967 }
19621968 }
19631969
19641970 /**
19661972 */
19671973 @Override
19681974 public void moveFolder(String folderPath, String targetPath) throws IOException {
1969 MoveMethod method = new MoveMethod(URIUtil.encodePath(getFolderPath(folderPath)),
1975 HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(folderPath)),
19701976 URIUtil.encodePath(getFolderPath(targetPath)), false);
1971 try {
1972 int statusCode = httpClient.executeMethod(method);
1977 try (CloseableHttpResponse response = httpClientAdapter.execute(httpMove)) {
1978 int statusCode = response.getStatusLine().getStatusCode();
19731979 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
19741980 throw new HttpPreconditionFailedException(BundleMessage.format("EXCEPTION_UNABLE_TO_MOVE_FOLDER"));
19751981 } else if (statusCode != HttpStatus.SC_CREATED) {
1976 throw DavGatewayHttpClientFacade.buildHttpResponseException(method);
1982 throw HttpClientAdapter.buildHttpResponseException(httpMove, response);
19771983 } else if (folderPath.equalsIgnoreCase("/users/" + getEmail() + "/calendar")) {
1978 // calendar renamed, need to reload well known folders
1984 // calendar renamed, need to reload well known folders
19791985 getWellKnownFolders();
19801986 }
1981 } finally {
1982 method.releaseConnection();
19831987 }
19841988 }
19851989
19881992 */
19891993 @Override
19901994 public void moveItem(String sourcePath, String targetPath) throws IOException {
1991 MoveMethod method = new MoveMethod(URIUtil.encodePath(getFolderPath(sourcePath)),
1995 HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(sourcePath)),
19921996 URIUtil.encodePath(getFolderPath(targetPath)), false);
1993 moveItem(method);
1994 }
1995
1996 protected void moveItem(MoveMethod method) throws IOException {
1997 try {
1998 int statusCode = httpClient.executeMethod(method);
1997 moveItem(httpMove);
1998 }
1999
2000 protected void moveItem(HttpMove httpMove) throws IOException {
2001 try (CloseableHttpResponse response = httpClientAdapter.execute(httpMove)) {
2002 int statusCode = response.getStatusLine().getStatusCode();
19992003 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
20002004 throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_ITEM");
20012005 } else if (statusCode != HttpStatus.SC_CREATED && statusCode != HttpStatus.SC_OK) {
2002 throw DavGatewayHttpClientFacade.buildHttpResponseException(method);
2003 }
2004 } finally {
2005 method.releaseConnection();
2006 throw HttpClientAdapter.buildHttpResponseException(httpMove, response);
2007 }
20062008 }
20072009 }
20082010
22462248 }
22472249 searchRequest.append(" ORDER BY ").append(Field.getRequestPropertyString("imapUid")).append(" DESC");
22482250 DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_QUERY", searchRequest));
2249 MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeSearchMethod(
2250 httpClient, encodeAndFixUrl(folderUrl), searchRequest.toString(), maxCount);
2251 MultiStatusResponse[] responses = httpClientAdapter.executeSearchRequest(
2252 encodeAndFixUrl(folderUrl), searchRequest.toString(), maxCount);
22512253 DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_RESULT", responses.length));
22522254 return responses;
22532255 }
22782280 String itemPath = getFolderPath(folderPath) + '/' + emlItemName;
22792281 MultiStatusResponse[] responses = null;
22802282 try {
2281 try {
2282 responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, URIUtil.encodePath(itemPath), 0, EVENT_REQUEST_PROPERTIES_NAME_SET);
2283 } catch (HttpNotFoundException e) {
2283 HttpPropfind httpPropfind = new HttpPropfind(URIUtil.encodePath(itemPath), EVENT_REQUEST_PROPERTIES_NAME_SET, 0);
2284 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) {
2285 responses = httpPropfind.getResponseBodyAsMultiStatus(response).getResponses();
2286 } catch (HttpNotFoundException | DavException e) {
22842287 // ignore
22852288 }
22862289 if (responses == null || responses.length == 0 && isMainCalendar(folderPath)) {
22882291 itemName = itemName.substring(0, itemName.length() - 3) + "EML";
22892292 }
22902293 // look for item in tasks folder
2291 responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, URIUtil.encodePath(getFolderPath(TASKS) + '/' + emlItemName), 0, EVENT_REQUEST_PROPERTIES_NAME_SET);
2294 HttpPropfind taskHttpPropfind = new HttpPropfind(URIUtil.encodePath(getFolderPath(TASKS) + '/' + emlItemName), EVENT_REQUEST_PROPERTIES_NAME_SET, 0);
2295 try (CloseableHttpResponse response = httpClientAdapter.execute(taskHttpPropfind)) {
2296 responses = taskHttpPropfind.getResponseBodyAsMultiStatus(response).getResponses();
2297 } catch (HttpNotFoundException | DavException e) {
2298 // ignore
2299 }
22922300 }
22932301 if (responses == null || responses.length == 0) {
22942302 throw new HttpNotFoundException(itemPath + " not found");
23092317 List<ExchangeSession.Event> events = getAllEvents(folderPath);
23102318 for (ExchangeSession.Event event : events) {
23112319 if (itemName.equals(event.getName())) {
2312 responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, encodeAndFixUrl(((DavExchangeSession.Event) event).getPermanentUrl()), 0, EVENT_REQUEST_PROPERTIES_NAME_SET);
2320 HttpPropfind permanentHttpPropfind = new HttpPropfind(encodeAndFixUrl(((DavExchangeSession.Event) event).getPermanentUrl()), EVENT_REQUEST_PROPERTIES_NAME_SET, 0);
2321 try (CloseableHttpResponse response = httpClientAdapter.execute(permanentHttpPropfind)) {
2322 responses = permanentHttpPropfind.getResponseBodyAsMultiStatus(response).getResponses();
2323 } catch (DavException e3) {
2324 // ignore
2325 }
23132326 break;
23142327 }
23152328 }
23462359 @Override
23472360 public ExchangeSession.ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
23482361 ContactPhoto contactPhoto;
2349 final GetMethod method = new GetMethod(URIUtil.encodePath(contact.getHref()) + "/ContactPicture.jpg");
2350 method.setRequestHeader("Translate", "f");
2351 method.setRequestHeader("Accept-Encoding", "gzip");
2362 final HttpGet httpGet = new HttpGet(URIUtil.encodePath(contact.getHref()) + "/ContactPicture.jpg");
2363 httpGet.setHeader("Translate", "f");
2364 httpGet.setHeader("Accept-Encoding", "gzip");
23522365
23532366 InputStream inputStream = null;
2354 try {
2355 DavGatewayHttpClientFacade.executeGetMethod(httpClient, method, true);
2356 if (DavGatewayHttpClientFacade.isGzipEncoded(method)) {
2357 inputStream = (new GZIPInputStream(method.getResponseBodyAsStream()));
2367 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
2368 if (HttpClientAdapter.isGzipEncoded(response)) {
2369 inputStream = (new GZIPInputStream(response.getEntity().getContent()));
23582370 } else {
2359 inputStream = method.getResponseBodyAsStream();
2371 inputStream = response.getEntity().getContent();
23602372 }
23612373
23622374 contactPhoto = new ContactPhoto();
23742386 LOGGER.debug(e);
23752387 }
23762388 }
2377 method.releaseConnection();
23782389 }
23792390 return contactPhoto;
23802391 }
23952406 @Override
23962407 public void deleteItem(String folderPath, String itemName) throws IOException {
23972408 String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName));
2398 int status = DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, eventPath);
2409 HttpDelete httpDelete = new HttpDelete(eventPath);
2410 int status;
2411 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2412 status = response.getStatusLine().getStatusCode();
2413 }
23992414 if (status == HttpStatus.SC_NOT_FOUND && isMainCalendar(folderPath)) {
24002415 // retry in tasks folder
24012416 eventPath = URIUtil.encodePath(getFolderPath(TASKS) + '/' + convertItemNameToEML(itemName));
2402 status = DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, eventPath);
2417 httpDelete = new HttpDelete(eventPath);
2418 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2419 status = response.getStatusLine().getStatusCode();
2420 }
24032421 }
24042422 if (status == HttpStatus.SC_NOT_FOUND) {
24052423 LOGGER.debug("Unable to delete " + itemName + ": item not found");
24132431 ArrayList<PropEntry> list = new ArrayList<>();
24142432 list.add(Field.createDavProperty("processed", "true"));
24152433 list.add(Field.createDavProperty("read", "1"));
2416 PropPatchMethod patchMethod = new PropPatchMethod(eventPath, list);
2417 DavGatewayHttpClientFacade.executeMethod(httpClient, patchMethod);
2434 HttpProppatch patchMethod = new HttpProppatch(eventPath, list);
2435 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) {
2436 LOGGER.debug("Processed " + itemName + " " + response.getStatusLine().getStatusCode());
2437 }
24182438 }
24192439
24202440 @Override
24342454
24352455 String fakeEventUrl = null;
24362456 if ("Exchange2003".equals(serverVersion)) {
2437 PostMethod postMethod = new PostMethod(URIUtil.encodePath(folderPath));
2438 postMethod.addParameter("Cmd", "saveappt");
2439 postMethod.addParameter("FORMTYPE", "appointment");
2440 try {
2457 HttpPost httpPost = new HttpPost(URIUtil.encodePath(folderPath));
2458 ArrayList<NameValuePair> parameters = new ArrayList<>();
2459 parameters.add(new BasicNameValuePair("Cmd", "saveappt"));
2460 parameters.add(new BasicNameValuePair("FORMTYPE", "appointment"));
2461 httpPost.setEntity(new UrlEncodedFormEntity(parameters, Consts.UTF_8));
2462
2463 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPost)) {
24412464 // create fake event
2442 int statusCode = httpClient.executeMethod(postMethod);
2465 int statusCode = response.getStatusLine().getStatusCode();
24432466 if (statusCode == HttpStatus.SC_OK) {
2444 fakeEventUrl = StringUtil.getToken(postMethod.getResponseBodyAsString(), "<span id=\"itemHREF\">", "</span>");
2467 fakeEventUrl = StringUtil.getToken(new BasicResponseHandler().handleResponse(response), "<span id=\"itemHREF\">", "</span>");
24452468 if (fakeEventUrl != null) {
24462469 fakeEventUrl = URIUtil.decode(fakeEventUrl);
24472470 }
24482471 }
2449 } finally {
2450 postMethod.releaseConnection();
24512472 }
24522473 }
24532474 // failover for Exchange 2007, use PROPPATCH with forced timezone
24682489 propertyList.add(Field.createDavProperty("timezoneid", timezoneId));
24692490 }
24702491 String patchMethodUrl = folderPath + '/' + UUID.randomUUID().toString() + ".EML";
2471 PropPatchMethod patchMethod = new PropPatchMethod(URIUtil.encodePath(patchMethodUrl), propertyList);
2472 try {
2473 int statusCode = httpClient.executeMethod(patchMethod);
2492 HttpProppatch patchMethod = new HttpProppatch(URIUtil.encodePath(patchMethodUrl), propertyList);
2493 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) {
2494 int statusCode = response.getStatusLine().getStatusCode();
24742495 if (statusCode == HttpStatus.SC_MULTI_STATUS) {
24752496 fakeEventUrl = patchMethodUrl;
24762497 }
2477 } finally {
2478 patchMethod.releaseConnection();
24792498 }
24802499 }
24812500 if (fakeEventUrl != null) {
24822501 // get fake event body
2483 GetMethod getMethod = new GetMethod(URIUtil.encodePath(fakeEventUrl));
2484 getMethod.setRequestHeader("Translate", "f");
2485 try {
2486 httpClient.executeMethod(getMethod);
2502 HttpGet httpGet = new HttpGet(URIUtil.encodePath(fakeEventUrl));
2503 httpGet.setHeader("Translate", "f");
2504 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
24872505 this.vTimezone = new VObject("BEGIN:VTIMEZONE" +
2488 StringUtil.getToken(getMethod.getResponseBodyAsString(), "BEGIN:VTIMEZONE", "END:VTIMEZONE") +
2506 StringUtil.getToken(new BasicResponseHandler().handleResponse(response), "BEGIN:VTIMEZONE", "END:VTIMEZONE") +
24892507 "END:VTIMEZONE\r\n");
2490 } finally {
2491 getMethod.releaseConnection();
24922508 }
24932509 }
24942510
25982614 @Override
25992615 public void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
26002616 String messageUrl = URIUtil.encodePathQuery(getFolderPath(folderPath) + '/' + messageName);
2601 PropPatchMethod patchMethod;
2617
26022618 List<PropEntry> davProperties = buildProperties(properties);
26032619
26042620 if (properties != null && properties.containsKey("draft")) {
26122628 davProperties.add(Field.createDavProperty("messageFormat", properties.get("messageFormat")));
26132629 }
26142630 if (!davProperties.isEmpty()) {
2615 patchMethod = new PropPatchMethod(messageUrl, davProperties);
2616 try {
2631 HttpProppatch httpProppatch = new HttpProppatch(messageUrl, davProperties);
2632 try (CloseableHttpResponse response = httpClientAdapter.execute(httpProppatch)) {
26172633 // update message with blind carbon copy and other flags
2618 int statusCode = httpClient.executeMethod(patchMethod);
2634 int statusCode = response.getStatusLine().getStatusCode();
26192635 if (statusCode != HttpStatus.SC_MULTI_STATUS) {
2620 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', patchMethod.getStatusLine());
2621 }
2622
2623 } finally {
2624 patchMethod.releaseConnection();
2636 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', response.getStatusLine().getReasonPhrase());
2637 }
2638
26252639 }
26262640 }
26272641
26282642 // update message body
2629 PutMethod putmethod = new PutMethod(messageUrl);
2630 putmethod.setRequestHeader("Translate", "f");
2631 putmethod.setRequestHeader("Content-Type", "message/rfc822");
2643 HttpPut putmethod = new HttpPut(messageUrl);
2644 putmethod.setHeader("Translate", "f");
2645 putmethod.setHeader("Content-Type", "message/rfc822");
26322646
26332647 try {
26342648 // use same encoding as client socket reader
26352649 ByteArrayOutputStream baos = new ByteArrayOutputStream();
26362650 mimeMessage.writeTo(baos);
26372651 baos.close();
2638 putmethod.setRequestEntity(new ByteArrayRequestEntity(baos.toByteArray()));
2639 int code = httpClient.executeMethod(putmethod);
2652 putmethod.setEntity(new ByteArrayEntity(baos.toByteArray()));
2653
2654 int code;
2655 String reasonPhrase;
2656 try (CloseableHttpResponse response = httpClientAdapter.execute(putmethod)) {
2657 code = response.getStatusLine().getStatusCode();
2658 reasonPhrase = response.getStatusLine().getReasonPhrase();
2659 }
26402660
26412661 // workaround for misconfigured Exchange server
26422662 if (code == HttpStatus.SC_NOT_ACCEPTABLE) {
26662686 } else if (contentType.startsWith("text/html")) {
26672687 propertyList.add(Field.createDavProperty("htmldescription", (String) mimePart.getContent()));
26682688 } else {
2669 LOGGER.warn("Unsupported content type: " + contentType + " message body will be empty");
2689 LOGGER.warn("Unsupported content type: " + contentType.replaceAll("[\n\r\t]", "_") + " message body will be empty");
26702690 }
26712691
26722692 propertyList.add(Field.createDavProperty("subject", mimeMessage.getHeader("subject", ",")));
2673 PropPatchMethod propPatchMethod = new PropPatchMethod(messageUrl, propertyList);
2674 try {
2675 int patchStatus = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propPatchMethod);
2693 HttpProppatch propPatchMethod = new HttpProppatch(messageUrl, propertyList);
2694 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) {
2695 int patchStatus = response.getStatusLine().getStatusCode();
26762696 if (patchStatus == HttpStatus.SC_MULTI_STATUS) {
26772697 code = HttpStatus.SC_OK;
26782698 }
2679 } finally {
2680 propPatchMethod.releaseConnection();
2681 }
2682 }
2699 }
2700 }
2701
26832702
26842703 if (code != HttpStatus.SC_OK && code != HttpStatus.SC_CREATED) {
26852704
26862705 // first delete draft message
26872706 if (!davProperties.isEmpty()) {
2688 try {
2689 DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, messageUrl);
2707 HttpDelete httpDelete = new HttpDelete(messageUrl);
2708 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2709 int status = response.getStatusLine().getStatusCode();
2710 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
2711 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response);
2712 }
26902713 } catch (IOException e) {
26912714 LOGGER.warn("Unable to delete draft message");
26922715 }
26932716 }
26942717 if (code == HttpStatus.SC_INSUFFICIENT_STORAGE) {
2695 throw new InsufficientStorageException(putmethod.getStatusText());
2718 throw new InsufficientStorageException(reasonPhrase);
26962719 } else {
2697 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, code, ' ', putmethod.getStatusLine());
2720 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, code, ' ', reasonPhrase);
26982721 }
26992722 }
27002723 } catch (MessagingException e) {
27082731 if (mimeMessage.getHeader("Bcc") != null) {
27092732 davProperties = new ArrayList<>();
27102733 davProperties.add(Field.createDavProperty("bcc", mimeMessage.getHeader("Bcc", ",")));
2711 patchMethod = new PropPatchMethod(messageUrl, davProperties);
2712 try {
2713 // update message with blind carbon copy
2714 int statusCode = httpClient.executeMethod(patchMethod);
2734 HttpProppatch httpProppatch = new HttpProppatch(messageUrl, davProperties);
2735 // update message with blind carbon copy
2736 try (CloseableHttpResponse response = httpClientAdapter.execute(httpProppatch)) {
2737 int statusCode = response.getStatusLine().getStatusCode();
27152738 if (statusCode != HttpStatus.SC_MULTI_STATUS) {
2716 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', patchMethod.getStatusLine());
2717 }
2718
2719 } finally {
2720 patchMethod.releaseConnection();
2739 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', response.getStatusLine().getReasonPhrase());
2740 }
27212741 }
27222742 }
27232743 } catch (MessagingException e) {
27312751 */
27322752 @Override
27332753 public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
2734 PropPatchMethod patchMethod = new PropPatchMethod(encodeAndFixUrl(message.permanentUrl), buildProperties(properties)) {
2754 HttpProppatch patchMethod = new HttpProppatch(encodeAndFixUrl(message.permanentUrl), buildProperties(properties)) {
27352755 @Override
2736 protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
2756 public MultiStatus getResponseBodyAsMultiStatus(HttpResponse response) {
27372757 // ignore response body, sometimes invalid with exchange mapi properties
2758 throw new UnsupportedOperationException();
27382759 }
27392760 };
2740 try {
2741 int statusCode = httpClient.executeMethod(patchMethod);
2761 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) {
2762 int statusCode = response.getStatusLine().getStatusCode();
27422763 if (statusCode != HttpStatus.SC_MULTI_STATUS) {
27432764 throw new DavMailException("EXCEPTION_UNABLE_TO_UPDATE_MESSAGE");
27442765 }
2745
2746 } finally {
2747 patchMethod.releaseConnection();
27482766 }
27492767 }
27502768
27542772 @Override
27552773 public void deleteMessage(ExchangeSession.Message message) throws IOException {
27562774 LOGGER.debug("Delete " + message.permanentUrl + " (" + message.messageUrl + ')');
2757 DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, encodeAndFixUrl(message.permanentUrl));
2775 HttpDelete httpDelete = new HttpDelete(encodeAndFixUrl(message.permanentUrl));
2776 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2777 int status = response.getStatusLine().getStatusCode();
2778 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
2779 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response);
2780 }
2781 }
27582782 }
27592783
27602784 /**
27962820 properties.put("messageFormat", "2");
27972821 }
27982822 createMessage(DRAFTS, itemName, properties, mimeMessage);
2799 MoveMethod method = new MoveMethod(URIUtil.encodePath(getFolderPath(DRAFTS + '/' + itemName)),
2823 HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(DRAFTS + '/' + itemName)),
28002824 URIUtil.encodePath(getFolderPath(SENDMSG)), false);
2801 // set header if saveInSent is disabled
2825 // set header if saveInSent is disabled
28022826 if (!Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) {
2803 method.setRequestHeader("Saveinsent", "f");
2804 }
2805 moveItem(method);
2827 httpMove.setHeader("Saveinsent", "f");
2828 }
2829 moveItem(httpMove);
28062830 } catch (MessagingException e) {
28072831 throw new IOException(e.getMessage());
28082832 }
28572881 messageProperties.add(Field.getPropertyName("date"));
28582882 messageProperties.add(Field.getPropertyName("htmldescription"));
28592883 messageProperties.add(Field.getPropertyName("body"));
2860 PropFindMethod propFindMethod = new PropFindMethod(encodeAndFixUrl(message.permanentUrl), messageProperties, 0);
2861 DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod);
2862 MultiStatus responses = propFindMethod.getResponseBodyAsMultiStatus();
2863 if (responses.getResponses().length > 0) {
2864 MimeMessage mimeMessage = new MimeMessage((Session) null);
2865
2866 DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
2867 String propertyValue = getPropertyIfExists(properties, "contentclass");
2868 if (propertyValue != null) {
2869 mimeMessage.addHeader("Content-class", propertyValue);
2870 }
2871 propertyValue = getPropertyIfExists(properties, "date");
2872 if (propertyValue != null) {
2873 mimeMessage.setSentDate(parseDateFromExchange(propertyValue));
2874 }
2875 propertyValue = getPropertyIfExists(properties, "from");
2876 if (propertyValue != null) {
2877 mimeMessage.addHeader("From", propertyValue);
2878 }
2879 propertyValue = getPropertyIfExists(properties, "to");
2880 if (propertyValue != null) {
2881 mimeMessage.addHeader("To", propertyValue);
2882 }
2883 propertyValue = getPropertyIfExists(properties, "cc");
2884 if (propertyValue != null) {
2885 mimeMessage.addHeader("Cc", propertyValue);
2886 }
2887 propertyValue = getPropertyIfExists(properties, "subject");
2888 if (propertyValue != null) {
2889 mimeMessage.setSubject(propertyValue);
2890 }
2891 propertyValue = getPropertyIfExists(properties, "htmldescription");
2892 if (propertyValue != null) {
2893 mimeMessage.setContent(propertyValue, "text/html; charset=UTF-8");
2894 } else {
2895 propertyValue = getPropertyIfExists(properties, "body");
2884 HttpPropfind httpPropfind = new HttpPropfind(encodeAndFixUrl(message.permanentUrl), messageProperties, 0);
2885 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) {
2886 MultiStatus responses = httpPropfind.getResponseBodyAsMultiStatus(response);
2887 if (responses.getResponses().length > 0) {
2888 MimeMessage mimeMessage = new MimeMessage((Session) null);
2889
2890 DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
2891 String propertyValue = getPropertyIfExists(properties, "contentclass");
28962892 if (propertyValue != null) {
2897 mimeMessage.setText(propertyValue);
2893 mimeMessage.addHeader("Content-class", propertyValue);
28982894 }
2899 }
2900 mimeMessage.writeTo(baos);
2895 propertyValue = getPropertyIfExists(properties, "date");
2896 if (propertyValue != null) {
2897 mimeMessage.setSentDate(parseDateFromExchange(propertyValue));
2898 }
2899 propertyValue = getPropertyIfExists(properties, "from");
2900 if (propertyValue != null) {
2901 mimeMessage.addHeader("From", propertyValue);
2902 }
2903 propertyValue = getPropertyIfExists(properties, "to");
2904 if (propertyValue != null) {
2905 mimeMessage.addHeader("To", propertyValue);
2906 }
2907 propertyValue = getPropertyIfExists(properties, "cc");
2908 if (propertyValue != null) {
2909 mimeMessage.addHeader("Cc", propertyValue);
2910 }
2911 propertyValue = getPropertyIfExists(properties, "subject");
2912 if (propertyValue != null) {
2913 mimeMessage.setSubject(propertyValue);
2914 }
2915 propertyValue = getPropertyIfExists(properties, "htmldescription");
2916 if (propertyValue != null) {
2917 mimeMessage.setContent(propertyValue, "text/html; charset=UTF-8");
2918 } else {
2919 propertyValue = getPropertyIfExists(properties, "body");
2920 if (propertyValue != null) {
2921 mimeMessage.setText(propertyValue);
2922 }
2923 }
2924 mimeMessage.writeTo(baos);
2925 }
29012926 }
29022927 if (LOGGER.isDebugEnabled()) {
29032928 LOGGER.debug("Rebuilt message content: " + new String(baos.toByteArray(), StandardCharsets.UTF_8));
29202945 return baos.toByteArray();
29212946 }
29222947
2923 protected String getEscapedUrlFromPath(String escapedPath) throws URIException {
2924 URI uri = new URI(httpClient.getHostConfiguration().getHostURL(), true);
2925 uri.setEscapedPath(escapedPath);
2926 return uri.getEscapedURI();
2927 }
2928
2929 protected String encodeAndFixUrl(String url) throws URIException {
2930 String originalUrl = URIUtil.encodePath(url);
2931 if (restoreHostName && originalUrl.startsWith("http")) {
2932 String targetPath = new URI(originalUrl, true).getEscapedPath();
2933 originalUrl = getEscapedUrlFromPath(targetPath);
2934 }
2935 return originalUrl;
2948 /**
2949 * sometimes permanenturis inside items are wrong after an Exchange version migration
2950 * need to restore base uri to actual public Exchange uri
2951 *
2952 * @param url input uri
2953 * @return fixed uri
2954 * @throws IOException on error
2955 */
2956 protected String encodeAndFixUrl(String url) throws IOException {
2957 String fixedurl = URIUtil.encodePath(url);
2958 // sometimes permanenturis inside items are wrong after an Exchange version migration
2959 // need to restore base uri to actual public Exchange uri
2960 if (restoreHostName && fixedurl.startsWith("http")) {
2961 try {
2962 return URIUtils.rewriteURI(new java.net.URI(fixedurl), URIUtils.extractHost(httpClientAdapter.getUri())).toString();
2963 } catch (URISyntaxException e) {
2964 throw new IOException(e.getMessage(), e);
2965 }
2966 }
2967 return fixedurl;
29362968 }
29372969
29382970 protected InputStream getContentInputStream(String url) throws IOException {
29392971 String encodedUrl = encodeAndFixUrl(url);
29402972
2941 final GetMethod method = new GetMethod(encodedUrl);
2942 method.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
2943 method.setRequestHeader("Translate", "f");
2944 method.setRequestHeader("Accept-Encoding", "gzip");
2973 final HttpGet httpGet = new HttpGet(encodedUrl);
2974 httpGet.setHeader("Content-Type", "text/xml; charset=utf-8");
2975 httpGet.setHeader("Translate", "f");
2976 httpGet.setHeader("Accept-Encoding", "gzip");
29452977
29462978 InputStream inputStream;
2947 try {
2948 DavGatewayHttpClientFacade.executeGetMethod(httpClient, method, true);
2949 if (DavGatewayHttpClientFacade.isGzipEncoded(method)) {
2950 inputStream = new GZIPInputStream(method.getResponseBodyAsStream());
2979 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
2980 if (HttpClientAdapter.isGzipEncoded(response)) {
2981 inputStream = new GZIPInputStream(response.getEntity().getContent());
29512982 } else {
2952 inputStream = method.getResponseBodyAsStream();
2983 inputStream = response.getEntity().getContent();
29532984 }
29542985 inputStream = new FilterInputStream(inputStream) {
29552986 int totalCount;
29602991 int count = super.read(buffer, offset, length);
29612992 totalCount += count;
29622993 if (totalCount - lastLogCount > 1024 * 128) {
2963 DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), method.getURI()));
2994 DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), httpGet.getURI()));
29642995 DavGatewayTray.switchIcon();
29652996 lastLogCount = totalCount;
29662997 }
29723003 try {
29733004 super.close();
29743005 } finally {
2975 method.releaseConnection();
3006 httpGet.releaseConnection();
29763007 }
29773008 }
29783009 };
29793010
2980 } catch (HttpResponseException e) {
2981 method.releaseConnection();
3011 } catch (IOException e) {
29823012 LOGGER.warn("Unable to retrieve message at: " + url);
29833013 throw e;
29843014 }
30003030
30013031 protected void moveMessage(String sourceUrl, String targetFolder) throws IOException {
30023032 String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID().toString();
3003 MoveMethod method = new MoveMethod(URIUtil.encodePath(sourceUrl), targetPath, false);
3033 HttpMove method = new HttpMove(URIUtil.encodePath(sourceUrl), targetPath, false);
30043034 // allow rename if a message with the same name exists
3005 method.addRequestHeader("Allow-Rename", "t");
3006 try {
3007 int statusCode = httpClient.executeMethod(method);
3008 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
3035 method.setHeader("Allow-Rename", "t");
3036 try (CloseableHttpResponse response = httpClientAdapter.execute(method)) {
3037 int statusCode = response.getStatusLine().getStatusCode();
3038 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED ||
3039 statusCode == HttpStatus.SC_CONFLICT) {
30093040 throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_MESSAGE");
30103041 } else if (statusCode != HttpStatus.SC_CREATED) {
3011 throw DavGatewayHttpClientFacade.buildHttpResponseException(method);
3042 throw HttpClientAdapter.buildHttpResponseException(method, response);
30123043 }
30133044 } finally {
30143045 method.releaseConnection();
30303061
30313062 protected void copyMessage(String sourceUrl, String targetFolder) throws IOException {
30323063 String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID().toString();
3033 CopyMethod method = new CopyMethod(URIUtil.encodePath(sourceUrl), targetPath, false);
3064 HttpCopy httpCopy = new HttpCopy(URIUtil.encodePath(sourceUrl), targetPath, false, false);
30343065 // allow rename if a message with the same name exists
3035 method.addRequestHeader("Allow-Rename", "t");
3036 try {
3037 int statusCode = httpClient.executeMethod(method);
3066 httpCopy.addHeader("Allow-Rename", "t");
3067 try (CloseableHttpResponse response = httpClientAdapter.execute(httpCopy)) {
3068 int statusCode = response.getStatusLine().getStatusCode();
30383069 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
30393070 throw new DavMailException("EXCEPTION_UNABLE_TO_COPY_MESSAGE");
30403071 } else if (statusCode != HttpStatus.SC_CREATED) {
3041 throw DavGatewayHttpClientFacade.buildHttpResponseException(method);
3042 }
3043 } finally {
3044 method.releaseConnection();
3072 throw HttpClientAdapter.buildHttpResponseException(httpCopy, response);
3073 }
30453074 }
30463075 }
30473076
30493078 protected void moveToTrash(ExchangeSession.Message message) throws IOException {
30503079 String destination = URIUtil.encodePath(deleteditemsUrl) + '/' + UUID.randomUUID().toString();
30513080 LOGGER.debug("Deleting : " + message.permanentUrl + " to " + destination);
3052 MoveMethod method = new MoveMethod(encodeAndFixUrl(message.permanentUrl), destination, false);
3053 method.addRequestHeader("Allow-rename", "t");
3054
3055 int status = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, method);
3056 // do not throw error if already deleted
3057 if (status != HttpStatus.SC_CREATED && status != HttpStatus.SC_NOT_FOUND) {
3058 throw DavGatewayHttpClientFacade.buildHttpResponseException(method);
3059 }
3060 if (method.getResponseHeader("Location") != null) {
3061 destination = method.getResponseHeader("Location").getValue();
3081 HttpMove method = new HttpMove(encodeAndFixUrl(message.permanentUrl), destination, false);
3082 method.addHeader("Allow-rename", "t");
3083
3084 try (CloseableHttpResponse response = httpClientAdapter.execute(method)) {
3085 int status = response.getStatusLine().getStatusCode();
3086 // do not throw error if already deleted
3087 if (status != HttpStatus.SC_CREATED && status != HttpStatus.SC_NOT_FOUND) {
3088 throw HttpClientAdapter.buildHttpResponseException(method, response);
3089 }
3090 if (response.getFirstHeader("Location") != null) {
3091 destination = method.getFirstHeader("Location").getValue();
3092 }
30623093 }
30633094
30643095 LOGGER.debug("Deleted to :" + destination);
30683099 String result = null;
30693100 DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
30703101 davPropertyNameSet.add(Field.getPropertyName(propertyName));
3071 PropFindMethod propFindMethod = new PropFindMethod(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
3072 try {
3073 try {
3074 DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propFindMethod);
3075 } catch (UnknownHostException e) {
3076 propFindMethod.releaseConnection();
3077 // failover for misconfigured Exchange server, replace host name in url
3078 restoreHostName = true;
3079 propFindMethod = new PropFindMethod(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
3080 DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propFindMethod);
3081 }
3082
3083 MultiStatus responses = propFindMethod.getResponseBodyAsMultiStatus();
3084 if (responses.getResponses().length > 0) {
3085 DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
3086 result = getPropertyIfExists(properties, propertyName);
3087 }
3088 } finally {
3089 propFindMethod.releaseConnection();
3090 }
3102 HttpPropfind propFindMethod = new HttpPropfind(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
3103 MultiStatus responses;
3104 try (CloseableHttpResponse response = httpClientAdapter.execute(propFindMethod)) {
3105 responses = propFindMethod.getResponseBodyAsMultiStatus(response);
3106 } catch (UnknownHostException e) {
3107 // failover for misconfigured Exchange server, replace host name in url
3108 restoreHostName = true;
3109 propFindMethod = new HttpPropfind(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
3110 try (CloseableHttpResponse response = httpClientAdapter.execute(propFindMethod)) {
3111 responses = propFindMethod.getResponseBodyAsMultiStatus(response);
3112 }
3113 }
3114
3115 if (responses.getResponses().length > 0) {
3116 DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
3117 result = getPropertyIfExists(properties, propertyName);
3118 }
3119
30913120 return result;
30923121 }
30933122
31353164 return value;
31363165 }
31373166
3167
3168 @Override
3169 public void close() {
3170 httpClientAdapter.close();
3171 }
31383172
31393173 /**
31403174 * Format date to exchange search format.
31983232 }
31993233 return result;
32003234 }
3201
3202 /**
3203 * Close session.
3204 * Shutdown http client connection manager
3205 */
3206 @Override
3207 public void close() {
3208 DavGatewayHttpClientFacade.close(httpClient);
3209 }
32103235 }
+0
-268
src/java/davmail/exchange/dav/ExchangeDavMethod.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2012 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.exchange.dav;
19
20 import davmail.exchange.XMLStreamUtil;
21 import org.apache.commons.httpclient.Header;
22 import org.apache.commons.httpclient.HttpConnection;
23 import org.apache.commons.httpclient.HttpException;
24 import org.apache.commons.httpclient.HttpState;
25 import org.apache.commons.httpclient.HttpStatus;
26 import org.apache.commons.httpclient.methods.PostMethod;
27 import org.apache.commons.httpclient.methods.RequestEntity;
28 import org.apache.jackrabbit.webdav.MultiStatusResponse;
29 import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
30 import org.apache.jackrabbit.webdav.xml.Namespace;
31 import org.apache.log4j.Logger;
32
33 import javax.xml.stream.XMLStreamConstants;
34 import javax.xml.stream.XMLStreamException;
35 import javax.xml.stream.XMLStreamReader;
36 import java.io.FilterInputStream;
37 import java.io.IOException;
38 import java.io.OutputStream;
39 import java.util.ArrayList;
40 import java.util.List;
41
42 /**
43 * New stax based implementation to replace DOM based jackrabbit version an support Exchange only extensions.
44 */
45 public abstract class ExchangeDavMethod extends PostMethod {
46 protected static final Logger LOGGER = Logger.getLogger(ExchangeDavMethod.class);
47 List<MultiStatusResponse> responses;
48
49 /**
50 * Create PROPPATCH method.
51 *
52 * @param path path
53 */
54 public ExchangeDavMethod(String path) {
55 super(path);
56 setRequestEntity(new RequestEntity() {
57 byte[] content;
58
59 public boolean isRepeatable() {
60 return true;
61 }
62
63 public void writeRequest(OutputStream outputStream) throws IOException {
64 if (content == null) {
65 content = generateRequestContent();
66 }
67 outputStream.write(content);
68 }
69
70 public long getContentLength() {
71 if (content == null) {
72 content = generateRequestContent();
73 }
74 return content.length;
75 }
76
77 public String getContentType() {
78 return "text/xml;charset=UTF-8";
79 }
80 });
81 }
82
83 /**
84 * Generate request content from property values.
85 *
86 * @return request content as byte array
87 */
88 protected abstract byte[] generateRequestContent();
89
90 @Override
91 protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
92 Header contentTypeHeader = getResponseHeader("Content-Type");
93 if (contentTypeHeader != null && "text/xml".equals(contentTypeHeader.getValue())) {
94 responses = new ArrayList<>();
95 XMLStreamReader reader;
96 try {
97 reader = XMLStreamUtil.createXMLStreamReader(new FilterInputStream(getResponseBodyAsStream()) {
98 final byte[] lastbytes = new byte[3];
99
100 @Override
101 public int read(byte[] bytes, int off, int len) throws IOException {
102 int count = in.read(bytes, off, len);
103 // patch invalid element name
104 for (int i = 0; i < count; i++) {
105 byte currentByte = bytes[off + i];
106 if ((lastbytes[0] == '<') && (currentByte >= '0' && currentByte <= '9')) {
107 // move invalid first tag char to valid range
108 bytes[off + i] = (byte) (currentByte + 49);
109 }
110 lastbytes[0] = lastbytes[1];
111 lastbytes[1] = lastbytes[2];
112 lastbytes[2] = currentByte;
113 }
114 return count;
115 }
116
117 });
118 while (reader.hasNext()) {
119 reader.next();
120 if (XMLStreamUtil.isStartTag(reader, "response")) {
121 handleResponse(reader);
122 }
123 }
124
125 } catch (IOException | XMLStreamException e) {
126 LOGGER.error("Error while parsing soap response: " + e, e);
127 }
128 }
129 }
130
131 protected void handleResponse(XMLStreamReader reader) throws XMLStreamException {
132 MultiStatusResponse multiStatusResponse = null;
133 String href = null;
134 String responseStatus = "";
135 while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "response")) {
136 reader.next();
137 if (XMLStreamUtil.isStartTag(reader)) {
138 String tagLocalName = reader.getLocalName();
139 if ("href".equals(tagLocalName)) {
140 href = reader.getElementText();
141 } else if ("status".equals(tagLocalName)) {
142 responseStatus = reader.getElementText();
143 } else if ("propstat".equals(tagLocalName)) {
144 if (multiStatusResponse == null) {
145 multiStatusResponse = new MultiStatusResponse(href, responseStatus);
146 }
147 handlePropstat(reader, multiStatusResponse);
148 }
149 }
150 }
151 if (multiStatusResponse != null) {
152 responses.add(multiStatusResponse);
153 }
154 }
155
156 protected void handlePropstat(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
157 int propstatStatus = 0;
158 while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "propstat")) {
159 reader.next();
160 if (XMLStreamUtil.isStartTag(reader)) {
161 String tagLocalName = reader.getLocalName();
162 if ("status".equals(tagLocalName)) {
163 if ("HTTP/1.1 200 OK".equals(reader.getElementText())) {
164 propstatStatus = HttpStatus.SC_OK;
165 } else {
166 propstatStatus = 0;
167 }
168 } else if ("prop".equals(tagLocalName) && propstatStatus == HttpStatus.SC_OK) {
169 handleProperty(reader, multiStatusResponse);
170 }
171 }
172 }
173
174 }
175
176 protected void handleProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
177 while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "prop")) {
178 reader.next();
179 if (XMLStreamUtil.isStartTag(reader)) {
180 Namespace namespace = Namespace.getNamespace(reader.getNamespaceURI());
181 String tagLocalName = reader.getLocalName();
182 if (reader.getAttributeCount() > 0 && "mv.string".equals(reader.getAttributeValue(0))) {
183 handleMultiValuedProperty(reader, multiStatusResponse);
184 } else {
185 String tagContent = getTagContent(reader);
186 if (tagContent != null) {
187 multiStatusResponse.add(new DefaultDavProperty<>(tagLocalName, tagContent, namespace));
188 }
189 }
190 }
191 }
192 }
193
194 protected void handleMultiValuedProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
195 String tagLocalName = reader.getLocalName();
196 Namespace namespace = Namespace.getNamespace(reader.getNamespaceURI());
197 ArrayList<String> values = new ArrayList<>();
198 while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, tagLocalName)) {
199 reader.next();
200 if (XMLStreamUtil.isStartTag(reader)) {
201 String tagContent = getTagContent(reader);
202 if (tagContent != null) {
203 values.add(tagContent);
204 }
205 }
206 }
207 multiStatusResponse.add(new DefaultDavProperty<>(tagLocalName, values, namespace));
208 }
209
210 protected String getTagContent(XMLStreamReader reader) throws XMLStreamException {
211 String value = null;
212 String tagLocalName = reader.getLocalName();
213 while (reader.hasNext() &&
214 !((reader.getEventType() == XMLStreamConstants.END_ELEMENT) && tagLocalName.equals(reader.getLocalName()))) {
215 reader.next();
216 if (reader.getEventType() == XMLStreamConstants.CHARACTERS) {
217 value = reader.getText();
218 }
219 }
220 // empty tag
221 if (!reader.hasNext()) {
222 throw new XMLStreamException("End element for " + tagLocalName + " not found");
223 }
224 return value;
225 }
226
227 /**
228 * Get Multistatus responses.
229 *
230 * @return responses
231 * @throws HttpException on error
232 */
233 public MultiStatusResponse[] getResponses() throws HttpException {
234 if (responses == null) {
235 throw new HttpException(getStatusLine().toString());
236 }
237 return responses.toArray(new MultiStatusResponse[0]);
238 }
239
240 /**
241 * Get single Multistatus response.
242 *
243 * @return response
244 * @throws HttpException on error
245 */
246 public MultiStatusResponse getResponse() throws HttpException {
247 if (responses == null || responses.size() != 1) {
248 throw new HttpException(getStatusLine().toString());
249 }
250 return responses.get(0);
251 }
252
253 /**
254 * Return method http status code.
255 *
256 * @return http status code
257 * @throws HttpException on error
258 */
259 public int getResponseStatusCode() throws HttpException {
260 String responseDescription = getResponse().getResponseDescription();
261 if ("HTTP/1.1 201 Created".equals(responseDescription)) {
262 return HttpStatus.SC_CREATED;
263 } else {
264 return HttpStatus.SC_OK;
265 }
266 }
267 }
+0
-115
src/java/davmail/exchange/dav/ExchangePropFindMethod.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2011 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.exchange.dav;
19
20 import org.apache.jackrabbit.webdav.header.DepthHeader;
21 import org.apache.jackrabbit.webdav.property.DavPropertyName;
22 import org.apache.jackrabbit.webdav.property.DavPropertyNameIterator;
23 import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
24 import org.apache.log4j.Logger;
25
26 import java.io.ByteArrayOutputStream;
27 import java.io.IOException;
28 import java.io.OutputStreamWriter;
29 import java.nio.charset.StandardCharsets;
30 import java.util.HashMap;
31 import java.util.Map;
32
33 /**
34 * Custom Exchange PROPFIND method.
35 * Does not load full DOM in memory.
36 */
37 public class ExchangePropFindMethod extends ExchangeDavMethod {
38 protected static final Logger LOGGER = Logger.getLogger(ExchangePropFindMethod.class);
39
40 protected final DavPropertyNameSet propertyNameSet;
41
42 public ExchangePropFindMethod(String uri) {
43 this(uri, null, DepthHeader.DEPTH_INFINITY);
44 }
45
46 public ExchangePropFindMethod(String uri, DavPropertyNameSet propertyNameSet, int depth) {
47 super(uri);
48 this.propertyNameSet = propertyNameSet;
49 DepthHeader dh = new DepthHeader(depth);
50 setRequestHeader(dh.getHeaderName(), dh.getHeaderValue());
51 }
52
53 protected byte[] generateRequestContent() {
54 try {
55 // build namespace map
56 int currentChar = 'e';
57 final Map<String, Integer> nameSpaceMap = new HashMap<>();
58 nameSpaceMap.put("DAV:", (int) 'D');
59 if (propertyNameSet != null) {
60 DavPropertyNameIterator propertyNameIterator = propertyNameSet.iterator();
61 while (propertyNameIterator.hasNext()) {
62 DavPropertyName davPropertyName = propertyNameIterator.nextPropertyName();
63
64 davPropertyName.getName();
65 // property namespace
66 String namespaceUri = davPropertyName.getNamespace().getURI();
67 if (!nameSpaceMap.containsKey(namespaceUri)) {
68 nameSpaceMap.put(namespaceUri, currentChar++);
69 }
70 }
71 }
72 // <D:propfind xmlns:D="DAV:"><D:prop><D:displayname/></D:prop></D:propfind>
73 ByteArrayOutputStream baos = new ByteArrayOutputStream();
74 OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
75 writer.write("<D:propfind ");
76 for (Map.Entry<String, Integer> mapEntry : nameSpaceMap.entrySet()) {
77 writer.write(" xmlns:");
78 writer.write((char) mapEntry.getValue().intValue());
79 writer.write("=\"");
80 writer.write(mapEntry.getKey());
81 writer.write("\"");
82 }
83 writer.write(">");
84 if (propertyNameSet == null || propertyNameSet.isEmpty()) {
85 writer.write("<D:allprop/>");
86 } else {
87 writer.write("<D:prop>");
88 DavPropertyNameIterator propertyNameIterator = propertyNameSet.iterator();
89 while (propertyNameIterator.hasNext()) {
90 DavPropertyName davPropertyName = propertyNameIterator.nextPropertyName();
91 char nameSpaceChar = (char) nameSpaceMap.get(davPropertyName.getNamespace().getURI()).intValue();
92 writer.write('<');
93 writer.write(nameSpaceChar);
94 writer.write(':');
95 writer.write(davPropertyName.getName());
96 writer.write("/>");
97 }
98 writer.write("</D:prop>");
99 }
100 writer.write("</D:propfind>");
101 writer.close();
102 return baos.toByteArray();
103 } catch (IOException e) {
104 throw new RuntimeException(e);
105 }
106
107 }
108
109 @Override
110 public String getName() {
111 return "PROPFIND";
112 }
113
114 }
+0
-138
src/java/davmail/exchange/dav/ExchangePropPatchMethod.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2010 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.exchange.dav;
19
20 import org.apache.log4j.Logger;
21
22 import java.io.ByteArrayOutputStream;
23 import java.io.IOException;
24 import java.io.OutputStreamWriter;
25 import java.nio.charset.StandardCharsets;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Map;
29 import java.util.Set;
30
31 /**
32 * Custom Exchange PROPPATCH method.
33 * Supports extended property update with type.
34 */
35 public class ExchangePropPatchMethod extends ExchangeDavMethod {
36 protected static final Logger LOGGER = Logger.getLogger(ExchangePropPatchMethod.class);
37 static final String TYPE_NAMESPACE = "urn:schemas-microsoft-com:datatypes";
38 final Set<PropertyValue> propertyValues;
39
40 /**
41 * Create PROPPATCH method.
42 *
43 * @param path path
44 * @param propertyValues property values
45 */
46 public ExchangePropPatchMethod(String path, Set<PropertyValue> propertyValues) {
47 super(path);
48 this.propertyValues = propertyValues;
49 }
50
51 @Override
52 protected byte[] generateRequestContent() {
53 try {
54 // build namespace map
55 int currentChar = 'e';
56 final Map<String, Integer> nameSpaceMap = new HashMap<>();
57 final Set<PropertyValue> setPropertyValues = new HashSet<>();
58 final Set<PropertyValue> deletePropertyValues = new HashSet<>();
59 for (PropertyValue propertyValue : propertyValues) {
60 // data type namespace
61 if (!nameSpaceMap.containsKey(TYPE_NAMESPACE) && propertyValue.getTypeString() != null) {
62 nameSpaceMap.put(TYPE_NAMESPACE, currentChar++);
63 }
64 // property namespace
65 String namespaceUri = propertyValue.getNamespaceUri();
66 if (!nameSpaceMap.containsKey(namespaceUri)) {
67 nameSpaceMap.put(namespaceUri, currentChar++);
68 }
69 if (propertyValue.getXmlEncodedValue() == null) {
70 deletePropertyValues.add(propertyValue);
71 } else {
72 setPropertyValues.add(propertyValue);
73 }
74 }
75 ByteArrayOutputStream baos = new ByteArrayOutputStream();
76 OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
77 writer.write("<D:propertyupdate xmlns:D=\"DAV:\"");
78 for (Map.Entry<String, Integer> mapEntry : nameSpaceMap.entrySet()) {
79 writer.write(" xmlns:");
80 writer.write((char) mapEntry.getValue().intValue());
81 writer.write("=\"");
82 writer.write(mapEntry.getKey());
83 writer.write("\"");
84 }
85 writer.write(">");
86 if (!setPropertyValues.isEmpty()) {
87 writer.write("<D:set><D:prop>");
88 for (PropertyValue propertyValue : setPropertyValues) {
89 String typeString = propertyValue.getTypeString();
90 char nameSpaceChar = (char) nameSpaceMap.get(propertyValue.getNamespaceUri()).intValue();
91 writer.write('<');
92 writer.write(nameSpaceChar);
93 writer.write(':');
94 writer.write(propertyValue.getName());
95 if (typeString != null) {
96 writer.write(' ');
97 writer.write(nameSpaceMap.get(TYPE_NAMESPACE));
98 writer.write(":dt=\"");
99 writer.write(typeString);
100 writer.write("\"");
101 }
102 writer.write('>');
103 writer.write(propertyValue.getXmlEncodedValue());
104 writer.write("</");
105 writer.write(nameSpaceChar);
106 writer.write(':');
107 writer.write(propertyValue.getName());
108 writer.write('>');
109 }
110 writer.write("</D:prop></D:set>");
111 }
112 if (!deletePropertyValues.isEmpty()) {
113 writer.write("<D:remove><D:prop>");
114 for (PropertyValue propertyValue : deletePropertyValues) {
115 char nameSpaceChar = (char) nameSpaceMap.get(propertyValue.getNamespaceUri()).intValue();
116 writer.write('<');
117 writer.write(nameSpaceChar);
118 writer.write(':');
119 writer.write(propertyValue.getName());
120 writer.write("/>");
121 }
122 writer.write("</D:prop></D:remove>");
123 }
124 writer.write("</D:propertyupdate>");
125 writer.close();
126 return baos.toByteArray();
127 } catch (IOException e) {
128 throw new RuntimeException(e);
129 }
130 }
131
132 @Override
133 public String getName() {
134 return "PROPPATCH";
135 }
136
137 }
+0
-72
src/java/davmail/exchange/dav/ExchangeSearchMethod.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2012 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.exchange.dav;
19
20 import davmail.util.StringUtil;
21 import org.apache.log4j.Logger;
22
23 import java.io.ByteArrayOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStreamWriter;
26 import java.nio.charset.StandardCharsets;
27
28 /**
29 * Custom Exchange PROPFIND method.
30 * Does not load full DOM in memory.
31 */
32 public class ExchangeSearchMethod extends ExchangeDavMethod {
33 protected static final Logger LOGGER = Logger.getLogger(ExchangeSearchMethod.class);
34
35 protected final String searchRequest;
36
37 /**
38 * Create search method.
39 *
40 * @param uri method uri
41 * @param searchRequest Exchange search request
42 */
43 public ExchangeSearchMethod(String uri, String searchRequest) {
44 super(uri);
45 this.searchRequest = searchRequest;
46 }
47
48 protected byte[] generateRequestContent() {
49 try {
50 ByteArrayOutputStream baos = new ByteArrayOutputStream();
51 try (OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8)) {
52 writer.write("<?xml version=\"1.0\"?>\n");
53 writer.write("<d:searchrequest xmlns:d=\"DAV:\">\n");
54 writer.write(" <d:sql>");
55 writer.write(StringUtil.xmlEncode(searchRequest));
56 writer.write("</d:sql>\n");
57 writer.write("</d:searchrequest>");
58 }
59 return baos.toByteArray();
60 } catch (IOException e) {
61 throw new RuntimeException(e);
62 }
63
64 }
65
66 @Override
67 public String getName() {
68 return "SEARCH";
69 }
70
71 }
568568 * @param alias field alias
569569 * @param value field value
570570 * @return property value object
571 * @see ExchangePropPatchMethod
571 * @see davmail.http.request.ExchangePropPatchRequest
572572 */
573573 public static PropertyValue createPropertyValue(String alias, String value) {
574574 Field field = Field.get(alias);
+0
-3233
src/java/davmail/exchange/dav/HC4DavExchangeSession.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2010 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.exchange.dav;
19
20 import davmail.BundleMessage;
21 import davmail.Settings;
22 import davmail.exception.DavMailAuthenticationException;
23 import davmail.exception.DavMailException;
24 import davmail.exception.HttpNotFoundException;
25 import davmail.exception.HttpPreconditionFailedException;
26 import davmail.exception.InsufficientStorageException;
27 import davmail.exception.LoginTimeoutException;
28 import davmail.exception.WebdavNotAvailableException;
29 import davmail.exchange.ExchangeSession;
30 import davmail.exchange.VCalendar;
31 import davmail.exchange.VObject;
32 import davmail.exchange.VProperty;
33 import davmail.exchange.XMLStreamUtil;
34 import davmail.http.HttpClientAdapter;
35 import davmail.http.URIUtil;
36 import davmail.http.request.ExchangePropPatchRequest;
37 import davmail.ui.tray.DavGatewayTray;
38 import davmail.util.IOUtil;
39 import davmail.util.StringUtil;
40 import org.apache.http.Consts;
41 import org.apache.http.HttpResponse;
42 import org.apache.http.HttpStatus;
43 import org.apache.http.NameValuePair;
44 import org.apache.http.client.HttpResponseException;
45 import org.apache.http.client.entity.UrlEncodedFormEntity;
46 import org.apache.http.client.methods.CloseableHttpResponse;
47 import org.apache.http.client.methods.HttpDelete;
48 import org.apache.http.client.methods.HttpGet;
49 import org.apache.http.client.methods.HttpHead;
50 import org.apache.http.client.methods.HttpPost;
51 import org.apache.http.client.methods.HttpPut;
52 import org.apache.http.client.protocol.HttpClientContext;
53 import org.apache.http.client.utils.URIUtils;
54 import org.apache.http.entity.ByteArrayEntity;
55 import org.apache.http.entity.ContentType;
56 import org.apache.http.impl.client.BasicCookieStore;
57 import org.apache.http.impl.client.BasicResponseHandler;
58 import org.apache.http.message.BasicNameValuePair;
59 import org.apache.jackrabbit.webdav.DavException;
60 import org.apache.jackrabbit.webdav.MultiStatus;
61 import org.apache.jackrabbit.webdav.MultiStatusResponse;
62 import org.apache.jackrabbit.webdav.client.methods.HttpCopy;
63 import org.apache.jackrabbit.webdav.client.methods.HttpMove;
64 import org.apache.jackrabbit.webdav.client.methods.HttpPropfind;
65 import org.apache.jackrabbit.webdav.client.methods.HttpProppatch;
66 import org.apache.jackrabbit.webdav.property.DavProperty;
67 import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
68 import org.apache.jackrabbit.webdav.property.DavPropertySet;
69 import org.apache.jackrabbit.webdav.property.PropEntry;
70 import org.w3c.dom.Node;
71
72 import javax.mail.MessagingException;
73 import javax.mail.Session;
74 import javax.mail.internet.InternetAddress;
75 import javax.mail.internet.MimeMessage;
76 import javax.mail.internet.MimeMultipart;
77 import javax.mail.internet.MimePart;
78 import javax.mail.util.SharedByteArrayInputStream;
79 import javax.xml.stream.XMLStreamException;
80 import javax.xml.stream.XMLStreamReader;
81 import java.io.BufferedReader;
82 import java.io.ByteArrayInputStream;
83 import java.io.ByteArrayOutputStream;
84 import java.io.FilterInputStream;
85 import java.io.IOException;
86 import java.io.InputStream;
87 import java.io.InputStreamReader;
88 import java.net.NoRouteToHostException;
89 import java.net.SocketException;
90 import java.net.URISyntaxException;
91 import java.net.URL;
92 import java.net.UnknownHostException;
93 import java.nio.charset.StandardCharsets;
94 import java.text.ParseException;
95 import java.text.SimpleDateFormat;
96 import java.util.*;
97 import java.util.zip.GZIPInputStream;
98
99 /**
100 * Webdav Exchange adapter.
101 * Compatible with Exchange 2003 and 2007 with webdav available.
102 */
103 @SuppressWarnings("rawtypes")
104 public class HC4DavExchangeSession extends ExchangeSession {
105 protected enum FolderQueryTraversal {
106 Shallow, Deep
107 }
108
109 protected static final DavPropertyNameSet WELL_KNOWN_FOLDERS = new DavPropertyNameSet();
110
111 static {
112 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("inbox"));
113 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("deleteditems"));
114 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("sentitems"));
115 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("sendmsg"));
116 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("drafts"));
117 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("calendar"));
118 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("tasks"));
119 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("contacts"));
120 WELL_KNOWN_FOLDERS.add(Field.getPropertyName("outbox"));
121 }
122
123 static final Map<String, String> vTodoToTaskStatusMap = new HashMap<>();
124 static final Map<String, String> taskTovTodoStatusMap = new HashMap<>();
125
126 static {
127 //taskTovTodoStatusMap.put("0", null);
128 taskTovTodoStatusMap.put("1", "IN-PROCESS");
129 taskTovTodoStatusMap.put("2", "COMPLETED");
130 taskTovTodoStatusMap.put("3", "NEEDS-ACTION");
131 taskTovTodoStatusMap.put("4", "CANCELLED");
132
133 //vTodoToTaskStatusMap.put(null, "0");
134 vTodoToTaskStatusMap.put("IN-PROCESS", "1");
135 vTodoToTaskStatusMap.put("COMPLETED", "2");
136 vTodoToTaskStatusMap.put("NEEDS-ACTION", "3");
137 vTodoToTaskStatusMap.put("CANCELLED", "4");
138 }
139
140 /**
141 * HttpClient 4 adapter to replace httpClient
142 */
143 private HttpClientAdapter httpClientAdapter;
144
145 /**
146 * Various standard mail boxes Urls
147 */
148 protected String inboxUrl;
149 protected String deleteditemsUrl;
150 protected String sentitemsUrl;
151 protected String sendmsgUrl;
152 protected String draftsUrl;
153 protected String calendarUrl;
154 protected String tasksUrl;
155 protected String contactsUrl;
156 protected String outboxUrl;
157
158 protected String inboxName;
159 protected String deleteditemsName;
160 protected String sentitemsName;
161 protected String sendmsgName;
162 protected String draftsName;
163 protected String calendarName;
164 protected String tasksName;
165 protected String contactsName;
166 protected String outboxName;
167
168 protected static final String USERS = "/users/";
169
170 /**
171 * HttpClient4 conversion.
172 * TODO: move up to ExchangeSession
173 */
174 protected void getEmailAndAliasFromOptions() {
175 // get user mail URL from html body
176 HttpGet optionsMethod = new HttpGet("/owa/?ae=Options&t=About");
177 try (
178 CloseableHttpResponse response = httpClientAdapter.execute(optionsMethod, cloneContext());
179 InputStream inputStream = response.getEntity().getContent();
180 BufferedReader optionsPageReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
181 ) {
182 String line;
183
184 // find email and alias
185 //noinspection StatementWithEmptyBody
186 while ((line = optionsPageReader.readLine()) != null
187 && (line.indexOf('[') == -1
188 || line.indexOf('@') == -1
189 || line.indexOf(']') == -1
190 || !line.toLowerCase().contains(MAILBOX_BASE))) {
191 }
192 if (line != null) {
193 int start = line.toLowerCase().lastIndexOf(MAILBOX_BASE) + MAILBOX_BASE.length();
194 int end = line.indexOf('<', start);
195 alias = line.substring(start, end);
196 end = line.lastIndexOf(']');
197 start = line.lastIndexOf('[', end) + 1;
198 email = line.substring(start, end);
199 }
200 } catch (IOException e) {
201 LOGGER.error("Error parsing options page at " + optionsMethod.getURI());
202 }
203 }
204
205 /**
206 * Create a separate Http context to protect session cookies.
207 *
208 * @return HttpClientContext instance with cookies
209 */
210 private HttpClientContext cloneContext() {
211 // Create a local context to avoid cookie reset on error
212 BasicCookieStore cookieStore = new BasicCookieStore();
213 cookieStore.addCookies(httpClientAdapter.getCookies().toArray(new org.apache.http.cookie.Cookie[0]));
214 HttpClientContext context = HttpClientContext.create();
215 context.setCookieStore(cookieStore);
216 return context;
217 }
218
219 @Override
220 public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
221 // experimental: try to reset session timeout
222 if ("Exchange2007".equals(serverVersion)) {
223 HttpGet getMethod = new HttpGet("/owa/");
224 try (CloseableHttpResponse response = httpClientAdapter.execute(getMethod)) {
225 LOGGER.debug(response.getStatusLine().getStatusCode() + " at /owa/");
226 } catch (IOException e) {
227 LOGGER.warn(e.getMessage());
228 }
229 }
230
231 return super.isExpired();
232 }
233
234
235 /**
236 * Convert logical or relative folder path to exchange folder path.
237 *
238 * @param folderPath folder name
239 * @return folder path
240 */
241 public String getFolderPath(String folderPath) {
242 String exchangeFolderPath;
243 // IMAP path
244 if (folderPath.startsWith(INBOX)) {
245 exchangeFolderPath = mailPath + inboxName + folderPath.substring(INBOX.length());
246 } else if (folderPath.startsWith(TRASH)) {
247 exchangeFolderPath = mailPath + deleteditemsName + folderPath.substring(TRASH.length());
248 } else if (folderPath.startsWith(DRAFTS)) {
249 exchangeFolderPath = mailPath + draftsName + folderPath.substring(DRAFTS.length());
250 } else if (folderPath.startsWith(SENT)) {
251 exchangeFolderPath = mailPath + sentitemsName + folderPath.substring(SENT.length());
252 } else if (folderPath.startsWith(SENDMSG)) {
253 exchangeFolderPath = mailPath + sendmsgName + folderPath.substring(SENDMSG.length());
254 } else if (folderPath.startsWith(CONTACTS)) {
255 exchangeFolderPath = mailPath + contactsName + folderPath.substring(CONTACTS.length());
256 } else if (folderPath.startsWith(CALENDAR)) {
257 exchangeFolderPath = mailPath + calendarName + folderPath.substring(CALENDAR.length());
258 } else if (folderPath.startsWith(TASKS)) {
259 exchangeFolderPath = mailPath + tasksName + folderPath.substring(TASKS.length());
260 } else if (folderPath.startsWith("public")) {
261 exchangeFolderPath = publicFolderUrl + folderPath.substring("public".length());
262
263 // caldav path
264 } else if (folderPath.startsWith(USERS)) {
265 // get requested principal
266 String principal;
267 String localPath;
268 int principalIndex = folderPath.indexOf('/', USERS.length());
269 if (principalIndex >= 0) {
270 principal = folderPath.substring(USERS.length(), principalIndex);
271 localPath = folderPath.substring(USERS.length() + principal.length() + 1);
272 if (localPath.startsWith(LOWER_CASE_INBOX) || localPath.startsWith(INBOX) || localPath.startsWith(MIXED_CASE_INBOX)) {
273 localPath = inboxName + localPath.substring(LOWER_CASE_INBOX.length());
274 } else if (localPath.startsWith(CALENDAR)) {
275 localPath = calendarName + localPath.substring(CALENDAR.length());
276 } else if (localPath.startsWith(TASKS)) {
277 localPath = tasksName + localPath.substring(TASKS.length());
278 } else if (localPath.startsWith(CONTACTS)) {
279 localPath = contactsName + localPath.substring(CONTACTS.length());
280 } else if (localPath.startsWith(ADDRESSBOOK)) {
281 localPath = contactsName + localPath.substring(ADDRESSBOOK.length());
282 }
283 } else {
284 principal = folderPath.substring(USERS.length());
285 localPath = "";
286 }
287 if (principal.length() == 0) {
288 exchangeFolderPath = rootPath;
289 } else if (alias.equalsIgnoreCase(principal) || (email != null && email.equalsIgnoreCase(principal))) {
290 exchangeFolderPath = mailPath + localPath;
291 } else {
292 LOGGER.debug("Detected shared path for principal " + principal + ", user principal is " + email);
293 exchangeFolderPath = rootPath + principal + '/' + localPath;
294 }
295
296 // absolute folder path
297 } else if (folderPath.startsWith("/")) {
298 exchangeFolderPath = folderPath;
299 } else {
300 exchangeFolderPath = mailPath + folderPath;
301 }
302 return exchangeFolderPath;
303 }
304
305 /**
306 * Test if folderPath is inside user mailbox.
307 *
308 * @param folderPath absolute folder path
309 * @return true if folderPath is a public or shared folder
310 */
311 @Override
312 public boolean isSharedFolder(String folderPath) {
313 return !getFolderPath(folderPath).toLowerCase().startsWith(mailPath.toLowerCase());
314 }
315
316 /**
317 * Test if folderPath is main calendar.
318 *
319 * @param folderPath absolute folder path
320 * @return true if folderPath is a public or shared folder
321 */
322 @Override
323 public boolean isMainCalendar(String folderPath) {
324 return getFolderPath(folderPath).equalsIgnoreCase(getFolderPath("calendar"));
325 }
326
327 /**
328 * Build base path for cmd commands (galfind, gallookup).
329 *
330 * @return cmd base path
331 */
332 public String getCmdBasePath() {
333 if (("Exchange2003".equals(serverVersion) || PUBLIC_ROOT.equals(publicFolderUrl)) && mailPath != null) {
334 // public folder is not available => try to use mailbox path
335 // Note: This does not work with freebusy, which requires /public/
336 return mailPath;
337 } else {
338 // use public folder url
339 return publicFolderUrl;
340 }
341 }
342
343 /**
344 * LDAP to Exchange Criteria Map
345 */
346 static final HashMap<String, String> GALFIND_CRITERIA_MAP = new HashMap<>();
347
348 static {
349 GALFIND_CRITERIA_MAP.put("imapUid", "AN");
350 GALFIND_CRITERIA_MAP.put("smtpemail1", "EM");
351 GALFIND_CRITERIA_MAP.put("cn", "DN");
352 GALFIND_CRITERIA_MAP.put("givenName", "FN");
353 GALFIND_CRITERIA_MAP.put("sn", "LN");
354 GALFIND_CRITERIA_MAP.put("title", "TL");
355 GALFIND_CRITERIA_MAP.put("o", "CP");
356 GALFIND_CRITERIA_MAP.put("l", "OF");
357 GALFIND_CRITERIA_MAP.put("department", "DP");
358 }
359
360 static final HashSet<String> GALLOOKUP_ATTRIBUTES = new HashSet<>();
361
362 static {
363 GALLOOKUP_ATTRIBUTES.add("givenName");
364 GALLOOKUP_ATTRIBUTES.add("initials");
365 GALLOOKUP_ATTRIBUTES.add("sn");
366 GALLOOKUP_ATTRIBUTES.add("street");
367 GALLOOKUP_ATTRIBUTES.add("st");
368 GALLOOKUP_ATTRIBUTES.add("postalcode");
369 GALLOOKUP_ATTRIBUTES.add("co");
370 GALLOOKUP_ATTRIBUTES.add("departement");
371 GALLOOKUP_ATTRIBUTES.add("mobile");
372 }
373
374 /**
375 * Exchange to LDAP attribute map
376 */
377 static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<>();
378
379 static {
380 GALFIND_ATTRIBUTE_MAP.put("uid", "AN");
381 GALFIND_ATTRIBUTE_MAP.put("smtpemail1", "EM");
382 GALFIND_ATTRIBUTE_MAP.put("cn", "DN");
383 GALFIND_ATTRIBUTE_MAP.put("displayName", "DN");
384 GALFIND_ATTRIBUTE_MAP.put("telephoneNumber", "PH");
385 GALFIND_ATTRIBUTE_MAP.put("l", "OFFICE");
386 GALFIND_ATTRIBUTE_MAP.put("o", "CP");
387 GALFIND_ATTRIBUTE_MAP.put("title", "TL");
388
389 GALFIND_ATTRIBUTE_MAP.put("givenName", "first");
390 GALFIND_ATTRIBUTE_MAP.put("initials", "initials");
391 GALFIND_ATTRIBUTE_MAP.put("sn", "last");
392 GALFIND_ATTRIBUTE_MAP.put("street", "street");
393 GALFIND_ATTRIBUTE_MAP.put("st", "state");
394 GALFIND_ATTRIBUTE_MAP.put("postalcode", "zip");
395 GALFIND_ATTRIBUTE_MAP.put("co", "country");
396 GALFIND_ATTRIBUTE_MAP.put("department", "department");
397 GALFIND_ATTRIBUTE_MAP.put("mobile", "mobile");
398 GALFIND_ATTRIBUTE_MAP.put("roomnumber", "office");
399 }
400
401 boolean disableGalFind;
402
403 protected Map<String, Map<String, String>> galFind(String query) throws IOException {
404 Map<String, Map<String, String>> results;
405 String path = getCmdBasePath() + "?Cmd=galfind" + query;
406 HttpGet httpGet = new HttpGet(path);
407 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
408 results = XMLStreamUtil.getElementContentsAsMap(response.getEntity().getContent(), "item", "AN");
409 if (LOGGER.isDebugEnabled()) {
410 LOGGER.debug(path + ": " + results.size() + " result(s)");
411 }
412 } catch (IOException e) {
413 LOGGER.debug("GET " + path + " failed: " + e + ' ' + e.getMessage());
414 disableGalFind = true;
415 throw e;
416 }
417 return results;
418 }
419
420
421 @Override
422 public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
423 Map<String, ExchangeSession.Contact> contacts = new HashMap<>();
424 //noinspection StatementWithEmptyBody
425 if (disableGalFind) {
426 // do nothing
427 } else if (condition instanceof MultiCondition) {
428 List<Condition> conditions = ((ExchangeSession.MultiCondition) condition).getConditions();
429 Operator operator = ((ExchangeSession.MultiCondition) condition).getOperator();
430 if (operator == Operator.Or) {
431 for (Condition innerCondition : conditions) {
432 contacts.putAll(galFind(innerCondition, returningAttributes, sizeLimit));
433 }
434 } else if (operator == Operator.And && !conditions.isEmpty()) {
435 Map<String, ExchangeSession.Contact> innerContacts = galFind(conditions.get(0), returningAttributes, sizeLimit);
436 for (ExchangeSession.Contact contact : innerContacts.values()) {
437 if (condition.isMatch(contact)) {
438 contacts.put(contact.getName().toLowerCase(), contact);
439 }
440 }
441 }
442 } else if (condition instanceof AttributeCondition) {
443 String searchAttributeName = ((ExchangeSession.AttributeCondition) condition).getAttributeName();
444 String searchAttribute = GALFIND_CRITERIA_MAP.get(searchAttributeName);
445 if (searchAttribute != null) {
446 String searchValue = ((ExchangeSession.AttributeCondition) condition).getValue();
447 StringBuilder query = new StringBuilder();
448 if ("EM".equals(searchAttribute)) {
449 // mail search, split
450 int atIndex = searchValue.indexOf('@');
451 // remove suffix
452 if (atIndex >= 0) {
453 searchValue = searchValue.substring(0, atIndex);
454 }
455 // split firstname.lastname
456 int dotIndex = searchValue.indexOf('.');
457 if (dotIndex >= 0) {
458 // assume mail starts with firstname
459 query.append("&FN=").append(URIUtil.encodeWithinQuery(searchValue.substring(0, dotIndex)));
460 query.append("&LN=").append(URIUtil.encodeWithinQuery(searchValue.substring(dotIndex + 1)));
461 } else {
462 query.append("&FN=").append(URIUtil.encodeWithinQuery(searchValue));
463 }
464 } else {
465 query.append('&').append(searchAttribute).append('=').append(URIUtil.encodeWithinQuery(searchValue));
466 }
467 Map<String, Map<String, String>> results = galFind(query.toString());
468 for (Map<String, String> result : results.values()) {
469 Contact contact = new Contact();
470 contact.setName(result.get("AN"));
471 contact.put("imapUid", result.get("AN"));
472 buildGalfindContact(contact, result);
473 if (needGalLookup(searchAttributeName, returningAttributes)) {
474 galLookup(contact);
475 // iCal fix to suit both iCal 3 and 4: move cn to sn, remove cn
476 } else if (returningAttributes.contains("apple-serviceslocator")) {
477 if (contact.get("cn") != null && returningAttributes.contains("sn")) {
478 contact.put("sn", contact.get("cn"));
479 contact.remove("cn");
480 }
481 }
482 if (condition.isMatch(contact)) {
483 contacts.put(contact.getName().toLowerCase(), contact);
484 }
485 }
486 }
487
488 }
489 return contacts;
490 }
491
492 protected boolean needGalLookup(String searchAttributeName, Set<String> returningAttributes) {
493 // return all attributes => call gallookup
494 if (returningAttributes == null || returningAttributes.isEmpty()) {
495 return true;
496 // iCal search, do not call gallookup
497 } else if (returningAttributes.contains("apple-serviceslocator")) {
498 return false;
499 // Lightning search, no need to gallookup
500 } else if ("sn".equals(searchAttributeName)) {
501 return returningAttributes.contains("sn");
502 // search attribute is gallookup attribute, need to fetch value for isMatch
503 } else if (GALLOOKUP_ATTRIBUTES.contains(searchAttributeName)) {
504 return true;
505 }
506
507 for (String attributeName : GALLOOKUP_ATTRIBUTES) {
508 if (returningAttributes.contains(attributeName)) {
509 return true;
510 }
511 }
512 return false;
513 }
514
515 private boolean disableGalLookup;
516
517 /**
518 * Get extended address book information for person with gallookup.
519 * Does not work with Exchange 2007
520 *
521 * @param contact galfind contact
522 */
523 public void galLookup(Contact contact) {
524 if (!disableGalLookup) {
525 LOGGER.debug("galLookup(" + contact.get("smtpemail1") + ')');
526 HttpGet httpGet = new HttpGet(URIUtil.encodePathQuery(getCmdBasePath() + "?Cmd=gallookup&ADDR=" + contact.get("smtpemail1")));
527 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
528 Map<String, Map<String, String>> results = XMLStreamUtil.getElementContentsAsMap(response.getEntity().getContent(), "person", "alias");
529 // add detailed information
530 if (!results.isEmpty()) {
531 Map<String, String> personGalLookupDetails = results.get(contact.get("uid").toLowerCase());
532 if (personGalLookupDetails != null) {
533 buildGalfindContact(contact, personGalLookupDetails);
534 }
535 }
536 } catch (IOException e) {
537 LOGGER.warn("Unable to gallookup person: " + contact + ", disable GalLookup");
538 disableGalLookup = true;
539 }
540 }
541 }
542
543 protected void buildGalfindContact(Contact contact, Map<String, String> response) {
544 for (Map.Entry<String, String> entry : GALFIND_ATTRIBUTE_MAP.entrySet()) {
545 String attributeValue = response.get(entry.getValue());
546 if (attributeValue != null) {
547 contact.put(entry.getKey(), attributeValue);
548 }
549 }
550 }
551
552 @Override
553 protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException {
554 String freebusyUrl = publicFolderUrl + "/?cmd=freebusy" +
555 "&start=" + start +
556 "&end=" + end +
557 "&interval=" + interval +
558 "&u=SMTP:" + attendee;
559 HttpGet httpGet = new HttpGet(freebusyUrl);
560 httpGet.setHeader("Content-Type", "text/xml");
561 String fbdata;
562 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
563 fbdata = StringUtil.getLastToken(new BasicResponseHandler().handleResponse(response), "<a:fbdata>", "</a:fbdata>");
564 }
565 return fbdata;
566 }
567
568 public HC4DavExchangeSession(HttpClientAdapter httpClientAdapter, java.net.URI uri, String userName) throws IOException {
569 this.httpClientAdapter = httpClientAdapter;
570 this.userName = userName;
571 buildSessionInfo(uri);
572 }
573
574
575 @Override
576 public void buildSessionInfo(java.net.URI uri) throws DavMailException {
577 buildMailPath(uri);
578
579 // get base http mailbox http urls
580 getWellKnownFolders();
581 }
582
583 static final String BASE_HREF = "<base href=\"";
584
585 /**
586 * Exchange 2003: get mailPath from welcome page
587 *
588 * @param uri current uri
589 * @return mail path from body
590 */
591 protected String getMailpathFromWelcomePage(java.net.URI uri) {
592 String welcomePageMailPath = null;
593 // get user mail URL from html body (multi frame)
594 HttpGet method = new HttpGet(uri.toString());
595
596 try (
597 CloseableHttpResponse response = httpClientAdapter.execute(method);
598 InputStream inputStream = response.getEntity().getContent();
599 BufferedReader mainPageReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
600 ) {
601 String line;
602 //noinspection StatementWithEmptyBody
603 while ((line = mainPageReader.readLine()) != null && !line.toLowerCase().contains(BASE_HREF)) {
604 }
605 if (line != null) {
606 // Exchange 2003
607 int start = line.toLowerCase().indexOf(BASE_HREF) + BASE_HREF.length();
608 int end = line.indexOf('\"', start);
609 String mailBoxBaseHref = line.substring(start, end);
610 URL baseURL = new URL(mailBoxBaseHref);
611 welcomePageMailPath = URIUtil.decode(baseURL.getPath());
612 LOGGER.debug("Base href found in body, mailPath is " + welcomePageMailPath);
613 }
614 } catch (IOException e) {
615 LOGGER.error("Error parsing main page at " + method.getURI(), e);
616 }
617 return welcomePageMailPath;
618 }
619
620 protected void buildMailPath(java.net.URI uri) throws DavMailAuthenticationException {
621 // get mailPath from welcome page on Exchange 2003
622 mailPath = getMailpathFromWelcomePage(uri);
623
624 //noinspection VariableNotUsedInsideIf
625 if (mailPath != null) {
626 // Exchange 2003
627 serverVersion = "Exchange2003";
628 fixClientHost(uri);
629 checkPublicFolder();
630 buildEmail(uri.getHost());
631 } else {
632 // Exchange 2007 : get alias and email from options page
633 serverVersion = "Exchange2007";
634
635 // Gallookup is an Exchange 2003 only feature
636 disableGalLookup = true;
637 fixClientHost(uri);
638 getEmailAndAliasFromOptions();
639
640 checkPublicFolder();
641
642 // failover: try to get email through Webdav and Galfind
643 if (alias == null || email == null) {
644 buildEmail(uri.getHost());
645 }
646
647 // build standard mailbox link with email
648 mailPath = "/exchange/" + email + '/';
649 }
650
651 if (mailPath == null || email == null) {
652 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_PASSWORD_EXPIRED");
653 }
654 LOGGER.debug("Current user email is " + email + ", alias is " + alias + ", mailPath is " + mailPath + " on " + serverVersion);
655 rootPath = mailPath.substring(0, mailPath.lastIndexOf('/', mailPath.length() - 2) + 1);
656 }
657
658 /**
659 * Determine user email through various means.
660 *
661 * @param hostName Exchange server host name for last failover
662 */
663 public void buildEmail(String hostName) {
664 String mailBoxPath = getMailboxPath();
665 // mailPath contains either alias or email
666 if (mailBoxPath != null && mailBoxPath.indexOf('@') >= 0) {
667 email = mailBoxPath;
668 alias = getAliasFromMailboxDisplayName();
669 if (alias == null) {
670 alias = getAliasFromLogin();
671 }
672 } else {
673 // use mailbox name as alias
674 alias = mailBoxPath;
675 email = getEmail(alias);
676 if (email == null) {
677 // failover: try to get email from login name
678 alias = getAliasFromLogin();
679 email = getEmail(alias);
680 }
681 // another failover : get alias from mailPath display name
682 if (email == null) {
683 alias = getAliasFromMailboxDisplayName();
684 email = getEmail(alias);
685 }
686 if (email == null) {
687 LOGGER.debug("Unable to get user email with alias " + mailBoxPath
688 + " or " + getAliasFromLogin()
689 + " or " + alias
690 );
691 // last failover: build email from domain name and mailbox display name
692 StringBuilder buffer = new StringBuilder();
693 // most reliable alias
694 if (mailBoxPath != null) {
695 alias = mailBoxPath;
696 } else {
697 alias = getAliasFromLogin();
698 }
699 if (alias == null) {
700 alias = "unknown";
701 }
702 buffer.append(alias);
703 if (alias.indexOf('@') < 0) {
704 buffer.append('@');
705 if (hostName == null) {
706 hostName = "mail.unknown.com";
707 }
708 int dotIndex = hostName.indexOf('.');
709 if (dotIndex >= 0) {
710 buffer.append(hostName.substring(dotIndex + 1));
711 }
712 }
713 email = buffer.toString();
714 }
715 }
716 }
717
718 /**
719 * Get user alias from mailbox display name over Webdav.
720 *
721 * @return user alias
722 */
723 public String getAliasFromMailboxDisplayName() {
724 if (mailPath == null) {
725 return null;
726 }
727 String displayName = null;
728 try {
729 Folder rootFolder = getFolder("");
730 if (rootFolder == null) {
731 LOGGER.warn(new BundleMessage("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath));
732 } else {
733 displayName = rootFolder.displayName;
734 }
735 } catch (IOException e) {
736 LOGGER.warn(new BundleMessage("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath));
737 }
738 return displayName;
739 }
740
741 /**
742 * Get current Exchange alias name from mailbox name
743 *
744 * @return user name
745 */
746 protected String getMailboxPath() {
747 if (mailPath == null) {
748 return null;
749 }
750 int index = mailPath.lastIndexOf('/', mailPath.length() - 2);
751 if (index >= 0 && mailPath.endsWith("/")) {
752 return mailPath.substring(index + 1, mailPath.length() - 1);
753 } else {
754 LOGGER.warn(new BundleMessage("EXCEPTION_INVALID_MAIL_PATH", mailPath));
755 return null;
756 }
757 }
758
759 /**
760 * Get user email from global address list (galfind).
761 *
762 * @param alias user alias
763 * @return user email
764 */
765 public String getEmail(String alias) {
766 String emailResult = null;
767 if (alias != null && !disableGalFind) {
768 try {
769 Map<String, Map<String, String>> results = galFind("&AN=" + URIUtil.encodeWithinQuery(alias));
770 Map<String, String> result = results.get(alias.toLowerCase());
771 if (result != null) {
772 emailResult = result.get("EM");
773 }
774 } catch (IOException e) {
775 // galfind not available
776 disableGalFind = true;
777 LOGGER.debug("getEmail(" + alias + ") failed");
778 }
779 }
780 return emailResult;
781 }
782
783 protected String getURIPropertyIfExists(DavPropertySet properties, String alias) throws IOException {
784 DavProperty property = properties.get(Field.getPropertyName(alias));
785 if (property == null) {
786 return null;
787 } else {
788 return URIUtil.decode((String) property.getValue());
789 }
790 }
791
792 // return last folder name from url
793
794 protected String getFolderName(String url) {
795 if (url != null) {
796 if (url.endsWith("/")) {
797 return url.substring(url.lastIndexOf('/', url.length() - 2) + 1, url.length() - 1);
798 } else if (url.indexOf('/') > 0) {
799 return url.substring(url.lastIndexOf('/') + 1);
800 } else {
801 return null;
802 }
803 } else {
804 return null;
805 }
806 }
807
808 protected void fixClientHost(java.net.URI currentUri) {
809 // update client host, workaround for Exchange 2003 mailbox with an Exchange 2007 frontend
810 if (currentUri != null && currentUri.getHost() != null && currentUri.getScheme() != null) {
811 httpClientAdapter.setUri(currentUri);
812 }
813 }
814
815 protected void checkPublicFolder() {
816 // check public folder access
817 try {
818 publicFolderUrl = URIUtils.resolve(httpClientAdapter.getUri(), PUBLIC_ROOT).toString();
819 DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
820 davPropertyNameSet.add(Field.getPropertyName("displayname"));
821
822 HttpPropfind httpPropfind = new HttpPropfind(publicFolderUrl, davPropertyNameSet, 0);
823 httpClientAdapter.executeDavRequest(httpPropfind);
824 // update public folder URI
825 publicFolderUrl = httpPropfind.getURI().toString();
826
827 } catch (IOException e) {
828 LOGGER.warn("Public folders not available: " + (e.getMessage() == null ? e : e.getMessage()));
829 // default public folder path
830 publicFolderUrl = PUBLIC_ROOT;
831 }
832 }
833
834
835 protected void getWellKnownFolders() throws DavMailException {
836 // Retrieve well known URLs
837 try {
838 HttpPropfind httpPropfind = new HttpPropfind(mailPath, WELL_KNOWN_FOLDERS, 0);
839 MultiStatus multiStatus;
840 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) {
841 multiStatus = httpPropfind.getResponseBodyAsMultiStatus(response);
842 }
843 MultiStatusResponse[] responses = multiStatus.getResponses();
844 if (responses.length == 0) {
845 throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath);
846 }
847 DavPropertySet properties = responses[0].getProperties(org.apache.http.HttpStatus.SC_OK);
848 inboxUrl = getURIPropertyIfExists(properties, "inbox");
849 inboxName = getFolderName(inboxUrl);
850 deleteditemsUrl = getURIPropertyIfExists(properties, "deleteditems");
851 deleteditemsName = getFolderName(deleteditemsUrl);
852 sentitemsUrl = getURIPropertyIfExists(properties, "sentitems");
853 sentitemsName = getFolderName(sentitemsUrl);
854 sendmsgUrl = getURIPropertyIfExists(properties, "sendmsg");
855 sendmsgName = getFolderName(sendmsgUrl);
856 draftsUrl = getURIPropertyIfExists(properties, "drafts");
857 draftsName = getFolderName(draftsUrl);
858 calendarUrl = getURIPropertyIfExists(properties, "calendar");
859 calendarName = getFolderName(calendarUrl);
860 tasksUrl = getURIPropertyIfExists(properties, "tasks");
861 tasksName = getFolderName(tasksUrl);
862 contactsUrl = getURIPropertyIfExists(properties, "contacts");
863 contactsName = getFolderName(contactsUrl);
864 outboxUrl = getURIPropertyIfExists(properties, "outbox");
865 outboxName = getFolderName(outboxUrl);
866 // junk folder not available over webdav
867
868 LOGGER.debug("Inbox URL: " + inboxUrl +
869 " Trash URL: " + deleteditemsUrl +
870 " Sent URL: " + sentitemsUrl +
871 " Send URL: " + sendmsgUrl +
872 " Drafts URL: " + draftsUrl +
873 " Calendar URL: " + calendarUrl +
874 " Tasks URL: " + tasksUrl +
875 " Contacts URL: " + contactsUrl +
876 " Outbox URL: " + outboxUrl +
877 " Public folder URL: " + publicFolderUrl
878 );
879 } catch (IOException | DavException e) {
880 LOGGER.error(e.getMessage());
881 throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath);
882 }
883 }
884
885 protected static class MultiCondition extends ExchangeSession.MultiCondition {
886 protected MultiCondition(Operator operator, Condition... condition) {
887 super(operator, condition);
888 }
889
890 public void appendTo(StringBuilder buffer) {
891 boolean first = true;
892
893 for (Condition condition : conditions) {
894 if (condition != null && !condition.isEmpty()) {
895 if (first) {
896 buffer.append('(');
897 first = false;
898 } else {
899 buffer.append(' ').append(operator).append(' ');
900 }
901 condition.appendTo(buffer);
902 }
903 }
904 // at least one non empty condition
905 if (!first) {
906 buffer.append(')');
907 }
908 }
909 }
910
911 protected static class NotCondition extends ExchangeSession.NotCondition {
912 protected NotCondition(Condition condition) {
913 super(condition);
914 }
915
916 public void appendTo(StringBuilder buffer) {
917 buffer.append("(Not ");
918 condition.appendTo(buffer);
919 buffer.append(')');
920 }
921 }
922
923 static final Map<Operator, String> OPERATOR_MAP = new HashMap<>();
924
925 static {
926 OPERATOR_MAP.put(Operator.IsEqualTo, " = ");
927 OPERATOR_MAP.put(Operator.IsGreaterThanOrEqualTo, " >= ");
928 OPERATOR_MAP.put(Operator.IsGreaterThan, " > ");
929 OPERATOR_MAP.put(Operator.IsLessThanOrEqualTo, " <= ");
930 OPERATOR_MAP.put(Operator.IsLessThan, " < ");
931 OPERATOR_MAP.put(Operator.Like, " like ");
932 OPERATOR_MAP.put(Operator.IsNull, " is null");
933 OPERATOR_MAP.put(Operator.IsFalse, " = false");
934 OPERATOR_MAP.put(Operator.IsTrue, " = true");
935 OPERATOR_MAP.put(Operator.StartsWith, " = ");
936 OPERATOR_MAP.put(Operator.Contains, " = ");
937 }
938
939 protected static class AttributeCondition extends ExchangeSession.AttributeCondition {
940 protected boolean isIntValue;
941
942 protected AttributeCondition(String attributeName, Operator operator, String value) {
943 super(attributeName, operator, value);
944 }
945
946 protected AttributeCondition(String attributeName, Operator operator, int value) {
947 super(attributeName, operator, String.valueOf(value));
948 isIntValue = true;
949 }
950
951 public void appendTo(StringBuilder buffer) {
952 Field field = Field.get(attributeName);
953 buffer.append('"').append(field.getUri()).append('"');
954 buffer.append(OPERATOR_MAP.get(operator));
955 //noinspection VariableNotUsedInsideIf
956 if (field.cast != null) {
957 buffer.append("CAST (\"");
958 } else if (!isIntValue && !field.isIntValue()) {
959 buffer.append('\'');
960 }
961 if (Operator.Like == operator) {
962 buffer.append('%');
963 }
964 if ("urlcompname".equals(field.alias)) {
965 buffer.append(StringUtil.encodeUrlcompname(StringUtil.davSearchEncode(value)));
966 } else if (field.isIntValue()) {
967 // check value
968 try {
969 Integer.parseInt(value);
970 buffer.append(value);
971 } catch (NumberFormatException e) {
972 // invalid value, replace with 0
973 buffer.append('0');
974 }
975 } else {
976 buffer.append(StringUtil.davSearchEncode(value));
977 }
978 if (Operator.Like == operator || Operator.StartsWith == operator) {
979 buffer.append('%');
980 }
981 if (field.cast != null) {
982 buffer.append("\" as '").append(field.cast).append("')");
983 } else if (!isIntValue && !field.isIntValue()) {
984 buffer.append('\'');
985 }
986 }
987
988 public boolean isMatch(ExchangeSession.Contact contact) {
989 String lowerCaseValue = value.toLowerCase();
990 String actualValue = contact.get(attributeName);
991 Operator actualOperator = operator;
992 // patch for iCal or Lightning search without galLookup
993 if (actualValue == null && ("givenName".equals(attributeName) || "sn".equals(attributeName))) {
994 actualValue = contact.get("cn");
995 actualOperator = Operator.Like;
996 }
997 if (actualValue == null) {
998 return false;
999 }
1000 actualValue = actualValue.toLowerCase();
1001 return (actualOperator == Operator.IsEqualTo && actualValue.equals(lowerCaseValue)) ||
1002 (actualOperator == Operator.Like && actualValue.contains(lowerCaseValue)) ||
1003 (actualOperator == Operator.StartsWith && actualValue.startsWith(lowerCaseValue));
1004 }
1005 }
1006
1007 protected static class HeaderCondition extends AttributeCondition {
1008
1009 protected HeaderCondition(String attributeName, Operator operator, String value) {
1010 super(attributeName, operator, value);
1011 }
1012
1013 @Override
1014 public void appendTo(StringBuilder buffer) {
1015 buffer.append('"').append(Field.getHeader(attributeName).getUri()).append('"');
1016 buffer.append(OPERATOR_MAP.get(operator));
1017 buffer.append('\'');
1018 if (Operator.Like == operator) {
1019 buffer.append('%');
1020 }
1021 buffer.append(value);
1022 if (Operator.Like == operator) {
1023 buffer.append('%');
1024 }
1025 buffer.append('\'');
1026 }
1027 }
1028
1029 protected static class MonoCondition extends ExchangeSession.MonoCondition {
1030 protected MonoCondition(String attributeName, Operator operator) {
1031 super(attributeName, operator);
1032 }
1033
1034 public void appendTo(StringBuilder buffer) {
1035 buffer.append('"').append(Field.get(attributeName).getUri()).append('"');
1036 buffer.append(OPERATOR_MAP.get(operator));
1037 }
1038 }
1039
1040 @Override
1041 public ExchangeSession.MultiCondition and(Condition... condition) {
1042 return new MultiCondition(Operator.And, condition);
1043 }
1044
1045 @Override
1046 public ExchangeSession.MultiCondition or(Condition... condition) {
1047 return new MultiCondition(Operator.Or, condition);
1048 }
1049
1050 @Override
1051 public Condition not(Condition condition) {
1052 if (condition == null) {
1053 return null;
1054 } else {
1055 return new NotCondition(condition);
1056 }
1057 }
1058
1059 @Override
1060 public Condition isEqualTo(String attributeName, String value) {
1061 return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
1062 }
1063
1064 @Override
1065 public Condition isEqualTo(String attributeName, int value) {
1066 return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
1067 }
1068
1069 @Override
1070 public Condition headerIsEqualTo(String headerName, String value) {
1071 return new HeaderCondition(headerName, Operator.IsEqualTo, value);
1072 }
1073
1074 @Override
1075 public Condition gte(String attributeName, String value) {
1076 return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value);
1077 }
1078
1079 @Override
1080 public Condition lte(String attributeName, String value) {
1081 return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value);
1082 }
1083
1084 @Override
1085 public Condition lt(String attributeName, String value) {
1086 return new AttributeCondition(attributeName, Operator.IsLessThan, value);
1087 }
1088
1089 @Override
1090 public Condition gt(String attributeName, String value) {
1091 return new AttributeCondition(attributeName, Operator.IsGreaterThan, value);
1092 }
1093
1094 @Override
1095 public Condition contains(String attributeName, String value) {
1096 return new AttributeCondition(attributeName, Operator.Like, value);
1097 }
1098
1099 @Override
1100 public Condition startsWith(String attributeName, String value) {
1101 return new AttributeCondition(attributeName, Operator.StartsWith, value);
1102 }
1103
1104 @Override
1105 public Condition isNull(String attributeName) {
1106 return new MonoCondition(attributeName, Operator.IsNull);
1107 }
1108
1109 @Override
1110 public Condition exists(String attributeName) {
1111 return not(new MonoCondition(attributeName, Operator.IsNull));
1112 }
1113
1114 @Override
1115 public Condition isTrue(String attributeName) {
1116 if ("Exchange2003".equals(this.serverVersion) && "deleted".equals(attributeName)) {
1117 return isEqualTo(attributeName, "1");
1118 } else {
1119 return new MonoCondition(attributeName, Operator.IsTrue);
1120 }
1121 }
1122
1123 @Override
1124 public Condition isFalse(String attributeName) {
1125 if ("Exchange2003".equals(this.serverVersion) && "deleted".equals(attributeName)) {
1126 return or(isEqualTo(attributeName, "0"), isNull(attributeName));
1127 } else {
1128 return new MonoCondition(attributeName, Operator.IsFalse);
1129 }
1130 }
1131
1132 /**
1133 * @inheritDoc
1134 */
1135 public class Message extends ExchangeSession.Message {
1136
1137 @Override
1138 public String getPermanentId() {
1139 return permanentUrl;
1140 }
1141
1142 @Override
1143 protected InputStream getMimeHeaders() {
1144 InputStream result = null;
1145 try {
1146 String messageHeaders = getItemProperty(permanentUrl, "messageheaders");
1147 if (messageHeaders != null) {
1148 final String MS_HEADER = "Microsoft Mail Internet Headers Version 2.0";
1149 if (messageHeaders.startsWith(MS_HEADER)) {
1150 messageHeaders = messageHeaders.substring(MS_HEADER.length());
1151 if (!messageHeaders.isEmpty() && messageHeaders.charAt(0) == '\r') {
1152 messageHeaders = messageHeaders.substring(1);
1153 }
1154 if (!messageHeaders.isEmpty() && messageHeaders.charAt(0) == '\n') {
1155 messageHeaders = messageHeaders.substring(1);
1156 }
1157 }
1158 // workaround for messages in Sent folder
1159 if (!messageHeaders.contains("From:")) {
1160 String from = getItemProperty(permanentUrl, "from");
1161 messageHeaders = "From: " + from + '\n' + messageHeaders;
1162 }
1163 result = new ByteArrayInputStream(messageHeaders.getBytes(StandardCharsets.UTF_8));
1164 }
1165 } catch (Exception e) {
1166 LOGGER.warn(e.getMessage());
1167 }
1168
1169 return result;
1170 }
1171 }
1172
1173
1174 /**
1175 * @inheritDoc
1176 */
1177 public class Contact extends ExchangeSession.Contact {
1178 /**
1179 * Build Contact instance from multistatusResponse info
1180 *
1181 * @param multiStatusResponse response
1182 * @throws IOException on error
1183 * @throws DavMailException on error
1184 */
1185 public Contact(MultiStatusResponse multiStatusResponse) throws IOException, DavMailException {
1186 setHref(URIUtil.decode(multiStatusResponse.getHref()));
1187 DavPropertySet properties = multiStatusResponse.getProperties(HttpStatus.SC_OK);
1188 permanentUrl = getURLPropertyIfExists(properties, "permanenturl");
1189 etag = getPropertyIfExists(properties, "etag");
1190 displayName = getPropertyIfExists(properties, "displayname");
1191 for (String attributeName : CONTACT_ATTRIBUTES) {
1192 String value = getPropertyIfExists(properties, attributeName);
1193 if (value != null) {
1194 if ("bday".equals(attributeName) || "anniversary".equals(attributeName)
1195 || "lastmodified".equals(attributeName) || "datereceived".equals(attributeName)) {
1196 value = convertDateFromExchange(value);
1197 } else if ("haspicture".equals(attributeName) || "private".equals(attributeName)) {
1198 value = "1".equals(value) ? "true" : "false";
1199 }
1200 put(attributeName, value);
1201 }
1202 }
1203 }
1204
1205 public Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
1206 super(folderPath, itemName, properties, etag, noneMatch);
1207 }
1208
1209 /**
1210 * Default constructor for galFind
1211 */
1212 public Contact() {
1213 }
1214
1215 protected Set<PropertyValue> buildProperties() {
1216 Set<PropertyValue> propertyValues = new HashSet<>();
1217 for (Map.Entry<String, String> entry : entrySet()) {
1218 String key = entry.getKey();
1219 if (!"photo".equals(key)) {
1220 propertyValues.add(Field.createPropertyValue(key, entry.getValue()));
1221 if (key.startsWith("email")) {
1222 propertyValues.add(Field.createPropertyValue(key + "type", "SMTP"));
1223 }
1224 }
1225 }
1226
1227 return propertyValues;
1228 }
1229
1230 protected ExchangePropPatchRequest internalCreateOrUpdate(String encodedHref) throws IOException {
1231 ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(encodedHref, buildProperties());
1232 propPatchRequest.setHeader("Translate", "f");
1233 if (etag != null) {
1234 propPatchRequest.setHeader("If-Match", etag);
1235 }
1236 if (noneMatch != null) {
1237 propPatchRequest.setHeader("If-None-Match", noneMatch);
1238 }
1239 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) {
1240 LOGGER.debug("internalCreateOrUpdate returned " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase());
1241 }
1242 return propPatchRequest;
1243 }
1244
1245 /**
1246 * Create or update contact
1247 *
1248 * @return action result
1249 * @throws IOException on error
1250 */
1251 @Override
1252 public ItemResult createOrUpdate() throws IOException {
1253 String encodedHref = URIUtil.encodePath(getHref());
1254 ExchangePropPatchRequest propPatchRequest = internalCreateOrUpdate(encodedHref);
1255 int status = propPatchRequest.getStatusLine().getStatusCode();
1256 if (status == HttpStatus.SC_MULTI_STATUS) {
1257 try {
1258 status = propPatchRequest.getResponseStatusCode();
1259 } catch (HttpResponseException e) {
1260 throw new IOException(e.getMessage(), e);
1261 }
1262 //noinspection VariableNotUsedInsideIf
1263 if (status == HttpStatus.SC_CREATED) {
1264 LOGGER.debug("Created contact " + encodedHref);
1265 } else {
1266 LOGGER.debug("Updated contact " + encodedHref);
1267 }
1268 } else if (status == HttpStatus.SC_NOT_FOUND) {
1269 LOGGER.debug("Contact not found at " + encodedHref + ", searching permanenturl by urlcompname");
1270 // failover, search item by urlcompname
1271 MultiStatusResponse[] responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, HC4DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1);
1272 if (responses.length == 1) {
1273 encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl");
1274 LOGGER.warn("Contact found, permanenturl is " + encodedHref);
1275 propPatchRequest = internalCreateOrUpdate(encodedHref);
1276 status = propPatchRequest.getStatusLine().getStatusCode();
1277 if (status == HttpStatus.SC_MULTI_STATUS) {
1278 try {
1279 status = propPatchRequest.getResponseStatusCode();
1280 } catch (HttpResponseException e) {
1281 throw new IOException(e.getMessage(), e);
1282 }
1283 LOGGER.debug("Updated contact " + encodedHref);
1284 } else {
1285 LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchRequest.getStatusLine());
1286 }
1287 }
1288
1289 } else {
1290 LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchRequest.getStatusLine().getReasonPhrase());
1291 }
1292 ItemResult itemResult = new ItemResult();
1293 // 440 means forbidden on Exchange
1294 if (status == 440) {
1295 status = HttpStatus.SC_FORBIDDEN;
1296 }
1297 itemResult.status = status;
1298
1299 if (status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) {
1300 String contactPictureUrl = URIUtil.encodePath(getHref() + "/ContactPicture.jpg");
1301 String photo = get("photo");
1302 if (photo != null) {
1303 try {
1304 final HttpPut httpPut = new HttpPut(contactPictureUrl);
1305 // need to update photo
1306 byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90);
1307
1308 httpPut.setHeader("Overwrite", "t");
1309 // TODO: required ?
1310 httpPut.setHeader("Content-Type", "image/jpeg");
1311 httpPut.setEntity(new ByteArrayEntity(resizedImageBytes, ContentType.IMAGE_JPEG));
1312
1313 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPut)) {
1314 status = response.getStatusLine().getStatusCode();
1315 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED) {
1316 throw new IOException("Unable to update contact picture: " + status + ' ' + response.getStatusLine().getReasonPhrase());
1317 }
1318 }
1319 } catch (IOException e) {
1320 LOGGER.error("Error in contact photo create or update", e);
1321 throw e;
1322 }
1323
1324 Set<PropertyValue> picturePropertyValues = new HashSet<>();
1325 picturePropertyValues.add(Field.createPropertyValue("attachmentContactPhoto", "true"));
1326 // picturePropertyValues.add(Field.createPropertyValue("renderingPosition", "-1"));
1327 picturePropertyValues.add(Field.createPropertyValue("attachExtension", ".jpg"));
1328
1329 final ExchangePropPatchRequest attachmentPropPatchRequest = new ExchangePropPatchRequest(contactPictureUrl, picturePropertyValues);
1330 try (CloseableHttpResponse response = httpClientAdapter.execute(attachmentPropPatchRequest)) {
1331 attachmentPropPatchRequest.handleResponse(response);
1332 status = response.getStatusLine().getStatusCode();
1333 if (status != HttpStatus.SC_MULTI_STATUS) {
1334 LOGGER.error("Error in contact photo create or update: " + response.getStatusLine().getStatusCode());
1335 throw new IOException("Unable to update contact picture");
1336 }
1337 }
1338
1339 } else {
1340 // try to delete picture
1341 HttpDelete httpDelete = new HttpDelete(contactPictureUrl);
1342 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
1343 status = response.getStatusLine().getStatusCode();
1344 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
1345 LOGGER.error("Error in contact photo delete: " + status);
1346 throw new IOException("Unable to delete contact picture");
1347 }
1348 }
1349 }
1350 // need to retrieve new etag
1351 HttpHead headMethod = new HttpHead(URIUtil.encodePath(getHref()));
1352 try (CloseableHttpResponse response = httpClientAdapter.execute(headMethod)) {
1353 if (response.getFirstHeader("ETag") != null) {
1354 itemResult.etag = response.getFirstHeader("ETag").getValue();
1355 }
1356 }
1357 }
1358 return itemResult;
1359
1360 }
1361
1362 }
1363
1364 /**
1365 * @inheritDoc
1366 */
1367 public class Event extends ExchangeSession.Event {
1368 protected String instancetype;
1369
1370 /**
1371 * Build Event instance from response info.
1372 *
1373 * @param multiStatusResponse response
1374 * @throws IOException on error
1375 */
1376 public Event(MultiStatusResponse multiStatusResponse) throws IOException {
1377 setHref(URIUtil.decode(multiStatusResponse.getHref()));
1378 DavPropertySet properties = multiStatusResponse.getProperties(HttpStatus.SC_OK);
1379 permanentUrl = getURLPropertyIfExists(properties, "permanenturl");
1380 etag = getPropertyIfExists(properties, "etag");
1381 displayName = getPropertyIfExists(properties, "displayname");
1382 subject = getPropertyIfExists(properties, "subject");
1383 instancetype = getPropertyIfExists(properties, "instancetype");
1384 contentClass = getPropertyIfExists(properties, "contentclass");
1385 }
1386
1387
1388 public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
1389 super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
1390 }
1391
1392 protected byte[] getICSFromInternetContentProperty() throws IOException, DavException, MessagingException {
1393 byte[] result = null;
1394 // PropFind PR_INTERNET_CONTENT
1395 String propertyValue = getItemProperty(permanentUrl, "internetContent");
1396 if (propertyValue != null) {
1397 result = getICS(new ByteArrayInputStream(IOUtil.decodeBase64(propertyValue)));
1398 }
1399 return result;
1400 }
1401
1402 /**
1403 * Load ICS content from Exchange server.
1404 * User Translate: f header to get MIME event content and get ICS attachment from it
1405 *
1406 * @return ICS (iCalendar) event
1407 * @throws IOException on error
1408 */
1409 @Override
1410 public byte[] getEventContent() throws IOException {
1411 byte[] result = null;
1412 LOGGER.debug("Get event subject: " + subject + " contentclass: " + contentClass + " href: " + getHref() + " permanentUrl: " + permanentUrl);
1413 // do not try to load tasks MIME body
1414 if (!"urn:content-classes:task".equals(contentClass)) {
1415 // try to get PR_INTERNET_CONTENT
1416 try {
1417 result = getICSFromInternetContentProperty();
1418 if (result == null) {
1419 HttpGet httpGet = new HttpGet(encodeAndFixUrl(permanentUrl));
1420 httpGet.setHeader("Content-Type", "text/xml; charset=utf-8");
1421 httpGet.setHeader("Translate", "f");
1422 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
1423 result = getICS(response.getEntity().getContent());
1424 }
1425 }
1426 } catch (DavException | IOException | MessagingException e) {
1427 LOGGER.warn(e.getMessage());
1428 }
1429 }
1430
1431 // failover: rebuild event from MAPI properties
1432 if (result == null) {
1433 try {
1434 result = getICSFromItemProperties();
1435 } catch (IOException e) {
1436 deleteBroken();
1437 throw e;
1438 }
1439 }
1440 // debug code
1441 /*if (new String(result).indexOf("VTODO") < 0) {
1442 LOGGER.debug("Original body: " + new String(result));
1443 result = getICSFromItemProperties();
1444 LOGGER.debug("Rebuilt body: " + new String(result));
1445 }*/
1446
1447 return result;
1448 }
1449
1450 private byte[] getICSFromItemProperties() throws HttpNotFoundException {
1451 byte[] result;
1452
1453 // experimental: build VCALENDAR from properties
1454
1455 try {
1456 //MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod);
1457 Set<String> eventProperties = new HashSet<>();
1458 eventProperties.add("method");
1459
1460 eventProperties.add("created");
1461 eventProperties.add("calendarlastmodified");
1462 eventProperties.add("dtstamp");
1463 eventProperties.add("calendaruid");
1464 eventProperties.add("subject");
1465 eventProperties.add("dtstart");
1466 eventProperties.add("dtend");
1467 eventProperties.add("transparent");
1468 eventProperties.add("organizer");
1469 eventProperties.add("to");
1470 eventProperties.add("description");
1471 eventProperties.add("rrule");
1472 eventProperties.add("exdate");
1473 eventProperties.add("sensitivity");
1474 eventProperties.add("alldayevent");
1475 eventProperties.add("busystatus");
1476 eventProperties.add("reminderset");
1477 eventProperties.add("reminderdelta");
1478 // task
1479 eventProperties.add("importance");
1480 eventProperties.add("uid");
1481 eventProperties.add("taskstatus");
1482 eventProperties.add("percentcomplete");
1483 eventProperties.add("keywords");
1484 eventProperties.add("startdate");
1485 eventProperties.add("duedate");
1486 eventProperties.add("datecompleted");
1487
1488 MultiStatusResponse[] responses = searchItems(folderPath, eventProperties, HC4DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1);
1489 if (responses.length == 0) {
1490 throw new HttpNotFoundException(permanentUrl + " not found");
1491 }
1492 DavPropertySet davPropertySet = responses[0].getProperties(HttpStatus.SC_OK);
1493 VCalendar localVCalendar = new VCalendar();
1494 localVCalendar.setPropertyValue("PRODID", "-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN");
1495 localVCalendar.setPropertyValue("VERSION", "2.0");
1496 localVCalendar.setPropertyValue("METHOD", getPropertyIfExists(davPropertySet, "method"));
1497 VObject vEvent = new VObject();
1498 vEvent.setPropertyValue("CREATED", convertDateFromExchange(getPropertyIfExists(davPropertySet, "created")));
1499 vEvent.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getPropertyIfExists(davPropertySet, "calendarlastmodified")));
1500 vEvent.setPropertyValue("DTSTAMP", convertDateFromExchange(getPropertyIfExists(davPropertySet, "dtstamp")));
1501
1502 String uid = getPropertyIfExists(davPropertySet, "calendaruid");
1503 if (uid == null) {
1504 uid = getPropertyIfExists(davPropertySet, "uid");
1505 }
1506 vEvent.setPropertyValue("UID", uid);
1507 vEvent.setPropertyValue("SUMMARY", getPropertyIfExists(davPropertySet, "subject"));
1508 vEvent.setPropertyValue("DESCRIPTION", getPropertyIfExists(davPropertySet, "description"));
1509 vEvent.setPropertyValue("PRIORITY", convertPriorityFromExchange(getPropertyIfExists(davPropertySet, "importance")));
1510 vEvent.setPropertyValue("CATEGORIES", getPropertyIfExists(davPropertySet, "keywords"));
1511 String sensitivity = getPropertyIfExists(davPropertySet, "sensitivity");
1512 if ("2".equals(sensitivity)) {
1513 vEvent.setPropertyValue("CLASS", "PRIVATE");
1514 } else if ("3".equals(sensitivity)) {
1515 vEvent.setPropertyValue("CLASS", "CONFIDENTIAL");
1516 } else if ("0".equals(sensitivity)) {
1517 vEvent.setPropertyValue("CLASS", "PUBLIC");
1518 }
1519
1520 if (instancetype == null) {
1521 vEvent.type = "VTODO";
1522 double percentComplete = getDoublePropertyIfExists(davPropertySet, "percentcomplete");
1523 if (percentComplete > 0) {
1524 vEvent.setPropertyValue("PERCENT-COMPLETE", String.valueOf((int) (percentComplete * 100)));
1525 }
1526 vEvent.setPropertyValue("STATUS", taskTovTodoStatusMap.get(getPropertyIfExists(davPropertySet, "taskstatus")));
1527 vEvent.setPropertyValue("DUE;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "duedate")));
1528 vEvent.setPropertyValue("DTSTART;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "startdate")));
1529 vEvent.setPropertyValue("COMPLETED;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "datecompleted")));
1530
1531 } else {
1532 vEvent.type = "VEVENT";
1533 // check mandatory dtstart value
1534 String dtstart = getPropertyIfExists(davPropertySet, "dtstart");
1535 if (dtstart != null) {
1536 vEvent.setPropertyValue("DTSTART", convertDateFromExchange(dtstart));
1537 } else {
1538 LOGGER.warn("missing dtstart on item, using fake value. Set davmail.deleteBroken=true to delete broken events");
1539 vEvent.setPropertyValue("DTSTART", "20000101T000000Z");
1540 deleteBroken();
1541 }
1542 // same on DTEND
1543 String dtend = getPropertyIfExists(davPropertySet, "dtend");
1544 if (dtend != null) {
1545 vEvent.setPropertyValue("DTEND", convertDateFromExchange(dtend));
1546 } else {
1547 LOGGER.warn("missing dtend on item, using fake value. Set davmail.deleteBroken=true to delete broken events");
1548 vEvent.setPropertyValue("DTEND", "20000101T010000Z");
1549 deleteBroken();
1550 }
1551 vEvent.setPropertyValue("TRANSP", getPropertyIfExists(davPropertySet, "transparent"));
1552 vEvent.setPropertyValue("RRULE", getPropertyIfExists(davPropertySet, "rrule"));
1553 String exdates = getPropertyIfExists(davPropertySet, "exdate");
1554 if (exdates != null) {
1555 String[] exdatearray = exdates.split(",");
1556 for (String exdate : exdatearray) {
1557 vEvent.addPropertyValue("EXDATE",
1558 StringUtil.convertZuluDateTimeToAllDay(convertDateFromExchange(exdate)));
1559 }
1560 }
1561 String organizer = getPropertyIfExists(davPropertySet, "organizer");
1562 String organizerEmail = null;
1563 if (organizer != null) {
1564 InternetAddress organizerAddress = new InternetAddress(organizer);
1565 organizerEmail = organizerAddress.getAddress();
1566 vEvent.setPropertyValue("ORGANIZER", "MAILTO:" + organizerEmail);
1567 }
1568
1569 // Parse attendee list
1570 String toHeader = getPropertyIfExists(davPropertySet, "to");
1571 if (toHeader != null && !toHeader.equals(organizerEmail)) {
1572 InternetAddress[] attendees = InternetAddress.parseHeader(toHeader, false);
1573 for (InternetAddress attendee : attendees) {
1574 if (!attendee.getAddress().equalsIgnoreCase(organizerEmail)) {
1575 VProperty vProperty = new VProperty("ATTENDEE", attendee.getAddress());
1576 if (attendee.getPersonal() != null) {
1577 vProperty.addParam("CN", attendee.getPersonal());
1578 }
1579 vEvent.addProperty(vProperty);
1580 }
1581 }
1582
1583 }
1584 vEvent.setPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT",
1585 "1".equals(getPropertyIfExists(davPropertySet, "alldayevent")) ? "TRUE" : "FALSE");
1586 vEvent.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS", getPropertyIfExists(davPropertySet, "busystatus"));
1587
1588 if ("1".equals(getPropertyIfExists(davPropertySet, "reminderset"))) {
1589 VObject vAlarm = new VObject();
1590 vAlarm.type = "VALARM";
1591 vAlarm.setPropertyValue("ACTION", "DISPLAY");
1592 vAlarm.setPropertyValue("DISPLAY", "Reminder");
1593 String reminderdelta = getPropertyIfExists(davPropertySet, "reminderdelta");
1594 VProperty vProperty = new VProperty("TRIGGER", "-PT" + reminderdelta + 'M');
1595 vProperty.addParam("VALUE", "DURATION");
1596 vAlarm.addProperty(vProperty);
1597 vEvent.addVObject(vAlarm);
1598 }
1599 }
1600
1601 localVCalendar.addVObject(vEvent);
1602 result = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
1603 } catch (MessagingException | IOException e) {
1604 LOGGER.warn("Unable to rebuild event content: " + e.getMessage(), e);
1605 throw new HttpNotFoundException("Unable to get event " + getName() + " subject: " + subject + " at " + permanentUrl + ": " + e.getMessage());
1606 }
1607
1608 return result;
1609 }
1610
1611 protected void deleteBroken() {
1612 // try to delete broken event
1613 if (Settings.getBooleanProperty("davmail.deleteBroken")) {
1614 LOGGER.warn("Deleting broken event at: " + permanentUrl);
1615 try {
1616 HttpDelete httpDelete = new HttpDelete(encodeAndFixUrl(permanentUrl));
1617 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
1618 LOGGER.warn("deleteBroken returned " + response.getStatusLine().getStatusCode());
1619 }
1620 } catch (IOException e) {
1621 LOGGER.warn("Unable to delete broken event at: " + permanentUrl);
1622 }
1623 }
1624 }
1625
1626 protected CloseableHttpResponse internalCreateOrUpdate(String encodedHref, byte[] mimeContent) throws IOException {
1627 HttpPut httpPut = new HttpPut(encodedHref);
1628 httpPut.setHeader("Translate", "f");
1629 httpPut.setHeader("Overwrite", "f");
1630 if (etag != null) {
1631 httpPut.setHeader("If-Match", etag);
1632 }
1633 if (noneMatch != null) {
1634 httpPut.setHeader("If-None-Match", noneMatch);
1635 }
1636 httpPut.setHeader("Content-Type", "message/rfc822");
1637 httpPut.setEntity(new ByteArrayEntity(mimeContent, ContentType.getByMimeType("message/rfc822")));
1638 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPut)) {
1639 return response;
1640 }
1641 }
1642
1643 /**
1644 * @inheritDoc
1645 */
1646 @Override
1647 public ItemResult createOrUpdate() throws IOException {
1648 ItemResult itemResult = new ItemResult();
1649 if (vCalendar.isTodo()) {
1650 if ((mailPath + calendarName).equals(folderPath)) {
1651 folderPath = mailPath + tasksName;
1652 }
1653 String encodedHref = URIUtil.encodePath(getHref());
1654 Set<PropertyValue> propertyValues = new HashSet<>();
1655 // set contentclass on create
1656 if (noneMatch != null) {
1657 propertyValues.add(Field.createPropertyValue("contentclass", "urn:content-classes:task"));
1658 propertyValues.add(Field.createPropertyValue("outlookmessageclass", "IPM.Task"));
1659 propertyValues.add(Field.createPropertyValue("calendaruid", vCalendar.getFirstVeventPropertyValue("UID")));
1660 }
1661 propertyValues.add(Field.createPropertyValue("subject", vCalendar.getFirstVeventPropertyValue("SUMMARY")));
1662 propertyValues.add(Field.createPropertyValue("description", vCalendar.getFirstVeventPropertyValue("DESCRIPTION")));
1663 propertyValues.add(Field.createPropertyValue("importance", convertPriorityToExchange(vCalendar.getFirstVeventPropertyValue("PRIORITY"))));
1664 String percentComplete = vCalendar.getFirstVeventPropertyValue("PERCENT-COMPLETE");
1665 if (percentComplete == null) {
1666 percentComplete = "0";
1667 }
1668 propertyValues.add(Field.createPropertyValue("percentcomplete", String.valueOf(Double.parseDouble(percentComplete) / 100)));
1669 String taskStatus = vTodoToTaskStatusMap.get(vCalendar.getFirstVeventPropertyValue("STATUS"));
1670 propertyValues.add(Field.createPropertyValue("taskstatus", taskStatus));
1671 propertyValues.add(Field.createPropertyValue("keywords", vCalendar.getFirstVeventPropertyValue("CATEGORIES")));
1672 propertyValues.add(Field.createPropertyValue("startdate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
1673 propertyValues.add(Field.createPropertyValue("duedate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
1674 propertyValues.add(Field.createPropertyValue("datecompleted", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("COMPLETED"))));
1675
1676 propertyValues.add(Field.createPropertyValue("iscomplete", "2".equals(taskStatus) ? "true" : "false"));
1677 propertyValues.add(Field.createPropertyValue("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
1678 propertyValues.add(Field.createPropertyValue("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
1679
1680 ExchangePropPatchRequest propPatchMethod = new ExchangePropPatchRequest(encodedHref, propertyValues);
1681 propPatchMethod.setHeader("Translate", "f");
1682 if (etag != null) {
1683 propPatchMethod.setHeader("If-Match", etag);
1684 }
1685 if (noneMatch != null) {
1686 propPatchMethod.setHeader("If-None-Match", noneMatch);
1687 }
1688 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) {
1689 int status = response.getStatusLine().getStatusCode();
1690
1691 if (status == HttpStatus.SC_MULTI_STATUS) {
1692 Item newItem = getItem(folderPath, itemName);
1693 try {
1694 itemResult.status = propPatchMethod.getResponseStatusCode();
1695 } catch (HttpResponseException e) {
1696 throw new IOException(e.getMessage(), e);
1697 }
1698 itemResult.etag = newItem.etag;
1699 } else {
1700 itemResult.status = status;
1701 }
1702 }
1703
1704 } else {
1705 String encodedHref = URIUtil.encodePath(getHref());
1706 byte[] mimeContent = createMimeContent();
1707 HttpResponse httpResponse = internalCreateOrUpdate(encodedHref, mimeContent);
1708 int status = httpResponse.getStatusLine().getStatusCode();
1709
1710 if (status == HttpStatus.SC_OK) {
1711 LOGGER.debug("Updated event " + encodedHref);
1712 } else if (status == HttpStatus.SC_CREATED) {
1713 LOGGER.debug("Created event " + encodedHref);
1714 } else if (status == HttpStatus.SC_NOT_FOUND) {
1715 LOGGER.debug("Event not found at " + encodedHref + ", searching permanenturl by urlcompname");
1716 // failover, search item by urlcompname
1717 MultiStatusResponse[] responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, HC4DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1);
1718 if (responses.length == 1) {
1719 encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl");
1720 LOGGER.warn("Event found, permanenturl is " + encodedHref);
1721 httpResponse = internalCreateOrUpdate(encodedHref, mimeContent);
1722 status = httpResponse.getStatusLine().getStatusCode();
1723 if (status == HttpStatus.SC_OK) {
1724 LOGGER.debug("Updated event " + encodedHref);
1725 } else {
1726 LOGGER.warn("Unable to create or update event " + status + ' ' + httpResponse.getStatusLine().getReasonPhrase());
1727 }
1728 }
1729 } else {
1730 LOGGER.warn("Unable to create or update event " + status + ' ' + httpResponse.getStatusLine().getReasonPhrase());
1731 }
1732
1733 // 440 means forbidden on Exchange
1734 if (status == 440) {
1735 status = HttpStatus.SC_FORBIDDEN;
1736 } else if (status == HttpStatus.SC_UNAUTHORIZED && getHref().startsWith("/public")) {
1737 LOGGER.warn("Ignore 401 unauthorized on public event");
1738 status = HttpStatus.SC_OK;
1739 }
1740 itemResult.status = status;
1741 if (httpResponse.getFirstHeader("GetETag") != null) {
1742 itemResult.etag = httpResponse.getFirstHeader("GetETag").getValue();
1743 }
1744
1745 // trigger activeSync push event, only if davmail.forceActiveSyncUpdate setting is true
1746 if ((status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) &&
1747 (Settings.getBooleanProperty("davmail.forceActiveSyncUpdate"))) {
1748 ArrayList<PropEntry> propertyList = new ArrayList<>();
1749 // Set contentclass to make ActiveSync happy
1750 propertyList.add(Field.createDavProperty("contentclass", contentClass));
1751 // ... but also set PR_INTERNET_CONTENT to preserve custom properties
1752 propertyList.add(Field.createDavProperty("internetContent", IOUtil.encodeBase64AsString(mimeContent)));
1753 HttpProppatch propPatchMethod = new HttpProppatch(encodedHref, propertyList);
1754 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) {
1755 int patchStatus = response.getStatusLine().getStatusCode();
1756 if (patchStatus != HttpStatus.SC_MULTI_STATUS) {
1757 LOGGER.warn("Unable to patch event to trigger activeSync push");
1758 } else {
1759 // need to retrieve new etag
1760 Item newItem = getItem(folderPath, itemName);
1761 itemResult.etag = newItem.etag;
1762 }
1763 }
1764 }
1765 }
1766 return itemResult;
1767 }
1768
1769
1770 }
1771
1772 protected Folder buildFolder(MultiStatusResponse entity) throws IOException {
1773 String href = URIUtil.decode(entity.getHref());
1774 Folder folder = new Folder();
1775 DavPropertySet properties = entity.getProperties(HttpStatus.SC_OK);
1776 folder.displayName = getPropertyIfExists(properties, "displayname");
1777 folder.folderClass = getPropertyIfExists(properties, "folderclass");
1778 folder.hasChildren = "1".equals(getPropertyIfExists(properties, "hassubs"));
1779 folder.noInferiors = "1".equals(getPropertyIfExists(properties, "nosubs"));
1780 folder.count = getIntPropertyIfExists(properties, "count");
1781 folder.unreadCount = getIntPropertyIfExists(properties, "unreadcount");
1782 // fake recent value
1783 folder.recent = folder.unreadCount;
1784 folder.ctag = getPropertyIfExists(properties, "contenttag");
1785 folder.etag = getPropertyIfExists(properties, "lastmodified");
1786
1787 folder.uidNext = getIntPropertyIfExists(properties, "uidNext");
1788
1789 // replace well known folder names
1790 if (inboxUrl != null && href.startsWith(inboxUrl)) {
1791 folder.folderPath = href.replaceFirst(inboxUrl, INBOX);
1792 } else if (sentitemsUrl != null && href.startsWith(sentitemsUrl)) {
1793 folder.folderPath = href.replaceFirst(sentitemsUrl, SENT);
1794 } else if (draftsUrl != null && href.startsWith(draftsUrl)) {
1795 folder.folderPath = href.replaceFirst(draftsUrl, DRAFTS);
1796 } else if (deleteditemsUrl != null && href.startsWith(deleteditemsUrl)) {
1797 folder.folderPath = href.replaceFirst(deleteditemsUrl, TRASH);
1798 } else if (calendarUrl != null && href.startsWith(calendarUrl)) {
1799 folder.folderPath = href.replaceFirst(calendarUrl, CALENDAR);
1800 } else if (contactsUrl != null && href.startsWith(contactsUrl)) {
1801 folder.folderPath = href.replaceFirst(contactsUrl, CONTACTS);
1802 } else {
1803 int index = href.indexOf(mailPath.substring(0, mailPath.length() - 1));
1804 if (index >= 0) {
1805 if (index + mailPath.length() > href.length()) {
1806 folder.folderPath = "";
1807 } else {
1808 folder.folderPath = href.substring(index + mailPath.length());
1809 }
1810 } else {
1811 try {
1812 java.net.URI folderURI = new java.net.URI(href);
1813 folder.folderPath = folderURI.getPath();
1814 if (folder.folderPath == null) {
1815 throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href);
1816 }
1817 } catch (URISyntaxException e) {
1818 throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href);
1819 }
1820 }
1821 }
1822 if (folder.folderPath.endsWith("/")) {
1823 folder.folderPath = folder.folderPath.substring(0, folder.folderPath.length() - 1);
1824 }
1825 return folder;
1826 }
1827
1828 protected static final Set<String> FOLDER_PROPERTIES = new HashSet<>();
1829
1830 static {
1831 FOLDER_PROPERTIES.add("displayname");
1832 FOLDER_PROPERTIES.add("folderclass");
1833 FOLDER_PROPERTIES.add("hassubs");
1834 FOLDER_PROPERTIES.add("nosubs");
1835 FOLDER_PROPERTIES.add("count");
1836 FOLDER_PROPERTIES.add("unreadcount");
1837 FOLDER_PROPERTIES.add("contenttag");
1838 FOLDER_PROPERTIES.add("lastmodified");
1839 FOLDER_PROPERTIES.add("uidNext");
1840 }
1841
1842 protected static final DavPropertyNameSet FOLDER_PROPERTIES_NAME_SET = new DavPropertyNameSet();
1843
1844 static {
1845 for (String attribute : FOLDER_PROPERTIES) {
1846 FOLDER_PROPERTIES_NAME_SET.add(Field.getPropertyName(attribute));
1847 }
1848 }
1849
1850 /**
1851 * @inheritDoc
1852 */
1853 @Override
1854 protected Folder internalGetFolder(String folderPath) throws IOException {
1855 MultiStatus multiStatus = httpClientAdapter.executeDavRequest(new HttpPropfind(
1856 URIUtil.encodePath(getFolderPath(folderPath)),
1857 FOLDER_PROPERTIES_NAME_SET, 0));
1858 MultiStatusResponse[] responses = multiStatus.getResponses();
1859
1860 Folder folder = null;
1861 if (responses.length > 0) {
1862 folder = buildFolder(responses[0]);
1863 folder.folderPath = folderPath;
1864 }
1865 return folder;
1866 }
1867
1868 /**
1869 * @inheritDoc
1870 */
1871 @Override
1872 public List<Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
1873 boolean isPublic = folderPath.startsWith("/public");
1874 FolderQueryTraversal mode = (!isPublic && recursive) ? FolderQueryTraversal.Deep : FolderQueryTraversal.Shallow;
1875 List<Folder> folders = new ArrayList<>();
1876
1877 MultiStatusResponse[] responses = searchItems(folderPath, FOLDER_PROPERTIES, and(isTrue("isfolder"), isFalse("ishidden"), condition), mode, 0);
1878
1879 for (MultiStatusResponse response : responses) {
1880 Folder folder = buildFolder(response);
1881 folders.add(buildFolder(response));
1882 if (isPublic && recursive) {
1883 getSubFolders(folder.folderPath, condition, recursive);
1884 }
1885 }
1886 return folders;
1887 }
1888
1889 /**
1890 * @inheritDoc
1891 */
1892 @Override
1893 public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException {
1894 Set<PropertyValue> propertyValues = new HashSet<>();
1895 if (properties != null) {
1896 for (Map.Entry<String, String> entry : properties.entrySet()) {
1897 propertyValues.add(Field.createPropertyValue(entry.getKey(), entry.getValue()));
1898 }
1899 }
1900 propertyValues.add(Field.createPropertyValue("folderclass", folderClass));
1901
1902 // standard MkColMethod does not take properties, override ExchangePropPatchRequest instead
1903 ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues) {
1904 @Override
1905 public String getMethod() {
1906 return "MKCOL";
1907 }
1908 };
1909 int status;
1910 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) {
1911 propPatchRequest.handleResponse(response);
1912 status = response.getStatusLine().getStatusCode();
1913 if (status == HttpStatus.SC_MULTI_STATUS) {
1914 status = propPatchRequest.getResponseStatusCode();
1915 } else if (status == HttpStatus.SC_METHOD_NOT_ALLOWED) {
1916 LOGGER.info("Folder " + folderPath + " already exists");
1917 }
1918 } catch (HttpResponseException e) {
1919 throw new IOException(e.getMessage(), e);
1920 }
1921 LOGGER.debug("Create folder " + folderPath + " returned " + status);
1922 return status;
1923 }
1924
1925 /**
1926 * @inheritDoc
1927 */
1928 @Override
1929 public int updateFolder(String folderPath, Map<String, String> properties) throws IOException {
1930 Set<PropertyValue> propertyValues = new HashSet<>();
1931 if (properties != null) {
1932 for (Map.Entry<String, String> entry : properties.entrySet()) {
1933 propertyValues.add(Field.createPropertyValue(entry.getKey(), entry.getValue()));
1934 }
1935 }
1936
1937 ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues);
1938 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) {
1939 propPatchRequest.handleResponse(response);
1940 int status = response.getStatusLine().getStatusCode();
1941 if (status == HttpStatus.SC_MULTI_STATUS) {
1942 try {
1943 status = propPatchRequest.getResponseStatusCode();
1944 } catch (HttpResponseException e) {
1945 throw new IOException(e.getMessage(), e);
1946 }
1947 }
1948
1949 return status;
1950 }
1951 }
1952
1953 /**
1954 * @inheritDoc
1955 */
1956 @Override
1957 public void deleteFolder(String folderPath) throws IOException {
1958 HttpDelete httpDelete = new HttpDelete(URIUtil.encodePath(getFolderPath(folderPath)));
1959 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
1960 int status = response.getStatusLine().getStatusCode();
1961 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
1962 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response);
1963 }
1964 }
1965 }
1966
1967 /**
1968 * @inheritDoc
1969 */
1970 @Override
1971 public void moveFolder(String folderPath, String targetPath) throws IOException {
1972 HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(folderPath)),
1973 URIUtil.encodePath(getFolderPath(targetPath)), false);
1974 try (CloseableHttpResponse response = httpClientAdapter.execute(httpMove)) {
1975 int statusCode = response.getStatusLine().getStatusCode();
1976 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
1977 throw new HttpPreconditionFailedException(BundleMessage.format("EXCEPTION_UNABLE_TO_MOVE_FOLDER"));
1978 } else if (statusCode != HttpStatus.SC_CREATED) {
1979 throw HttpClientAdapter.buildHttpResponseException(httpMove, response);
1980 } else if (folderPath.equalsIgnoreCase("/users/" + getEmail() + "/calendar")) {
1981 // calendar renamed, need to reload well known folders
1982 getWellKnownFolders();
1983 }
1984 }
1985 }
1986
1987 /**
1988 * @inheritDoc
1989 */
1990 @Override
1991 public void moveItem(String sourcePath, String targetPath) throws IOException {
1992 HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(sourcePath)),
1993 URIUtil.encodePath(getFolderPath(targetPath)), false);
1994 moveItem(httpMove);
1995 }
1996
1997 protected void moveItem(HttpMove httpMove) throws IOException {
1998 try (CloseableHttpResponse response = httpClientAdapter.execute(httpMove)) {
1999 int statusCode = response.getStatusLine().getStatusCode();
2000 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
2001 throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_ITEM");
2002 } else if (statusCode != HttpStatus.SC_CREATED && statusCode != HttpStatus.SC_OK) {
2003 throw HttpClientAdapter.buildHttpResponseException(httpMove, response);
2004 }
2005 }
2006 }
2007
2008 protected String getPropertyIfExists(DavPropertySet properties, String alias) {
2009 DavProperty property = properties.get(Field.getResponsePropertyName(alias));
2010 if (property == null) {
2011 return null;
2012 } else {
2013 Object value = property.getValue();
2014 if (value instanceof Node) {
2015 return ((Node) value).getTextContent();
2016 } else if (value instanceof List) {
2017 StringBuilder buffer = new StringBuilder();
2018 for (Object node : (List) value) {
2019 if (buffer.length() > 0) {
2020 buffer.append(',');
2021 }
2022 if (node instanceof Node) {
2023 // jackrabbit
2024 buffer.append(((Node) node).getTextContent());
2025 } else {
2026 // ExchangeDavMethod
2027 buffer.append(node);
2028 }
2029 }
2030 return buffer.toString();
2031 } else {
2032 return (String) value;
2033 }
2034 }
2035 }
2036
2037 protected String getURLPropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) throws IOException {
2038 String result = getPropertyIfExists(properties, alias);
2039 if (result != null) {
2040 result = URIUtil.decode(result);
2041 }
2042 return result;
2043 }
2044
2045 protected int getIntPropertyIfExists(DavPropertySet properties, String alias) {
2046 DavProperty property = properties.get(Field.getPropertyName(alias));
2047 if (property == null) {
2048 return 0;
2049 } else {
2050 return Integer.parseInt((String) property.getValue());
2051 }
2052 }
2053
2054 protected long getLongPropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) {
2055 DavProperty property = properties.get(Field.getPropertyName(alias));
2056 if (property == null) {
2057 return 0;
2058 } else {
2059 return Long.parseLong((String) property.getValue());
2060 }
2061 }
2062
2063 protected double getDoublePropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) {
2064 DavProperty property = properties.get(Field.getResponsePropertyName(alias));
2065 if (property == null) {
2066 return 0;
2067 } else {
2068 return Double.parseDouble((String) property.getValue());
2069 }
2070 }
2071
2072 protected byte[] getBinaryPropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) {
2073 byte[] property = null;
2074 String base64Property = getPropertyIfExists(properties, alias);
2075 if (base64Property != null) {
2076 property = IOUtil.decodeBase64(base64Property);
2077 }
2078 return property;
2079 }
2080
2081
2082 protected Message buildMessage(MultiStatusResponse responseEntity) throws IOException {
2083 Message message = new Message();
2084 message.messageUrl = URIUtil.decode(responseEntity.getHref());
2085 DavPropertySet properties = responseEntity.getProperties(HttpStatus.SC_OK);
2086
2087 message.permanentUrl = getURLPropertyIfExists(properties, "permanenturl");
2088 message.size = getIntPropertyIfExists(properties, "messageSize");
2089 message.uid = getPropertyIfExists(properties, "uid");
2090 message.contentClass = getPropertyIfExists(properties, "contentclass");
2091 message.imapUid = getLongPropertyIfExists(properties, "imapUid");
2092 message.read = "1".equals(getPropertyIfExists(properties, "read"));
2093 message.junk = "1".equals(getPropertyIfExists(properties, "junk"));
2094 message.flagged = "2".equals(getPropertyIfExists(properties, "flagStatus"));
2095 message.draft = (getIntPropertyIfExists(properties, "messageFlags") & 8) != 0;
2096 String lastVerbExecuted = getPropertyIfExists(properties, "lastVerbExecuted");
2097 message.answered = "102".equals(lastVerbExecuted) || "103".equals(lastVerbExecuted);
2098 message.forwarded = "104".equals(lastVerbExecuted);
2099 message.date = convertDateFromExchange(getPropertyIfExists(properties, "date"));
2100 message.deleted = "1".equals(getPropertyIfExists(properties, "deleted"));
2101
2102 String lastmodified = convertDateFromExchange(getPropertyIfExists(properties, "lastmodified"));
2103 message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date);
2104
2105 message.keywords = getPropertyIfExists(properties, "keywords");
2106
2107 if (LOGGER.isDebugEnabled()) {
2108 StringBuilder buffer = new StringBuilder();
2109 buffer.append("Message");
2110 if (message.imapUid != 0) {
2111 buffer.append(" IMAP uid: ").append(message.imapUid);
2112 }
2113 if (message.uid != null) {
2114 buffer.append(" uid: ").append(message.uid);
2115 }
2116 buffer.append(" href: ").append(responseEntity.getHref()).append(" permanenturl:").append(message.permanentUrl);
2117 LOGGER.debug(buffer.toString());
2118 }
2119 return message;
2120 }
2121
2122 @Override
2123 public MessageList searchMessages(String folderPath, Set<String> attributes, Condition condition) throws IOException {
2124 MessageList messages = new MessageList();
2125 int maxCount = Settings.getIntProperty("davmail.folderSizeLimit", 0);
2126 MultiStatusResponse[] responses = searchItems(folderPath, attributes, and(isFalse("isfolder"), isFalse("ishidden"), condition), FolderQueryTraversal.Shallow, maxCount);
2127
2128 for (MultiStatusResponse response : responses) {
2129 Message message = buildMessage(response);
2130 message.messageList = messages;
2131 messages.add(message);
2132 }
2133 Collections.sort(messages);
2134 return messages;
2135 }
2136
2137 /**
2138 * @inheritDoc
2139 */
2140 @Override
2141 public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
2142 List<ExchangeSession.Contact> contacts = new ArrayList<>();
2143 MultiStatusResponse[] responses = searchItems(folderPath, attributes,
2144 and(isEqualTo("outlookmessageclass", "IPM.Contact"), isFalse("isfolder"), isFalse("ishidden"), condition),
2145 FolderQueryTraversal.Shallow, maxCount);
2146 for (MultiStatusResponse response : responses) {
2147 contacts.add(new Contact(response));
2148 }
2149 return contacts;
2150 }
2151
2152 /**
2153 * Common item properties
2154 */
2155 protected static final Set<String> ITEM_PROPERTIES = new HashSet<>();
2156
2157 static {
2158 ITEM_PROPERTIES.add("etag");
2159 ITEM_PROPERTIES.add("displayname");
2160 // calendar CdoInstanceType
2161 ITEM_PROPERTIES.add("instancetype");
2162 ITEM_PROPERTIES.add("urlcompname");
2163 ITEM_PROPERTIES.add("subject");
2164 ITEM_PROPERTIES.add("contentclass");
2165 }
2166
2167 @Override
2168 protected Set<String> getItemProperties() {
2169 return ITEM_PROPERTIES;
2170 }
2171
2172
2173 /**
2174 * @inheritDoc
2175 */
2176 @Override
2177 public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
2178 return searchEvents(folderPath, ITEM_PROPERTIES,
2179 and(isEqualTo("contentclass", "urn:content-classes:calendarmessage"),
2180 or(isNull("processed"), isFalse("processed"))));
2181 }
2182
2183
2184 @Override
2185 public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
2186 List<ExchangeSession.Event> events = new ArrayList<>();
2187 MultiStatusResponse[] responses = searchItems(folderPath, attributes, and(isFalse("isfolder"), isFalse("ishidden"), condition), FolderQueryTraversal.Shallow, 0);
2188 for (MultiStatusResponse response : responses) {
2189 String instancetype = getPropertyIfExists(response.getProperties(HttpStatus.SC_OK), "instancetype");
2190 Event event = new Event(response);
2191 //noinspection VariableNotUsedInsideIf
2192 if (instancetype == null) {
2193 // check ics content
2194 try {
2195 event.getBody();
2196 // getBody success => add event or task
2197 events.add(event);
2198 } catch (IOException e) {
2199 // invalid event: exclude from list
2200 LOGGER.warn("Invalid event " + event.displayName + " found at " + response.getHref(), e);
2201 }
2202 } else {
2203 events.add(event);
2204 }
2205 }
2206 return events;
2207 }
2208
2209 @Override
2210 protected Condition getCalendarItemCondition(Condition dateCondition) {
2211 boolean caldavEnableLegacyTasks = Settings.getBooleanProperty("davmail.caldavEnableLegacyTasks", false);
2212 if (caldavEnableLegacyTasks) {
2213 // return tasks created in calendar folder
2214 return or(isNull("instancetype"),
2215 isEqualTo("instancetype", 1),
2216 and(isEqualTo("instancetype", 0), dateCondition));
2217 } else {
2218 // instancetype 0 single appointment / 1 master recurring appointment
2219 return and(or(isEqualTo("outlookmessageclass", "IPM.Appointment"), isEqualTo("outlookmessageclass", "IPM.Appointment.MeetingEvent")),
2220 or(isEqualTo("instancetype", 1),
2221 and(isEqualTo("instancetype", 0), dateCondition)));
2222 }
2223 }
2224
2225 protected MultiStatusResponse[] searchItems(String folderPath, Set<String> attributes, Condition condition,
2226 FolderQueryTraversal folderQueryTraversal, int maxCount) throws IOException {
2227 String folderUrl;
2228 if (folderPath.startsWith("http")) {
2229 folderUrl = folderPath;
2230 } else {
2231 folderUrl = getFolderPath(folderPath);
2232 }
2233 StringBuilder searchRequest = new StringBuilder();
2234 searchRequest.append("SELECT ")
2235 .append(Field.getRequestPropertyString("permanenturl"));
2236 if (attributes != null) {
2237 for (String attribute : attributes) {
2238 searchRequest.append(',').append(Field.getRequestPropertyString(attribute));
2239 }
2240 }
2241 searchRequest.append(" FROM SCOPE('").append(folderQueryTraversal).append(" TRAVERSAL OF \"").append(folderUrl).append("\"')");
2242 if (condition != null) {
2243 searchRequest.append(" WHERE ");
2244 condition.appendTo(searchRequest);
2245 }
2246 searchRequest.append(" ORDER BY ").append(Field.getRequestPropertyString("imapUid")).append(" DESC");
2247 DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_QUERY", searchRequest));
2248 MultiStatusResponse[] responses = httpClientAdapter.executeSearchRequest(
2249 encodeAndFixUrl(folderUrl), searchRequest.toString(), maxCount);
2250 DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_RESULT", responses.length));
2251 return responses;
2252 }
2253
2254 protected static final Set<String> EVENT_REQUEST_PROPERTIES = new HashSet<>();
2255
2256 static {
2257 EVENT_REQUEST_PROPERTIES.add("permanenturl");
2258 EVENT_REQUEST_PROPERTIES.add("urlcompname");
2259 EVENT_REQUEST_PROPERTIES.add("etag");
2260 EVENT_REQUEST_PROPERTIES.add("contentclass");
2261 EVENT_REQUEST_PROPERTIES.add("displayname");
2262 EVENT_REQUEST_PROPERTIES.add("subject");
2263 }
2264
2265 protected static final DavPropertyNameSet EVENT_REQUEST_PROPERTIES_NAME_SET = new DavPropertyNameSet();
2266
2267 static {
2268 for (String attribute : EVENT_REQUEST_PROPERTIES) {
2269 EVENT_REQUEST_PROPERTIES_NAME_SET.add(Field.getPropertyName(attribute));
2270 }
2271
2272 }
2273
2274 @Override
2275 public Item getItem(String folderPath, String itemName) throws IOException {
2276 String emlItemName = convertItemNameToEML(itemName);
2277 String itemPath = getFolderPath(folderPath) + '/' + emlItemName;
2278 MultiStatusResponse[] responses = null;
2279 try {
2280 HttpPropfind httpPropfind = new HttpPropfind(URIUtil.encodePath(itemPath), EVENT_REQUEST_PROPERTIES_NAME_SET, 0);
2281 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) {
2282 responses = httpPropfind.getResponseBodyAsMultiStatus(response).getResponses();
2283 } catch (HttpNotFoundException | DavException e) {
2284 // ignore
2285 }
2286 if (responses == null || responses.length == 0 && isMainCalendar(folderPath)) {
2287 if (itemName.endsWith(".ics")) {
2288 itemName = itemName.substring(0, itemName.length() - 3) + "EML";
2289 }
2290 // look for item in tasks folder
2291 HttpPropfind taskHttpPropfind = new HttpPropfind(URIUtil.encodePath(getFolderPath(TASKS) + '/' + emlItemName), EVENT_REQUEST_PROPERTIES_NAME_SET, 0);
2292 try (CloseableHttpResponse response = httpClientAdapter.execute(taskHttpPropfind)) {
2293 responses = taskHttpPropfind.getResponseBodyAsMultiStatus(response).getResponses();
2294 } catch (HttpNotFoundException | DavException e) {
2295 // ignore
2296 }
2297 }
2298 if (responses == null || responses.length == 0) {
2299 throw new HttpNotFoundException(itemPath + " not found");
2300 }
2301 } catch (HttpNotFoundException e) {
2302 try {
2303 LOGGER.debug(itemPath + " not found, searching by urlcompname");
2304 // failover: try to get event by displayname
2305 responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", emlItemName), FolderQueryTraversal.Shallow, 1);
2306 if (responses.length == 0 && isMainCalendar(folderPath)) {
2307 responses = searchItems(TASKS, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", emlItemName), FolderQueryTraversal.Shallow, 1);
2308 }
2309 if (responses.length == 0) {
2310 throw new HttpNotFoundException(itemPath + " not found");
2311 }
2312 } catch (HttpNotFoundException e2) {
2313 LOGGER.debug("last failover: search all items");
2314 List<ExchangeSession.Event> events = getAllEvents(folderPath);
2315 for (ExchangeSession.Event event : events) {
2316 if (itemName.equals(event.getName())) {
2317 HttpPropfind permanentHttpPropfind = new HttpPropfind(encodeAndFixUrl(((DavExchangeSession.Event) event).getPermanentUrl()), EVENT_REQUEST_PROPERTIES_NAME_SET, 0);
2318 try (CloseableHttpResponse response = httpClientAdapter.execute(permanentHttpPropfind)) {
2319 responses = permanentHttpPropfind.getResponseBodyAsMultiStatus(response).getResponses();
2320 } catch (DavException e3) {
2321 // ignore
2322 }
2323 break;
2324 }
2325 }
2326 if (responses == null || responses.length == 0) {
2327 throw new HttpNotFoundException(itemPath + " not found");
2328 }
2329 LOGGER.warn("search by urlcompname failed, actual value is " + getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "urlcompname"));
2330 }
2331 }
2332 // build item
2333 String contentClass = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "contentclass");
2334 String urlcompname = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "urlcompname");
2335 if ("urn:content-classes:person".equals(contentClass)) {
2336 // retrieve Contact properties
2337 List<ExchangeSession.Contact> contacts = searchContacts(folderPath, CONTACT_ATTRIBUTES,
2338 isEqualTo("urlcompname", StringUtil.decodeUrlcompname(urlcompname)), 1);
2339 if (contacts.isEmpty()) {
2340 LOGGER.warn("Item found, but unable to build contact");
2341 throw new HttpNotFoundException(itemPath + " not found");
2342 }
2343 return contacts.get(0);
2344 } else if ("urn:content-classes:appointment".equals(contentClass)
2345 || "urn:content-classes:calendarmessage".equals(contentClass)
2346 || "urn:content-classes:task".equals(contentClass)) {
2347 return new Event(responses[0]);
2348 } else {
2349 LOGGER.warn("wrong contentclass on item " + itemPath + ": " + contentClass);
2350 // return item anyway
2351 return new Event(responses[0]);
2352 }
2353
2354 }
2355
2356 @Override
2357 public ExchangeSession.ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
2358 ContactPhoto contactPhoto;
2359 final HttpGet httpGet = new HttpGet(URIUtil.encodePath(contact.getHref()) + "/ContactPicture.jpg");
2360 httpGet.setHeader("Translate", "f");
2361 httpGet.setHeader("Accept-Encoding", "gzip");
2362
2363 InputStream inputStream = null;
2364 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
2365 if (HttpClientAdapter.isGzipEncoded(response)) {
2366 inputStream = (new GZIPInputStream(response.getEntity().getContent()));
2367 } else {
2368 inputStream = response.getEntity().getContent();
2369 }
2370
2371 contactPhoto = new ContactPhoto();
2372 contactPhoto.contentType = "image/jpeg";
2373
2374 ByteArrayOutputStream baos = new ByteArrayOutputStream();
2375 InputStream partInputStream = inputStream;
2376 IOUtil.write(partInputStream, baos);
2377 contactPhoto.content = IOUtil.encodeBase64AsString(baos.toByteArray());
2378 } finally {
2379 if (inputStream != null) {
2380 try {
2381 inputStream.close();
2382 } catch (IOException e) {
2383 LOGGER.debug(e);
2384 }
2385 }
2386 }
2387 return contactPhoto;
2388 }
2389
2390 @Override
2391 public int sendEvent(String icsBody) throws IOException {
2392 String itemName = UUID.randomUUID().toString() + ".EML";
2393 byte[] mimeContent = (new Event(getFolderPath(DRAFTS), itemName, "urn:content-classes:calendarmessage", icsBody, null, null)).createMimeContent();
2394 if (mimeContent == null) {
2395 // no recipients, cancel
2396 return HttpStatus.SC_NO_CONTENT;
2397 } else {
2398 sendMessage(mimeContent);
2399 return HttpStatus.SC_OK;
2400 }
2401 }
2402
2403 @Override
2404 public void deleteItem(String folderPath, String itemName) throws IOException {
2405 String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName));
2406 HttpDelete httpDelete = new HttpDelete(eventPath);
2407 int status;
2408 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2409 status = response.getStatusLine().getStatusCode();
2410 }
2411 if (status == HttpStatus.SC_NOT_FOUND && isMainCalendar(folderPath)) {
2412 // retry in tasks folder
2413 eventPath = URIUtil.encodePath(getFolderPath(TASKS) + '/' + convertItemNameToEML(itemName));
2414 httpDelete = new HttpDelete(eventPath);
2415 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2416 status = response.getStatusLine().getStatusCode();
2417 }
2418 }
2419 if (status == HttpStatus.SC_NOT_FOUND) {
2420 LOGGER.debug("Unable to delete " + itemName + ": item not found");
2421 }
2422 }
2423
2424 @Override
2425 public void processItem(String folderPath, String itemName) throws IOException {
2426 String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName));
2427 // do not delete calendar messages, mark read and processed
2428 ArrayList<PropEntry> list = new ArrayList<>();
2429 list.add(Field.createDavProperty("processed", "true"));
2430 list.add(Field.createDavProperty("read", "1"));
2431 HttpProppatch patchMethod = new HttpProppatch(eventPath, list);
2432 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) {
2433 LOGGER.debug("Processed " + itemName + " " + response.getStatusLine().getStatusCode());
2434 }
2435 }
2436
2437 @Override
2438 public ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
2439 return new Event(getFolderPath(folderPath), itemName, contentClass, icsBody, etag, noneMatch).createOrUpdate();
2440 }
2441
2442 /**
2443 * create a fake event to get VTIMEZONE body
2444 */
2445 @Override
2446 protected void loadVtimezone() {
2447 try {
2448 // create temporary folder
2449 String folderPath = getFolderPath("davmailtemp");
2450 createCalendarFolder(folderPath, null);
2451
2452 String fakeEventUrl = null;
2453 if ("Exchange2003".equals(serverVersion)) {
2454 HttpPost httpPost = new HttpPost(URIUtil.encodePath(folderPath));
2455 ArrayList<NameValuePair> parameters = new ArrayList<>();
2456 parameters.add(new BasicNameValuePair("Cmd", "saveappt"));
2457 parameters.add(new BasicNameValuePair("FORMTYPE", "appointment"));
2458 httpPost.setEntity(new UrlEncodedFormEntity(parameters, Consts.UTF_8));
2459
2460 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPost)) {
2461 // create fake event
2462 int statusCode = response.getStatusLine().getStatusCode();
2463 if (statusCode == HttpStatus.SC_OK) {
2464 fakeEventUrl = StringUtil.getToken(new BasicResponseHandler().handleResponse(response), "<span id=\"itemHREF\">", "</span>");
2465 if (fakeEventUrl != null) {
2466 fakeEventUrl = URIUtil.decode(fakeEventUrl);
2467 }
2468 }
2469 }
2470 }
2471 // failover for Exchange 2007, use PROPPATCH with forced timezone
2472 if (fakeEventUrl == null) {
2473 ArrayList<PropEntry> propertyList = new ArrayList<>();
2474 propertyList.add(Field.createDavProperty("contentclass", "urn:content-classes:appointment"));
2475 propertyList.add(Field.createDavProperty("outlookmessageclass", "IPM.Appointment"));
2476 propertyList.add(Field.createDavProperty("instancetype", "0"));
2477
2478 // get forced timezone id from settings
2479 String timezoneId = Settings.getProperty("davmail.timezoneId");
2480 if (timezoneId == null) {
2481 // get timezoneid from OWA settings
2482 timezoneId = getTimezoneIdFromExchange();
2483 }
2484 // without a timezoneId, use Exchange timezone
2485 if (timezoneId != null) {
2486 propertyList.add(Field.createDavProperty("timezoneid", timezoneId));
2487 }
2488 String patchMethodUrl = folderPath + '/' + UUID.randomUUID().toString() + ".EML";
2489 HttpProppatch patchMethod = new HttpProppatch(URIUtil.encodePath(patchMethodUrl), propertyList);
2490 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) {
2491 int statusCode = response.getStatusLine().getStatusCode();
2492 if (statusCode == HttpStatus.SC_MULTI_STATUS) {
2493 fakeEventUrl = patchMethodUrl;
2494 }
2495 }
2496 }
2497 if (fakeEventUrl != null) {
2498 // get fake event body
2499 HttpGet httpGet = new HttpGet(URIUtil.encodePath(fakeEventUrl));
2500 httpGet.setHeader("Translate", "f");
2501 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
2502 this.vTimezone = new VObject("BEGIN:VTIMEZONE" +
2503 StringUtil.getToken(new BasicResponseHandler().handleResponse(response), "BEGIN:VTIMEZONE", "END:VTIMEZONE") +
2504 "END:VTIMEZONE\r\n");
2505 }
2506 }
2507
2508 // delete temporary folder
2509 deleteFolder("davmailtemp");
2510 } catch (IOException e) {
2511 LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
2512 }
2513 }
2514
2515 protected String getTimezoneIdFromExchange() {
2516 String timezoneId = null;
2517 String timezoneName = null;
2518 try {
2519 Set<String> attributes = new HashSet<>();
2520 attributes.add("roamingdictionary");
2521
2522 MultiStatusResponse[] responses = searchItems("/users/" + getEmail() + "/NON_IPM_SUBTREE", attributes, isEqualTo("messageclass", "IPM.Configuration.OWA.UserOptions"), HC4DavExchangeSession.FolderQueryTraversal.Deep, 1);
2523 if (responses.length == 1) {
2524 byte[] roamingdictionary = getBinaryPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "roamingdictionary");
2525 if (roamingdictionary != null) {
2526 timezoneName = getTimezoneNameFromRoamingDictionary(roamingdictionary);
2527 if (timezoneName != null) {
2528 timezoneId = ResourceBundle.getBundle("timezoneids").getString(timezoneName);
2529 }
2530 }
2531 }
2532 } catch (MissingResourceException e) {
2533 LOGGER.warn("Unable to retrieve Exchange timezone id for name " + timezoneName);
2534 } catch (IOException e) {
2535 LOGGER.warn("Unable to retrieve Exchange timezone id: " + e.getMessage(), e);
2536 }
2537 return timezoneId;
2538 }
2539
2540 protected String getTimezoneNameFromRoamingDictionary(byte[] roamingdictionary) {
2541 String timezoneName = null;
2542 XMLStreamReader reader;
2543 try {
2544 reader = XMLStreamUtil.createXMLStreamReader(roamingdictionary);
2545 while (reader.hasNext()) {
2546 reader.next();
2547 if (XMLStreamUtil.isStartTag(reader, "e")
2548 && "18-timezone".equals(reader.getAttributeValue(null, "k"))) {
2549 String value = reader.getAttributeValue(null, "v");
2550 if (value != null && value.startsWith("18-")) {
2551 timezoneName = value.substring(3);
2552 }
2553 }
2554 }
2555
2556 } catch (XMLStreamException e) {
2557 LOGGER.error("Error while parsing RoamingDictionary: " + e, e);
2558 }
2559 return timezoneName;
2560 }
2561
2562 @Override
2563 protected Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
2564 return new Contact(getFolderPath(folderPath), itemName, properties, etag, noneMatch);
2565 }
2566
2567 protected List<PropEntry> buildProperties(Map<String, String> properties) {
2568 ArrayList<PropEntry> list = new ArrayList<>();
2569 if (properties != null) {
2570 for (Map.Entry<String, String> entry : properties.entrySet()) {
2571 if ("read".equals(entry.getKey())) {
2572 list.add(Field.createDavProperty("read", entry.getValue()));
2573 } else if ("junk".equals(entry.getKey())) {
2574 list.add(Field.createDavProperty("junk", entry.getValue()));
2575 } else if ("flagged".equals(entry.getKey())) {
2576 list.add(Field.createDavProperty("flagStatus", entry.getValue()));
2577 } else if ("answered".equals(entry.getKey())) {
2578 list.add(Field.createDavProperty("lastVerbExecuted", entry.getValue()));
2579 if ("102".equals(entry.getValue())) {
2580 list.add(Field.createDavProperty("iconIndex", "261"));
2581 }
2582 } else if ("forwarded".equals(entry.getKey())) {
2583 list.add(Field.createDavProperty("lastVerbExecuted", entry.getValue()));
2584 if ("104".equals(entry.getValue())) {
2585 list.add(Field.createDavProperty("iconIndex", "262"));
2586 }
2587 } else if ("bcc".equals(entry.getKey())) {
2588 list.add(Field.createDavProperty("bcc", entry.getValue()));
2589 } else if ("deleted".equals(entry.getKey())) {
2590 list.add(Field.createDavProperty("deleted", entry.getValue()));
2591 } else if ("datereceived".equals(entry.getKey())) {
2592 list.add(Field.createDavProperty("datereceived", entry.getValue()));
2593 } else if ("keywords".equals(entry.getKey())) {
2594 list.add(Field.createDavProperty("keywords", entry.getValue()));
2595 }
2596 }
2597 }
2598 return list;
2599 }
2600
2601 /**
2602 * Create message in specified folder.
2603 * Will overwrite an existing message with same messageName in the same folder
2604 *
2605 * @param folderPath Exchange folder path
2606 * @param messageName message name
2607 * @param properties message properties (flags)
2608 * @param mimeMessage MIME message
2609 * @throws IOException when unable to create message
2610 */
2611 @Override
2612 public void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
2613 String messageUrl = URIUtil.encodePathQuery(getFolderPath(folderPath) + '/' + messageName);
2614
2615 List<PropEntry> davProperties = buildProperties(properties);
2616
2617 if (properties != null && properties.containsKey("draft")) {
2618 // note: draft is readonly after create, create the message first with requested messageFlags
2619 davProperties.add(Field.createDavProperty("messageFlags", properties.get("draft")));
2620 }
2621 if (properties != null && properties.containsKey("mailOverrideFormat")) {
2622 davProperties.add(Field.createDavProperty("mailOverrideFormat", properties.get("mailOverrideFormat")));
2623 }
2624 if (properties != null && properties.containsKey("messageFormat")) {
2625 davProperties.add(Field.createDavProperty("messageFormat", properties.get("messageFormat")));
2626 }
2627 if (!davProperties.isEmpty()) {
2628 HttpProppatch httpProppatch = new HttpProppatch(messageUrl, davProperties);
2629 try (CloseableHttpResponse response = httpClientAdapter.execute(httpProppatch)) {
2630 // update message with blind carbon copy and other flags
2631 int statusCode = response.getStatusLine().getStatusCode();
2632 if (statusCode != HttpStatus.SC_MULTI_STATUS) {
2633 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', response.getStatusLine().getReasonPhrase());
2634 }
2635
2636 }
2637 }
2638
2639 // update message body
2640 HttpPut putmethod = new HttpPut(messageUrl);
2641 putmethod.setHeader("Translate", "f");
2642 putmethod.setHeader("Content-Type", "message/rfc822");
2643
2644 try {
2645 // use same encoding as client socket reader
2646 ByteArrayOutputStream baos = new ByteArrayOutputStream();
2647 mimeMessage.writeTo(baos);
2648 baos.close();
2649 putmethod.setEntity(new ByteArrayEntity(baos.toByteArray()));
2650
2651 int code;
2652 String reasonPhrase;
2653 try (CloseableHttpResponse response = httpClientAdapter.execute(putmethod)) {
2654 code = response.getStatusLine().getStatusCode();
2655 reasonPhrase = response.getStatusLine().getReasonPhrase();
2656 }
2657
2658 // workaround for misconfigured Exchange server
2659 if (code == HttpStatus.SC_NOT_ACCEPTABLE) {
2660 LOGGER.warn("Draft message creation failed, failover to property update. Note: attachments are lost");
2661
2662 ArrayList<PropEntry> propertyList = new ArrayList<>();
2663 propertyList.add(Field.createDavProperty("to", mimeMessage.getHeader("to", ",")));
2664 propertyList.add(Field.createDavProperty("cc", mimeMessage.getHeader("cc", ",")));
2665 propertyList.add(Field.createDavProperty("message-id", mimeMessage.getHeader("message-id", ",")));
2666
2667 MimePart mimePart = mimeMessage;
2668 if (mimeMessage.getContent() instanceof MimeMultipart) {
2669 MimeMultipart multiPart = (MimeMultipart) mimeMessage.getContent();
2670 for (int i = 0; i < multiPart.getCount(); i++) {
2671 String contentType = multiPart.getBodyPart(i).getContentType();
2672 if (contentType.startsWith("text/")) {
2673 mimePart = (MimePart) multiPart.getBodyPart(i);
2674 break;
2675 }
2676 }
2677 }
2678
2679 String contentType = mimePart.getContentType();
2680
2681 if (contentType.startsWith("text/plain")) {
2682 propertyList.add(Field.createDavProperty("description", (String) mimePart.getContent()));
2683 } else if (contentType.startsWith("text/html")) {
2684 propertyList.add(Field.createDavProperty("htmldescription", (String) mimePart.getContent()));
2685 } else {
2686 LOGGER.warn("Unsupported content type: " + contentType.replaceAll("[\n\r\t]", "_") + " message body will be empty");
2687 }
2688
2689 propertyList.add(Field.createDavProperty("subject", mimeMessage.getHeader("subject", ",")));
2690 HttpProppatch propPatchMethod = new HttpProppatch(messageUrl, propertyList);
2691 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) {
2692 int patchStatus = response.getStatusLine().getStatusCode();
2693 if (patchStatus == HttpStatus.SC_MULTI_STATUS) {
2694 code = HttpStatus.SC_OK;
2695 }
2696 }
2697 }
2698
2699
2700 if (code != HttpStatus.SC_OK && code != HttpStatus.SC_CREATED) {
2701
2702 // first delete draft message
2703 if (!davProperties.isEmpty()) {
2704 HttpDelete httpDelete = new HttpDelete(messageUrl);
2705 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2706 int status = response.getStatusLine().getStatusCode();
2707 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
2708 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response);
2709 }
2710 } catch (IOException e) {
2711 LOGGER.warn("Unable to delete draft message");
2712 }
2713 }
2714 if (code == HttpStatus.SC_INSUFFICIENT_STORAGE) {
2715 throw new InsufficientStorageException(reasonPhrase);
2716 } else {
2717 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, code, ' ', reasonPhrase);
2718 }
2719 }
2720 } catch (MessagingException e) {
2721 throw new IOException(e.getMessage());
2722 } finally {
2723 putmethod.releaseConnection();
2724 }
2725
2726 try {
2727 // need to update bcc after put
2728 if (mimeMessage.getHeader("Bcc") != null) {
2729 davProperties = new ArrayList<>();
2730 davProperties.add(Field.createDavProperty("bcc", mimeMessage.getHeader("Bcc", ",")));
2731 HttpProppatch httpProppatch = new HttpProppatch(messageUrl, davProperties);
2732 // update message with blind carbon copy
2733 try (CloseableHttpResponse response = httpClientAdapter.execute(httpProppatch)) {
2734 int statusCode = response.getStatusLine().getStatusCode();
2735 if (statusCode != HttpStatus.SC_MULTI_STATUS) {
2736 throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', response.getStatusLine().getReasonPhrase());
2737 }
2738 }
2739 }
2740 } catch (MessagingException e) {
2741 throw new IOException(e.getMessage());
2742 }
2743
2744 }
2745
2746 /**
2747 * @inheritDoc
2748 */
2749 @Override
2750 public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
2751 HttpProppatch patchMethod = new HttpProppatch(encodeAndFixUrl(message.permanentUrl), buildProperties(properties)) {
2752 @Override
2753 public MultiStatus getResponseBodyAsMultiStatus(HttpResponse response) {
2754 // ignore response body, sometimes invalid with exchange mapi properties
2755 throw new UnsupportedOperationException();
2756 }
2757 };
2758 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) {
2759 int statusCode = response.getStatusLine().getStatusCode();
2760 if (statusCode != HttpStatus.SC_MULTI_STATUS) {
2761 throw new DavMailException("EXCEPTION_UNABLE_TO_UPDATE_MESSAGE");
2762 }
2763 }
2764 }
2765
2766 /**
2767 * @inheritDoc
2768 */
2769 @Override
2770 public void deleteMessage(ExchangeSession.Message message) throws IOException {
2771 LOGGER.debug("Delete " + message.permanentUrl + " (" + message.messageUrl + ')');
2772 HttpDelete httpDelete = new HttpDelete(encodeAndFixUrl(message.permanentUrl));
2773 try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2774 int status = response.getStatusLine().getStatusCode();
2775 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
2776 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response);
2777 }
2778 }
2779 }
2780
2781 /**
2782 * Send message.
2783 *
2784 * @param messageBody MIME message body
2785 * @throws IOException on error
2786 */
2787 public void sendMessage(byte[] messageBody) throws IOException {
2788 try {
2789 sendMessage(new MimeMessage(null, new SharedByteArrayInputStream(messageBody)));
2790 } catch (MessagingException e) {
2791 throw new IOException(e.getMessage());
2792 }
2793 }
2794
2795 //protected static final long MAPI_SEND_NO_RICH_INFO = 0x00010000L;
2796 protected static final long ENCODING_PREFERENCE = 0x00020000L;
2797 protected static final long ENCODING_MIME = 0x00040000L;
2798 //protected static final long BODY_ENCODING_HTML = 0x00080000L;
2799 protected static final long BODY_ENCODING_TEXT_AND_HTML = 0x00100000L;
2800 //protected static final long MAC_ATTACH_ENCODING_UUENCODE = 0x00200000L;
2801 //protected static final long MAC_ATTACH_ENCODING_APPLESINGLE = 0x00400000L;
2802 //protected static final long MAC_ATTACH_ENCODING_APPLEDOUBLE = 0x00600000L;
2803 //protected static final long OOP_DONT_LOOKUP = 0x10000000L;
2804
2805 @Override
2806 public void sendMessage(MimeMessage mimeMessage) throws IOException {
2807 try {
2808 // need to create draft first
2809 String itemName = UUID.randomUUID().toString() + ".EML";
2810 HashMap<String, String> properties = new HashMap<>();
2811 properties.put("draft", "9");
2812 String contentType = mimeMessage.getContentType();
2813 if (contentType != null && contentType.startsWith("text/plain")) {
2814 properties.put("messageFormat", "1");
2815 } else {
2816 properties.put("mailOverrideFormat", String.valueOf(ENCODING_PREFERENCE | ENCODING_MIME | BODY_ENCODING_TEXT_AND_HTML));
2817 properties.put("messageFormat", "2");
2818 }
2819 createMessage(DRAFTS, itemName, properties, mimeMessage);
2820 HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(DRAFTS + '/' + itemName)),
2821 URIUtil.encodePath(getFolderPath(SENDMSG)), false);
2822 // set header if saveInSent is disabled
2823 if (!Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) {
2824 httpMove.setHeader("Saveinsent", "f");
2825 }
2826 moveItem(httpMove);
2827 } catch (MessagingException e) {
2828 throw new IOException(e.getMessage());
2829 }
2830 }
2831
2832 // wrong hostname fix flag
2833 protected boolean restoreHostName;
2834
2835 /**
2836 * @inheritDoc
2837 */
2838 @Override
2839 protected byte[] getContent(ExchangeSession.Message message) throws IOException {
2840 ByteArrayOutputStream baos = new ByteArrayOutputStream();
2841 InputStream contentInputStream;
2842 try {
2843 try {
2844 try {
2845 contentInputStream = getContentInputStream(message.messageUrl);
2846 } catch (UnknownHostException e) {
2847 // failover for misconfigured Exchange server, replace host name in url
2848 restoreHostName = true;
2849 contentInputStream = getContentInputStream(message.messageUrl);
2850 }
2851 } catch (HttpNotFoundException e) {
2852 LOGGER.debug("Message not found at: " + message.messageUrl + ", retrying with permanenturl");
2853 contentInputStream = getContentInputStream(message.permanentUrl);
2854 }
2855
2856 try {
2857 IOUtil.write(contentInputStream, baos);
2858 } finally {
2859 contentInputStream.close();
2860 }
2861
2862 } catch (LoginTimeoutException | SocketException e) {
2863 // throw error on expired session
2864 LOGGER.warn(e.getMessage());
2865 throw e;
2866 } // throw error on broken connection
2867 catch (IOException e) {
2868 LOGGER.warn("Broken message at: " + message.messageUrl + " permanentUrl: " + message.permanentUrl + ", trying to rebuild from properties");
2869
2870 try {
2871 DavPropertyNameSet messageProperties = new DavPropertyNameSet();
2872 messageProperties.add(Field.getPropertyName("contentclass"));
2873 messageProperties.add(Field.getPropertyName("message-id"));
2874 messageProperties.add(Field.getPropertyName("from"));
2875 messageProperties.add(Field.getPropertyName("to"));
2876 messageProperties.add(Field.getPropertyName("cc"));
2877 messageProperties.add(Field.getPropertyName("subject"));
2878 messageProperties.add(Field.getPropertyName("date"));
2879 messageProperties.add(Field.getPropertyName("htmldescription"));
2880 messageProperties.add(Field.getPropertyName("body"));
2881 HttpPropfind httpPropfind = new HttpPropfind(encodeAndFixUrl(message.permanentUrl), messageProperties, 0);
2882 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) {
2883 MultiStatus responses = httpPropfind.getResponseBodyAsMultiStatus(response);
2884 if (responses.getResponses().length > 0) {
2885 MimeMessage mimeMessage = new MimeMessage((Session) null);
2886
2887 DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
2888 String propertyValue = getPropertyIfExists(properties, "contentclass");
2889 if (propertyValue != null) {
2890 mimeMessage.addHeader("Content-class", propertyValue);
2891 }
2892 propertyValue = getPropertyIfExists(properties, "date");
2893 if (propertyValue != null) {
2894 mimeMessage.setSentDate(parseDateFromExchange(propertyValue));
2895 }
2896 propertyValue = getPropertyIfExists(properties, "from");
2897 if (propertyValue != null) {
2898 mimeMessage.addHeader("From", propertyValue);
2899 }
2900 propertyValue = getPropertyIfExists(properties, "to");
2901 if (propertyValue != null) {
2902 mimeMessage.addHeader("To", propertyValue);
2903 }
2904 propertyValue = getPropertyIfExists(properties, "cc");
2905 if (propertyValue != null) {
2906 mimeMessage.addHeader("Cc", propertyValue);
2907 }
2908 propertyValue = getPropertyIfExists(properties, "subject");
2909 if (propertyValue != null) {
2910 mimeMessage.setSubject(propertyValue);
2911 }
2912 propertyValue = getPropertyIfExists(properties, "htmldescription");
2913 if (propertyValue != null) {
2914 mimeMessage.setContent(propertyValue, "text/html; charset=UTF-8");
2915 } else {
2916 propertyValue = getPropertyIfExists(properties, "body");
2917 if (propertyValue != null) {
2918 mimeMessage.setText(propertyValue);
2919 }
2920 }
2921 mimeMessage.writeTo(baos);
2922 }
2923 }
2924 if (LOGGER.isDebugEnabled()) {
2925 LOGGER.debug("Rebuilt message content: " + new String(baos.toByteArray(), StandardCharsets.UTF_8));
2926 }
2927 } catch (IOException | DavException | MessagingException e2) {
2928 LOGGER.warn(e2);
2929 }
2930 // other exception
2931 if (baos.size() == 0 && Settings.getBooleanProperty("davmail.deleteBroken")) {
2932 LOGGER.warn("Deleting broken message at: " + message.messageUrl + " permanentUrl: " + message.permanentUrl);
2933 try {
2934 message.delete();
2935 } catch (IOException ioe) {
2936 LOGGER.warn("Unable to delete broken message at: " + message.permanentUrl);
2937 }
2938 throw e;
2939 }
2940 }
2941
2942 return baos.toByteArray();
2943 }
2944
2945 /**
2946 * sometimes permanenturis inside items are wrong after an Exchange version migration
2947 * need to restore base uri to actual public Exchange uri
2948 *
2949 * @param url input uri
2950 * @return fixed uri
2951 * @throws IOException on error
2952 */
2953 protected String encodeAndFixUrl(String url) throws IOException {
2954 String fixedurl = URIUtil.encodePath(url);
2955 // sometimes permanenturis inside items are wrong after an Exchange version migration
2956 // need to restore base uri to actual public Exchange uri
2957 if (restoreHostName && fixedurl.startsWith("http")) {
2958 try {
2959 return URIUtils.rewriteURI(new java.net.URI(fixedurl), URIUtils.extractHost(httpClientAdapter.getUri())).toString();
2960 } catch (URISyntaxException e) {
2961 throw new IOException(e.getMessage(), e);
2962 }
2963 }
2964 return fixedurl;
2965 }
2966
2967 protected InputStream getContentInputStream(String url) throws IOException {
2968 String encodedUrl = encodeAndFixUrl(url);
2969
2970 final HttpGet httpGet = new HttpGet(encodedUrl);
2971 httpGet.setHeader("Content-Type", "text/xml; charset=utf-8");
2972 httpGet.setHeader("Translate", "f");
2973 httpGet.setHeader("Accept-Encoding", "gzip");
2974
2975 InputStream inputStream;
2976 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
2977 if (HttpClientAdapter.isGzipEncoded(response)) {
2978 inputStream = new GZIPInputStream(response.getEntity().getContent());
2979 } else {
2980 inputStream = response.getEntity().getContent();
2981 }
2982 inputStream = new FilterInputStream(inputStream) {
2983 int totalCount;
2984 int lastLogCount;
2985
2986 @Override
2987 public int read(byte[] buffer, int offset, int length) throws IOException {
2988 int count = super.read(buffer, offset, length);
2989 totalCount += count;
2990 if (totalCount - lastLogCount > 1024 * 128) {
2991 DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), httpGet.getURI()));
2992 DavGatewayTray.switchIcon();
2993 lastLogCount = totalCount;
2994 }
2995 return count;
2996 }
2997
2998 @Override
2999 public void close() throws IOException {
3000 try {
3001 super.close();
3002 } finally {
3003 httpGet.releaseConnection();
3004 }
3005 }
3006 };
3007
3008 } catch (IOException e) {
3009 LOGGER.warn("Unable to retrieve message at: " + url);
3010 throw e;
3011 }
3012 return inputStream;
3013 }
3014
3015 /**
3016 * @inheritDoc
3017 */
3018 @Override
3019 public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
3020 try {
3021 moveMessage(message.permanentUrl, targetFolder);
3022 } catch (HttpNotFoundException e) {
3023 LOGGER.debug("404 not found at permanenturl: " + message.permanentUrl + ", retry with messageurl");
3024 moveMessage(message.messageUrl, targetFolder);
3025 }
3026 }
3027
3028 protected void moveMessage(String sourceUrl, String targetFolder) throws IOException {
3029 String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID().toString();
3030 HttpMove method = new HttpMove(URIUtil.encodePath(sourceUrl), targetPath, false);
3031 // allow rename if a message with the same name exists
3032 method.setHeader("Allow-Rename", "t");
3033 try (CloseableHttpResponse response = httpClientAdapter.execute(method)) {
3034 int statusCode = response.getStatusLine().getStatusCode();
3035 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED ||
3036 statusCode == HttpStatus.SC_CONFLICT) {
3037 throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_MESSAGE");
3038 } else if (statusCode != HttpStatus.SC_CREATED) {
3039 throw HttpClientAdapter.buildHttpResponseException(method, response);
3040 }
3041 } finally {
3042 method.releaseConnection();
3043 }
3044 }
3045
3046 /**
3047 * @inheritDoc
3048 */
3049 @Override
3050 public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
3051 try {
3052 copyMessage(message.permanentUrl, targetFolder);
3053 } catch (HttpNotFoundException e) {
3054 LOGGER.debug("404 not found at permanenturl: " + message.permanentUrl + ", retry with messageurl");
3055 copyMessage(message.messageUrl, targetFolder);
3056 }
3057 }
3058
3059 protected void copyMessage(String sourceUrl, String targetFolder) throws IOException {
3060 String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID().toString();
3061 HttpCopy httpCopy = new HttpCopy(URIUtil.encodePath(sourceUrl), targetPath, false, false);
3062 // allow rename if a message with the same name exists
3063 httpCopy.addHeader("Allow-Rename", "t");
3064 try (CloseableHttpResponse response = httpClientAdapter.execute(httpCopy)) {
3065 int statusCode = response.getStatusLine().getStatusCode();
3066 if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
3067 throw new DavMailException("EXCEPTION_UNABLE_TO_COPY_MESSAGE");
3068 } else if (statusCode != HttpStatus.SC_CREATED) {
3069 throw HttpClientAdapter.buildHttpResponseException(httpCopy, response);
3070 }
3071 }
3072 }
3073
3074 @Override
3075 protected void moveToTrash(ExchangeSession.Message message) throws IOException {
3076 String destination = URIUtil.encodePath(deleteditemsUrl) + '/' + UUID.randomUUID().toString();
3077 LOGGER.debug("Deleting : " + message.permanentUrl + " to " + destination);
3078 HttpMove method = new HttpMove(encodeAndFixUrl(message.permanentUrl), destination, false);
3079 method.addHeader("Allow-rename", "t");
3080
3081 try (CloseableHttpResponse response = httpClientAdapter.execute(method)) {
3082 int status = response.getStatusLine().getStatusCode();
3083 // do not throw error if already deleted
3084 if (status != HttpStatus.SC_CREATED && status != HttpStatus.SC_NOT_FOUND) {
3085 throw HttpClientAdapter.buildHttpResponseException(method, response);
3086 }
3087 if (response.getFirstHeader("Location") != null) {
3088 destination = method.getFirstHeader("Location").getValue();
3089 }
3090 }
3091
3092 LOGGER.debug("Deleted to :" + destination);
3093 }
3094
3095 protected String getItemProperty(String permanentUrl, String propertyName) throws IOException, DavException {
3096 String result = null;
3097 DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
3098 davPropertyNameSet.add(Field.getPropertyName(propertyName));
3099 HttpPropfind propFindMethod = new HttpPropfind(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
3100 MultiStatus responses;
3101 try (CloseableHttpResponse response = httpClientAdapter.execute(propFindMethod)) {
3102 responses = propFindMethod.getResponseBodyAsMultiStatus(response);
3103 } catch (UnknownHostException e) {
3104 // failover for misconfigured Exchange server, replace host name in url
3105 restoreHostName = true;
3106 propFindMethod = new HttpPropfind(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
3107 try (CloseableHttpResponse response = httpClientAdapter.execute(propFindMethod)) {
3108 responses = propFindMethod.getResponseBodyAsMultiStatus(response);
3109 }
3110 }
3111
3112 if (responses.getResponses().length > 0) {
3113 DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
3114 result = getPropertyIfExists(properties, propertyName);
3115 }
3116
3117 return result;
3118 }
3119
3120 protected String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
3121 String zuluDateValue = null;
3122 if (exchangeDateValue != null) {
3123 try {
3124 zuluDateValue = getZuluDateFormat().format(getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue));
3125 } catch (ParseException e) {
3126 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3127 }
3128 }
3129 return zuluDateValue;
3130 }
3131
3132 protected static final Map<String, String> importanceToPriorityMap = new HashMap<>();
3133
3134 static {
3135 importanceToPriorityMap.put("high", "1");
3136 importanceToPriorityMap.put("normal", "5");
3137 importanceToPriorityMap.put("low", "9");
3138 }
3139
3140 protected static final Map<String, String> priorityToImportanceMap = new HashMap<>();
3141
3142 static {
3143 priorityToImportanceMap.put("1", "high");
3144 priorityToImportanceMap.put("5", "normal");
3145 priorityToImportanceMap.put("9", "low");
3146 }
3147
3148 protected String convertPriorityFromExchange(String exchangeImportanceValue) {
3149 String value = null;
3150 if (exchangeImportanceValue != null) {
3151 value = importanceToPriorityMap.get(exchangeImportanceValue);
3152 }
3153 return value;
3154 }
3155
3156 protected String convertPriorityToExchange(String vTodoPriorityValue) {
3157 String value = null;
3158 if (vTodoPriorityValue != null) {
3159 value = priorityToImportanceMap.get(vTodoPriorityValue);
3160 }
3161 return value;
3162 }
3163
3164
3165 @Override
3166 public void close() {
3167 httpClientAdapter.close();
3168 }
3169
3170 /**
3171 * Format date to exchange search format.
3172 *
3173 * @param date date object
3174 * @return formatted search date
3175 */
3176 @Override
3177 public String formatSearchDate(Date date) {
3178 SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS, Locale.ENGLISH);
3179 dateFormatter.setTimeZone(GMT_TIMEZONE);
3180 return dateFormatter.format(date);
3181 }
3182
3183 protected String convertTaskDateToZulu(String value) {
3184 String result = null;
3185 if (value != null && value.length() > 0) {
3186 try {
3187 SimpleDateFormat parser = ExchangeSession.getExchangeDateFormat(value);
3188
3189 Calendar calendarValue = Calendar.getInstance(GMT_TIMEZONE);
3190 calendarValue.setTime(parser.parse(value));
3191 // zulu time: add 12 hours
3192 if (value.length() == 16) {
3193 calendarValue.add(Calendar.HOUR, 12);
3194 }
3195 calendarValue.set(Calendar.HOUR, 0);
3196 calendarValue.set(Calendar.MINUTE, 0);
3197 calendarValue.set(Calendar.SECOND, 0);
3198 result = ExchangeSession.getExchangeZuluDateFormatMillisecond().format(calendarValue.getTime());
3199 } catch (ParseException e) {
3200 LOGGER.warn("Invalid date: " + value);
3201 }
3202 }
3203
3204 return result;
3205 }
3206
3207 protected String convertDateFromExchangeToTaskDate(String exchangeDateValue) throws DavMailException {
3208 String result = null;
3209 if (exchangeDateValue != null) {
3210 try {
3211 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
3212 dateFormat.setTimeZone(GMT_TIMEZONE);
3213 result = dateFormat.format(getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue));
3214 } catch (ParseException e) {
3215 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3216 }
3217 }
3218 return result;
3219 }
3220
3221 protected Date parseDateFromExchange(String exchangeDateValue) throws DavMailException {
3222 Date result = null;
3223 if (exchangeDateValue != null) {
3224 try {
3225 result = getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue);
3226 } catch (ParseException e) {
3227 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3228 }
3229 }
3230 return result;
3231 }
3232 }
1818 package davmail.exchange.ews;
1919
2020 import davmail.Settings;
21 import org.apache.http.entity.AbstractHttpEntity;
2122
2223 /**
2324 * Create Item method.
3536 this.savedItemFolderId = savedItemFolderId;
3637 this.item = item;
3738 addMethodOption(messageDisposition);
38 setContentChunked(Settings.getBooleanProperty("davmail.enableChunkedRequest", false));
39 ((AbstractHttpEntity)getEntity()).setChunked(Settings.getBooleanProperty("davmail.enableChunkedRequest", false));
3940 }
4041
4142 /**
2020 import davmail.BundleMessage;
2121 import davmail.Settings;
2222 import davmail.exchange.XMLStreamUtil;
23 import davmail.http.DavGatewayHttpClientFacade;
23 import davmail.http.HttpClientAdapter;
2424 import davmail.ui.tray.DavGatewayTray;
2525 import davmail.util.StringUtil;
2626 import org.apache.commons.codec.binary.Base64;
27 import org.apache.commons.httpclient.Header;
28 import org.apache.commons.httpclient.HttpConnection;
29 import org.apache.commons.httpclient.HttpState;
30 import org.apache.commons.httpclient.HttpStatus;
31 import org.apache.commons.httpclient.methods.PostMethod;
32 import org.apache.commons.httpclient.methods.RequestEntity;
27 import org.apache.http.HttpResponse;
28 import org.apache.http.HttpStatus;
29 import org.apache.http.client.ResponseHandler;
30 import org.apache.http.client.methods.HttpPost;
31 import org.apache.http.entity.AbstractHttpEntity;
32 import org.apache.http.entity.ContentType;
33 import org.apache.http.util.EntityUtils;
3334 import org.apache.log4j.Level;
3435 import org.apache.log4j.Logger;
3536 import org.codehaus.stax2.typed.TypedXMLStreamReader;
3738 import javax.xml.stream.XMLStreamConstants;
3839 import javax.xml.stream.XMLStreamException;
3940 import javax.xml.stream.XMLStreamReader;
40 import java.io.*;
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;
4150 import java.nio.charset.StandardCharsets;
42 import java.util.*;
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;
4356 import java.util.zip.GZIPInputStream;
4457
4558 /**
4659 * EWS SOAP method.
4760 */
48 public abstract class EWSMethod extends PostMethod {
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();
4963 protected static final Logger LOGGER = Logger.getLogger(EWSMethod.class);
5064 protected static final int CHUNK_LENGTH = 131072;
5165
91105
92106 protected String serverVersion;
93107 protected String timezoneContext;
108 private HttpResponse response;
94109
95110 /**
96111 * Build EWS method
110125 * @param responseCollectionName item response collection name
111126 */
112127 public EWSMethod(String itemType, String methodName, String responseCollectionName) {
113 super("/ews/exchange.asmx");
128 super(URI.create("/ews/exchange.asmx"));
114129 this.itemType = itemType;
115130 this.methodName = methodName;
116131 this.responseCollectionName = responseCollectionName;
117132 if (Settings.getBooleanProperty("davmail.acceptEncodingGzip", true) &&
118133 !Level.DEBUG.toString().equals(Settings.getProperty("log4j.logger.httpclient.wire"))) {
119 setRequestHeader("Accept-Encoding", "gzip");
120 }
121
122 setRequestEntity(new RequestEntity() {
134 setHeader("Accept-Encoding", "gzip");
135 }
136
137 AbstractHttpEntity httpEntity = new AbstractHttpEntity() {
123138 byte[] content;
124139
140 @Override
125141 public boolean isRepeatable() {
126142 return true;
127143 }
128144
129 public void writeRequest(OutputStream outputStream) throws IOException {
145 @Override
146 public long getContentLength() {
147 if (content == null) {
148 content = generateSoapEnvelope();
149 }
150 return content.length;
151 }
152
153 @Override
154 public InputStream getContent() throws UnsupportedOperationException {
155 if (content == null) {
156 content = generateSoapEnvelope();
157 }
158 return new ByteArrayInputStream(content);
159 }
160
161 @Override
162 public void writeTo(OutputStream outputStream) throws IOException {
130163 boolean firstPass = content == null;
131164 if (content == null) {
132165 content = generateSoapEnvelope();
150183 }
151184 }
152185
153 public long getContentLength() {
154 if (content == null) {
155 content = generateSoapEnvelope();
156 }
157 return content.length;
158 }
159
160 public String getContentType() {
161 return "text/xml; charset=UTF-8";
162 }
163 });
164 }
165
166
167 @Override
168 public String getName() {
169 return "POST";
186 @Override
187 public boolean isStreaming() {
188 return false;
189 }
190 };
191
192 httpEntity.setContentType(CONTENT_TYPE);
193 setEntity(httpEntity);
170194 }
171195
172196 protected void addAdditionalProperty(FieldURI additionalProperty) {
770794 && !"ErrorMailRecipientNotFound".equals(errorDetail)
771795 && !"ErrorItemNotFound".equals(errorDetail)
772796 && !"ErrorCalendarOccurrenceIsDeletedFromRecurrence".equals(errorDetail)
773 ) {
797 ) {
774798 throw new EWSException(errorDetail
775799 + ' ' + ((errorDescription != null) ? errorDescription : "")
776800 + ' ' + ((errorValue != null) ? errorValue : "")
778802 }
779803 }
780804 if (getStatusCode() == HttpStatus.SC_BAD_REQUEST || getStatusCode() == HttpStatus.SC_INSUFFICIENT_STORAGE) {
781 throw new EWSException(getStatusText());
782 }
783 }
784
785 @Override
805 throw new EWSException(response.getStatusLine().getReasonPhrase());
806 }
807 }
808
786809 public int getStatusCode() {
787810 if ("ErrorAccessDenied".equals(errorDetail)) {
788811 return HttpStatus.SC_FORBIDDEN;
789812 } else if ("ErrorItemNotFound".equals(errorDetail)) {
790813 return HttpStatus.SC_NOT_FOUND;
791814 } else {
792 return super.getStatusCode();
815 return response.getStatusLine().getStatusCode();
793816 }
794817 }
795818
850873 if (event == XMLStreamConstants.CHARACTERS) {
851874 result.append(reader.getText());
852875 } else if ("MessageXml".equals(localName) && event == XMLStreamConstants.START_ELEMENT) {
853 for (int i = 0;i<reader.getAttributeCount();i++) {
876 for (int i = 0; i < reader.getAttributeCount(); i++) {
854877 if (result.length() > 0) {
855878 result.append(", ");
856879 }
883906 && !"ErrorNameResolutionMultipleResults".equals(result)
884907 && !"ErrorNameResolutionNoResults".equals(result)
885908 && !"ErrorFolderExists".equals(result)
886 ) {
909 ) {
887910 errorDetail = result;
888911 }
889912 if (XMLStreamUtil.isStartTag(reader, "faultstring")) {
10031026 if (XMLStreamUtil.isStartTag(reader)) {
10041027 String tagLocalName = reader.getLocalName();
10051028 if ("EmailAddress".equals(tagLocalName) && member == null) {
1006 member = "mailto:"+XMLStreamUtil.getElementText(reader);
1029 member = "mailto:" + XMLStreamUtil.getElementText(reader);
10071030 }
10081031 }
10091032 }
11651188 }
11661189
11671190 @Override
1168 protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
1169 Header contentTypeHeader = getResponseHeader("Content-Type");
1191 public EWSMethod handleResponse(HttpResponse response) {
1192 this.response = response;
1193 org.apache.http.Header contentTypeHeader = response.getFirstHeader("Content-Type");
11701194 if (contentTypeHeader != null && "text/xml; charset=utf-8".equals(contentTypeHeader.getValue())) {
1171 try {
1172 if (DavGatewayHttpClientFacade.isGzipEncoded(this)) {
1173 processResponseStream(new GZIPInputStream(getResponseBodyAsStream()));
1195 try (
1196 InputStream inputStream = response.getEntity().getContent();
1197 ) {
1198 if (HttpClientAdapter.isGzipEncoded(response)) {
1199 processResponseStream(new GZIPInputStream(inputStream));
11741200 } else {
1175 processResponseStream(getResponseBodyAsStream());
1201 processResponseStream(inputStream);
11761202 }
11771203 } catch (IOException e) {
11781204 LOGGER.error("Error while parsing soap response: " + e, e);
11791205 }
11801206 }
1207 return this;
11811208 }
11821209
11831210 protected void processResponseStream(InputStream inputStream) {
2727 import davmail.exchange.VObject;
2828 import davmail.exchange.VProperty;
2929 import davmail.exchange.auth.O365Token;
30 import davmail.http.DavGatewayHttpClientFacade;
30 import davmail.http.HttpClientAdapter;
31 import davmail.http.request.GetRequest;
3132 import davmail.ui.NotificationDialog;
3233 import davmail.util.IOUtil;
3334 import davmail.util.StringUtil;
34 import org.apache.commons.httpclient.HttpClient;
35 import org.apache.commons.httpclient.HttpStatus;
36 import org.apache.commons.httpclient.methods.GetMethod;
37 import org.apache.commons.httpclient.params.HttpClientParams;
35 import org.apache.http.HttpStatus;
36 import org.apache.http.client.methods.CloseableHttpResponse;
3837
3938 import javax.mail.MessagingException;
4039 import javax.mail.Session;
137136 // Unable to map CANCELLED: cancelled events are directly deleted on Exchange
138137 }
139138
140 protected HttpClient httpClient;
139 protected HttpClientAdapter httpClient;
141140
142141 protected Map<String, String> folderIdMap;
143142 protected boolean directEws;
167166 }
168167 }
169168
170 public EwsExchangeSession(HttpClient httpClient, String userName) throws IOException {
169 public EwsExchangeSession(HttpClientAdapter httpClient, String userName) throws IOException {
171170 this.httpClient = httpClient;
172171 this.userName = userName;
173172 if (userName.contains("@")) {
176175 buildSessionInfo(null);
177176 }
178177
179 public EwsExchangeSession(HttpClient httpClient, URI uri, String userName) throws IOException {
178 public EwsExchangeSession(HttpClientAdapter httpClient, URI uri, String userName) throws IOException {
180179 this.httpClient = httpClient;
181180 this.userName = userName;
182181 if (userName.contains("@")) {
186185 buildSessionInfo(uri);
187186 }
188187
189 public EwsExchangeSession(HttpClient httpClient, O365Token token, String userName) throws IOException {
188 public EwsExchangeSession(HttpClientAdapter httpClient, O365Token token, String userName) throws IOException {
190189 this.httpClient = httpClient;
191190 this.userName = userName;
192191 if (userName.contains("@")) {
197196 buildSessionInfo(null);
198197 }
199198
199 public EwsExchangeSession(URI uri, O365Token token, String userName) throws IOException {
200 this(new HttpClientAdapter(uri, true), token, userName);
201 }
202
203 public EwsExchangeSession(String url, String userName, String password) throws IOException {
204 this(new HttpClientAdapter(url, userName, password, true), userName);
205 }
206
200207 /**
201208 * EWS fetch page size.
209 *
202210 * @return page size
203211 */
204212 private static int getPageSize() {
205213 return Settings.getIntProperty("davmail.folderFetchPageSize", PAGE_SIZE);
206 }
207
208 /**
209 * Authentication mode test: EWS is never form based.
210 *
211 * @param url exchange base URL
212 * @param httpClient httpClient instance
213 * @return true if basic authentication detected
214 */
215 protected boolean isBasicAuthentication(HttpClient httpClient, String url) {
216 return !url.toLowerCase().endsWith("/ews/exchange.asmx")
217 && DavGatewayHttpClientFacade.getHttpStatus(httpClient, url) == org.apache.http.HttpStatus.SC_UNAUTHORIZED;
218214 }
219215
220216 /**
226222 GetFolderMethod checkMethod = new GetFolderMethod(BaseShape.ID_ONLY,
227223 DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null);
228224 int status = executeMethod(checkMethod);
229 // add NTLM if required
230 if ((status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED)
231 && DavGatewayHttpClientFacade.acceptsNTLMOnly(checkMethod) && !DavGatewayHttpClientFacade.hasNTLMorNegotiate(httpClient)) {
232 LOGGER.debug("Received " + status + " unauthorized at " + checkMethod.getURI() + ", retrying with NTLM");
233 DavGatewayHttpClientFacade.addNTLM(httpClient);
234 checkMethod = new GetFolderMethod(BaseShape.ID_ONLY,
235 DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null);
236 status = executeMethod(checkMethod);
237 }
225
238226 if (status == HttpStatus.SC_UNAUTHORIZED) {
239227 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
240228 } else if (status != HttpStatus.SC_OK) {
244232
245233 @Override
246234 public void buildSessionInfo(java.net.URI uri) throws IOException {
247 // send a first request to get server version
235 // send a first request to get server version
248236 checkEndPointUrl();
249237
250238 // new approach based on ConvertId to find primary email address
263251 if (convertIdItem != null && !convertIdItem.isEmpty()) {
264252 email = convertIdItem.get("Mailbox");
265253 alias = email.substring(0, email.indexOf('@'));
254 } else {
255 LOGGER.error("Unable to resolve email from root folder");
256 throw new IOException();
266257 }
267258
268259 } catch (IOException e) {
274265 || "/ews/services.wsdl".equalsIgnoreCase(uri.getPath())
275266 || "/ews/exchange.asmx".equalsIgnoreCase(uri.getPath());
276267
277 // TODO: no longer needed
278 // options page is not available in direct EWS mode
279 //if (!directEws && (email == null || alias == null)) {
280 // retrieve email and alias from options page
281 // getEmailAndAliasFromOptions();
282 //}
283
284 // failover, should not happen
285 if (email == null || alias == null) {
286 // OWA authentication failed, get email address from login
287 if (userName.indexOf('@') >= 0) {
288 // userName is email address
289 email = userName;
290 alias = userName.substring(0, userName.indexOf('@'));
291 } else {
292 // userName or domain\\username, rebuild email address
293 alias = getAliasFromLogin();
294
295 // try to get email address with ResolveNames
296 resolveEmailAddress(userName);
297 // failover, build from host name
298 if (email == null) {
299 email = getAliasFromLogin() + getEmailSuffixFromHostname();
300 }
301 }
302 }
303
304268 currentMailboxPath = "/users/" + email.toLowerCase();
305
306 // enable preemptive authentication on non NTLM endpoints
307 if (!DavGatewayHttpClientFacade.hasNTLMorNegotiate(httpClient)) {
308 httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, true);
309 }
310
311 // direct EWS: get primary smtp email address with ResolveNames
312 if (directEws && email == null) {
313 try {
314 ResolveNamesMethod resolveNamesMethod = new ResolveNamesMethod(alias);
315 executeMethod(resolveNamesMethod);
316 List<EWSMethod.Item> responses = resolveNamesMethod.getResponseItems();
317 for (EWSMethod.Item response : responses) {
318 if (alias.equalsIgnoreCase(response.get("Name"))) {
319 email = response.get("EmailAddress");
320 currentMailboxPath = "/users/" + email.toLowerCase();
321 }
322 }
323 } catch (IOException e) {
324 LOGGER.warn("Unable to get primary email address with ResolveNames", e);
325 }
326 }
327269
328270 try {
329271 folderIdMap = new HashMap<>();
344286 }
345287
346288 protected String getEmailSuffixFromHostname() {
347 String domain = httpClient.getHostConfiguration().getHost();
289 String domain = httpClient.getHost();
348290 int start = domain.lastIndexOf('.', domain.lastIndexOf('.') - 1);
349291 if (start >= 0) {
350292 return '@' + domain.substring(start + 1);
703645 resultCount = results.size();
704646 if (resultCount > 0 && LOGGER.isDebugEnabled()) {
705647 LOGGER.debug("Folder " + folderPath + " - Search items count: " + resultCount + " maxCount: " + maxCount
706 + " highest uid: " + results.get(0).get(Field.get("imapUid").getResponseName())
707 + " lowest uid: " + results.get(resultCount - 1).get(Field.get("imapUid").getResponseName()));
648 + " highest uid: " + results.get(0).getLong(Field.get("imapUid").getResponseName())
649 + " lowest uid: " + results.get(resultCount - 1).getLong(Field.get("imapUid").getResponseName()));
708650 }
709651
710652
750692
751693 long highestUid = 0;
752694 if (resultCount > 0) {
753 highestUid = Long.parseLong(results.get(resultCount - 1).get(Field.get("imapUid").getResponseName()));
695 highestUid = results.get(resultCount - 1).getLong(Field.get("imapUid").getResponseName());
754696 }
755697 // Only add new result if not already available (concurrent folder changes issue)
756698 for (EWSMethod.Item item : findItemMethod.getResponseItems()) {
757 long imapUid = Long.parseLong(item.get(Field.get("imapUid").getResponseName()));
699 long imapUid = item.getLong(Field.get("imapUid").getResponseName());
758700 if (imapUid > highestUid) {
759701 results.add(item);
760702 }
762704 resultCount = results.size();
763705 if (resultCount > 0 && LOGGER.isDebugEnabled()) {
764706 LOGGER.debug("Folder " + folderPath + " - Search items current count: " + resultCount + " fetchCount: " + getPageSize()
765 + " highest uid: " + results.get(resultCount - 1).get(Field.get("imapUid").getResponseName())
766 + " lowest uid: " + results.get(0).get(Field.get("imapUid").getResponseName()));
707 + " highest uid: " + results.get(resultCount - 1).getLong(Field.get("imapUid").getResponseName())
708 + " lowest uid: " + results.get(0).getLong(Field.get("imapUid").getResponseName()));
767709 }
768710 if (Thread.interrupted()) {
769711 LOGGER.debug("Folder " + folderPath + " - Search items failed: Interrupted by client");
795737 }
796738
797739 if (actualConditionCount > 1) {
798 buffer.append("</t:").append(operator.toString()).append('>');
740 buffer.append("</t:").append(operator).append('>');
799741 }
800742 }
801743 }
831773
832774 protected FieldURI getFieldURI() {
833775 FieldURI fieldURI = Field.get(attributeName);
776 // check to detect broken field mapping
777 //noinspection ConstantConditions
834778 if (fieldURI == null) {
835779 throw new IllegalArgumentException("Unknown field: " + attributeName);
836780 }
878822 buffer.append("</t:FieldURIOrConstant>");
879823 }
880824
881 buffer.append("</t:").append(operator.toString()).append('>');
825 buffer.append("</t:").append(operator).append('>');
882826 }
883827
884828 public boolean isMatch(ExchangeSession.Contact contact) {
21082052 }
21092053
21102054 // handle deleted occurrences
2111 if (!vCalendar.isTodo() && currentItemId != null && !isMeetingResponse &&
2112 Settings.getBooleanProperty("davmail.caldavRealUpdate", false)) {
2055 if (!vCalendar.isTodo() && currentItemId != null && !isMeetingResponse) {
21132056 handleExcludedDates(currentItemId, vCalendar);
21142057 handleModifiedOccurrences(currentItemId, vCalendar);
21152058 }
26502593
26512594 @Override
26522595 public int sendEvent(String icsBody) throws IOException {
2653 String itemName = UUID.randomUUID().toString() + ".EML";
2596 String itemName = UUID.randomUUID() + ".EML";
26542597 byte[] mimeContent = new Event(DRAFTS, itemName, "urn:content-classes:calendarmessage", icsBody, null, null).createMimeContent();
26552598 if (mimeContent == null) {
26562599 // no recipients, cancel
27562699 protected String getTimezoneidFromOptions() {
27572700 String result = null;
27582701 // get time zone setting from html body
2759 BufferedReader optionsPageReader = null;
2760 GetMethod optionsMethod = new GetMethod("/owa/?ae=Options&t=Regional");
2761 try {
2762 DavGatewayHttpClientFacade.executeGetMethod(httpClient, optionsMethod, false);
2763 optionsPageReader = new BufferedReader(new InputStreamReader(optionsMethod.getResponseBodyAsStream(), StandardCharsets.UTF_8));
2702 String optionsPath = "/owa/?ae=Options&t=Regional";
2703 GetRequest optionsMethod = new GetRequest(optionsPath);
2704 try (
2705 CloseableHttpResponse response = httpClient.execute(optionsMethod);
2706 BufferedReader optionsPageReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))
2707 ) {
27642708 String line;
27652709 // find timezone
27662710 //noinspection StatementWithEmptyBody
27802724 }
27812725 }
27822726 } catch (IOException e) {
2783 LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
2784 } finally {
2785 if (optionsPageReader != null) {
2786 try {
2787 optionsPageReader.close();
2788 } catch (IOException e) {
2789 LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
2790 }
2791 }
2792 optionsMethod.releaseConnection();
2727 LOGGER.error("Error parsing options page at " + optionsPath);
27932728 }
27942729
27952730 return result;
29842919 }
29852920
29862921 protected void internalExecuteMethod(EWSMethod ewsMethod) throws IOException {
2987 try {
2988 ewsMethod.setServerVersion(serverVersion);
2989 if (token != null) {
2990 ewsMethod.setRequestHeader("Authorization", "Bearer " + token.getAccessToken());
2991 }
2992 httpClient.executeMethod(ewsMethod);
2993 if (serverVersion == null) {
2994 serverVersion = ewsMethod.getServerVersion();
2995 }
2996 ewsMethod.checkSuccess();
2997 } finally {
2998 ewsMethod.releaseConnection();
2999 }
2922 ewsMethod.setServerVersion(serverVersion);
2923 if (token != null) {
2924 ewsMethod.setHeader("Authorization", "Bearer " + token.getAccessToken());
2925 }
2926 try (CloseableHttpResponse response = httpClient.execute(ewsMethod)) {
2927 ewsMethod.handleResponse(response);
2928 }
2929 if (serverVersion == null) {
2930 serverVersion = ewsMethod.getServerVersion();
2931 }
2932 ewsMethod.checkSuccess();
30002933 }
30012934
30022935 protected static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<>();
32183151 * @return true if itemName is an EWS item id
32193152 */
32203153 protected static boolean isItemId(String itemName) {
3221 return itemName.length() >= 152
3154 return itemName.length() >= 144
32223155 // item name is base64url
3223 //&& itemName.matches("^([A-Za-z0-9-_]{4})*([A-Za-z0-9-_]{4}|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{2}==)$")
3156 && itemName.matches("^([A-Za-z0-9-_]{4})*([A-Za-z0-9-_]{4}|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{2}==)\\.EML$")
32243157 && itemName.indexOf(' ') < 0;
32253158 }
32263159
32723205 */
32733206 @Override
32743207 public void close() {
3275 DavGatewayHttpClientFacade.close(httpClient);
3208 httpClient.close();
32763209 }
32773210
32783211 }
+0
-863
src/java/davmail/http/DavGatewayHttpClientFacade.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2009 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.http;
19
20 import davmail.BundleMessage;
21 import davmail.Settings;
22 import davmail.exception.*;
23 import davmail.exchange.dav.ExchangeDavMethod;
24 import davmail.exchange.dav.ExchangeSearchMethod;
25 import davmail.ui.tray.DavGatewayTray;
26 import org.apache.commons.httpclient.*;
27 import org.apache.commons.httpclient.URI;
28 import org.apache.commons.httpclient.auth.AuthPolicy;
29 import org.apache.commons.httpclient.auth.AuthScope;
30 import org.apache.commons.httpclient.cookie.CookiePolicy;
31 import org.apache.commons.httpclient.methods.DeleteMethod;
32 import org.apache.commons.httpclient.methods.GetMethod;
33 import org.apache.commons.httpclient.params.HttpClientParams;
34 import org.apache.commons.httpclient.params.HttpMethodParams;
35 import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
36 import org.apache.http.client.HttpResponseException;
37 import org.apache.jackrabbit.webdav.DavException;
38 import org.apache.jackrabbit.webdav.MultiStatusResponse;
39 import org.apache.jackrabbit.webdav.client.methods.CopyMethod;
40 import org.apache.jackrabbit.webdav.client.methods.DavMethodBase;
41 import org.apache.jackrabbit.webdav.client.methods.MoveMethod;
42 import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
43 import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
44 import org.apache.log4j.Logger;
45
46 import java.io.IOException;
47 import java.net.*;
48 import java.security.Security;
49 import java.util.*;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
52
53 /**
54 * Create HttpClient instance according to DavGateway Settings
55 */
56 public final class DavGatewayHttpClientFacade {
57 static final Logger LOGGER = Logger.getLogger("davmail.http.DavGatewayHttpClientFacade");
58
59 public static final String IE_USER_AGENT = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; Microsoft Outlook 15.0.4420)";
60 static final int MAX_REDIRECTS = 10;
61 static final Object LOCK = new Object();
62 private static boolean needNTLM;
63
64 static final long ONE_MINUTE = 60000;
65
66 static String WORKSTATION_NAME = "UNKNOWN";
67
68 private static IdleConnectionTimeoutThread httpConnectionManagerThread;
69
70 private static Set<MultiThreadedHttpConnectionManager> ALL_CONNECTION_MANAGERS = new HashSet<>();
71
72 static {
73 // disable Client-initiated TLS renegotiation
74 System.setProperty("jdk.tls.rejectClientInitiatedRenegotiation", "true");
75 // force strong ephemeral Diffie-Hellman parameter
76 System.setProperty("jdk.tls.ephemeralDHKeySize", "2048");
77
78 Security.setProperty("ssl.SocketFactory.provider", "davmail.http.DavGatewaySSLSocketFactory");
79
80 // reenable basic proxy authentication on Java >= 1.8.111
81 System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
82
83 DavGatewayHttpClientFacade.start();
84
85 // register custom cookie policy
86 CookiePolicy.registerCookieSpec("DavMailCookieSpec", DavMailCookieSpec.class);
87
88 AuthPolicy.registerAuthScheme(AuthPolicy.BASIC, LenientBasicScheme.class);
89 // register the jcifs based NTLMv2 implementation
90 AuthPolicy.registerAuthScheme(AuthPolicy.NTLM, NTLMv2Scheme.class);
91 try {
92 WORKSTATION_NAME = InetAddress.getLocalHost().getHostName();
93 } catch (Throwable t) {
94 // ignore
95 }
96
97 // set system property *before* calling ProxySelector.getDefault()
98 if (Settings.getBooleanProperty("davmail.useSystemProxies", Boolean.FALSE)) {
99 System.setProperty("java.net.useSystemProxies", "true");
100 }
101 ProxySelector.setDefault(new DavGatewayProxySelector(ProxySelector.getDefault()));
102 }
103
104
105 private DavGatewayHttpClientFacade() {
106 }
107
108 /**
109 * Create basic http client with default params.
110 *
111 * @return HttpClient instance
112 */
113 private static HttpClient getBaseInstance() {
114 HttpClient httpClient = new HttpClient();
115 httpClient.getParams().setParameter(HttpMethodParams.USER_AGENT, getUserAgent());
116 httpClient.getParams().setParameter(HttpClientParams.MAX_REDIRECTS, Settings.getIntProperty("davmail.httpMaxRedirects", MAX_REDIRECTS));
117 httpClient.getParams().setCookiePolicy("DavMailCookieSpec");
118 return httpClient;
119 }
120
121 /**
122 * Create a configured HttpClient instance.
123 *
124 * @param url target url
125 * @return httpClient
126 * @throws DavMailException on error
127 */
128 public static HttpClient getInstance(String url) throws DavMailException {
129 // create an HttpClient instance
130 HttpClient httpClient = getBaseInstance();
131 configureClient(httpClient, url);
132 return httpClient;
133 }
134
135 /**
136 * Set credentials on HttpClient instance.
137 *
138 * @param httpClient httpClient instance
139 * @param userName user name
140 * @param password user password
141 */
142 public static void setCredentials(HttpClient httpClient, String userName, String password) {
143 // some Exchange servers redirect to a different host for freebusy, use wide auth scope
144 AuthScope authScope = new AuthScope(null, -1);
145 int backSlashIndex = userName.indexOf('\\');
146 if (needNTLM && backSlashIndex >= 0) {
147 // separate domain from username in credentials
148 String domain = userName.substring(0, backSlashIndex);
149 userName = userName.substring(backSlashIndex + 1);
150 httpClient.getState().setCredentials(authScope, new NTCredentials(userName, password, WORKSTATION_NAME, domain));
151 } else {
152 httpClient.getState().setCredentials(authScope, new NTCredentials(userName, password, WORKSTATION_NAME, ""));
153 }
154 }
155
156 /**
157 * Set http client current host configuration.
158 *
159 * @param httpClient current Http client
160 * @param url target url
161 * @throws DavMailException on error
162 */
163 public static void setClientHost(HttpClient httpClient, String url) throws DavMailException {
164 try {
165 HostConfiguration hostConfig = httpClient.getHostConfiguration();
166 URI httpURI = new URI(url, true);
167 hostConfig.setHost(httpURI);
168 } catch (URIException e) {
169 throw new DavMailException("LOG_INVALID_URL", url);
170 }
171 }
172
173 protected static boolean isNoProxyFor(java.net.URI uri) {
174 final String noProxyFor = Settings.getProperty("davmail.noProxyFor");
175 if (noProxyFor != null) {
176 final String urihost = uri.getHost().toLowerCase();
177 final String[] domains = noProxyFor.toLowerCase().split(",\\s*");
178 for (String domain : domains) {
179 if (urihost.endsWith(domain)) {
180 return true; //break;
181 }
182 }
183 }
184 return false;
185 }
186
187 /**
188 * Update http client configuration (proxy)
189 *
190 * @param httpClient current Http client
191 * @param url target url
192 * @throws DavMailException on error
193 */
194 public static void configureClient(HttpClient httpClient, String url) throws DavMailException {
195 setClientHost(httpClient, url);
196
197 // force NTLM in direct EWS mode
198 if (!needNTLM && url.toLowerCase().endsWith("/ews/exchange.asmx") && !Settings.getBooleanProperty("davmail.disableNTLM", false)) {
199 needNTLM = true;
200 }
201
202 if (Settings.getBooleanProperty("davmail.enableKerberos", false)) {
203 AuthPolicy.registerAuthScheme("Negotiate", SpNegoScheme.class);
204 ArrayList<String> authPrefs = new ArrayList<>();
205 authPrefs.add("Negotiate");
206 httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
207 } else if (!needNTLM) {
208 ArrayList<String> authPrefs = new ArrayList<>();
209 authPrefs.add(AuthPolicy.DIGEST);
210 authPrefs.add(AuthPolicy.BASIC);
211 // exclude NTLM authentication scheme
212 httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
213 }
214
215 boolean enableProxy = Settings.getBooleanProperty("davmail.enableProxy");
216 boolean useSystemProxies = Settings.getBooleanProperty("davmail.useSystemProxies", Boolean.FALSE);
217 String proxyHost = null;
218 int proxyPort = 0;
219 String proxyUser = null;
220 String proxyPassword = null;
221
222 try {
223 java.net.URI uri = new java.net.URI(url);
224 if (useSystemProxies) {
225 // get proxy for url from system settings
226 System.setProperty("java.net.useSystemProxies", "true");
227 List<Proxy> proxyList = getProxyForURI(uri);
228 if (!proxyList.isEmpty() && proxyList.get(0).address() != null) {
229 InetSocketAddress inetSocketAddress = (InetSocketAddress) proxyList.get(0).address();
230 proxyHost = inetSocketAddress.getHostName();
231 proxyPort = inetSocketAddress.getPort();
232
233 // we may still need authentication credentials
234 proxyUser = Settings.getProperty("davmail.proxyUser");
235 proxyPassword = Settings.getProperty("davmail.proxyPassword");
236 }
237 } else if (isNoProxyFor(uri)) {
238 LOGGER.debug("no proxy for " + uri.getHost());
239 } else if (enableProxy) {
240 proxyHost = Settings.getProperty("davmail.proxyHost");
241 proxyPort = Settings.getIntProperty("davmail.proxyPort");
242 proxyUser = Settings.getProperty("davmail.proxyUser");
243 proxyPassword = Settings.getProperty("davmail.proxyPassword");
244 }
245 } catch (URISyntaxException e) {
246 throw new DavMailException("LOG_INVALID_URL", url);
247 }
248
249 // configure proxy
250 if (proxyHost != null && proxyHost.length() > 0) {
251 httpClient.getHostConfiguration().setProxy(proxyHost, proxyPort);
252 if (proxyUser != null && proxyUser.length() > 0) {
253
254 AuthScope authScope = new AuthScope(proxyHost, proxyPort, AuthScope.ANY_REALM);
255
256 // detect ntlm authentication (windows domain name in user name)
257 int backslashindex = proxyUser.indexOf('\\');
258 if (backslashindex > 0) {
259 httpClient.getState().setProxyCredentials(authScope,
260 new NTCredentials(proxyUser.substring(backslashindex + 1),
261 proxyPassword, WORKSTATION_NAME,
262 proxyUser.substring(0, backslashindex)));
263 } else {
264 httpClient.getState().setProxyCredentials(authScope,
265 new NTCredentials(proxyUser, proxyPassword, WORKSTATION_NAME, ""));
266 }
267 }
268 }
269
270 }
271
272 /**
273 * Make sure we close all connections immediately after a session creation failure.
274 *
275 * @param httpClient http client to close
276 */
277 public static void close(HttpClient httpClient) {
278 if (httpClient != null) {
279 synchronized (LOCK) {
280 MultiThreadedHttpConnectionManager httpConnectionManager = ((MultiThreadedHttpConnectionManager) httpClient.getHttpConnectionManager());
281 ALL_CONNECTION_MANAGERS.remove(httpConnectionManager);
282 httpConnectionManager.shutdown();
283 }
284 }
285 }
286
287 /**
288 * Retrieve Proxy Selector
289 *
290 * @param uri target uri
291 * @return proxy selector
292 */
293 private static List<Proxy> getProxyForURI(java.net.URI uri) {
294 LOGGER.debug("get Default proxy selector");
295 ProxySelector proxySelector = ProxySelector.getDefault();
296 LOGGER.debug("getProxyForURI(" + uri + ')');
297 List<Proxy> proxies = proxySelector.select(uri);
298 LOGGER.debug("got system proxies:" + proxies);
299 return proxies;
300 }
301
302
303 /**
304 * Get Http Status code for the given URL
305 *
306 * @param httpClient httpClient instance
307 * @param url url string
308 * @return HttpStatus code
309 */
310 public static int getHttpStatus(HttpClient httpClient, String url) {
311 int status = 0;
312 HttpMethod testMethod = new GetMethod(url);
313 testMethod.setDoAuthentication(false);
314 try {
315 status = httpClient.executeMethod(testMethod);
316 } catch (IOException e) {
317 LOGGER.warn(e.getMessage(), e);
318 } finally {
319 testMethod.releaseConnection();
320 }
321 return status;
322 }
323
324 /**
325 * Check if status is a redirect (various 30x values).
326 *
327 * @param status Http status
328 * @return true if status is a redirect
329 */
330 public static boolean isRedirect(int status) {
331 return status == HttpStatus.SC_MOVED_PERMANENTLY
332 || status == HttpStatus.SC_MOVED_TEMPORARILY
333 || status == HttpStatus.SC_SEE_OTHER
334 || status == HttpStatus.SC_TEMPORARY_REDIRECT;
335 }
336
337 /**
338 * Execute given url, manually follow redirects.
339 * Workaround for HttpClient bug (GET full URL over HTTPS and proxy)
340 *
341 * @param httpClient HttpClient instance
342 * @param url url string
343 * @return executed method
344 * @throws IOException on error
345 */
346 public static HttpMethod executeFollowRedirects(HttpClient httpClient, String url) throws IOException {
347 HttpMethod method = new GetMethod(url);
348 method.setFollowRedirects(false);
349 return executeFollowRedirects(httpClient, method);
350 }
351
352 private static int executeMethod(HttpClient httpClient, HttpMethod currentMethod) throws IOException {
353 httpClient.executeMethod(currentMethod);
354 int status = currentMethod.getStatusCode();
355 if ((status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED)
356 && acceptsNTLMOnly(currentMethod) && !hasNTLMorNegotiate(httpClient)) {
357 LOGGER.debug("Received " + status + " unauthorized at " + currentMethod.getURI() + ", retrying with NTLM");
358 resetMethod(currentMethod);
359 addNTLM(httpClient);
360 status = httpClient.executeMethod(currentMethod);
361 }
362 return status;
363 }
364
365 /**
366 * Checks if there is a Javascript redirect inside the page,
367 * and returns it.
368 * <p/>
369 * A Javascript redirect is usually found on OTP pre-auth page,
370 * when the pre-auth form is in a distinct page from the regular Exchange login one.
371 *
372 * @param method http method
373 * @return the redirect URL if found, or null if no Javascript redirect has been found
374 */
375 private static String getJavascriptRedirectUrl(HttpMethod method) throws IOException {
376 String responseBody = method.getResponseBodyAsString();
377 String jsRedirectionUrl = null;
378 if (responseBody.indexOf("javascript:go_url()") > 0) {
379 // Create a pattern to match a javascript redirect url
380 Pattern p = Pattern.compile("go_url\\(\\)[^{]+\\{[^l]+location.replace\\(\"(/[^\"]+)\"\\)");
381 Matcher m = p.matcher(responseBody);
382 if (m.find()) {
383 // Javascript redirect found!
384 jsRedirectionUrl = m.group(1);
385 }
386 }
387 return jsRedirectionUrl;
388 }
389
390
391 private static String getLocationValue(HttpMethod method) {
392 String locationValue = null;
393 Header location = method.getResponseHeader("Location");
394 if (location != null && isRedirect(method.getStatusCode())) {
395 locationValue = location.getValue();
396 // Novell iChain workaround
397 if (locationValue.indexOf('"') >= 0) {
398 locationValue = URIUtil.encodePath(locationValue);
399 }
400 // workaround for invalid relative location
401 if (locationValue.startsWith("./")) {
402 locationValue = locationValue.substring(1);
403 }
404 }
405 return locationValue;
406 }
407
408 /**
409 * Execute method with httpClient, follow 30x redirects.
410 *
411 * @param httpClient Http client instance
412 * @param method Http method
413 * @return last http method after redirects
414 * @throws IOException on error
415 */
416 public static HttpMethod executeFollowRedirects(HttpClient httpClient, HttpMethod method) throws IOException {
417 HttpMethod currentMethod = method;
418 try {
419 DavGatewayTray.debug(new BundleMessage("LOG_EXECUTE_FOLLOW_REDIRECTS", currentMethod.getURI()));
420 executeMethod(httpClient, currentMethod);
421
422 String locationValue = getLocationValue(currentMethod);
423 // check javascript redirect (multiple authentication pages)
424 if (locationValue == null) {
425 locationValue = getJavascriptRedirectUrl(currentMethod);
426 }
427
428 int redirectCount = 0;
429 while (redirectCount++ < Settings.getIntProperty("davmail.httpMaxRedirects", MAX_REDIRECTS)
430 && locationValue != null) {
431 currentMethod.releaseConnection();
432 currentMethod = new GetMethod(locationValue);
433 currentMethod.setFollowRedirects(false);
434 DavGatewayTray.debug(new BundleMessage("LOG_EXECUTE_FOLLOW_REDIRECTS_COUNT", currentMethod.getURI(), redirectCount));
435 executeMethod(httpClient, currentMethod);
436 locationValue = getLocationValue(currentMethod);
437 }
438 if (locationValue != null) {
439 currentMethod.releaseConnection();
440 throw new HttpServerErrorException("Maximum redirections reached");
441 }
442 } catch (IOException e) {
443 currentMethod.releaseConnection();
444 throw e;
445 }
446 // caller will need to release connection
447 return currentMethod;
448 }
449
450 /**
451 * Execute method with httpClient, do not follow 30x redirects.
452 *
453 * @param httpClient Http client instance
454 * @param method Http method
455 * @return status
456 * @throws IOException on error
457 */
458 public static int executeNoRedirect(HttpClient httpClient, HttpMethod method) throws IOException {
459 int status;
460 try {
461 status = executeMethod(httpClient, method);
462 } finally {
463 method.releaseConnection();
464 }
465 // caller will need to release connection
466 return status;
467 }
468
469 /**
470 * Execute webdav search method.
471 *
472 * @param httpClient http client instance
473 * @param path <i>encoded</i> searched folder path
474 * @param searchRequest (SQL like) search request
475 * @param maxCount max item count
476 * @return Responses enumeration
477 * @throws IOException on error
478 */
479 public static MultiStatusResponse[] executeSearchMethod(HttpClient httpClient, String path, String searchRequest,
480 int maxCount) throws IOException {
481 ExchangeSearchMethod searchMethod = new ExchangeSearchMethod(path, searchRequest);
482 if (maxCount > 0) {
483 searchMethod.addRequestHeader("Range", "rows=0-" + (maxCount - 1));
484 }
485 return executeMethod(httpClient, searchMethod);
486 }
487
488 /**
489 * Execute webdav propfind method.
490 *
491 * @param httpClient http client instance
492 * @param path <i>encoded</i> searched folder path
493 * @param depth propfind request depth
494 * @param properties propfind requested properties
495 * @return Responses enumeration
496 * @throws IOException on error
497 */
498 public static MultiStatusResponse[] executePropFindMethod(HttpClient httpClient, String path, int depth, DavPropertyNameSet properties) throws IOException {
499 PropFindMethod propFindMethod = new PropFindMethod(path, properties, depth);
500 return executeMethod(httpClient, propFindMethod);
501 }
502
503 /**
504 * Execute a delete method on the given path with httpClient.
505 *
506 * @param httpClient Http client instance
507 * @param path Path to be deleted
508 * @return http status
509 * @throws IOException on error
510 */
511 public static int executeDeleteMethod(HttpClient httpClient, String path) throws IOException {
512 DeleteMethod deleteMethod = new DeleteMethod(path);
513 deleteMethod.setFollowRedirects(false);
514
515 int status = executeHttpMethod(httpClient, deleteMethod);
516 // do not throw error if already deleted
517 if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
518 throw DavGatewayHttpClientFacade.buildHttpResponseException(deleteMethod);
519 }
520 return status;
521 }
522
523 /**
524 * Execute webdav request.
525 *
526 * @param httpClient http client instance
527 * @param method webdav method
528 * @return Responses enumeration
529 * @throws IOException on error
530 */
531 public static MultiStatusResponse[] executeMethod(HttpClient httpClient, DavMethodBase method) throws IOException {
532 MultiStatusResponse[] responses;
533 try {
534 int status = executeMethodFollowRedirectOnce(httpClient, method);
535
536 if (status != HttpStatus.SC_MULTI_STATUS) {
537 throw buildHttpResponseException(method);
538 }
539 responses = method.getResponseBodyAsMultiStatus().getResponses();
540
541 } catch (DavException e) {
542 throw new IOException(e.getMessage());
543 } finally {
544 method.releaseConnection();
545 }
546 return responses;
547 }
548
549 /**
550 * Execute method, redirect once if returned status is redirect.
551 *
552 * @param httpClient http client
553 * @param method http method
554 * @return status
555 * @throws IOException on error
556 */
557 protected static int executeMethodFollowRedirectOnce(HttpClient httpClient, HttpMethod method) throws IOException {
558 int status = httpClient.executeMethod(method);
559
560 // need to follow redirects (once) on public folders
561 if (isRedirect(status)) {
562 method.releaseConnection();
563 URI targetUri = new URI(method.getResponseHeader("Location").getValue(), true);
564 checkExpiredSession(targetUri.getQuery());
565 method.setURI(targetUri);
566 status = httpClient.executeMethod(method);
567 }
568 return status;
569 }
570
571 /**
572 * Execute webdav request.
573 *
574 * @param httpClient http client instance
575 * @param method webdav method
576 * @return Responses enumeration
577 * @throws IOException on error
578 */
579 public static MultiStatusResponse[] executeMethod(HttpClient httpClient, ExchangeDavMethod method) throws IOException {
580 MultiStatusResponse[] responses;
581 try {
582 int status = executeMethodFollowRedirectOnce(httpClient, method);
583
584 if (status != HttpStatus.SC_MULTI_STATUS) {
585 throw buildHttpResponseException(method);
586 }
587 responses = method.getResponses();
588
589 } finally {
590 method.releaseConnection();
591 }
592 return responses;
593 }
594
595 /**
596 * Execute method with httpClient.
597 *
598 * @param httpClient Http client instance
599 * @param method Http method
600 * @return Http status
601 * @throws IOException on error
602 */
603 public static int executeHttpMethod(HttpClient httpClient, HttpMethod method) throws IOException {
604 int status;
605 try {
606 status = httpClient.executeMethod(method);
607 } finally {
608 method.releaseConnection();
609 }
610 return status;
611 }
612
613 /**
614 * Test if NTLM auth scheme is enabled.
615 *
616 * @param httpClient HttpClient instance
617 * @return true if NTLM is enabled
618 */
619 public static boolean hasNTLMorNegotiate(HttpClient httpClient) {
620 Object authPrefs = httpClient.getParams().getParameter(AuthPolicy.AUTH_SCHEME_PRIORITY);
621 return authPrefs == null || (authPrefs instanceof List<?> &&
622 (((Collection) authPrefs).contains(AuthPolicy.NTLM) || ((Collection) authPrefs).contains("Negotiate")));
623 }
624
625 /**
626 * Enable NTLM authentication on http client
627 *
628 * @param httpClient HttpClient instance
629 */
630 public static void addNTLM(HttpClient httpClient) {
631 // disable preemptive authentication
632 httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, false);
633
634 ArrayList<String> authPrefs = new ArrayList<>();
635 authPrefs.add(AuthPolicy.NTLM);
636 authPrefs.add(AuthPolicy.DIGEST);
637 authPrefs.add(AuthPolicy.BASIC);
638 httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
639
640 // make sure NTLM is always active
641 needNTLM = true;
642
643 // separate domain from username in credentials
644 AuthScope authScope = new AuthScope(null, -1);
645 NTCredentials credentials = (NTCredentials) httpClient.getState().getCredentials(authScope);
646 if (credentials != null && (credentials.getDomain() == null || credentials.getDomain().isEmpty())) {
647 setCredentials(httpClient, credentials.getUserName(), credentials.getPassword());
648 }
649 }
650
651 /**
652 * Test method header for supported authentication mode,
653 * return true if Basic authentication is not available
654 *
655 * @param getMethod http method
656 * @return true if only NTLM is enabled
657 */
658 public static boolean acceptsNTLMOnly(HttpMethod getMethod) {
659 Header authenticateHeader = null;
660 if (getMethod.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
661 authenticateHeader = getMethod.getResponseHeader("WWW-Authenticate");
662 } else if (getMethod.getStatusCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
663 authenticateHeader = getMethod.getResponseHeader("Proxy-Authenticate");
664 }
665 if (authenticateHeader == null) {
666 return false;
667 } else {
668 boolean acceptBasic = false;
669 boolean acceptNTLM = false;
670 HeaderElement[] headerElements = authenticateHeader.getElements();
671 for (HeaderElement headerElement : headerElements) {
672 if ("NTLM".equalsIgnoreCase(headerElement.getName())) {
673 acceptNTLM = true;
674 }
675 if ("Basic realm".equalsIgnoreCase(headerElement.getName())) {
676 acceptBasic = true;
677 }
678 }
679 return acceptNTLM && !acceptBasic;
680
681 }
682 }
683
684 /**
685 * Execute test method from checkConfig, with proxy credentials, but without Exchange credentials.
686 *
687 * @param httpClient Http client instance
688 * @param method Http method
689 * @return Http status
690 * @throws IOException on error
691 */
692 public static int executeTestMethod(HttpClient httpClient, GetMethod method) throws IOException {
693 // do not follow redirects in expired sessions
694 method.setFollowRedirects(false);
695 int status = httpClient.executeMethod(method);
696 if (status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED
697 && acceptsNTLMOnly(method) && !hasNTLMorNegotiate(httpClient)) {
698 resetMethod(method);
699 LOGGER.debug("Received " + status + " unauthorized at " + method.getURI() + ", retrying with NTLM");
700 addNTLM(httpClient);
701 status = httpClient.executeMethod(method);
702 }
703
704 return status;
705 }
706
707 /**
708 * Execute Get method, do not follow redirects.
709 *
710 * @param httpClient Http client instance
711 * @param method Http method
712 * @param followRedirects Follow redirects flag
713 * @throws IOException on error
714 */
715 public static void executeGetMethod(HttpClient httpClient, GetMethod method, boolean followRedirects) throws IOException {
716 // do not follow redirects in expired sessions
717 method.setFollowRedirects(followRedirects);
718 int status = httpClient.executeMethod(method);
719 if ((status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED)
720 && acceptsNTLMOnly(method) && !hasNTLMorNegotiate(httpClient)) {
721 resetMethod(method);
722 LOGGER.debug("Received " + status + " unauthorized at " + method.getURI() + ", retrying with NTLM");
723 addNTLM(httpClient);
724 status = httpClient.executeMethod(method);
725 }
726 if (status != HttpStatus.SC_OK && (followRedirects || !isRedirect(status))) {
727 LOGGER.warn("GET failed with status " + status + " at " + method.getURI());
728 if (status != HttpStatus.SC_NOT_FOUND && status != HttpStatus.SC_FORBIDDEN) {
729 LOGGER.warn(method.getResponseBodyAsString());
730 }
731 throw DavGatewayHttpClientFacade.buildHttpResponseException(method);
732 }
733 // check for expired session
734 if (followRedirects) {
735 String queryString = method.getQueryString();
736 checkExpiredSession(queryString);
737 }
738 }
739
740 private static void resetMethod(HttpMethod method) {
741 // reset method state
742 method.releaseConnection();
743 method.getHostAuthState().invalidate();
744 method.getProxyAuthState().invalidate();
745 }
746
747 private static void checkExpiredSession(String queryString) throws DavMailAuthenticationException {
748 if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=0"))) {
749 LOGGER.warn("Request failed, session expired");
750 throw new DavMailAuthenticationException("EXCEPTION_SESSION_EXPIRED");
751 }
752 }
753
754 /**
755 * Build Http Exception from method status
756 *
757 * @param method Http Method
758 * @return Http Exception
759 */
760 public static HttpResponseException buildHttpResponseException(HttpMethod method) {
761 int status = method.getStatusCode();
762 StringBuilder message = new StringBuilder();
763 message.append(status).append(' ').append(method.getStatusText());
764 try {
765 message.append(" at ").append(method.getURI().getURI());
766 if (method instanceof CopyMethod || method instanceof MoveMethod) {
767 message.append(" to ").append(method.getRequestHeader("Destination"));
768 }
769 } catch (URIException e) {
770 message.append(method.getPath());
771 }
772 // 440 means forbidden on Exchange
773 if (status == 440) {
774 return new LoginTimeoutException(message.toString());
775 } else if (status == HttpStatus.SC_FORBIDDEN) {
776 return new HttpForbiddenException(message.toString());
777 } else if (status == HttpStatus.SC_NOT_FOUND) {
778 return new HttpNotFoundException(message.toString());
779 } else if (status == HttpStatus.SC_PRECONDITION_FAILED) {
780 return new HttpPreconditionFailedException(message.toString());
781 } else if (status == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
782 return new HttpServerErrorException(message.toString());
783 } else {
784 return new HttpResponseException(status, message.toString());
785 }
786 }
787
788 /**
789 * Test if the method response is gzip encoded
790 *
791 * @param method http method
792 * @return true if response is gzip encoded
793 */
794 public static boolean isGzipEncoded(HttpMethod method) {
795 Header[] contentEncodingHeaders = method.getResponseHeaders("Content-Encoding");
796 if (contentEncodingHeaders != null) {
797 for (Header header : contentEncodingHeaders) {
798 if ("gzip".equals(header.getValue())) {
799 return true;
800 }
801 }
802 }
803 return false;
804 }
805
806 /**
807 * Stop HttpConnectionManager.
808 */
809 public static void stop() {
810 synchronized (LOCK) {
811 if (httpConnectionManagerThread != null) {
812 httpConnectionManagerThread.shutdown();
813 httpConnectionManagerThread.interrupt();
814 httpConnectionManagerThread = null;
815 }
816 for (MultiThreadedHttpConnectionManager httpConnectionManager : ALL_CONNECTION_MANAGERS) {
817 // try to avoid deadlock by connection manager level lock,
818 // used internally in MultiThreadedHttpConnectionManager.freeConnection()
819 //noinspection SynchronizationOnLocalVariableOrMethodParameter
820 synchronized (httpConnectionManager) {
821 httpConnectionManager.shutdown();
822 }
823 }
824 }
825 }
826
827 /**
828 * Create and set connection pool.
829 *
830 * @param httpClient httpClient instance
831 */
832 public static void createMultiThreadedHttpConnectionManager(HttpClient httpClient) {
833 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
834 connectionManager.getParams().setDefaultMaxConnectionsPerHost(Settings.getIntProperty("davmail.exchange.maxConnections", 100));
835 connectionManager.getParams().setConnectionTimeout(Settings.getIntProperty("davmail.exchange.connectionTimeout", 10) * 1000);
836 connectionManager.getParams().setSoTimeout(Settings.getIntProperty("davmail.exchange.soTimeout", 120) * 1000);
837 synchronized (LOCK) {
838 ALL_CONNECTION_MANAGERS.add(connectionManager);
839 httpConnectionManagerThread.addConnectionManager(connectionManager);
840 }
841 httpClient.setHttpConnectionManager(connectionManager);
842 }
843
844 /**
845 * Create and start a new HttpConnectionManager, close idle connections every minute.
846 */
847 public static void start() {
848 synchronized (LOCK) {
849 if (httpConnectionManagerThread == null) {
850 httpConnectionManagerThread = new IdleConnectionTimeoutThread();
851 httpConnectionManagerThread.setName(IdleConnectionTimeoutThread.class.getSimpleName());
852 httpConnectionManagerThread.setConnectionTimeout(ONE_MINUTE);
853 httpConnectionManagerThread.setTimeoutInterval(ONE_MINUTE);
854 httpConnectionManagerThread.start();
855 }
856 }
857 }
858
859 public static String getUserAgent() {
860 return Settings.getProperty("davmail.userAgent", IE_USER_AGENT);
861 }
862 }
2222 import org.apache.log4j.Logger;
2323
2424 import java.io.IOException;
25 import java.net.*;
25 import java.net.InetSocketAddress;
26 import java.net.Proxy;
27 import java.net.ProxySelector;
28 import java.net.SocketAddress;
29 import java.net.URI;
2630 import java.util.ArrayList;
2731 import java.util.Collections;
2832 import java.util.List;
5761 return proxyes;
5862 } else if (enableProxy
5963 && proxyHost != null && proxyHost.length() > 0 && proxyPort > 0
60 && !DavGatewayHttpClientFacade.isNoProxyFor(uri)
64 && !isNoProxyFor(uri)
6165 && ("http".equals(scheme) || "https".equals(scheme))) {
6266 // DavMail defined proxies
6367 ArrayList<Proxy> proxies = new ArrayList<>();
6872 }
6973 }
7074
75 private boolean isNoProxyFor(URI uri) {
76 final String noProxyFor = Settings.getProperty("davmail.noProxyFor");
77 if (noProxyFor != null) {
78 final String urihost = uri.getHost().toLowerCase();
79 final String[] domains = noProxyFor.toLowerCase().split(",\\s*");
80 for (String domain : domains) {
81 if (urihost.endsWith(domain)) {
82 return true; //break;
83 }
84 }
85 }
86 return false;
87 }
88
7189 @Override
7290 public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
73 LOGGER.debug("Connection to "+uri+" failed, socket address "+sa+" "+ioe);
91 LOGGER.debug("Connection to " + uri + " failed, socket address " + sa + " " + ioe);
7492 proxySelector.connectFailed(uri, sa, ioe);
7593 }
7694 }
+0
-269
src/java/davmail/http/DavGatewaySSLProtocolSocketFactory.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2009 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.http;
19
20 import davmail.BundleMessage;
21 import davmail.Settings;
22 import davmail.ui.PasswordPromptDialog;
23 import davmail.ui.tray.DavGatewayTray;
24 import org.apache.commons.httpclient.HttpsURL;
25 import org.apache.commons.httpclient.params.HttpConnectionParams;
26 import org.apache.commons.httpclient.protocol.Protocol;
27 import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
28 import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
29
30 import javax.net.ssl.KeyManager;
31 import javax.net.ssl.KeyManagerFactory;
32 import javax.net.ssl.KeyStoreBuilderParameters;
33 import javax.net.ssl.ManagerFactoryParameters;
34 import javax.net.ssl.SSLContext;
35 import javax.net.ssl.TrustManager;
36 import javax.net.ssl.X509KeyManager;
37 import javax.security.auth.callback.PasswordCallback;
38 import java.awt.*;
39 import java.io.BufferedReader;
40 import java.io.File;
41 import java.io.IOException;
42 import java.io.InputStreamReader;
43 import java.net.InetAddress;
44 import java.net.MalformedURLException;
45 import java.net.Socket;
46 import java.net.URL;
47 import java.security.InvalidAlgorithmParameterException;
48 import java.security.KeyManagementException;
49 import java.security.KeyStore;
50 import java.security.KeyStoreException;
51 import java.security.NoSuchAlgorithmException;
52 import java.security.Provider;
53 import java.util.ArrayList;
54
55 /**
56 * Manual Socket Factory.
57 * Let user choose to accept or reject certificate
58 * Used by commons httpclient 3
59 */
60 public class DavGatewaySSLProtocolSocketFactory implements SecureProtocolSocketFactory {
61 /**
62 * Register custom Socket Factory to let user accept or reject certificate
63 */
64 public static void register() {
65 String urlString = Settings.getProperty("davmail.url");
66 try {
67 URL url = new URL(urlString);
68 String protocol = url.getProtocol();
69 if ("https".equals(protocol)) {
70 int port = url.getPort();
71 if (port < 0) {
72 port = HttpsURL.DEFAULT_PORT;
73 }
74 Protocol.registerProtocol(url.getProtocol(),
75 new Protocol(protocol, (ProtocolSocketFactory) new DavGatewaySSLProtocolSocketFactory(), port));
76 }
77 } catch (MalformedURLException e) {
78 DavGatewayTray.error(new BundleMessage("LOG_INVALID_URL", urlString));
79 }
80 }
81
82 private KeyStore.ProtectionParameter getProtectionParameter(String password) {
83 if (password != null && password.length() > 0) {
84 // password provided: create a PasswordProtection
85 return new KeyStore.PasswordProtection(password.toCharArray());
86 } else {
87 // request password at runtime through a callback
88 return new KeyStore.CallbackHandlerProtection(callbacks -> {
89 if (callbacks.length > 0 && callbacks[0] instanceof PasswordCallback) {
90 if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
91 // headless or server mode
92 System.out.print(((PasswordCallback) callbacks[0]).getPrompt() + ": ");
93 String password1 = new BufferedReader(new InputStreamReader(System.in)).readLine();
94 ((PasswordCallback) callbacks[0]).setPassword(password1.toCharArray());
95 } else {
96 PasswordPromptDialog passwordPromptDialog = new PasswordPromptDialog(((PasswordCallback) callbacks[0]).getPrompt());
97 ((PasswordCallback) callbacks[0]).setPassword(passwordPromptDialog.getPassword());
98 }
99 }
100 });
101 }
102 }
103
104 private SSLContext sslcontext;
105
106 public String[] getDefaultCipherSuites() {
107 try {
108 return getSSLContext().getSocketFactory().getDefaultCipherSuites();
109 } catch (Exception e) {
110 // ignore
111 }
112 return new String[]{};
113 }
114
115 public String[] getSupportedCipherSuites() {
116 try {
117 return getSSLContext().getSocketFactory().getSupportedCipherSuites();
118 } catch (Exception e) {
119 // ignore
120 }
121 return new String[]{};
122 }
123
124 private SSLContext createSSLContext() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyManagementException, KeyStoreException {
125 // PKCS11 client certificate settings
126 String pkcs11Library = Settings.getProperty("davmail.ssl.pkcs11Library");
127
128 String clientKeystoreType = Settings.getProperty("davmail.ssl.clientKeystoreType");
129 // set default keystore type
130 if (clientKeystoreType == null || clientKeystoreType.length() == 0) {
131 clientKeystoreType = "PKCS11";
132 }
133
134 if (pkcs11Library != null && pkcs11Library.length() > 0 && "PKCS11".equals(clientKeystoreType)) {
135 StringBuilder pkcs11Buffer = new StringBuilder();
136 pkcs11Buffer.append("name=DavMail\n");
137 pkcs11Buffer.append("library=").append(pkcs11Library).append('\n');
138 String pkcs11Config = Settings.getProperty("davmail.ssl.pkcs11Config");
139 if (pkcs11Config != null && pkcs11Config.length() > 0) {
140 pkcs11Buffer.append(pkcs11Config).append('\n');
141 }
142 SunPKCS11ProviderHandler.registerProvider(pkcs11Buffer.toString());
143 }
144 String algorithm = KeyManagerFactory.getDefaultAlgorithm();
145 if ("SunX509".equals(algorithm)) {
146 algorithm = "NewSunX509";
147 } else if ("IbmX509".equals(algorithm)) {
148 algorithm = "NewIbmX509";
149 }
150 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
151
152 ArrayList<KeyStore.Builder> keyStoreBuilders = new ArrayList<>();
153 // PKCS11 (smartcard) keystore with password callback
154 KeyStore.Builder scBuilder = KeyStore.Builder.newInstance("PKCS11", null, getProtectionParameter(null));
155 keyStoreBuilders.add(scBuilder);
156
157 String clientKeystoreFile = Settings.getProperty("davmail.ssl.clientKeystoreFile");
158 String clientKeystorePass = Settings.getProperty("davmail.ssl.clientKeystorePass");
159 if (clientKeystoreFile != null && clientKeystoreFile.length() > 0
160 && ("PKCS12".equals(clientKeystoreType) || "JKS".equals(clientKeystoreType))) {
161 // PKCS12 file based keystore
162 KeyStore.Builder fsBuilder = KeyStore.Builder.newInstance(clientKeystoreType, null,
163 new File(clientKeystoreFile), getProtectionParameter(clientKeystorePass));
164 keyStoreBuilders.add(fsBuilder);
165 }
166 // Enable native Windows SmartCard access through MSCAPI (no PKCS11 config required)
167 if ("MSCAPI".equals(clientKeystoreType)) {
168 try {
169 Provider provider = (Provider) Class.forName("sun.security.mscapi.SunMSCAPI").newInstance();
170 KeyStore keyStore = KeyStore.getInstance("Windows-MY", provider);
171 keyStore.load(null, null);
172 keyStoreBuilders.add(KeyStore.Builder.newInstance(keyStore, new KeyStore.PasswordProtection(null)));
173 } catch (Exception e) {
174 // ignore
175 }
176 }
177
178 ManagerFactoryParameters keyStoreBuilderParameters = new KeyStoreBuilderParameters(keyStoreBuilders);
179 keyManagerFactory.init(keyStoreBuilderParameters);
180
181 // Get a list of key managers
182 KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
183
184 // Walk through the key managers and replace all X509 Key Managers with
185 // a specialized wrapped DavMail X509 Key Manager
186 for (int i = 0; i < keyManagers.length; i++) {
187 KeyManager keyManager = keyManagers[i];
188 if (keyManager instanceof X509KeyManager) {
189 keyManagers[i] = new DavMailX509KeyManager((X509KeyManager) keyManager);
190 }
191 }
192
193 SSLContext context = SSLContext.getInstance("TLS");
194 context.init(keyManagers, new TrustManager[]{new DavGatewayX509TrustManager()}, null);
195 return context;
196 }
197
198 private SSLContext getSSLContext() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, InvalidAlgorithmParameterException {
199 if (this.sslcontext == null) {
200 this.sslcontext = createSSLContext();
201 }
202 return this.sslcontext;
203 }
204
205 public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException {
206 try {
207 return getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);
208 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
209 throw new IOException(e + " " + e.getMessage());
210 }
211 }
212
213 public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort, HttpConnectionParams params) throws IOException {
214 try {
215 return getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);
216 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
217 throw new IOException(e + " " + e.getMessage());
218 }
219 }
220
221 public Socket createSocket(String host, int port) throws IOException {
222 try {
223 return getSSLContext().getSocketFactory().createSocket(host, port);
224 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
225 throw new IOException(e + " " + e.getMessage());
226 }
227 }
228
229 public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
230 try {
231 return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);
232 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
233 throw new IOException(e + " " + e.getMessage());
234 }
235 }
236
237 public Socket createSocket(InetAddress host, int port) throws IOException {
238 try {
239 return getSSLContext().getSocketFactory().createSocket(host, port);
240 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
241 throw new IOException(e + " " + e.getMessage());
242 }
243 }
244
245 public Socket createSocket(InetAddress host, int port, InetAddress clientHost, int clientPort) throws IOException {
246 try {
247 return getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);
248 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
249 throw new IOException(e + " " + e.getMessage());
250 }
251 }
252
253 /**
254 * All instances of SSLProtocolSocketFactory are the same.
255 */
256 @Override
257 public boolean equals(Object obj) {
258 return ((obj != null) && obj.getClass().equals(this.getClass()));
259 }
260
261 /**
262 * All instances of SSLProtocolSocketFactory have the same hash code.
263 */
264 @Override
265 public int hashCode() {
266 return this.getClass().hashCode();
267 }
268 }
1818
1919 package davmail.http;
2020
21 import davmail.Settings;
22 import davmail.ui.PasswordPromptDialog;
2123 import org.apache.log4j.Logger;
2224
25 import javax.net.ssl.KeyManager;
26 import javax.net.ssl.KeyManagerFactory;
27 import javax.net.ssl.KeyStoreBuilderParameters;
28 import javax.net.ssl.ManagerFactoryParameters;
29 import javax.net.ssl.SSLContext;
2330 import javax.net.ssl.SSLSocketFactory;
31 import javax.net.ssl.TrustManager;
32 import javax.net.ssl.X509KeyManager;
33 import javax.security.auth.callback.PasswordCallback;
34 import java.awt.*;
35 import java.io.BufferedReader;
36 import java.io.File;
2437 import java.io.IOException;
38 import java.io.InputStreamReader;
2539 import java.net.InetAddress;
2640 import java.net.Socket;
41 import java.security.InvalidAlgorithmParameterException;
42 import java.security.KeyManagementException;
43 import java.security.KeyStore;
44 import java.security.KeyStoreException;
45 import java.security.NoSuchAlgorithmException;
46 import java.security.Provider;
47 import java.util.ArrayList;
2748
2849 /**
2950 * SSLSocketFactory implementation.
3253 public class DavGatewaySSLSocketFactory extends SSLSocketFactory {
3354 static final Logger LOGGER = Logger.getLogger(DavGatewaySSLSocketFactory.class);
3455
35 private DavGatewaySSLProtocolSocketFactory socketFactory;
36
37 public DavGatewaySSLSocketFactory() {
38 socketFactory = new DavGatewaySSLProtocolSocketFactory();
56 private SSLContext sslcontext;
57
58 private SSLContext getSSLContext() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, InvalidAlgorithmParameterException {
59 if (this.sslcontext == null) {
60 this.sslcontext = createSSLContext();
61 }
62 return this.sslcontext;
63 }
64
65 private SSLContext createSSLContext() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyManagementException, KeyStoreException {
66 // PKCS11 client certificate settings
67 String pkcs11Library = Settings.getProperty("davmail.ssl.pkcs11Library");
68
69 String clientKeystoreType = Settings.getProperty("davmail.ssl.clientKeystoreType");
70 // set default keystore type
71 if (clientKeystoreType == null || clientKeystoreType.length() == 0) {
72 clientKeystoreType = "PKCS11";
73 }
74
75 if (pkcs11Library != null && pkcs11Library.length() > 0 && "PKCS11".equals(clientKeystoreType)) {
76 StringBuilder pkcs11Buffer = new StringBuilder();
77 pkcs11Buffer.append("name=DavMail\n");
78 pkcs11Buffer.append("library=").append(pkcs11Library).append('\n');
79 String pkcs11Config = Settings.getProperty("davmail.ssl.pkcs11Config");
80 if (pkcs11Config != null && pkcs11Config.length() > 0) {
81 pkcs11Buffer.append(pkcs11Config).append('\n');
82 }
83 SunPKCS11ProviderHandler.registerProvider(pkcs11Buffer.toString());
84 }
85 String algorithm = KeyManagerFactory.getDefaultAlgorithm();
86 if ("SunX509".equals(algorithm)) {
87 algorithm = "NewSunX509";
88 } else if ("IbmX509".equals(algorithm)) {
89 algorithm = "NewIbmX509";
90 }
91 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
92
93 ArrayList<KeyStore.Builder> keyStoreBuilders = new ArrayList<>();
94 // PKCS11 (smartcard) keystore with password callback
95 KeyStore.Builder scBuilder = KeyStore.Builder.newInstance("PKCS11", null, getProtectionParameter(null));
96 keyStoreBuilders.add(scBuilder);
97
98 String clientKeystoreFile = Settings.getProperty("davmail.ssl.clientKeystoreFile");
99 String clientKeystorePass = Settings.getProperty("davmail.ssl.clientKeystorePass");
100 if (clientKeystoreFile != null && clientKeystoreFile.length() > 0
101 && ("PKCS12".equals(clientKeystoreType) || "JKS".equals(clientKeystoreType))) {
102 // PKCS12 file based keystore
103 KeyStore.Builder fsBuilder = KeyStore.Builder.newInstance(clientKeystoreType, null,
104 new File(clientKeystoreFile), getProtectionParameter(clientKeystorePass));
105 keyStoreBuilders.add(fsBuilder);
106 }
107 // Enable native Windows SmartCard access through MSCAPI (no PKCS11 config required)
108 if ("MSCAPI".equals(clientKeystoreType)) {
109 try {
110 Provider provider = (Provider) Class.forName("sun.security.mscapi.SunMSCAPI").getDeclaredConstructor().newInstance();
111 KeyStore keyStore = KeyStore.getInstance("Windows-MY", provider);
112 keyStore.load(null, null);
113 keyStoreBuilders.add(KeyStore.Builder.newInstance(keyStore, new KeyStore.PasswordProtection(null)));
114 } catch (Exception e) {
115 // ignore
116 }
117 }
118
119 ManagerFactoryParameters keyStoreBuilderParameters = new KeyStoreBuilderParameters(keyStoreBuilders);
120 keyManagerFactory.init(keyStoreBuilderParameters);
121
122 // Get a list of key managers
123 KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
124
125 // Walk through the key managers and replace all X509 Key Managers with
126 // a specialized wrapped DavMail X509 Key Manager
127 for (int i = 0; i < keyManagers.length; i++) {
128 KeyManager keyManager = keyManagers[i];
129 if (keyManager instanceof X509KeyManager) {
130 keyManagers[i] = new DavMailX509KeyManager((X509KeyManager) keyManager);
131 }
132 }
133
134 SSLContext context = SSLContext.getInstance("TLS");
135 context.init(keyManagers, new TrustManager[]{new DavGatewayX509TrustManager()}, null);
136 return context;
137 }
138
139 private KeyStore.ProtectionParameter getProtectionParameter(String password) {
140 if (password != null && password.length() > 0) {
141 // password provided: create a PasswordProtection
142 return new KeyStore.PasswordProtection(password.toCharArray());
143 } else {
144 // request password at runtime through a callback
145 return new KeyStore.CallbackHandlerProtection(callbacks -> {
146 if (callbacks.length > 0 && callbacks[0] instanceof PasswordCallback) {
147 if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
148 // headless or server mode
149 System.out.print(((PasswordCallback) callbacks[0]).getPrompt() + ": ");
150 String password1 = new BufferedReader(new InputStreamReader(System.in)).readLine();
151 ((PasswordCallback) callbacks[0]).setPassword(password1.toCharArray());
152 } else {
153 PasswordPromptDialog passwordPromptDialog = new PasswordPromptDialog(((PasswordCallback) callbacks[0]).getPrompt());
154 ((PasswordCallback) callbacks[0]).setPassword(passwordPromptDialog.getPassword());
155 }
156 }
157 });
158 }
39159 }
40160
41161 @Override
42162 public String[] getDefaultCipherSuites() {
43 return socketFactory.getDefaultCipherSuites();
163 try {
164 return getSSLContext().getSocketFactory().getDefaultCipherSuites();
165 } catch (Exception e) {
166 // ignore
167 }
168 return new String[]{};
44169 }
45170
46171 @Override
47172 public String[] getSupportedCipherSuites() {
48 return socketFactory.getSupportedCipherSuites();
173 try {
174 return getSSLContext().getSocketFactory().getSupportedCipherSuites();
175 } catch (Exception e) {
176 // ignore
177 }
178 return new String[]{};
49179 }
50180
51181 @Override
52182 public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
53183 LOGGER.debug("createSocket " + host + " " + port);
54 return socketFactory.createSocket(socket, host, port, autoClose);
184 try {
185 return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);
186 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
187 throw new IOException(e + " " + e.getMessage());
188 }
55189 }
56190
57191 @Override
58192 public Socket createSocket(String host, int port) throws IOException {
59193 LOGGER.debug("createSocket " + host + " " + port);
60 return socketFactory.createSocket(host, port);
194 try {
195 return getSSLContext().getSocketFactory().createSocket(host, port);
196 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
197 throw new IOException(e + " " + e.getMessage());
198 }
61199 }
62200
63201 @Override
64202 public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException {
65203 LOGGER.debug("createSocket " + host + " " + port + " " + clientHost + " " + clientPort);
66 return socketFactory.createSocket(host, port, clientHost, clientPort);
204 try {
205 return getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);
206 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
207 throw new IOException(e + " " + e.getMessage());
208 }
67209 }
68210
69211 @Override
70212 public Socket createSocket(InetAddress host, int port) throws IOException {
71213 LOGGER.debug("createSocket " + host + " " + port);
72 return socketFactory.createSocket(host, port);
214 try {
215 return getSSLContext().getSocketFactory().createSocket(host, port);
216 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
217 throw new IOException(e + " " + e.getMessage());
218 }
73219 }
74220
75221 @Override
76222 public Socket createSocket(InetAddress host, int port, InetAddress clientHost, int clientPort) throws IOException {
77223 LOGGER.debug("createSocket " + host + " " + port + " " + clientHost + " " + clientPort);
78 return socketFactory.createSocket(host, port, clientHost, clientPort);
224 try {
225 return getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);
226 } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | InvalidAlgorithmParameterException e) {
227 throw new IOException(e + " " + e.getMessage());
228 }
79229 }
80230 }
+0
-69
src/java/davmail/http/DavMailCookieSpec.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2010 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.http;
19
20 import org.apache.commons.httpclient.Cookie;
21 import org.apache.commons.httpclient.cookie.MalformedCookieException;
22 import org.apache.commons.httpclient.cookie.RFC2109Spec;
23
24 /**
25 * Custom CookieSpec to allow extended domain names.
26 */
27 public class DavMailCookieSpec extends RFC2109Spec {
28 @Override
29 public void validate(String host, int port, String path,
30 boolean secure, final Cookie cookie) throws MalformedCookieException {
31 // workaround for space in cookie name
32 String cookieName = cookie.getName();
33 if (cookieName != null && cookieName.indexOf(' ') >= 0) {
34 cookie.setName(cookieName.replaceAll(" ", ""));
35 } else {
36 cookieName = null;
37 }
38 // workaround for invalid cookie path
39 String cookiePath = cookie.getPath();
40 if (cookiePath != null && !path.startsWith(cookiePath)) {
41 cookie.setPath(path);
42 } else {
43 cookiePath = null;
44 }
45 // workaround for invalid cookie domain
46 int dotIndex = -1;
47 if (host.endsWith(cookie.getDomain())) {
48 String hostWithoutDomain = host.substring(0, host.length()
49 - cookie.getDomain().length());
50 dotIndex = hostWithoutDomain.indexOf('.');
51 }
52 if (".login.microsoftonline.com".equals(cookie.getDomain())) {
53 cookie.setDomain(host);
54 }
55 if (dotIndex != -1) {
56 // discard additional host name part
57 super.validate(host.substring(dotIndex + 1), port, path, secure, cookie);
58 } else {
59 super.validate(host, port, path, secure, cookie);
60 }
61 if (cookieName != null) {
62 cookie.setName(cookieName);
63 }
64 if (cookiePath != null) {
65 cookie.setPath(cookiePath);
66 }
67 }
68 }
5555 }
5656 }
5757 }
58 } catch (InterruptedException e) {
59 LOGGER.warn("Thread interrupted", e);
60 Thread.currentThread().interrupt();
5861 } catch (final Exception ex) {
5962 LOGGER.error(ex);
6063 }
301301
302302 private RequestConfig getRequestConfig() {
303303 HashSet<String> authSchemes = new HashSet<>();
304 authSchemes.add(AuthSchemes.NTLM);
305 authSchemes.add(AuthSchemes.BASIC);
306 authSchemes.add(AuthSchemes.DIGEST);
307304 if (Settings.getBooleanProperty("davmail.enableKerberos")) {
308305 authSchemes.add(AuthSchemes.SPNEGO);
309306 authSchemes.add(AuthSchemes.KERBEROS);
307 } else {
308 authSchemes.add(AuthSchemes.NTLM);
309 authSchemes.add(AuthSchemes.BASIC);
310 authSchemes.add(AuthSchemes.DIGEST);
310311 }
311312 return RequestConfig.custom()
312313 // socket connect timeout
620621 }
621622
622623 public String getUserAgent() {
623 return Settings.getProperty("davmail.userAgent", DavGatewayHttpClientFacade.IE_USER_AGENT);
624 return Settings.getUserAgent();
624625 }
625626
626627 public static HttpResponseException buildHttpResponseException(HttpRequestBase request, HttpResponse response) {
+0
-36
src/java/davmail/http/LenientBasicScheme.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2011 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.http;
19
20 import org.apache.commons.httpclient.auth.BasicScheme;
21 import org.apache.commons.httpclient.auth.MalformedChallengeException;
22
23 /**
24 * Workaround for broken servers that send invalid Basic authentication challenge.
25 */
26 public class LenientBasicScheme extends BasicScheme {
27 public void processChallenge(String challenge)
28 throws MalformedChallengeException {
29 if ("Basic".equalsIgnoreCase(challenge)) {
30 super.processChallenge("Basic \"default\"");
31 } else {
32 super.processChallenge(challenge);
33 }
34 }
35 }
+0
-196
src/java/davmail/http/NTLMv2Scheme.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2010 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.http;
19
20 import jcifs.ntlmssp.NtlmFlags;
21 import jcifs.ntlmssp.Type1Message;
22 import jcifs.ntlmssp.Type2Message;
23 import jcifs.ntlmssp.Type3Message;
24 import org.apache.commons.codec.binary.Base64;
25 import org.apache.commons.httpclient.Credentials;
26 import org.apache.commons.httpclient.HttpMethod;
27 import org.apache.commons.httpclient.NTCredentials;
28 import org.apache.commons.httpclient.auth.*;
29 import org.apache.commons.httpclient.util.EncodingUtil;
30
31 import java.io.IOException;
32
33 /**
34 * NTLMv2 scheme implementation.
35 */
36 public class NTLMv2Scheme implements AuthScheme {
37 private static final int UNINITIATED = 0;
38 private static final int INITIATED = 1;
39 private static final int TYPE1_MSG_GENERATED = 2;
40 private static final int TYPE2_MSG_RECEIVED = 3;
41 private static final int TYPE3_MSG_GENERATED = 4;
42 private static final int FAILED = Integer.MAX_VALUE;
43
44 private Type2Message type2Message;
45 /**
46 * Authentication process state
47 */
48 private int state;
49
50 /**
51 * Processes the NTLM challenge.
52 *
53 * @param challenge the challenge string
54 * @throws MalformedChallengeException is thrown if the authentication challenge
55 * is malformed
56 */
57 public void processChallenge(final String challenge) throws MalformedChallengeException {
58 String authScheme = AuthChallengeParser.extractScheme(challenge);
59 if (!authScheme.equalsIgnoreCase(getSchemeName())) {
60 throw new MalformedChallengeException("Invalid NTLM challenge: " + challenge);
61 }
62 int spaceIndex = challenge.indexOf(' ');
63 if (spaceIndex != -1) {
64 try {
65 type2Message = new Type2Message(Base64.decodeBase64(EncodingUtil.getBytes(
66 challenge.substring(spaceIndex).trim(), "ASCII")));
67 } catch (IOException e) {
68 throw new MalformedChallengeException("Invalid NTLM challenge: " + challenge, e);
69 }
70 this.state = TYPE2_MSG_RECEIVED;
71 } else {
72 this.type2Message = null;
73 if (this.state == UNINITIATED) {
74 this.state = INITIATED;
75 } else {
76 this.state = FAILED;
77 }
78 }
79 }
80
81
82 /**
83 * Returns textual designation of the NTLM authentication scheme.
84 *
85 * @return <code>ntlm</code>
86 */
87 public String getSchemeName() {
88 return "ntlm";
89 }
90
91 /**
92 * Not used with NTLM.
93 *
94 * @return null
95 */
96 public String getParameter(String s) {
97 return null;
98 }
99
100 /**
101 * Not used with NTLM.
102 *
103 * @return null
104 */
105 public String getRealm() {
106 return null;
107 }
108
109 /**
110 * Deprecated.
111 */
112 @Deprecated
113 public String getID() {
114 throw new UnsupportedOperationException();
115 }
116
117 /**
118 * NTLM is connection based.
119 *
120 * @return true
121 */
122 public boolean isConnectionBased() {
123 return true;
124 }
125
126 /**
127 * Tests if the NTLM authentication process has been completed.
128 *
129 * @return <tt>true</tt> if authorization has been processed
130 */
131 public boolean isComplete() {
132 return state == TYPE3_MSG_GENERATED || state == FAILED;
133 }
134
135 /**
136 * Not implemented.
137 *
138 * @param credentials user credentials
139 * @param method method name
140 * @param uri URI
141 * @return an NTLM authorization string
142 */
143 @Deprecated
144 public String authenticate(final Credentials credentials, String method, String uri) {
145 throw new UnsupportedOperationException();
146 }
147
148 /**
149 * Produces NTLM authorization string for the given set of
150 * {@link Credentials}.
151 *
152 * @param credentials The set of credentials to be used for authentication
153 * @param httpMethod The method being authenticated
154 * @return an NTLM authorization string
155 * @throws InvalidCredentialsException if authentication credentials
156 * are not valid or not applicable for this authentication scheme
157 * @throws AuthenticationException if authorization string cannot
158 * be generated due to an authentication failure
159 */
160 public String authenticate(Credentials credentials, HttpMethod httpMethod) throws AuthenticationException {
161 if (this.state == UNINITIATED) {
162 throw new IllegalStateException("NTLM authentication process has not been initiated");
163 }
164
165 NTCredentials ntcredentials;
166 try {
167 ntcredentials = (NTCredentials) credentials;
168 } catch (ClassCastException e) {
169 throw new InvalidCredentialsException(
170 "Credentials cannot be used for NTLM authentication: "
171 + credentials.getClass().getName());
172 }
173 String response;
174 if (this.state == INITIATED || this.state == FAILED) {
175 int flags = NtlmFlags.NTLMSSP_NEGOTIATE_NTLM2 | NtlmFlags.NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
176 NtlmFlags.NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED | NtlmFlags.NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED |
177 NtlmFlags.NTLMSSP_NEGOTIATE_NTLM | NtlmFlags.NTLMSSP_REQUEST_TARGET |
178 NtlmFlags.NTLMSSP_NEGOTIATE_OEM | NtlmFlags.NTLMSSP_NEGOTIATE_UNICODE |
179 NtlmFlags.NTLMSSP_NEGOTIATE_56 | NtlmFlags.NTLMSSP_NEGOTIATE_128;
180 Type1Message type1Message = new Type1Message(flags, ntcredentials.getDomain(), ntcredentials.getHost());
181 response = EncodingUtil.getAsciiString(Base64.encodeBase64(type1Message.toByteArray()));
182 this.state = TYPE1_MSG_GENERATED;
183 } else {
184 int flags = NtlmFlags.NTLMSSP_NEGOTIATE_NTLM2 | NtlmFlags.NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
185 NtlmFlags.NTLMSSP_NEGOTIATE_NTLM | NtlmFlags.NTLMSSP_NEGOTIATE_UNICODE;
186 Type3Message type3Message = new Type3Message(type2Message, ntcredentials.getPassword(),
187 ntcredentials.getDomain(), ntcredentials.getUserName(), ntcredentials.getHost(), flags);
188 response = EncodingUtil.getAsciiString(Base64.encodeBase64(type3Message.toByteArray()));
189 this.state = TYPE3_MSG_GENERATED;
190 }
191 return "NTLM " + response;
192 }
193
194
195 }
+0
-109
src/java/davmail/http/RestMethod.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2010 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18
19 package davmail.http;
20
21 import davmail.util.IOUtil;
22 import org.apache.commons.httpclient.Header;
23 import org.apache.commons.httpclient.HttpConnection;
24 import org.apache.commons.httpclient.HttpState;
25 import org.apache.commons.httpclient.methods.PostMethod;
26 import org.apache.commons.httpclient.methods.RequestEntity;
27 import org.apache.log4j.Logger;
28 import org.codehaus.jettison.json.JSONException;
29 import org.codehaus.jettison.json.JSONObject;
30
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.OutputStream;
34 import java.nio.charset.StandardCharsets;
35 import java.util.zip.GZIPInputStream;
36
37 /**
38 * REST/JSON method implementation
39 */
40 public class RestMethod extends PostMethod {
41 protected static final Logger LOGGER = Logger.getLogger(RestMethod.class);
42
43 JSONObject jsonBody;
44 JSONObject jsonResponse;
45
46 public RestMethod(String uri) {
47 super(uri);
48
49 setRequestEntity(new RequestEntity() {
50 byte[] content;
51
52 public boolean isRepeatable() {
53 return true;
54 }
55
56 public void writeRequest(OutputStream outputStream) throws IOException {
57 if (content == null) {
58 content = getJsonContent();
59 }
60 outputStream.write(content);
61 }
62
63 public long getContentLength() {
64 if (content == null) {
65 content = getJsonContent();
66 }
67 return content.length;
68 }
69
70 public String getContentType() {
71 return "application/json; charset=UTF-8";
72 }
73 });
74 }
75
76 public void setJsonBody(JSONObject jsonBody) {
77 this.jsonBody = jsonBody;
78 }
79
80 public JSONObject getJsonResponse() {
81 return jsonResponse;
82 }
83
84 protected byte[] getJsonContent() {
85 return jsonBody.toString().getBytes(StandardCharsets.UTF_8);
86 }
87
88 @Override
89 protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
90 Header contentTypeHeader = getResponseHeader("Content-Type");
91 if (contentTypeHeader != null && "application/json; charset=utf-8".equals(contentTypeHeader.getValue())) {
92 try {
93 if (DavGatewayHttpClientFacade.isGzipEncoded(this)) {
94 processResponseStream(new GZIPInputStream(getResponseBodyAsStream()));
95 } else {
96 processResponseStream(getResponseBodyAsStream());
97 }
98 } catch (IOException | JSONException e) {
99 LOGGER.error("Error while parsing json response: " + e, e);
100 }
101 }
102 }
103
104 private void processResponseStream(InputStream responseBodyAsStream) throws IOException, JSONException {
105 // quick non streaming implementation
106 jsonResponse = new JSONObject(new String(IOUtil.readFully(responseBodyAsStream), StandardCharsets.UTF_8));
107 }
108 }
+0
-211
src/java/davmail/http/SpNegoScheme.java less more
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2012 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18 package davmail.http;
19
20 import org.apache.commons.codec.binary.Base64;
21 import org.apache.commons.httpclient.Credentials;
22 import org.apache.commons.httpclient.Header;
23 import org.apache.commons.httpclient.HttpMethod;
24 import org.apache.commons.httpclient.URIException;
25 import org.apache.commons.httpclient.auth.*;
26 import org.apache.commons.httpclient.util.EncodingUtil;
27 import org.ietf.jgss.GSSException;
28
29 import javax.security.auth.login.LoginException;
30
31 /**
32 * Implement spnego (Negotiate) authentication scheme.
33 */
34 public class SpNegoScheme implements AuthScheme {
35 private static final int UNINITIATED = 0;
36 private static final int INITIATED = 1;
37 private static final int TYPE1_MSG_GENERATED = 2;
38 private static final int TYPE2_MSG_RECEIVED = 3;
39 private static final int TYPE3_MSG_GENERATED = 4;
40 private static final int FAILED = Integer.MAX_VALUE;
41
42 private byte[] serverToken;
43 /**
44 * Authentication process state
45 */
46 private int state;
47
48 /**
49 * Processes the Negotiate challenge.
50 *
51 * @param challenge the challenge string
52 * @throws MalformedChallengeException is thrown if the authentication challenge is malformed
53 */
54 public void processChallenge(final String challenge) throws MalformedChallengeException {
55 String authScheme = AuthChallengeParser.extractScheme(challenge);
56 if (!authScheme.equalsIgnoreCase(getSchemeName())) {
57 throw new MalformedChallengeException("Invalid Negotiate challenge: " + challenge);
58 }
59 int spaceIndex = challenge.indexOf(' ');
60 if (spaceIndex != -1) {
61 // step 2: received server challenge
62 serverToken = Base64.decodeBase64(EncodingUtil.getBytes(
63 challenge.substring(spaceIndex).trim(), "ASCII"));
64 this.state = TYPE2_MSG_RECEIVED;
65 } else {
66 this.serverToken = null;
67 if (this.state == UNINITIATED) {
68 this.state = INITIATED;
69 } else {
70 this.state = FAILED;
71 }
72 }
73 }
74
75
76 /**
77 * Returns textual designation of the Negotiate authentication scheme.
78 *
79 * @return <code>Negotiate</code>
80 */
81 public String getSchemeName() {
82 return "Negotiate";
83 }
84
85 /**
86 * Not used with Negotiate.
87 *
88 * @return null
89 */
90 public String getParameter(String s) {
91 return null;
92 }
93
94 /**
95 * Not used with Negotiate.
96 *
97 * @return null
98 */
99 public String getRealm() {
100 return null;
101 }
102
103 /**
104 * Deprecated.
105 */
106 @Deprecated
107 public String getID() {
108 throw new UnsupportedOperationException();
109 }
110
111 /**
112 * Negotiate is connection based.
113 *
114 * @return true
115 */
116 public boolean isConnectionBased() {
117 return true;
118 }
119
120 /**
121 * Tests if the Negotiate authentication process has been completed.
122 *
123 * @return <tt>true</tt> if authorization has been processed
124 */
125 public boolean isComplete() {
126 return state == TYPE3_MSG_GENERATED || state == FAILED;
127 }
128
129 /**
130 * Not implemented.
131 *
132 * @param credentials user credentials
133 * @param method method name
134 * @param uri URI
135 * @return an Negotiate authorization string
136 */
137 @Deprecated
138 public String authenticate(final Credentials credentials, String method, String uri) {
139 throw new UnsupportedOperationException();
140 }
141
142 /**
143 * Produces Negotiate authorization string for the given set of
144 * {@link Credentials}.
145 *
146 * @param credentials The set of credentials to be used for authentication
147 * @param httpMethod The method being authenticated
148 * @return an Negotiate authorization string
149 * @throws org.apache.commons.httpclient.auth.InvalidCredentialsException
150 * if authentication credentials
151 * are not valid or not applicable for this authentication scheme
152 * @throws AuthenticationException if authorization string cannot
153 * be generated due to an authentication failure
154 */
155 public String authenticate(Credentials credentials, HttpMethod httpMethod) throws AuthenticationException {
156 if (this.state == UNINITIATED) {
157 throw new IllegalStateException("Negotiate authentication process has not been initiated");
158 }
159 String host = null;
160 try {
161 host = httpMethod.getURI().getHost();
162 } catch (URIException e) {
163 // ignore
164 }
165 if (host == null) {
166 Header header = httpMethod.getRequestHeader("Host");
167 if (header != null) {
168 host = header.getValue();
169 if (host.indexOf(':') >= 0) {
170 host = host.substring(0, host.indexOf(':'));
171 }
172 }
173 }
174 if (host == null) {
175 throw new IllegalStateException("Negotiate authentication failed: empty host");
176 }
177
178 // no credentials needed
179 String response;
180 try {
181 if (this.state == INITIATED || this.state == FAILED) {
182 // send initial token to server
183 response = EncodingUtil.getAsciiString(Base64.encodeBase64(KerberosHelper.initSecurityContext("HTTP", host, new byte[0])));
184 this.state = TYPE1_MSG_GENERATED;
185 } else {
186 // send challenge response
187 response = EncodingUtil.getAsciiString(Base64.encodeBase64(KerberosHelper.initSecurityContext("HTTP", host, serverToken)));
188 this.state = TYPE3_MSG_GENERATED;
189 }
190 } catch (GSSException gsse) {
191 state = FAILED;
192 if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL
193 || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED)
194 throw new InvalidCredentialsException(gsse.getMessage(), gsse);
195 if (gsse.getMajor() == GSSException.NO_CRED)
196 throw new CredentialsNotAvailableException(gsse.getMessage(), gsse);
197 if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN
198 || gsse.getMajor() == GSSException.DUPLICATE_TOKEN
199 || gsse.getMajor() == GSSException.OLD_TOKEN)
200 throw new AuthChallengeException(gsse.getMessage(), gsse);
201 // other error
202 throw new AuthenticationException(gsse.getMessage(), gsse);
203 } catch (LoginException e) {
204 state = FAILED;
205 throw new InvalidCredentialsException(e.getMessage(), e);
206 }
207 return "Negotiate " + response;
208 }
209
210 }
1919 package davmail.http.request;
2020
2121 import davmail.exchange.XMLStreamUtil;
22 import davmail.exchange.dav.ExchangeDavMethod;
2322 import org.apache.http.Header;
2423 import org.apache.http.HttpResponse;
2524 import org.apache.http.HttpStatus;
4443 import java.util.List;
4544
4645 public abstract class ExchangeDavRequest extends HttpPost implements ResponseHandler<List<MultiStatusResponse>> {
47 protected static final Logger LOGGER = Logger.getLogger(ExchangeDavMethod.class);
46 protected static final Logger LOGGER = Logger.getLogger(ExchangeDavRequest.class);
4847 private static final String XML_CONTENT_TYPE = "text/xml; charset=UTF-8";
4948
5049 private HttpResponse response;
2727 import davmail.exception.HttpForbiddenException;
2828 import davmail.exception.HttpNotFoundException;
2929 import davmail.exception.InsufficientStorageException;
30 import davmail.exchange.*;
30 import davmail.exchange.ExchangeSession;
31 import davmail.exchange.ExchangeSessionFactory;
32 import davmail.exchange.FolderLoadThread;
33 import davmail.exchange.MessageCreateThread;
34 import davmail.exchange.MessageLoadThread;
3135 import davmail.ui.tray.DavGatewayTray;
3236 import davmail.util.IOUtil;
3337 import davmail.util.StringUtil;
3539 import org.apache.log4j.Logger;
3640
3741 import javax.mail.MessagingException;
38 import javax.mail.internet.*;
42 import javax.mail.internet.AddressException;
43 import javax.mail.internet.InternetAddress;
44 import javax.mail.internet.MimeBodyPart;
45 import javax.mail.internet.MimeMessage;
46 import javax.mail.internet.MimeMultipart;
47 import javax.mail.internet.MimePart;
48 import javax.mail.internet.MimeUtility;
3949 import javax.mail.util.SharedByteArrayInputStream;
40 import java.io.*;
50 import java.io.ByteArrayOutputStream;
51 import java.io.FilterOutputStream;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.io.OutputStream;
55 import java.io.UnsupportedEncodingException;
4156 import java.net.Socket;
4257 import java.net.SocketException;
4358 import java.net.SocketTimeoutException;
411426 throw e;
412427 } catch (IOException e) {
413428 DavGatewayTray.log(e);
414 LOGGER.warn("Ignore broken message " + rangeIterator.currentIndex+ ' ' +e.getMessage());
429 LOGGER.warn("Ignore broken message " + rangeIterator.currentIndex + ' ' + e.getMessage());
415430 }
416431
417432 }
747762 }
748763
749764 protected String encodeFolderPath(String folderPath) {
750 return BASE64MailboxEncoder.encode(folderPath).replaceAll("\"","\\\\\"");
765 return BASE64MailboxEncoder.encode(folderPath).replaceAll("\"", "\\\\\"");
751766 }
752767
753768 protected String decodeFolderPath(String folderPath) {
801816 this.message = message;
802817 }
803818
804 public int getMimeMessageSize() throws IOException, MessagingException, InterruptedException {
819 public int getMimeMessageSize() throws IOException, MessagingException {
805820 loadMessage();
806821 return message.getMimeMessageSize();
807822 }
809824 /**
810825 * Monitor full message download
811826 */
812 protected void loadMessage() throws IOException, MessagingException, InterruptedException {
827 protected void loadMessage() throws IOException, MessagingException {
813828 if (!message.isLoaded()) {
814829 // flush current buffer
815830 String flushString = buffer.toString();
820835 }
821836 }
822837
823 public MimeMessage getMimeMessage() throws IOException, MessagingException, InterruptedException {
838 public MimeMessage getMimeMessage() throws IOException, MessagingException {
824839 loadMessage();
825840 return message.getMimeMessage();
826841 }
827842
828 public InputStream getRawInputStream() throws IOException, MessagingException, InterruptedException {
843 public InputStream getRawInputStream() throws IOException, MessagingException {
829844 loadMessage();
830845 return message.getRawInputStream();
831846 }
832847
833 public Enumeration getMatchingHeaderLines(String[] requestedHeaders) throws IOException, MessagingException, InterruptedException {
848 public Enumeration getMatchingHeaderLines(String[] requestedHeaders) throws IOException, MessagingException {
834849 Enumeration result = message.getMatchingHeaderLinesFromHeaders(requestedHeaders);
835850 if (result == null) {
836851 loadMessage();
841856 }
842857
843858
844 private void handleFetch(ExchangeSession.Message message, int currentIndex, String parameters) throws IOException, MessagingException, InterruptedException {
859 private void handleFetch(ExchangeSession.Message message, int currentIndex, String parameters) throws IOException, MessagingException {
845860 StringBuilder buffer = new StringBuilder();
846861 MessageWrapper messageWrapper = new MessageWrapper(os, buffer, message);
847862 buffer.append("* ").append(currentIndex).append(" FETCH (UID ").append(message.getImapUid());
854869 buffer.append(" FLAGS (").append(message.getImapFlags()).append(')');
855870 } else if ("RFC822.SIZE".equals(param)) {
856871 int size;
857 if ( ( parameters.contains("BODY.PEEK[HEADER.FIELDS (")
872 if ((parameters.contains("BODY.PEEK[HEADER.FIELDS (")
858873 // exclude mutt header request
859 && !parameters.contains("X-LABEL") )
874 && !parameters.contains("X-LABEL"))
860875 || parameters.equals("RFC822.SIZE RFC822.HEADER FLAGS") // icedove
861 || Settings.getBooleanProperty("davmail.imapAlwaysApproxMsgSize") )
862 { // Send approximate size
876 || Settings.getBooleanProperty("davmail.imapAlwaysApproxMsgSize")) { // Send approximate size
863877 size = message.size;
864 LOGGER.debug(String.format("Message %s sent approximate size %d bytes", message.getImapUid(), size));
878 LOGGER.debug(String.format("Message %s sent approximate size %d bytes", message.getImapUid(), size));
865879 } else {
866880 size = messageWrapper.getMimeMessageSize();
867881 }
11171131
11181132 /**
11191133 * Check NOT UID condition.
1134 *
11201135 * @param notUidRange excluded uid range
1121 * @param imapUid current message imap uid
1136 * @param imapUid current message imap uid
11221137 * @return true if not excluded
11231138 */
11241139 private boolean isNotExcluded(String notUidRange, long imapUid) {
11261141 return true;
11271142 }
11281143 String imapUidAsString = String.valueOf(imapUid);
1129 for (String rangeValue: notUidRange.split(",")) {
1144 for (String rangeValue : notUidRange.split(",")) {
11301145 if (imapUidAsString.equals(rangeValue)) {
11311146 return false;
11321147 }
11341149 return true;
11351150 }
11361151
1137 protected void appendEnvelope(StringBuilder buffer, MessageWrapper message) throws IOException, InterruptedException {
1152 protected void appendEnvelope(StringBuilder buffer, MessageWrapper message) throws IOException {
11381153
11391154 try {
11401155 MimeMessage mimeMessage = message.getMimeMessage();
12251240
12261241 }
12271242
1228 protected void appendBodyStructure(StringBuilder buffer, MessageWrapper message) throws IOException, InterruptedException {
1243 protected void appendBodyStructure(StringBuilder buffer, MessageWrapper message) throws IOException {
12291244
12301245 buffer.append(" BODYSTRUCTURE ");
12311246 try {
13441359 buffer.append(" NIL");
13451360 }
13461361 }
1362 int bodySize = getBodyPartSize(bodyPart);
13471363 appendBodyStructureValue(buffer, bodyPart.getContentID());
13481364 appendBodyStructureValue(buffer, bodyPart.getDescription());
13491365 appendBodyStructureValue(buffer, bodyPart.getEncoding());
1350 appendBodyStructureValue(buffer, bodyPart.getSize());
1366 appendBodyStructureValue(buffer, bodySize);
13511367 if ("MESSAGE".equals(type) || "TEXT".equals(type)) {
13521368 // line count not implemented in JavaMail, return fake line count
1353 appendBodyStructureValue(buffer, bodyPart.getSize() / 80);
1369 appendBodyStructureValue(buffer, bodySize / 80);
13541370 } else {
13551371 // do not send line count for non text bodyparts
13561372 appendBodyStructureValue(buffer, -1);
13571373 }
13581374 buffer.append(')');
1375 }
1376
1377 /**
1378 * Compute body part size with failover.
1379 * @param bodyPart MIME body part
1380 * @return body part size or 0 on error
1381 */
1382 private int getBodyPartSize(MimePart bodyPart) {
1383 int bodySize = 0;
1384 try {
1385 bodySize = bodyPart.getSize();
1386 if (bodySize == -1) {
1387 // failover, try to get size
1388 ByteArrayOutputStream baos = new ByteArrayOutputStream();
1389 bodyPart.writeTo(baos);
1390 bodySize = baos.size();
1391 }
1392 } catch (IOException | MessagingException e) {
1393 LOGGER.warn("Unable to get body part size " + e.getMessage(), e);
1394 }
1395 return bodySize;
13591396 }
13601397
13611398 protected void appendBodyStructureValue(StringBuilder buffer, String value) {
14201457 protected ExchangeSession.Condition appendNotSearchParams(String token, SearchConditions conditions) throws IOException {
14211458 ImapTokenizer innerTokens = new ImapTokenizer(token);
14221459 ExchangeSession.Condition cond = buildConditions(conditions, innerTokens);
1423 if (cond==null || cond.isEmpty()) {
1424 return null;
1460 if (cond == null || cond.isEmpty()) {
1461 return null;
14251462 }
14261463 return session.not(cond);
14271464 }
14331470 // conditions.deleted = Boolean.FALSE;
14341471 return session.isNull("deleted");
14351472 } else if ("KEYWORD".equals(nextToken)) {
1436 return appendNotSearchParams(nextToken+" "+tokens.nextToken(), conditions);
1473 return appendNotSearchParams(nextToken + " " + tokens.nextToken(), conditions);
14371474 } else if ("UID".equals(nextToken)) {
14381475 conditions.notUidRange = tokens.nextToken();
14391476 } else {
15031540 }
15041541 } else //noinspection StatementWithEmptyBody
15051542 if ("OLD".equals(token) || "RECENT".equals(token) || "ALL".equals(token)) {
1506 // ignore
1507 } else if (token.indexOf(':') >= 0 || token.matches("\\d+")) {
1508 // range search
1509 conditions.indexRange = token;
1510 } else {
1511 throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", token);
1512 }
1543 // ignore
1544 } else if (token.indexOf(':') >= 0 || token.matches("\\d+")) {
1545 // range search
1546 conditions.indexRange = token;
1547 } else {
1548 throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", token);
1549 }
15131550 // client side search token
15141551 return null;
15151552 }
16051642 }
16061643 } else //noinspection StatementWithEmptyBody
16071644 if ("\\Draft".equalsIgnoreCase(flag)) {
1608 // ignore, draft is readonly after create
1609 } else if (message.keywords != null) {
1610 properties.put("keywords", message.removeFlag(flag));
1611 }
1645 // ignore, draft is readonly after create
1646 } else if (message.keywords != null) {
1647 properties.put("keywords", message.removeFlag(flag));
1648 }
16121649 }
16131650 } else if ("+Flags".equalsIgnoreCase(action) || "+FLAGS.SILENT".equalsIgnoreCase(action)) {
16141651 ImapTokenizer flagtokenizer = new ImapTokenizer(flags);
16461683 }
16471684 } else //noinspection StatementWithEmptyBody
16481685 if ("\\Draft".equalsIgnoreCase(flag)) {
1649 // ignore, draft is readonly after create
1650 } else {
1651 properties.put("keywords", message.addFlag(flag));
1652 }
1686 // ignore, draft is readonly after create
1687 } else {
1688 properties.put("keywords", message.addFlag(flag));
1689 }
16531690 }
16541691 } else if ("FLAGS".equalsIgnoreCase(action) || "FLAGS.SILENT".equalsIgnoreCase(action)) {
16551692 // flag list with default values
16781715 junk = true;
16791716 } else //noinspection StatementWithEmptyBody
16801717 if ("\\Draft".equalsIgnoreCase(flag)) {
1681 // ignore, draft is readonly after create
1682 } else {
1683 if (keywords == null) {
1684 keywords = new HashSet<>();
1685 }
1686 keywords.add(flag);
1687 }
1718 // ignore, draft is readonly after create
1719 } else {
1720 if (keywords == null) {
1721 keywords = new HashSet<>();
1722 }
1723 keywords.add(flag);
1724 }
16881725 }
16891726 if (keywords != null) {
16901727 properties.put("keywords", message.setFlags(keywords));
18011838 size++;
18021839 if (((state != State.BODY && writeHeaders) || (state == State.BODY && writeBody)) &&
18031840 (size > startIndex) && (bufferSize < maxSize)
1804 ) {
1841 ) {
18051842 super.write(b);
18061843 bufferSize++;
18071844 }
20702107 } else if (currentQuote == '"' && currentChar == '"' ||
20712108 currentQuote == '(' && currentChar == ')' ||
20722109 currentQuote == '[' && currentChar == ']'
2073 ) {
2110 ) {
20742111 // end quote
20752112 quotes.pop();
20762113 } else {
20812118 currentIndex++;
20822119 }
20832120 String result = new String(value, startIndex, currentIndex - startIndex);
2084 startIndex = currentIndex+1;
2121 startIndex = currentIndex + 1;
20852122 return result;
20862123 }
20872124 }
552552
553553 rootLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("rootLogger"));
554554 davmailLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("davmail"));
555 httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("org.apache.commons.httpclient"));
555 httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient"));
556556 wireLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient.wire"));
557557
558558 addSettingComponent(leftLoggingPanel, BundleMessage.format("UI_LOG_DEFAULT"), rootLoggingLevelField);
668668
669669 rootLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("rootLogger"));
670670 davmailLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("davmail"));
671 httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("org.apache.commons.httpclient"));
671 httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient"));
672672 wireLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient.wire"));
673673 logFilePathField.setText(Settings.getProperty("davmail.logFilePath"));
674674 logFileSizeField.setText(Settings.getProperty("davmail.logFileSize"));
839839
840840 Settings.setLoggingLevel("rootLogger", (Level) rootLoggingLevelField.getSelectedItem());
841841 Settings.setLoggingLevel("davmail", (Level) davmailLoggingLevelField.getSelectedItem());
842 Settings.setLoggingLevel("org.apache.commons.httpclient", (Level) httpclientLoggingLevelField.getSelectedItem());
842 Settings.setLoggingLevel("httpclient", (Level) httpclientLoggingLevelField.getSelectedItem());
843843 Settings.setLoggingLevel("httpclient.wire", (Level) wireLoggingLevelField.getSelectedItem());
844844 Settings.setProperty("davmail.logFilePath", logFilePathField.getText());
845845 Settings.setProperty("davmail.logFileSize", logFileSizeField.getText());
243243 }
244244
245245 if (!Settings.getBooleanProperty("davmail.server")) {
246 if ("GNOME-Classic:GNOME".equals(currentDesktop) || "ubuntu:GNOME".equals(currentDesktop)) {
247 LOGGER.info("System tray is not supported on Gnome, will switch to frame mode");
248 } else if (!notray) {
246 if (!notray) {
249247 if ("Unity".equals(currentDesktop)) {
250248 LOGGER.info("Detected Unity desktop, please follow instructions at " +
251249 "http://davmail.sourceforge.net/linuxsetup.html to restore normal systray " +
252250 "or run DavMail in server mode");
251 } else if (currentDesktop != null && currentDesktop.contains("GNOME")) {
252 LOGGER.info("Detected Gnome desktop, please follow instructions at " +
253 "http://davmail.sourceforge.net/linuxsetup.html or " +
254 "https://extensions.gnome.org/extension/1503/tray-icons/ " +
255 "to restore normal systray or run DavMail in server mode");
253256 }
254257 if (Settings.O365_INTERACTIVE.equals(Settings.getProperty("davmail.mode"))) {
255258 LOGGER.info("O365Interactive is not compatible with SWT, do not try to create SWT tray");
356359 boolean isXFCE = "XFCE".equals(xdgCurrentDesktop);
357360 boolean isUnity = "Unity".equals(xdgCurrentDesktop);
358361 boolean isCinnamon = "X-Cinnamon".equals(xdgCurrentDesktop);
362 boolean isGnome = xdgCurrentDesktop != null && xdgCurrentDesktop.contains("GNOME");
359363
360364 if (backgroundColorString == null || backgroundColorString.length() == 0) {
361365 // define color for default theme
370374 }
371375 if (isCinnamon) {
372376 backgroundColorString = "#2E2E2E";
377 }
378 if (isGnome) {
379 backgroundColorString = "#000000";
373380 }
374381 }
375382
383390 imageType = BufferedImage.TYPE_INT_RGB;
384391 }
385392
386 if (backgroundColor != null || isKDE || isUnity || isXFCE) {
393 if (backgroundColor != null || isKDE || isUnity || isXFCE || isGnome) {
387394 int width = image.getWidth();
388395 int height = image.getHeight();
389396 int x = 0;
398405 height = 24;
399406 x = 4;
400407 y = 4;
401 } else if (isCinnamon) {
408 } else if (isCinnamon | isGnome) {
402409 width = 24;
403410 height = 24;
404411 x = 4;
00 # Warning : actual log levels set in davmail.properties
11 log4j.rootLogger=WARN, ConsoleAppender
22 log4j.logger.davmail=DEBUG
3 log4j.logger.httpclient.wire=WARN
4 log4j.logger.org.apache.commons.httpclient=WARN
3 log4j.logger.org.apache.http=WARN
54
65 # ConsoleAppender is set to be a ConsoleAppender.
76 log4j.appender.ConsoleAppender=org.apache.log4j.ConsoleAppender
1817 #log4j.appender.ConnectionAppender.layout=org.apache.log4j.PatternLayout
1918 #log4j.appender.ConnectionAppender.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c %x - %m%n
2019
21 #log4j.logger.org.apache.commons.httpclient.util.IdleConnectionHandler=DEBUG
22 #log4j.logger.org.apache.commons.httpclient.util.MultiThreadedHttpConnectionManager=DEBUG
23
2420 #log4j.appender.defaultSocketAppender=org.apache.log4j.net.SocketAppender
2521 #log4j.appender.defaultSocketAppender.RemoteHost=localhost
2622 #log4j.appender.defaultSocketAppender.port=4560
src/site/resources/images/osxgrowl.png less more
Binary diff not shown
331331 </p>
332332 <p>The server sets a timeout on the socket which listens for client connections, controlled by the property <code>davmail.clientSoTimeout</code>. If there is no activity before the timeout period elapses, the connection will be closed. Setting this to <code>0</code> will disable the socket timeout.
333333 </p>
334 <p>If you have enabled the IDLE extension in DavMail, check your client is checking for new mail more frequently than the timeout you have set.
335 </p>
334336 <p>
335337 <strong>Message deleted over IMAP still visible through OWA</strong>
336338 </p>
4545 <code>davmail -notray</code>
4646 </subsection>
4747 <subsection name="Manual setup">
48 <p>Prerequisite: OpenJDK 6 or later or Sun (Oracle) JRE 6 or later.
48 <p>Prerequisite: OpenJDK 8 or later or Sun (Oracle) JRE 8 or later.
4949 If SWT is available it provides an improved tray icon.
5050 If you do not want any tray icon run DavMail with the <code>-notray</code> option.
5151 </p>
1010 <body>
1111
1212 <section name="DavMail Setup as a standalone server">
13 <p>Prerequisite : Sun (Oracle) JRE or OpenJDK 6 to 11.
13 <p>Prerequisite : Sun (Oracle) JRE or OpenJDK 8 or later.
1414 </p>
1515
1616 <p>Davmail Gateway can run in server mode as a gateway between the mail
118118
119119 # Delete messages immediately on IMAP STORE \Deleted flag
120120 davmail.imapAutoExpunge=true
121 # Enable IDLE support, set polling delay in minutes
121 # To enable IDLE support, set a maximum client polling delay in minutes
122 # Clients using IDLE should poll more frequently than this delay
122123 davmail.imapIdleDelay=
123124 # Always reply to IMAP RFC822.SIZE requests with Exchange approximate message size for performance reasons
124125 davmail.imapAlwaysApproxMsgSize=
220221 </section>
221222
222223 <section name="DavMail Setup as a JEE Web Application">
223 <p>Prerequisites : Oracle JRE 6 or later or OpenJDK 6 or later and any JEE compliant web container
224 <p>Prerequisites : Oracle JRE 8 or later or OpenJDK 8 or later and any JEE compliant web container
224225 </p>
225226
226227 <p>Davmail Gateway can now be deployed in any JEE application server using
1010 <body>
1111
1212 <section name="DavMail Setup on windows">
13 <p>Prerequisite : Sun JRE 5, 6 or 7. Tray icon is now implemented with SWT and compatible with
14 Java 5. You may use DavMail with an older version, but the gateway will run as a
15 command line application.
13 <p>Prerequisite : OpenJDK 8 or later.
14 On windows,
15 <a href="https://www.azul.com/downloads/zulu-community/?os=windows&amp;architecture=x86-64-bit&amp;package=jre-fx">Zulu JRE FX</a>
16 is a good option to have OpenJFX available for O365Interactive.
1617 </p>
1718
1819 <p>If Java is not available, DavMail Jsmooth launcher will trigger java download and
0 /*
1 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
2 * Copyright (C) 2010 Mickael Guessant
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18
19 package davmail.caldav;
20
21 import davmail.AbstractDavMailTestCase;
22 import davmail.Settings;
23 import davmail.exchange.ExchangeSession;
24 import davmail.exchange.ExchangeSessionFactory;
25 import davmail.exchange.VCalendar;
26 import davmail.exchange.VObject;
27 import davmail.exchange.VProperty;
28
29 import java.io.IOException;
30 import java.text.SimpleDateFormat;
31 import java.util.Calendar;
32 import java.util.Date;
33 import java.util.ResourceBundle;
34 import java.util.TimeZone;
35 import java.util.UUID;
36
37 public class TestCaldavRecurringEvent extends AbstractDavMailTestCase {
38
39 @Override
40 public void setUp() throws IOException {
41 super.setUp();
42 if (session == null) {
43 session = ExchangeSessionFactory.getInstance(Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"));
44 }
45 }
46
47 public String getFormattedDateTime(Date date, int hour) {
48 SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd");
49 return formatter.format(date)+"T"+hour+"0000";
50 }
51
52 public String getZuluFormattedDateTime(Calendar cal) {
53
54 SimpleDateFormat utcFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
55 utcFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
56
57 return utcFormat.format(cal.getTime());
58 }
59
60 public VCalendar buildEvent(VObject timeZone, int startHour, int endHour) {
61
62 VCalendar vCalendar = new VCalendar();
63 vCalendar.setTimezone(timeZone);
64 VObject vEvent = new VObject();
65 vEvent.setType("VEVENT");
66 vEvent.addPropertyValue("UID", UUID.randomUUID().toString());
67 vEvent.addPropertyValue("SUMMARY", "Unit test event");
68 vEvent.addPropertyValue("LOCATION", "Location");
69
70 VProperty dtStart = new VProperty("DTSTART", getFormattedDateTime(new Date(), startHour));
71 dtStart.addParam("TZID", timeZone.getPropertyValue("TZID"));
72 vEvent.addProperty(dtStart);
73
74 VProperty dtEnd = new VProperty("DTEND", getFormattedDateTime(new Date(), endHour));
75 dtEnd.addParam("TZID", timeZone.getPropertyValue("TZID"));
76 vEvent.addProperty(dtEnd);
77
78 vCalendar.addVObject(vEvent);
79 return vCalendar;
80 }
81
82 public void dumpEvent(VCalendar vCalendar) {
83 VObject vEvent = vCalendar.getFirstVevent();
84 System.out.println("**********");
85 System.out.println("Summary: "+vEvent.getPropertyValue("SUMMARY"));
86 String organizer = vEvent.getPropertyValue("ORGANIZER");
87 if (organizer != null) {
88 System.out.println("Organizer: " +organizer);
89 }
90 System.out.println("Start: "+vEvent.getPropertyValue("DTSTART"));
91 System.out.println("End: "+vEvent.getPropertyValue("DTEND"));
92
93 System.out.println("**********");
94 }
95
96
97 public void testCreateUpdateRecurringEvent() throws IOException {
98 VCalendar vEvent = buildEvent(session.getVTimezone(), 10, 11);
99 // set weekly recurrence
100 vEvent.setFirstVeventPropertyValue("RRULE", "FREQ=WEEKLY");
101
102 dumpEvent(vEvent);
103
104 String itemName = vEvent.getFirstVeventPropertyValue("UID")+".EML";
105
106 ExchangeSession.ItemResult itemResult = session.createOrUpdateItem("calendar",
107 itemName,
108 vEvent.toString(),null, null);
109
110 assertEquals(201, itemResult.status);
111
112 ExchangeSession.Item item = session.getItem("calendar", itemName);
113
114 VCalendar createdEvent = new VCalendar(item.getBody(), session.getEmail(), session.getVTimezone());
115 dumpEvent(createdEvent);
116
117 assertEquals(
118 vEvent.getFirstVeventPropertyValue("DTSTART"),
119 createdEvent.getFirstVeventPropertyValue("DTSTART")
120 );
121
122 assertEquals(
123 vEvent.getFirstVeventPropertyValue("DTEND"),
124 createdEvent.getFirstVeventPropertyValue("DTEND")
125 );
126
127 assertEquals(
128 vEvent.getFirstVeventPropertyValue("LOCATION"),
129 createdEvent.getFirstVeventPropertyValue("LOCATION")
130 );
131
132
133
134 createdEvent.setFirstVeventPropertyValue("DTEND", getFormattedDateTime(new Date(), 12));
135 createdEvent.setFirstVeventPropertyValue("LOCATION", "Location updated");
136
137 session.createOrUpdateItem("calendar", itemName, createdEvent.toString(), null, null);
138
139 ExchangeSession.Item updatedItem = session.getItem("calendar", itemName);
140 VCalendar updatedEvent = new VCalendar(updatedItem.getBody(), session.getEmail(), session.getVTimezone());
141 dumpEvent(updatedEvent);
142
143 assertEquals(
144 createdEvent.getFirstVeventPropertyValue("DTEND"),
145 updatedEvent.getFirstVeventPropertyValue("DTEND")
146 );
147
148 assertEquals(
149 createdEvent.getFirstVeventPropertyValue("LOCATION"),
150 updatedEvent.getFirstVeventPropertyValue("LOCATION")
151 );
152
153 session.deleteItem("calendar", itemName);
154 }
155
156 public void testExclusion() throws IOException {
157
158 VCalendar vEvent = buildEvent(session.getVTimezone(), 10, 11);
159 // set dayly recurrence
160 vEvent.setFirstVeventPropertyValue("RRULE", "FREQ=WEEKLY");
161
162 dumpEvent(vEvent);
163
164 String itemName = vEvent.getFirstVeventPropertyValue("UID")+".EML";
165
166 ExchangeSession.ItemResult itemResult = session.createOrUpdateItem("calendar",
167 itemName,
168 vEvent.toString(),null, null);
169
170 assertEquals(201, itemResult.status);
171
172 ExchangeSession.Item item = session.getItem("calendar", itemName);
173
174 VCalendar createdEvent = new VCalendar(item.getBody(), session.getEmail(), session.getVTimezone());
175 dumpEvent(createdEvent);
176
177 // need to find current session timezone
178 String tzid = session.getVTimezone().getPropertyValue("TZID");
179 System.out.println(tzid);
180 // convert to standard timezone
181 ResourceBundle tzBundle = ResourceBundle.getBundle("stdtimezones");
182 String stdtzid = tzBundle.getString(tzid);
183 TimeZone javaTimezone = TimeZone.getTimeZone(stdtzid);
184
185 Calendar nextWeek = Calendar.getInstance();
186 nextWeek.setTimeZone(javaTimezone);
187 nextWeek.set(Calendar.HOUR, 10);
188 nextWeek.set(Calendar.MINUTE, 0);
189 nextWeek.set(Calendar.SECOND, 0);
190 nextWeek.add(Calendar.DAY_OF_MONTH, 7);
191
192 createdEvent.setFirstVeventPropertyValue("EXDATE", getZuluFormattedDateTime(nextWeek));
193
194 session.createOrUpdateItem("calendar", itemName, createdEvent.toString(), null, null);
195
196 ExchangeSession.Item updatedItem = session.getItem("calendar", itemName);
197 VCalendar updatedEvent = new VCalendar(updatedItem.getBody(), session.getEmail(), session.getVTimezone());
198 dumpEvent(updatedEvent);
199
200 session.deleteItem("calendar", itemName);
201 }
202 }