New upstream version 6.0.0.3375
Alexandre Rossi
2 years ago
7 | 7 | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=mguessan_davmail&metric=alert_status)](https://sonarcloud.io/dashboard/index/mguessan_davmail) |
8 | 8 | [![SonarCloud Bugs](https://sonarcloud.io/api/project_badges/measure?project=mguessan_davmail&metric=bugs)](https://sonarcloud.io/dashboard/index/mguessan_davmail) |
9 | 9 | [![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 | |
10 | 12 | |
11 | 13 | 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 |
12 | 14 | |
27 | 29 | * 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) |
28 | 30 | * 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) |
29 | 31 | * 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) | |
30 | 33 | |
31 | 34 | * 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) |
32 | 35 |
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 | ||
0 | 98 | ## DavMail 5.5.1 2019-04-19 |
1 | 99 | Fix regression on domain\username authentication over IMAP and some cleanup |
2 | 100 |
5 | 5 | matrix: |
6 | 6 | - JAVA_HOME: C:\Program Files\Java\jdk9 |
7 | 7 | - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 |
8 | - JAVA_HOME: C:\Program Files\Java\jdk10 | |
9 | 8 | - 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 | |
12 | 10 | |
13 | 11 | install: |
14 | 12 | - ps: | |
15 | 13 | 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" )) { | |
17 | 15 | (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', | |
19 | 17 | 'C:\ant-bin.zip' |
20 | 18 | ) |
21 | 19 | [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\ant-bin.zip", "C:\ant") |
22 | 20 | } |
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% | |
24 | 22 | - cmd: set ANT_OPTS=-Dfile.encoding=UTF-8 |
25 | 23 | - cmd: java -version |
26 | 24 | - cmd: ant -version |
27 | 25 | - cmd: copy /y C:\projects\davmail\nsis\* "C:\Program Files (x86)\NSIS\Plugins\x86-ansi" |
28 | 26 | build_script: |
29 | 27 | - 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 | |
31 | 30 | test: false |
32 | 31 | cache: |
33 | 32 | - C:\ant |
0 | 0 | <project name="DavMail" default="dist" basedir="."> |
1 | 1 | <property file="user.properties"/> |
2 | <property name="version" value="5.5.1"/> | |
2 | <property name="version" value="6.0.0"/> | |
3 | 3 | |
4 | 4 | <path id="classpath"> |
5 | 5 | <pathelement location="classes"/> |
233 | 233 | <exclude name="winrun4j-*.jar"/> |
234 | 234 | </fileset> |
235 | 235 | </copy> |
236 | <!-- move libgrowl to library path --> | |
237 | <copy todir="dist/DavMail.app/Contents/MacOS"> | |
238 | <fileset file="lib/libgrowl.jnilib"/> | |
239 | </copy> | |
240 | 236 | <copy file="src/osx/tray.icns" todir="dist/DavMail.app/Contents/Resources" overwrite="true"/> |
241 | 237 | <!-- use generic app launcher --> |
242 | 238 | <copy file="src/osx/davmail" todir="dist/DavMail.app/Contents/MacOS" overwrite="true"/> |
293 | 289 | <include name="*.jar"/> |
294 | 290 | <!-- exclude swt jars from debian package --> |
295 | 291 | <exclude name="lib/swt*.jar"/> |
296 | <exclude name="lib/libgrowl-*.jar"/> | |
297 | 292 | <exclude name="lib/winrun4j-*.jar"/> |
298 | 293 | </tarfileset> |
299 | 294 | <tarfileset dir="src/bin" prefix="usr/bin" filemode="755"> |
329 | 324 | <exclude name="ant-deb*.jar"/> |
330 | 325 | <exclude name="junit-*.jar"/> |
331 | 326 | <exclude name="hamcrest-core-*.jar"/> |
332 | <exclude name="libgrowl-*.jar"/> | |
333 | 327 | <exclude name="nsisant-*.jar"/> |
334 | 328 | <exclude name="servlet-api-*.jar"/> |
335 | 329 | <exclude name="swt-*.jar"/> |
382 | 376 | <include name="*.jar"/> |
383 | 377 | <exclude name="nsisant*.jar"/> |
384 | 378 | <exclude name="swt*.jar"/> |
385 | <exclude name="libgrowl-*.jar"/> | |
386 | 379 | <exclude name="winrun4j-*.jar"/> |
387 | 380 | </fileset> |
388 | 381 | </copy> |
397 | 390 | <include name="*.jar"/> |
398 | 391 | <!-- exclude swt jars from platform independent package --> |
399 | 392 | <exclude name="lib/swt*.jar"/> |
400 | <exclude name="lib/libgrowl-*.jar"/> | |
401 | 393 | <exclude name="lib/junit-*.jar"/> |
402 | 394 | <exclude name="lib/hamcrest-core-*.jar"/> |
403 | 395 | <exclude name="lib/winrun4j-*.jar"/> |
415 | 407 | <include name="*.jar"/> |
416 | 408 | <include name="davmail*.exe"/> |
417 | 409 | <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"/> | |
419 | 423 | <exclude name="lib/junit-*.jar"/> |
420 | 424 | <exclude name="lib/hamcrest-core-*.jar"/> |
421 | 425 | </fileset> |
447 | 451 | <exclude name="target/**"/> |
448 | 452 | <exclude name="archive/**"/> |
449 | 453 | <exclude name="lib/**"/> |
450 | <exclude name="libgrowl/**"/> | |
451 | 454 | <exclude name="nsis/**"/> |
452 | 455 | <exclude name="svnant/**"/> |
453 | 456 | <exclude name="user.properties"/> |
543 | 546 | <sonar/> |
544 | 547 | </target> |
545 | 548 | |
549 | <target name="download-jre"> | |
550 | <get src="https://api.azul.com/zulu/download/community/v1.0/bundles/latest/binary/?jdk_version=15&ext=zip&os=windows&arch=x86&hw_bitness=64&bundle_type=jre&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 | ||
546 | 559 | </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} | |
2 | 2 | %define davver %{davrel}-%{davsvn} |
3 | 3 | |
4 | 4 | Summary: DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP gateway for Microsoft Exchange |
77 | 77 | [ -f %{_libdir}/java/swt.jar ] && ln -s %{_libdir}/java/swt.jar lib/swt.jar || ln -s /usr/lib/java/swt.jar lib/swt.jar |
78 | 78 | %endif |
79 | 79 | |
80 | # we have java 1.6 | |
80 | # we have java 8 | |
81 | 81 | ant -Dant.java.version=1.8 prepare-dist |
82 | 82 | |
83 | 83 | %install |
4 | 4 | <groupId>davmail</groupId> |
5 | 5 | <artifactId>davmail</artifactId> |
6 | 6 | <packaging>jar</packaging> |
7 | <version>5.5.1</version> | |
7 | <version>6.0.0</version> | |
8 | 8 | <name>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange and Office 365 Gateway</name> |
9 | 9 | <organization> |
10 | 10 | <name>Mickaël Guessant</name> |
163 | 163 | <dependency> |
164 | 164 | <groupId>junit</groupId> |
165 | 165 | <artifactId>junit</artifactId> |
166 | <version>4.12</version> | |
166 | <version>4.13.1</version> | |
167 | 167 | <scope>test</scope> |
168 | 168 | </dependency> |
169 | 169 | <dependency> |
191 | 191 | <artifactId>jcl-over-slf4j</artifactId> |
192 | 192 | </exclusion> |
193 | 193 | </exclusions> |
194 | </dependency> | |
195 | <dependency> | |
196 | <groupId>commons-httpclient</groupId> | |
197 | <artifactId>commons-httpclient</artifactId> | |
198 | <version>3.1</version> | |
199 | 194 | </dependency> |
200 | 195 | <dependency> |
201 | 196 | <groupId>commons-codec</groupId> |
253 | 248 | <dependency> |
254 | 249 | <groupId>com.fasterxml.woodstox</groupId> |
255 | 250 | <artifactId>woodstox-core</artifactId> |
256 | <version>5.1.0</version> | |
251 | <version>6.2.0</version> | |
257 | 252 | </dependency> |
258 | 253 | <dependency> |
259 | 254 | <groupId>org.samba.jcifs</groupId> |
273 | 268 | <version>0.4.5</version> |
274 | 269 | <scope>system</scope> |
275 | 270 | <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> | |
283 | 271 | </dependency> |
284 | 272 | <dependency> |
285 | 273 | <groupId>org.codehaus.jettison</groupId> |
38 | 38 | <developer_name>Mickaël Guessant</developer_name> |
39 | 39 | <content_rating type="oars-1.1" /> |
40 | 40 | <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> | |
41 | 142 | <release version="5.5.1" date="2019-04-19"> |
42 | 143 | <description> |
43 | 144 | <p> |
7 | 7 | # force GTK2 to avoid crash with OpenJDK 11 |
8 | 8 | JAVA_OPTS="-Xmx512M -Dsun.net.inetaddr.ttl=60 -Djdk.gtk.version=2.2" |
9 | 9 | 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 | ||
10 | 25 | # uncomment this to force JDK 8 |
11 | 26 | #JAVA=/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java |
12 | 27 | # add JFX to classpath with OpenJDK 11 |
22 | 22 | davmail.ldapPort=1389 |
23 | 23 | davmail.popPort=1110 |
24 | 24 | davmail.smtpPort=1025 |
25 | ||
26 | # Optional: separate file to store Oauth tokens | |
27 | #davmail.oauth.tokenFilePath= | |
25 | 28 | |
26 | 29 | ############################################################# |
27 | 30 | # Network settings |
87 | 90 | |
88 | 91 | # Delete messages immediately on IMAP STORE \Deleted flag |
89 | 92 | 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 | |
91 | 95 | davmail.imapIdleDelay= |
92 | 96 | # Always reply to IMAP RFC822.SIZE requests with Exchange approximate message size for performance reasons |
93 | 97 | davmail.imapAlwaysApproxMsgSize= |
118 | 122 | # log levels |
119 | 123 | log4j.logger.davmail=WARN |
120 | 124 | log4j.logger.httpclient.wire=WARN |
121 | log4j.logger.org.apache.commons.httpclient=WARN | |
125 | log4j.logger.httpclient=WARN | |
122 | 126 | log4j.rootLogger=WARN |
123 | 127 | |
124 | 128 | ############################################################# |
8 | 8 | Type=simple |
9 | 9 | User=davmail |
10 | 10 | PermissionsStartOnly=true |
11 | AmbientCapabilities=CAP_NET_BIND_SERVICE | |
11 | 12 | ExecStartPre=/usr/bin/touch /var/log/davmail.log |
12 | 13 | ExecStartPre=/bin/chown davmail:davmail /var/log/davmail.log |
13 | 14 | ExecStart=/usr/bin/davmail -server /etc/davmail.properties |
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 | } |
20 | 20 | import davmail.caldav.CaldavServer; |
21 | 21 | import davmail.exception.DavMailException; |
22 | 22 | import davmail.exchange.ExchangeSessionFactory; |
23 | import davmail.http.DavGatewayHttpClientFacade; | |
24 | import davmail.http.DavGatewaySSLProtocolSocketFactory; | |
25 | 23 | import davmail.http.HttpClientAdapter; |
26 | 24 | import davmail.http.request.GetRequest; |
27 | 25 | import davmail.imap.ImapServer; |
119 | 117 | * Start DavMail listeners. |
120 | 118 | */ |
121 | 119 | public static void start() { |
122 | // register custom SSL Socket factory | |
123 | DavGatewaySSLProtocolSocketFactory.register(); | |
124 | ||
125 | // prepare HTTP connection pool | |
126 | DavGatewayHttpClientFacade.start(); | |
127 | ||
128 | 120 | SERVER_LIST.clear(); |
129 | 121 | |
130 | 122 | int smtpPort = Settings.getIntProperty("davmail.smtpPort"); |
189 | 181 | public static void stop() { |
190 | 182 | DavGateway.stopServers(); |
191 | 183 | // close pooled connections |
192 | DavGatewayHttpClientFacade.stop(); | |
193 | // clear session cache | |
194 | ExchangeSessionFactory.reset(); | |
184 | ExchangeSessionFactory.shutdown(); | |
195 | 185 | DavGatewayTray.info(new BundleMessage("LOG_GATEWAY_STOP")); |
196 | 186 | DavGatewayTray.dispose(); |
197 | 187 | } |
202 | 192 | public static void restart() { |
203 | 193 | DavGateway.stopServers(); |
204 | 194 | // clear session cache |
205 | ExchangeSessionFactory.reset(); | |
195 | ExchangeSessionFactory.shutdown(); | |
206 | 196 | DavGateway.start(); |
207 | 197 | } |
208 | 198 |
42 | 42 | import java.util.Properties; |
43 | 43 | import java.util.TreeSet; |
44 | 44 | |
45 | import static org.apache.http.util.TextUtils.isEmpty; | |
46 | ||
45 | 47 | /** |
46 | 48 | * Settings facade. |
47 | 49 | * DavMail settings are stored in the .davmail.properties file in current |
48 | 50 | * user home directory or in the file specified on the command line. |
49 | 51 | */ |
50 | 52 | public final class Settings { |
53 | ||
54 | protected static final Logger LOGGER = Logger.getLogger(Settings.class); | |
51 | 55 | |
52 | 56 | public static final String O365_URL = "https://outlook.office365.com/EWS/Exchange.asmx"; |
53 | 57 | public static final String O365 = "O365"; |
57 | 61 | public static final String WEBDAV = "WebDav"; |
58 | 62 | public static final String EWS = "EWS"; |
59 | 63 | 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"; | |
60 | 66 | |
61 | 67 | private Settings() { |
62 | 68 | } |
212 | 218 | SETTINGS.put("log4j.rootLogger", Level.WARN.toString()); |
213 | 219 | SETTINGS.put("log4j.logger.davmail", Level.DEBUG.toString()); |
214 | 220 | 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()); | |
216 | 222 | SETTINGS.put("davmail.logFilePath", ""); |
217 | 223 | } |
218 | 224 | |
325 | 331 | // update logging levels |
326 | 332 | Settings.setLoggingLevel("rootLogger", Settings.getLoggingLevel("rootLogger")); |
327 | 333 | 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")); | |
330 | 334 | // set logging levels for HttpClient 4 |
331 | 335 | 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")); | |
333 | 338 | } |
334 | 339 | |
335 | 340 | /** |
356 | 361 | Enumeration<?> propertyEnumeration = properties.propertyNames(); |
357 | 362 | while (propertyEnumeration.hasMoreElements()) { |
358 | 363 | String propertyName = (String) propertyEnumeration.nextElement(); |
359 | writer.write(propertyName+"="+ escapeValue(properties.getProperty(propertyName))); | |
364 | writer.write(propertyName + "=" + escapeValue(properties.getProperty(propertyName))); | |
360 | 365 | writer.newLine(); |
361 | 366 | } |
362 | 367 | } catch (IOException e) { |
386 | 391 | * Convert input property line to new line with value from properties. |
387 | 392 | * Preserve comments |
388 | 393 | * |
389 | * @param line input line | |
394 | * @param line input line | |
390 | 395 | * @param properties new property values |
391 | 396 | * @return new line |
392 | 397 | */ |
403 | 408 | String value = properties.getProperty(key); |
404 | 409 | if (value != null) { |
405 | 410 | // build property with new value |
406 | line = key+"="+ escapeValue(value); | |
411 | line = key + "=" + escapeValue(value); | |
407 | 412 | // remove property from source |
408 | 413 | properties.remove(key); |
409 | 414 | } |
410 | 415 | } |
411 | return line+comment; | |
416 | return line + comment; | |
412 | 417 | } |
413 | 418 | |
414 | 419 | /** |
415 | 420 | * Escape backslash in value. |
421 | * | |
416 | 422 | * @param value value |
417 | 423 | * @return escaped value |
418 | 424 | */ |
419 | 425 | private static String escapeValue(String value) { |
420 | 426 | StringBuilder buffer = new StringBuilder(); |
421 | for (char c:value.toCharArray()) { | |
427 | for (char c : value.toCharArray()) { | |
422 | 428 | if (c == '\\') { |
423 | 429 | buffer.append('\\'); |
424 | 430 | } |
546 | 552 | } |
547 | 553 | |
548 | 554 | 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 | ||
556 | 631 | /** |
557 | 632 | * Build logging properties prefix. |
558 | 633 | * |
667 | 742 | System.getProperty("os.name").toLowerCase().startsWith("freebsd"); |
668 | 743 | } |
669 | 744 | |
745 | public static String getUserAgent() { | |
746 | return getProperty("davmail.userAgent", Settings.EDGE_USER_AGENT); | |
747 | } | |
670 | 748 | } |
471 | 471 | response.appendProperty("D:resourcetype"); |
472 | 472 | } |
473 | 473 | if (request.hasProperty("displayname")) { |
474 | response.appendProperty("D:displayname", item.getName()); | |
474 | response.appendProperty("D:displayname", StringUtil.xmlEncode(item.getName())); | |
475 | 475 | } |
476 | 476 | response.endPropStatOK(); |
477 | 477 | response.endResponse(); |
24 | 24 | import davmail.exception.WebdavNotAvailableException; |
25 | 25 | import davmail.exchange.auth.ExchangeAuthenticator; |
26 | 26 | import davmail.exchange.auth.ExchangeFormAuthenticator; |
27 | import davmail.exchange.auth.HC4ExchangeFormAuthenticator; | |
28 | 27 | import davmail.exchange.dav.DavExchangeSession; |
29 | import davmail.exchange.dav.HC4DavExchangeSession; | |
30 | 28 | 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; | |
35 | 33 | |
36 | 34 | import java.awt.*; |
37 | 35 | import java.io.IOException; |
124 | 122 | try { |
125 | 123 | String mode = Settings.getProperty("davmail.mode"); |
126 | 124 | if (Settings.O365.equals(mode)) { |
127 | // force url wit O365 | |
125 | // force url with O365 | |
128 | 126 | baseUrl = Settings.O365_URL; |
129 | 127 | } |
130 | 128 | |
176 | 174 | } |
177 | 175 | |
178 | 176 | if (authenticatorClass != null) { |
179 | ExchangeAuthenticator authenticator = (ExchangeAuthenticator) Class.forName(authenticatorClass).newInstance(); | |
177 | ExchangeAuthenticator authenticator = (ExchangeAuthenticator) Class.forName(authenticatorClass) | |
178 | .getDeclaredConstructor().newInstance(); | |
180 | 179 | authenticator.setUsername(poolKey.userName); |
181 | 180 | authenticator.setPassword(poolKey.password); |
182 | 181 | 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); | |
186 | 183 | |
187 | 184 | } 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")) { | |
190 | 187 | if (poolKey.url.toLowerCase().endsWith("/ews/exchange.asmx")) { |
191 | 188 | 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); | |
196 | 190 | } else { |
197 | 191 | ExchangeSession.LOGGER.debug("OWA authentication in EWS mode"); |
198 | 192 | ExchangeFormAuthenticator exchangeFormAuthenticator = new ExchangeFormAuthenticator(); |
200 | 194 | exchangeFormAuthenticator.setUsername(poolKey.userName); |
201 | 195 | exchangeFormAuthenticator.setPassword(poolKey.password); |
202 | 196 | exchangeFormAuthenticator.authenticate(); |
203 | session = new EwsExchangeSession(exchangeFormAuthenticator.getHttpClient(), | |
197 | session = new EwsExchangeSession(exchangeFormAuthenticator.getHttpClientAdapter(), | |
204 | 198 | exchangeFormAuthenticator.getExchangeUri(), exchangeFormAuthenticator.getUsername()); |
205 | 199 | } |
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()); | |
215 | 200 | } else { |
216 | 201 | ExchangeFormAuthenticator exchangeFormAuthenticator = new ExchangeFormAuthenticator(); |
217 | 202 | exchangeFormAuthenticator.setUrl(poolKey.url); |
219 | 204 | exchangeFormAuthenticator.setPassword(poolKey.password); |
220 | 205 | exchangeFormAuthenticator.authenticate(); |
221 | 206 | try { |
222 | session = new DavExchangeSession(exchangeFormAuthenticator.getHttpClient(), | |
207 | session = new DavExchangeSession(exchangeFormAuthenticator.getHttpClientAdapter(), | |
223 | 208 | exchangeFormAuthenticator.getExchangeUri(), |
224 | 209 | exchangeFormAuthenticator.getUsername()); |
225 | 210 | } catch (WebdavNotAvailableException e) { |
226 | 211 | if (Settings.AUTO.equals(mode)) { |
227 | 212 | 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); | |
231 | 214 | } else { |
232 | 215 | throw e; |
233 | 216 | } |
256 | 239 | * Check if whitelist is empty or email is allowed. |
257 | 240 | * userWhiteList is a comma separated list of values. |
258 | 241 | * \@company.com means all domain users are allowed |
242 | * | |
259 | 243 | * @param email user email |
260 | 244 | */ |
261 | 245 | private static void checkWhiteList(String email) throws DavMailAuthenticationException { |
318 | 302 | if (url == null || (!url.startsWith("http://") && !url.startsWith("https://"))) { |
319 | 303 | throw new DavMailException("LOG_INVALID_URL", url); |
320 | 304 | } |
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 | ) { | |
324 | 309 | // get webMail root url (will not follow redirects) |
325 | int status = DavGatewayHttpClientFacade.executeTestMethod(httpClient, testMethod); | |
310 | int status = response.getStatusLine().getStatusCode(); | |
326 | 311 | ExchangeSession.LOGGER.debug("Test configuration status: " + status); |
327 | 312 | if (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED |
328 | && !DavGatewayHttpClientFacade.isRedirect(status)) { | |
313 | && !HttpClientAdapter.isRedirect(status)) { | |
329 | 314 | throw new DavMailException("EXCEPTION_CONNECTION_FAILED", url, status); |
330 | 315 | } |
331 | 316 | // session opened, future failure will mean network down |
334 | 319 | errorSent = false; |
335 | 320 | } catch (Exception exc) { |
336 | 321 | handleNetworkDown(exc); |
337 | } finally { | |
338 | testMethod.releaseConnection(); | |
339 | 322 | } |
340 | 323 | |
341 | 324 | } |
408 | 391 | /** |
409 | 392 | * Reset config check status and clear session pool. |
410 | 393 | */ |
411 | public static void reset() { | |
394 | public static void shutdown() { | |
412 | 395 | configChecked = false; |
413 | 396 | 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 | } | |
415 | 403 | } |
416 | 404 | } |
61 | 61 | * @throws InterruptedException on error |
62 | 62 | * @throws IOException on error |
63 | 63 | */ |
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 { | |
65 | 65 | FolderLoadThread folderLoadThread = new FolderLoadThread(currentThread().getName(), folder); |
66 | 66 | folderLoadThread.start(); |
67 | 67 | 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 | } | |
69 | 74 | LOGGER.debug("Still loading " + folder.folderPath + " (" + folder.count() + " messages)"); |
70 | 75 | if (Settings.getBooleanProperty("davmail.enableKeepAlive", false)) { |
71 | 76 | try { |
46 | 46 | /** |
47 | 47 | * Create message in a separate thread. |
48 | 48 | * |
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 | |
54 | 54 | * @param outputStream output stream |
55 | 55 | * @param capabilities IMAP capabilities |
56 | 56 | * @throws InterruptedException on error |
57 | * @throws IOException on error | |
57 | * @throws IOException on error | |
58 | 58 | */ |
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 { | |
60 | 60 | MessageCreateThread messageCreateThread = new MessageCreateThread(currentThread().getName(), session, folderPath, messageName, properties, mimeMessage); |
61 | 61 | messageCreateThread.start(); |
62 | 62 | 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 | } | |
64 | 69 | if (!messageCreateThread.isComplete) { |
65 | 70 | if (Settings.getBooleanProperty("davmail.enableKeepAlive", false)) { |
66 | 71 | LOGGER.debug("Still loading message, send capabilities untagged response to avoid timeout"); |
67 | 72 | 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)); | |
70 | 75 | outputStream.write((char) 13); |
71 | 76 | outputStream.write((char) 10); |
72 | 77 | outputStream.flush(); |
46 | 46 | * @throws IOException on error |
47 | 47 | * @throws MessagingException on error |
48 | 48 | */ |
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 { | |
50 | 50 | if (message.size < 1024 * 1024) { |
51 | 51 | message.loadMimeMessage(); |
52 | 52 | } else { |
54 | 54 | MessageLoadThread messageLoadThread = new MessageLoadThread(currentThread().getName(), message); |
55 | 55 | messageLoadThread.start(); |
56 | 56 | 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 | } | |
58 | 63 | LOGGER.debug("Still loading uid " + message.getUid() + " imapUid " + message.getImapUid()); |
59 | 64 | if (Settings.getBooleanProperty("davmail.enableKeepAlive", false)) { |
60 | 65 | try { |
49 | 49 | inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); |
50 | 50 | inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE); |
51 | 51 | 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 | } | |
56 | 56 | return inputFactory; |
57 | 57 | } |
58 | 58 |
18 | 18 | |
19 | 19 | package davmail.exchange.auth; |
20 | 20 | |
21 | import davmail.http.HttpClientAdapter; | |
22 | ||
21 | 23 | import java.io.IOException; |
22 | 24 | import java.net.URI; |
23 | 25 | |
26 | /** | |
27 | * Common interface for all Exchange and O365 authenticators. | |
28 | * Implement this interface to build custom authenticators for unsupported Exchange architecture | |
29 | */ | |
24 | 30 | public interface ExchangeAuthenticator { |
25 | 31 | void setUsername(String username); |
26 | 32 | |
27 | 33 | void setPassword(String password); |
28 | 34 | |
35 | /** | |
36 | * Authenticate against Exchange or O365 | |
37 | * @throws IOException on error | |
38 | */ | |
29 | 39 | void authenticate() throws IOException; |
30 | 40 | |
31 | 41 | O365Token getToken() throws IOException; |
32 | 42 | |
43 | /** | |
44 | * Return default or computed Exchange or O365 url | |
45 | * @return target url | |
46 | */ | |
33 | 47 | URI getExchangeUri(); |
48 | ||
49 | /** | |
50 | * Return a new HttpClientAdapter instance with pooling enabled for ExchangeSession | |
51 | * @return HttpClientAdapter instance | |
52 | */ | |
53 | HttpClientAdapter getHttpClientAdapter(); | |
34 | 54 | } |
22 | 22 | import davmail.exception.DavMailAuthenticationException; |
23 | 23 | import davmail.exception.DavMailException; |
24 | 24 | import davmail.exception.WebdavNotAvailableException; |
25 | import davmail.http.DavGatewayHttpClientFacade; | |
26 | 25 | 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; | |
27 | 31 | 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; | |
36 | 32 | 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; | |
37 | 41 | import org.apache.log4j.Logger; |
42 | import org.htmlcleaner.BaseToken; | |
38 | 43 | import org.htmlcleaner.CommentNode; |
39 | 44 | import org.htmlcleaner.ContentNode; |
40 | 45 | import org.htmlcleaner.HtmlCleaner; |
45 | 50 | import java.io.ByteArrayInputStream; |
46 | 51 | import java.io.IOException; |
47 | 52 | import java.net.ConnectException; |
53 | import java.net.URI; | |
54 | import java.net.URISyntaxException; | |
48 | 55 | import java.net.UnknownHostException; |
49 | 56 | import java.nio.charset.StandardCharsets; |
50 | 57 | import java.util.ArrayList; |
53 | 60 | import java.util.Set; |
54 | 61 | |
55 | 62 | /** |
56 | * Form based Exchange authentication. | |
63 | * New Exchange form authenticator based on HttpClient 4. | |
57 | 64 | */ |
58 | 65 | 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"); | |
60 | 67 | |
61 | 68 | /** |
62 | 69 | * Various username fields found on custom Exchange authentication forms |
63 | 70 | */ |
64 | 71 | protected static final Set<String> USER_NAME_FIELDS = new HashSet<>(); |
72 | ||
65 | 73 | static { |
66 | 74 | USER_NAME_FIELDS.add("username"); |
67 | 75 | USER_NAME_FIELDS.add("txtusername"); |
76 | 84 | * Various password fields found on custom Exchange authentication forms |
77 | 85 | */ |
78 | 86 | protected static final Set<String> PASSWORD_FIELDS = new HashSet<>(); |
87 | ||
79 | 88 | static { |
80 | 89 | PASSWORD_FIELDS.add("password"); |
81 | 90 | PASSWORD_FIELDS.add("txtUserPass"); |
90 | 99 | * Used to open OTP dialog |
91 | 100 | */ |
92 | 101 | protected static final Set<String> TOKEN_FIELDS = new HashSet<>(); |
102 | ||
93 | 103 | static { |
94 | 104 | TOKEN_FIELDS.add("SafeWordPassword"); |
95 | 105 | TOKEN_FIELDS.add("passcode"); |
112 | 122 | */ |
113 | 123 | private String url; |
114 | 124 | /** |
115 | * HttpClient 3 instance | |
116 | */ | |
117 | private HttpClient httpClient; | |
125 | * HttpClient 4 adapter | |
126 | */ | |
127 | private HttpClientAdapter httpClientAdapter; | |
118 | 128 | /** |
119 | 129 | * A OTP pre-auth page may require a different username. |
120 | 130 | */ |
140 | 150 | * Maximum number of times the user can try to input again the OTP pre-auth key before giving up. |
141 | 151 | */ |
142 | 152 | private static final int MAX_OTP_RETRIES = 3; |
143 | // base Exchange URI after authentication | |
153 | ||
154 | /** | |
155 | * base Exchange URI after authentication | |
156 | */ | |
144 | 157 | private java.net.URI exchangeUri; |
145 | 158 | |
146 | 159 | |
161 | 174 | @Override |
162 | 175 | public void authenticate() throws DavMailException { |
163 | 176 | 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); | |
173 | 180 | |
174 | 181 | // The user may have configured an OTP pre-auth username. It is processed |
175 | 182 | // so early because OTP pre-auth may disappear in the Exchange LAN and this |
187 | 194 | } |
188 | 195 | } |
189 | 196 | |
190 | DavGatewayHttpClientFacade.setCredentials(httpClient, username, password); | |
197 | // set real credentials on http client | |
198 | httpClientAdapter.setCredentials(username, password); | |
191 | 199 | |
192 | 200 | // get webmail root url |
193 | 201 | // providing credentials |
194 | 202 | // 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)) { | |
198 | 206 | if (isHttpAuthentication) { |
199 | int status = method.getStatusCode(); | |
207 | int status = getRequest.getStatusCode(); | |
200 | 208 | |
201 | 209 | if (status == HttpStatus.SC_UNAUTHORIZED) { |
202 | method.releaseConnection(); | |
203 | 210 | throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED"); |
204 | 211 | } else if (status != HttpStatus.SC_OK) { |
205 | method.releaseConnection(); | |
206 | throw DavGatewayHttpClientFacade.buildHttpResponseException(method); | |
212 | throw HttpClientAdapter.buildHttpResponseException(getRequest, getRequest.getHttpResponse()); | |
207 | 213 | } |
208 | 214 | // 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); | |
211 | 217 | } |
212 | 218 | } 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 | } | |
224 | 222 | |
225 | 223 | } catch (DavMailAuthenticationException exc) { |
226 | 224 | close(); |
239 | 237 | LOGGER.error(BundleMessage.formatLog("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc)); |
240 | 238 | throw new DavMailException("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc); |
241 | 239 | } |
242 | LOGGER.debug("Authenticated with "+url); | |
240 | LOGGER.debug("Successfully authenticated to " + exchangeUri); | |
243 | 241 | } |
244 | 242 | |
245 | 243 | /** |
246 | 244 | * Test authentication mode : form based or basic. |
247 | 245 | * |
248 | 246 | * @param url exchange base URL |
249 | * @param httpClient httpClient instance | |
247 | * @param httpClient httpClientAdapter instance | |
250 | 248 | * @return true if basic authentication detected |
251 | 249 | */ |
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; | |
254 | 262 | } |
255 | 263 | |
256 | 264 | /** |
258 | 266 | * |
259 | 267 | * @return true if session cookies are available |
260 | 268 | */ |
261 | protected boolean isAuthenticated(HttpMethod method) { | |
269 | protected boolean isAuthenticated(ResponseWrapper getRequest) { | |
262 | 270 | 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())) { | |
265 | 273 | // direct EWS access returned wsdl |
266 | 274 | authenticated = true; |
267 | 275 | } else { |
268 | 276 | // check cookies |
269 | for (Cookie cookie : httpClient.getState().getCookies()) { | |
277 | for (Cookie cookie : httpClientAdapter.getCookies()) { | |
270 | 278 | // Exchange 2003 cookies |
271 | 279 | if (cookie.getName().startsWith("cadata") || "sessionid".equals(cookie.getName()) |
272 | 280 | // Exchange 2007 cookie |
280 | 288 | return authenticated; |
281 | 289 | } |
282 | 290 | |
283 | protected HttpMethod formLogin(HttpClient httpClient, HttpMethod initmethod, String password) throws IOException { | |
291 | protected void formLogin(HttpClientAdapter httpClient, ResponseWrapper initRequest, String password) throws IOException { | |
284 | 292 | LOGGER.debug("Form based authentication detected"); |
285 | 293 | |
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(); | |
294 | 301 | } |
295 | 302 | |
296 | 303 | /** |
297 | 304 | * Try to find logon method path from logon form body. |
298 | 305 | * |
299 | * @param httpClient httpClient instance | |
300 | * @param initmethod form body http method | |
306 | * @param httpClient httpClientAdapter instance | |
307 | * @param responseWrapper init request response wrapper | |
301 | 308 | * @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; | |
307 | 312 | |
308 | 313 | // create an instance of HtmlCleaner |
309 | 314 | HtmlCleaner cleaner = new HtmlCleaner(); |
312 | 317 | usernameInputs.clear(); |
313 | 318 | |
314 | 319 | 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); | |
317 | 324 | TagNode logonForm = null; |
318 | 325 | // select form |
319 | 326 | if (forms.size() == 1) { |
320 | logonForm = (TagNode) forms.get(0); | |
327 | logonForm = forms.get(0); | |
321 | 328 | } else if (forms.size() > 1) { |
322 | 329 | for (Object form : forms) { |
323 | 330 | if ("logonForm".equals(((TagNode) form).getAttributeByName("name"))) { |
333 | 340 | logonMethodPath = "/owa/auth.owa"; |
334 | 341 | } |
335 | 342 | |
336 | logonMethod = new PostMethod(getAbsoluteUri(initmethod, logonMethodPath)); | |
343 | logonMethod = new PostRequest(getAbsoluteUri(uri, logonMethodPath)); | |
337 | 344 | |
338 | 345 | // retrieve lost inputs attached to body |
339 | List inputList = node.getElementListByName("input", true); | |
346 | List<? extends TagNode> inputList = node.getElementListByName("input", true); | |
340 | 347 | |
341 | 348 | for (Object input : inputList) { |
342 | 349 | String type = ((TagNode) input).getAttributeByName("type"); |
343 | 350 | String name = ((TagNode) input).getAttributeByName("name"); |
344 | 351 | String value = ((TagNode) input).getAttributeByName("value"); |
345 | 352 | if ("hidden".equalsIgnoreCase(type) && name != null && value != null) { |
346 | ((PostMethod) logonMethod).addParameter(name, value); | |
353 | logonMethod.setParameter(name, value); | |
347 | 354 | } |
348 | 355 | // 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)) { | |
352 | 357 | usernameInputs.add(name); |
353 | 358 | } else if (PASSWORD_FIELDS.contains(name)) { |
354 | 359 | passwordInput = name; |
355 | 360 | } else if ("addr".equals(name)) { |
356 | 361 | // 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)); | |
359 | 363 | } else if (TOKEN_FIELDS.contains(name)) { |
360 | 364 | // one time password, ask it to the user |
361 | ((PostMethod) logonMethod).addParameter(name, DavGatewayOTPPrompt.getOneTimePassword()); | |
365 | logonMethod.setParameter(name, DavGatewayOTPPrompt.getOneTimePassword()); | |
362 | 366 | } else if ("otc".equals(name)) { |
363 | 367 | // captcha image, get image and ask user |
364 | 368 | String pinsafeUser = getAliasFromLogin(); |
365 | 369 | if (pinsafeUser == null) { |
366 | 370 | pinsafeUser = username; |
367 | 371 | } |
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(); | |
371 | 375 | if (status != HttpStatus.SC_OK) { |
372 | throw DavGatewayHttpClientFacade.buildHttpResponseException(getMethod); | |
376 | throw HttpClientAdapter.buildHttpResponseException(pinRequest, pinResponse.getStatusLine()); | |
373 | 377 | } |
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)); | |
379 | 380 | } |
380 | 381 | } |
381 | 382 | } |
382 | 383 | } else { |
383 | List frameList = node.getElementListByName("frame", true); | |
384 | List<? extends TagNode> frameList = node.getElementListByName("frame", true); | |
384 | 385 | if (frameList.size() == 1) { |
385 | String src = ((TagNode) frameList.get(0)).getAttributeByName("src"); | |
386 | String src = frameList.get(0).getAttributeByName("src"); | |
386 | 387 | if (src != null) { |
387 | 388 | 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))); | |
391 | 390 | } |
392 | 391 | } else { |
393 | 392 | // 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); | |
395 | 394 | for (Object script : scriptList) { |
396 | List contents = ((TagNode) script).getAllChildren(); | |
395 | List<? extends BaseToken> contents = ((TagNode) script).getAllChildren(); | |
397 | 396 | for (Object content : contents) { |
398 | 397 | if (content instanceof CommentNode) { |
399 | 398 | String scriptValue = ((CommentNode) content).getCommentedContent(); |
403 | 402 | sLgn = StringUtil.getToken(scriptValue, "var a_sLgn = \"", "\""); |
404 | 403 | } |
405 | 404 | if (sUrl != null && sLgn != null) { |
406 | String src = getScriptBasedFormURL(initmethod, sLgn + sUrl); | |
405 | URI src = getScriptBasedFormURL(uri, sLgn + sUrl); | |
407 | 406 | 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))); | |
410 | 408 | } |
411 | 409 | |
412 | 410 | } else if (content instanceof ContentNode) { |
415 | 413 | String location = StringUtil.getToken(scriptValue, "window.location.replace(\"", "\""); |
416 | 414 | if (location != null) { |
417 | 415 | LOGGER.debug("Post logon redirect to: " + location); |
418 | logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, location); | |
416 | logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(location))); | |
419 | 417 | } |
420 | 418 | } |
421 | 419 | } |
422 | 420 | } |
423 | 421 | } |
424 | 422 | } |
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()); | |
429 | 425 | } |
430 | 426 | |
431 | 427 | return logonMethod; |
432 | 428 | } |
433 | 429 | |
434 | 430 | |
435 | protected HttpMethod postLogonMethod(HttpClient httpClient, HttpMethod logonMethod, String password) throws IOException { | |
431 | protected ResponseWrapper postLogonMethod(HttpClientAdapter httpClient, PostRequest logonMethod, String password) throws IOException { | |
436 | 432 | |
437 | 433 | setAuthFormFields(logonMethod, httpClient, password); |
438 | 434 | |
439 | 435 | // 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); | |
443 | 442 | |
444 | 443 | // test form based authentication |
445 | checkFormLoginQueryString(logonMethod); | |
444 | checkFormLoginQueryString(resultRequest); | |
446 | 445 | |
447 | 446 | // workaround for post logon script redirect |
448 | if (!isAuthenticated(logonMethod)) { | |
447 | if (!isAuthenticated(resultRequest)) { | |
449 | 448 | // try to get new method from script based redirection |
450 | logonMethod = buildLogonMethod(httpClient, logonMethod); | |
449 | logonMethod = buildLogonMethod(httpClient, resultRequest); | |
451 | 450 | |
452 | 451 | if (logonMethod != null) { |
453 | 452 | if (otpPreAuthFound && otpPreAuthRetries < MAX_OTP_RETRIES) { |
459 | 458 | } |
460 | 459 | |
461 | 460 | // 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); | |
464 | 464 | // also check cookies |
465 | if (!isAuthenticated(logonMethod)) { | |
465 | if (!isAuthenticated(resultRequest)) { | |
466 | 466 | throwAuthenticationFailed(); |
467 | 467 | } |
468 | 468 | } else { |
472 | 472 | } |
473 | 473 | |
474 | 474 | // check for language selection form |
475 | if (logonMethod != null && "/owa/languageselection.aspx".equals(logonMethod.getPath())) { | |
475 | if ("/owa/languageselection.aspx".equals(resultRequest.getURI().getPath())) { | |
476 | 476 | // 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; | |
484 | 484 | // create an instance of HtmlCleaner |
485 | 485 | HtmlCleaner cleaner = new HtmlCleaner(); |
486 | 486 | |
487 | 487 | 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); | |
490 | 490 | TagNode languageForm; |
491 | 491 | // select form |
492 | 492 | if (forms.size() == 1) { |
493 | languageForm = (TagNode) forms.get(0); | |
493 | languageForm = forms.get(0); | |
494 | 494 | } else { |
495 | 495 | throw new IOException("Form not found"); |
496 | 496 | } |
497 | 497 | String languageMethodPath = languageForm.getAttributeByName("action"); |
498 | 498 | |
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); | |
502 | 502 | for (Object input : inputList) { |
503 | 503 | String name = ((TagNode) input).getAttributeByName("name"); |
504 | 504 | String value = ((TagNode) input).getAttributeByName("value"); |
505 | 505 | 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); | |
510 | 510 | for (Object select : selectList) { |
511 | 511 | String name = ((TagNode) select).getAttributeByName("name"); |
512 | List optionList = ((TagNode) select).getElementListByName("option", true); | |
512 | List<? extends TagNode> optionList = ((TagNode) select).getElementListByName("option", true); | |
513 | 513 | String value = null; |
514 | 514 | for (Object option : optionList) { |
515 | 515 | if (((TagNode) option).getAttributeByName("selected") != null) { |
518 | 518 | } |
519 | 519 | } |
520 | 520 | 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; | |
526 | 526 | LOGGER.error(errorMessage); |
527 | 527 | 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 { | |
536 | 534 | String usernameInput; |
537 | 535 | if (usernameInputs.size() == 2) { |
538 | 536 | String userid; |
545 | 543 | userid = username.substring(0, pipeIndex); |
546 | 544 | username = username.substring(pipeIndex + 1); |
547 | 545 | // 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); | |
552 | 550 | |
553 | 551 | usernameInput = "username"; |
554 | 552 | } else if (usernameInputs.size() == 1) { |
559 | 557 | usernameInput = "username"; |
560 | 558 | } |
561 | 559 | // make sure username and password fields are empty |
562 | ((PostMethod) logonMethod).removeParameter(usernameInput); | |
560 | ((PostRequest) logonMethod).removeParameter(usernameInput); | |
563 | 561 | 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"); | |
568 | 566 | |
569 | 567 | if (passwordInput == null) { |
570 | 568 | // This is a OTP pre-auth page. A different username may be required. |
571 | 569 | otpPreAuthFound = true; |
572 | 570 | otpPreAuthRetries++; |
573 | ((PostMethod) logonMethod).addParameter(usernameInput, preAuthusername); | |
571 | ((PostRequest) logonMethod).setParameter(usernameInput, preAuthusername); | |
574 | 572 | } else { |
575 | 573 | otpPreAuthFound = false; |
576 | 574 | otpPreAuthRetries = 0; |
577 | 575 | // 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); | |
587 | 585 | if (path != null) { |
588 | 586 | // reset query string |
589 | uri.setQuery(null); | |
587 | uriBuilder.clearParameters(); | |
590 | 588 | if (path.startsWith("/")) { |
591 | 589 | // path is absolute, replace method path |
592 | uri.setPath(path); | |
590 | uriBuilder.setPath(path); | |
593 | 591 | } else if (path.startsWith("http://") || path.startsWith("https://")) { |
594 | return path; | |
592 | return URI.create(path); | |
595 | 593 | } else { |
596 | 594 | // relative path, build new path |
597 | String currentPath = method.getPath(); | |
595 | String currentPath = uri.getPath(); | |
598 | 596 | int end = currentPath.lastIndexOf('/'); |
599 | 597 | if (end >= 0) { |
600 | uri.setPath(currentPath.substring(0, end + 1) + path); | |
598 | uriBuilder.setPath(currentPath.substring(0, end + 1) + path); | |
601 | 599 | } 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); | |
611 | 609 | int queryIndex = pathQuery.indexOf('?'); |
612 | 610 | if (queryIndex >= 0) { |
613 | 611 | if (queryIndex > 0) { |
615 | 613 | String newPath = pathQuery.substring(0, queryIndex); |
616 | 614 | if (newPath.startsWith("/")) { |
617 | 615 | // absolute path |
618 | initmethodURI.setPath(newPath); | |
616 | uriBuilder.setPath(newPath); | |
619 | 617 | } else { |
620 | String currentPath = initmethodURI.getPath(); | |
618 | String currentPath = uriBuilder.getPath(); | |
621 | 619 | int folderIndex = currentPath.lastIndexOf('/'); |
622 | 620 | if (folderIndex >= 0) { |
623 | 621 | // replace relative path |
624 | initmethodURI.setPath(currentPath.substring(0, folderIndex + 1) + newPath); | |
622 | uriBuilder.setPath(currentPath.substring(0, folderIndex + 1) + newPath); | |
625 | 623 | } else { |
626 | 624 | // 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(); | |
638 | 636 | if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=4"))) { |
639 | logonMethod.releaseConnection(); | |
640 | 637 | throwAuthenticationFailed(); |
641 | 638 | } |
642 | 639 | } |
673 | 670 | * Shutdown http client connection manager |
674 | 671 | */ |
675 | 672 | public void close() { |
676 | DavGatewayHttpClientFacade.close(httpClient); | |
673 | httpClientAdapter.close(); | |
677 | 674 | } |
678 | 675 | |
679 | 676 | /** |
680 | 677 | * Oauth token. |
681 | 678 | * Only for Office 365 authenticators |
679 | * | |
682 | 680 | * @return unsupported |
683 | 681 | */ |
684 | 682 | @Override |
698 | 696 | } |
699 | 697 | |
700 | 698 | /** |
701 | * Authenticated httpClient (with cookies). | |
699 | * Return authenticated HttpClient 4 HttpClientAdapter | |
702 | 700 | * |
703 | * @return http client | |
704 | */ | |
705 | public HttpClient getHttpClient() { | |
706 | return httpClient; | |
701 | * @return HttpClientAdapter instance | |
702 | */ | |
703 | public HttpClientAdapter getHttpClientAdapter() { | |
704 | return httpClientAdapter; | |
707 | 705 | } |
708 | 706 | |
709 | 707 | /** |
716 | 714 | return username; |
717 | 715 | } |
718 | 716 | } |
717 |
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 |
102 | 102 | return URI.create(RESOURCE + "/EWS/Exchange.asmx"); |
103 | 103 | } |
104 | 104 | |
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 | ||
105 | 114 | public void authenticate() throws IOException { |
106 | HttpClientAdapter httpClientAdapter = null; | |
107 | try { | |
108 | 115 | // common DavMail client id |
109 | 116 | String clientId = Settings.getProperty("davmail.oauth.clientId", "facd6cff-a294-4415-b59f-c5b01937d7bd"); |
110 | 117 | // standard native app redirectUri |
120 | 127 | |
121 | 128 | String url = O365Authenticator.buildAuthorizeUrl(tenantId, clientId, redirectUri, username); |
122 | 129 | |
123 | httpClientAdapter = new HttpClientAdapter(url, userid, password); | |
130 | try ( | |
131 | HttpClientAdapter httpClientAdapter = new HttpClientAdapter(url, userid, password) | |
132 | ){ | |
124 | 133 | |
125 | 134 | GetRequest getRequest = new GetRequest(url); |
126 | 135 | String responseBodyAsString = executeFollowRedirect(httpClientAdapter, getRequest); |
226 | 235 | |
227 | 236 | } catch (JSONException e) { |
228 | 237 | 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 { | |
239 | 243 | // get ADFS login form |
240 | 244 | GetRequest logonFormMethod = new GetRequest(federationRedirectUrl); |
241 | String responseBodyAsString = httpClientAdapter.executeGetRequest(logonFormMethod); | |
245 | logonFormMethod = httpClientAdapter.executeFollowRedirect(logonFormMethod); | |
246 | String responseBodyAsString = logonFormMethod.getResponseBodyAsString(); | |
242 | 247 | return authenticateADFS(httpClientAdapter, responseBodyAsString, authorizeUrl); |
243 | 248 | } |
244 | 249 | |
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 { | |
246 | 251 | URI location; |
247 | 252 | |
248 | 253 | if (responseBodyAsString.contains("login.microsoftonline.com")) { |
320 | 325 | throw new IOException("Unknown ADFS authentication failure"); |
321 | 326 | } |
322 | 327 | |
323 | private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throws IOException { | |
328 | private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throws IOException, JSONException { | |
324 | 329 | 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 | } | |
326 | 337 | GetRequest deviceLoginMethod = new GetRequest(location); |
327 | 338 | |
328 | 339 | String responseBodyAsString = httpClient.executeGetRequest(deviceLoginMethod); |
337 | 348 | processMethod.setParameter("ctx", ctx); |
338 | 349 | processMethod.setParameter("flowtoken", flowtoken); |
339 | 350 | |
340 | httpClient.executePostRequest(processMethod); | |
351 | responseBodyAsString = httpClient.executePostRequest(processMethod); | |
341 | 352 | 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 | } | |
342 | 358 | |
343 | 359 | if (result == null) { |
344 | 360 | throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED"); |
348 | 364 | return result; |
349 | 365 | } |
350 | 366 | |
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 { | |
352 | 368 | JSONObject config = extractConfig(logonMethod.getResponseBodyAsString()); |
353 | 369 | LOGGER.debug("Config=" + config); |
354 | 370 | |
355 | 371 | String urlBeginAuth = config.getString("urlBeginAuth"); |
356 | 372 | String urlEndAuth = config.getString("urlEndAuth"); |
373 | // Get processAuth url from config | |
374 | String urlProcessAuth = config.optString("urlPost", "https://login.microsoftonline.com/" + tenantId + "/SAS/ProcessAuth"); | |
357 | 375 | |
358 | 376 | boolean isMFAMethodSupported = false; |
359 | 377 | |
380 | 398 | String hpgact = config.getString("hpgact"); |
381 | 399 | String hpgid = config.getString("hpgid"); |
382 | 400 | |
401 | // clientRequestId is null coming from device login | |
402 | String correlationId = clientRequestId; | |
403 | if (correlationId == null) { | |
404 | correlationId = config.getString("correlationId"); | |
405 | } | |
406 | ||
383 | 407 | RestRequest beginAuthMethod = new RestRequest(urlBeginAuth); |
384 | 408 | beginAuthMethod.setRequestHeader("Accept", "application/json"); |
385 | 409 | beginAuthMethod.setRequestHeader("canary", apiCanary); |
386 | beginAuthMethod.setRequestHeader("client-request-id", clientRequestId); | |
410 | beginAuthMethod.setRequestHeader("client-request-id", correlationId); | |
387 | 411 | beginAuthMethod.setRequestHeader("hpgact", hpgact); |
388 | 412 | beginAuthMethod.setRequestHeader("hpgid", hpgid); |
389 | 413 | beginAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid); |
457 | 481 | flowToken = config.getString("FlowToken"); |
458 | 482 | |
459 | 483 | // process auth |
460 | PostRequest processAuthMethod = new PostRequest("https://login.microsoftonline.com/" + tenantId + "/SAS/ProcessAuth"); | |
484 | PostRequest processAuthMethod = new PostRequest(urlProcessAuth); | |
461 | 485 | processAuthMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); |
462 | 486 | processAuthMethod.setParameter("type", type); |
463 | 487 | processAuthMethod.setParameter("request", context); |
24 | 24 | import davmail.exchange.ews.DistinguishedFolderId; |
25 | 25 | import davmail.exchange.ews.GetFolderMethod; |
26 | 26 | 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; | |
29 | 29 | import org.apache.log4j.Logger; |
30 | 30 | |
31 | 31 | import javax.swing.*; |
33 | 33 | import java.net.Authenticator; |
34 | 34 | import java.net.PasswordAuthentication; |
35 | 35 | import java.net.URI; |
36 | import java.security.Security; | |
36 | 37 | |
37 | 38 | public class O365InteractiveAuthenticator implements ExchangeAuthenticator { |
38 | 39 | |
39 | 40 | private static final int MAX_COUNT = 300; |
40 | 41 | 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 | } | |
41 | 47 | |
42 | 48 | boolean isAuthenticated = false; |
43 | 49 | String errorCode = null; |
73 | 79 | this.password = password; |
74 | 80 | } |
75 | 81 | |
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 | } | |
76 | 91 | |
77 | 92 | public void authenticate() throws IOException { |
93 | ||
78 | 94 | // allow cross domain requests for Okta form support |
79 | 95 | System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); |
80 | 96 | // enable NTLM for ADFS support |
166 | 182 | } |
167 | 183 | |
168 | 184 | public static void main(String[] argv) { |
185 | ||
169 | 186 | try { |
187 | // set custom factory before loading OpenJFX | |
188 | Security.setProperty("ssl.SocketFactory.provider", "davmail.http.DavGatewaySSLSocketFactory"); | |
189 | ||
170 | 190 | 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(); | |
174 | 193 | |
175 | 194 | O365InteractiveAuthenticator authenticator = new O365InteractiveAuthenticator(); |
176 | 195 | authenticator.setUsername(""); |
177 | 196 | authenticator.authenticate(); |
178 | 197 | |
198 | HttpClientAdapter httpClientAdapter = new HttpClientAdapter(authenticator.getExchangeUri(), true); | |
199 | ||
179 | 200 | // switch to EWS url |
180 | HttpClient httpClient = DavGatewayHttpClientFacade.getInstance(authenticator.ewsUrl.toString()); | |
181 | ||
182 | 201 | 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); | |
188 | 207 | checkMethod.checkSuccess(); |
189 | } finally { | |
190 | checkMethod.releaseConnection(); | |
191 | 208 | } |
192 | 209 | System.out.println("Retrieved folder id " + checkMethod.getResponseItem().get("FolderId")); |
193 | 210 | |
195 | 212 | int i = 0; |
196 | 213 | while (i++ < 12 * 60 * 2) { |
197 | 214 | 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 | } | |
200 | 222 | System.out.println(getUserConfigurationMethod.getResponseItem()); |
201 | 223 | |
202 | 224 | Thread.sleep(5000); |
203 | 225 | } |
204 | ||
226 | } catch (InterruptedException e) { | |
227 | LOGGER.warn("Thread interrupted", e); | |
228 | Thread.currentThread().interrupt(); | |
205 | 229 | } catch (Exception e) { |
206 | 230 | LOGGER.error(e + " " + e.getMessage(), e); |
207 | 231 | } |
19 | 19 | package davmail.exchange.auth; |
20 | 20 | |
21 | 21 | import davmail.BundleMessage; |
22 | import davmail.Settings; | |
22 | 23 | import davmail.ui.tray.DavGatewayTray; |
23 | 24 | import davmail.util.IOUtil; |
24 | 25 | import javafx.application.Platform; |
54 | 55 | import java.net.URLStreamHandler; |
55 | 56 | import java.net.URLStreamHandlerFactory; |
56 | 57 | import java.nio.charset.StandardCharsets; |
58 | import java.util.logging.Level; | |
57 | 59 | |
58 | 60 | public class O365InteractiveAuthenticatorFrame extends JFrame { |
59 | 61 | private static final Logger LOGGER = Logger.getLogger(O365InteractiveAuthenticatorFrame.class); |
209 | 211 | Scene scene = new Scene(hBox); |
210 | 212 | fxPanel.setScene(scene); |
211 | 213 | |
214 | webViewEngine.setUserAgent(Settings.getUserAgent()); | |
215 | ||
212 | 216 | webViewEngine.setOnAlert(stringWebEvent -> SwingUtilities.invokeLater(() -> { |
213 | 217 | String message = stringWebEvent.getData(); |
214 | 218 | JOptionPane.showMessageDialog(O365InteractiveAuthenticatorFrame.this, message); |
217 | 221 | |
218 | 222 | |
219 | 223 | 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) { | |
221 | 226 | loadProgress.setVisible(false); |
222 | 227 | location = webViewEngine.getLocation(); |
223 | 228 | updateTitleAndFocus(location); |
251 | 256 | handleError(e); |
252 | 257 | } |
253 | 258 | close(); |
259 | } else { | |
260 | LOGGER.debug(webViewEngine.getLoadWorker().getState()+" "+webViewEngine.getLoadWorker().getMessage()+" " + webViewEngine.getLocation()+" "); | |
254 | 261 | } |
255 | 262 | |
256 | 263 | }); |
19 | 19 | package davmail.exchange.auth; |
20 | 20 | |
21 | 21 | import javafx.scene.web.WebEngine; |
22 | import netscape.javascript.JSObject; | |
23 | 22 | import org.apache.log4j.Logger; |
24 | 23 | |
25 | 24 | import java.lang.reflect.InvocationTargetException; |
38 | 37 | Class jsObjectClass = Class.forName("netscape.javascript.JSObject"); |
39 | 38 | Method setMemberMethod = jsObjectClass.getDeclaredMethod("setMember", String.class,Object.class); |
40 | 39 | |
41 | JSObject window = (JSObject) webEngine.executeScript("window"); | |
40 | Object window = webEngine.executeScript("window"); | |
42 | 41 | setMemberMethod.invoke(window, "davmail", new O365InteractiveJSLogger()); |
43 | 42 | |
44 | 43 | webEngine.executeScript("console.log = function(message) { davmail.log(message); }"); |
26 | 26 | import davmail.exchange.ews.DistinguishedFolderId; |
27 | 27 | import davmail.exchange.ews.GetFolderMethod; |
28 | 28 | 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; | |
31 | 31 | import org.apache.log4j.Logger; |
32 | 32 | |
33 | 33 | import javax.swing.*; |
75 | 75 | this.password = password; |
76 | 76 | } |
77 | 77 | |
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 | } | |
78 | 86 | |
79 | 87 | public void authenticate() throws IOException { |
80 | 88 | // common DavMail client id |
148 | 156 | authenticator.authenticate(); |
149 | 157 | |
150 | 158 | // switch to EWS url |
151 | HttpClient httpClient = DavGatewayHttpClientFacade.getInstance(authenticator.ewsUrl.toString()); | |
159 | HttpClientAdapter httpClientAdapter = new HttpClientAdapter(authenticator.getExchangeUri(), true); | |
152 | 160 | |
153 | 161 | 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); | |
159 | 167 | checkMethod.checkSuccess(); |
160 | } finally { | |
161 | checkMethod.releaseConnection(); | |
162 | 168 | } |
163 | 169 | System.out.println("Retrieved folder id " + checkMethod.getResponseItem().get("FolderId")); |
164 | 170 | |
166 | 172 | int i = 0; |
167 | 173 | while (i++ < 12 * 60 * 2) { |
168 | 174 | 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 | } | |
171 | 183 | System.out.println(getUserConfigurationMethod.getResponseItem()); |
172 | 184 | |
173 | 185 | Thread.sleep(5000); |
174 | 186 | } |
175 | 187 | |
188 | } catch (InterruptedException e) { | |
189 | LOGGER.warn("Thread interrupted", e); | |
190 | Thread.currentThread().interrupt(); | |
176 | 191 | } catch (Exception e) { |
177 | 192 | LOGGER.error(e + " " + e.getMessage(), e); |
178 | 193 | } |
19 | 19 | package davmail.exchange.auth; |
20 | 20 | |
21 | 21 | import davmail.Settings; |
22 | import davmail.http.HttpClientAdapter; | |
22 | 23 | import org.apache.log4j.Logger; |
23 | 24 | |
24 | 25 | import java.io.IOException; |
35 | 36 | URI ewsUrl = URI.create(resource + "/EWS/Exchange.asmx"); |
36 | 37 | |
37 | 38 | private String username; |
39 | private String password; | |
38 | 40 | private O365Token token; |
39 | 41 | |
40 | 42 | @Override |
44 | 46 | |
45 | 47 | @Override |
46 | 48 | 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); | |
48 | 59 | } |
49 | 60 | |
50 | 61 | @Override |
174 | 174 | O365Token token = new O365Token(tenantId, clientId, redirectUri, code); |
175 | 175 | if (Settings.getBooleanProperty("davmail.oauth.persistToken", true)) { |
176 | 176 | try { |
177 | Settings.storeRefreshToken(encryptToken(token.getRefreshToken(), password), token.getUsername()); | |
177 | Settings.storeRefreshToken(token.getUsername(), encryptToken(token.getRefreshToken(), password)); | |
178 | 178 | } catch (IOException e) { |
179 | 179 | LOGGER.warn("Unable to store refreshToken: "+e.getMessage()); |
180 | 180 | } |
31 | 31 | import davmail.exchange.VObject; |
32 | 32 | import davmail.exchange.VProperty; |
33 | 33 | import davmail.exchange.XMLStreamUtil; |
34 | import davmail.http.DavGatewayHttpClientFacade; | |
34 | import davmail.http.HttpClientAdapter; | |
35 | 35 | import davmail.http.URIUtil; |
36 | import davmail.http.request.ExchangePropPatchRequest; | |
36 | 37 | import davmail.ui.tray.DavGatewayTray; |
37 | 38 | import davmail.util.IOUtil; |
38 | 39 | 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; | |
52 | 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; | |
53 | 59 | import org.apache.jackrabbit.webdav.DavException; |
54 | 60 | import org.apache.jackrabbit.webdav.MultiStatus; |
55 | 61 | 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; | |
60 | 66 | import org.apache.jackrabbit.webdav.property.DavProperty; |
61 | 67 | import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; |
62 | 68 | import org.apache.jackrabbit.webdav.property.DavPropertySet; |
81 | 87 | import java.io.InputStreamReader; |
82 | 88 | import java.net.NoRouteToHostException; |
83 | 89 | import java.net.SocketException; |
90 | import java.net.URISyntaxException; | |
84 | 91 | import java.net.URL; |
85 | 92 | import java.net.UnknownHostException; |
86 | 93 | import java.nio.charset.StandardCharsets; |
93 | 100 | * Webdav Exchange adapter. |
94 | 101 | * Compatible with Exchange 2003 and 2007 with webdav available. |
95 | 102 | */ |
96 | @SuppressWarnings({"rawtypes", "deprecation"}) | |
103 | @SuppressWarnings("rawtypes") | |
97 | 104 | public class DavExchangeSession extends ExchangeSession { |
98 | 105 | protected enum FolderQueryTraversal { |
99 | 106 | Shallow, Deep |
131 | 138 | } |
132 | 139 | |
133 | 140 | /** |
141 | * HttpClient 4 adapter to replace httpClient | |
142 | */ | |
143 | private HttpClientAdapter httpClientAdapter; | |
144 | ||
145 | /** | |
134 | 146 | * Various standard mail boxes Urls |
135 | 147 | */ |
136 | 148 | protected String inboxUrl; |
155 | 167 | |
156 | 168 | protected static final String USERS = "/users/"; |
157 | 169 | |
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 | } | |
159 | 218 | |
160 | 219 | @Override |
161 | 220 | public boolean isExpired() throws NoRouteToHostException, UnknownHostException { |
162 | 221 | // experimental: try to reset session timeout |
163 | 222 | 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/"); | |
169 | 226 | } catch (IOException e) { |
170 | 227 | LOGGER.warn(e.getMessage()); |
171 | } finally { | |
172 | if (getMethod != null) { | |
173 | getMethod.releaseConnection(); | |
174 | } | |
175 | 228 | } |
176 | 229 | } |
177 | 230 | |
350 | 403 | protected Map<String, Map<String, String>> galFind(String query) throws IOException { |
351 | 404 | Map<String, Map<String, String>> results; |
352 | 405 | 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"); | |
357 | 409 | if (LOGGER.isDebugEnabled()) { |
358 | 410 | LOGGER.debug(path + ": " + results.size() + " result(s)"); |
359 | 411 | } |
361 | 413 | LOGGER.debug("GET " + path + " failed: " + e + ' ' + e.getMessage()); |
362 | 414 | disableGalFind = true; |
363 | 415 | throw e; |
364 | } finally { | |
365 | getMethod.releaseConnection(); | |
366 | 416 | } |
367 | 417 | return results; |
368 | 418 | } |
473 | 523 | public void galLookup(Contact contact) { |
474 | 524 | if (!disableGalLookup) { |
475 | 525 | 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"); | |
481 | 529 | // add detailed information |
482 | 530 | if (!results.isEmpty()) { |
483 | 531 | Map<String, String> personGalLookupDetails = results.get(contact.get("uid").toLowerCase()); |
488 | 536 | } catch (IOException e) { |
489 | 537 | LOGGER.warn("Unable to gallookup person: " + contact + ", disable GalLookup"); |
490 | 538 | disableGalLookup = true; |
491 | } finally { | |
492 | if (getMethod != null) { | |
493 | getMethod.releaseConnection(); | |
494 | } | |
495 | 539 | } |
496 | 540 | } |
497 | 541 | } |
512 | 556 | "&end=" + end + |
513 | 557 | "&interval=" + interval + |
514 | 558 | "&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"); | |
517 | 561 | 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>"); | |
523 | 564 | } |
524 | 565 | return fbdata; |
525 | 566 | } |
526 | 567 | |
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; | |
529 | 570 | this.userName = userName; |
530 | 571 | buildSessionInfo(uri); |
531 | 572 | } |
537 | 578 | |
538 | 579 | // get base http mailbox http urls |
539 | 580 | 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 | } | |
584 | 581 | } |
585 | 582 | |
586 | 583 | static final String BASE_HREF = "<base href=\""; |
594 | 591 | protected String getMailpathFromWelcomePage(java.net.URI uri) { |
595 | 592 | String welcomePageMailPath = null; |
596 | 593 | // 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 | ) { | |
603 | 601 | String line; |
604 | 602 | //noinspection StatementWithEmptyBody |
605 | 603 | while ((line = mainPageReader.readLine()) != null && !line.toLowerCase().contains(BASE_HREF)) { |
614 | 612 | LOGGER.debug("Base href found in body, mailPath is " + welcomePageMailPath); |
615 | 613 | } |
616 | 614 | } 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); | |
629 | 616 | } |
630 | 617 | return welcomePageMailPath; |
631 | 618 | } |
821 | 808 | protected void fixClientHost(java.net.URI currentUri) { |
822 | 809 | // update client host, workaround for Exchange 2003 mailbox with an Exchange 2007 frontend |
823 | 810 | if (currentUri != null && currentUri.getHost() != null && currentUri.getScheme() != null) { |
824 | httpClient.getHostConfiguration().setHost(currentUri.getHost(), currentUri.getPort(), currentUri.getScheme()); | |
811 | httpClientAdapter.setUri(currentUri); | |
825 | 812 | } |
826 | 813 | } |
827 | 814 | |
828 | 815 | 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 | ||
857 | 834 | |
858 | 835 | protected void getWellKnownFolders() throws DavMailException { |
859 | 836 | // Retrieve well known URLs |
860 | MultiStatusResponse[] responses; | |
861 | 837 | 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(); | |
864 | 844 | if (responses.length == 0) { |
865 | 845 | throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath); |
866 | 846 | } |
867 | DavPropertySet properties = responses[0].getProperties(HttpStatus.SC_OK); | |
847 | DavPropertySet properties = responses[0].getProperties(org.apache.http.HttpStatus.SC_OK); | |
868 | 848 | inboxUrl = getURIPropertyIfExists(properties, "inbox"); |
869 | 849 | inboxName = getFolderName(inboxUrl); |
870 | 850 | deleteditemsUrl = getURIPropertyIfExists(properties, "deleteditems"); |
896 | 876 | " Outbox URL: " + outboxUrl + |
897 | 877 | " Public folder URL: " + publicFolderUrl |
898 | 878 | ); |
899 | } catch (IOException e) { | |
879 | } catch (IOException | DavException e) { | |
900 | 880 | LOGGER.error(e.getMessage()); |
901 | 881 | throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath); |
902 | 882 | } |
1247 | 1227 | return propertyValues; |
1248 | 1228 | } |
1249 | 1229 | |
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"); | |
1253 | 1233 | if (etag != null) { |
1254 | propPatchMethod.setRequestHeader("If-Match", etag); | |
1234 | propPatchRequest.setHeader("If-Match", etag); | |
1255 | 1235 | } |
1256 | 1236 | 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; | |
1265 | 1243 | } |
1266 | 1244 | |
1267 | 1245 | /** |
1273 | 1251 | @Override |
1274 | 1252 | public ItemResult createOrUpdate() throws IOException { |
1275 | 1253 | 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(); | |
1278 | 1256 | 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 | } | |
1280 | 1262 | //noinspection VariableNotUsedInsideIf |
1281 | 1263 | if (status == HttpStatus.SC_CREATED) { |
1282 | 1264 | LOGGER.debug("Created contact " + encodedHref); |
1290 | 1272 | if (responses.length == 1) { |
1291 | 1273 | encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl"); |
1292 | 1274 | LOGGER.warn("Contact found, permanenturl is " + encodedHref); |
1293 | propPatchMethod = internalCreateOrUpdate(encodedHref); | |
1294 | status = propPatchMethod.getStatusCode(); | |
1275 | propPatchRequest = internalCreateOrUpdate(encodedHref); | |
1276 | status = propPatchRequest.getStatusLine().getStatusCode(); | |
1295 | 1277 | 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 | } | |
1297 | 1283 | LOGGER.debug("Updated contact " + encodedHref); |
1298 | 1284 | } 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()); | |
1300 | 1286 | } |
1301 | 1287 | } |
1302 | 1288 | |
1303 | 1289 | } 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()); | |
1305 | 1291 | } |
1306 | 1292 | ItemResult itemResult = new ItemResult(); |
1307 | 1293 | // 440 means forbidden on Exchange |
1314 | 1300 | String contactPictureUrl = URIUtil.encodePath(getHref() + "/ContactPicture.jpg"); |
1315 | 1301 | String photo = get("photo"); |
1316 | 1302 | if (photo != null) { |
1317 | final PutMethod putmethod = new PutMethod(contactPictureUrl); | |
1318 | 1303 | try { |
1304 | final HttpPut httpPut = new HttpPut(contactPictureUrl); | |
1319 | 1305 | // need to update photo |
1320 | 1306 | byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90); |
1321 | 1307 | |
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 | } | |
1329 | 1318 | } |
1330 | 1319 | } catch (IOException e) { |
1331 | 1320 | LOGGER.error("Error in contact photo create or update", e); |
1332 | 1321 | throw e; |
1333 | } finally { | |
1334 | putmethod.releaseConnection(); | |
1335 | 1322 | } |
1336 | 1323 | |
1337 | 1324 | Set<PropertyValue> picturePropertyValues = new HashSet<>(); |
1339 | 1326 | // picturePropertyValues.add(Field.createPropertyValue("renderingPosition", "-1")); |
1340 | 1327 | picturePropertyValues.add(Field.createPropertyValue("attachExtension", ".jpg")); |
1341 | 1328 | |
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(); | |
1345 | 1333 | 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()); | |
1347 | 1335 | throw new IOException("Unable to update contact picture"); |
1348 | 1336 | } |
1349 | } finally { | |
1350 | attachmentPropPatchMethod.releaseConnection(); | |
1351 | 1337 | } |
1352 | 1338 | |
1353 | 1339 | } else { |
1354 | 1340 | // 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(); | |
1358 | 1344 | if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) { |
1359 | 1345 | LOGGER.error("Error in contact photo delete: " + status); |
1360 | 1346 | throw new IOException("Unable to delete contact picture"); |
1361 | 1347 | } |
1362 | } finally { | |
1363 | deleteMethod.releaseConnection(); | |
1364 | 1348 | } |
1365 | 1349 | } |
1366 | 1350 | // 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 | } | |
1375 | 1356 | } |
1376 | 1357 | } |
1377 | 1358 | return itemResult; |
1390 | 1371 | * Build Event instance from response info. |
1391 | 1372 | * |
1392 | 1373 | * @param multiStatusResponse response |
1393 | * @throws URIException on error | |
1374 | * @throws IOException on error | |
1394 | 1375 | */ |
1395 | 1376 | public Event(MultiStatusResponse multiStatusResponse) throws IOException { |
1396 | 1377 | setHref(URIUtil.decode(multiStatusResponse.getHref())); |
1406 | 1387 | protected String getPermanentUrl() { |
1407 | 1388 | return permanentUrl; |
1408 | 1389 | } |
1409 | ||
1410 | 1390 | |
1411 | 1391 | public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException { |
1412 | 1392 | super(folderPath, itemName, contentClass, itemBody, etag, noneMatch); |
1439 | 1419 | try { |
1440 | 1420 | result = getICSFromInternetContentProperty(); |
1441 | 1421 | 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()); | |
1450 | 1427 | } |
1451 | 1428 | } |
1452 | 1429 | } catch (DavException | IOException | MessagingException e) { |
1458 | 1435 | if (result == null) { |
1459 | 1436 | try { |
1460 | 1437 | result = getICSFromItemProperties(); |
1461 | } catch (HttpResponseException e) { | |
1438 | } catch (IOException e) { | |
1462 | 1439 | deleteBroken(); |
1463 | 1440 | throw e; |
1464 | 1441 | } |
1473 | 1450 | return result; |
1474 | 1451 | } |
1475 | 1452 | |
1476 | private byte[] getICSFromItemProperties() throws IOException { | |
1453 | private byte[] getICSFromItemProperties() throws HttpNotFoundException { | |
1477 | 1454 | byte[] result; |
1478 | 1455 | |
1479 | 1456 | // experimental: build VCALENDAR from properties |
1628 | 1605 | result = localVCalendar.toString().getBytes(StandardCharsets.UTF_8); |
1629 | 1606 | } catch (MessagingException | IOException e) { |
1630 | 1607 | 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()); | |
1632 | 1609 | } |
1633 | 1610 | |
1634 | 1611 | return result; |
1639 | 1616 | if (Settings.getBooleanProperty("davmail.deleteBroken")) { |
1640 | 1617 | LOGGER.warn("Deleting broken event at: " + permanentUrl); |
1641 | 1618 | 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) { | |
1644 | 1624 | LOGGER.warn("Unable to delete broken event at: " + permanentUrl); |
1645 | 1625 | } |
1646 | 1626 | } |
1647 | 1627 | } |
1648 | 1628 | |
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"); | |
1653 | 1633 | if (etag != null) { |
1654 | putmethod.setRequestHeader("If-Match", etag); | |
1634 | httpPut.setHeader("If-Match", etag); | |
1655 | 1635 | } |
1656 | 1636 | 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 | } | |
1667 | 1644 | } |
1668 | 1645 | |
1669 | 1646 | /** |
1703 | 1680 | propertyValues.add(Field.createPropertyValue("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART")))); |
1704 | 1681 | propertyValues.add(Field.createPropertyValue("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE")))); |
1705 | 1682 | |
1706 | ExchangePropPatchMethod propPatchMethod = new ExchangePropPatchMethod(encodedHref, propertyValues); | |
1707 | propPatchMethod.setRequestHeader("Translate", "f"); | |
1683 | ExchangePropPatchRequest propPatchMethod = new ExchangePropPatchRequest(encodedHref, propertyValues); | |
1684 | propPatchMethod.setHeader("Translate", "f"); | |
1708 | 1685 | if (etag != null) { |
1709 | propPatchMethod.setRequestHeader("If-Match", etag); | |
1686 | propPatchMethod.setHeader("If-Match", etag); | |
1710 | 1687 | } |
1711 | 1688 | 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(); | |
1716 | 1693 | |
1717 | 1694 | if (status == HttpStatus.SC_MULTI_STATUS) { |
1718 | 1695 | 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 | } | |
1720 | 1701 | itemResult.etag = newItem.etag; |
1721 | 1702 | } else { |
1722 | 1703 | itemResult.status = status; |
1723 | 1704 | } |
1724 | } finally { | |
1725 | propPatchMethod.releaseConnection(); | |
1726 | 1705 | } |
1727 | 1706 | |
1728 | 1707 | } else { |
1729 | 1708 | String encodedHref = URIUtil.encodePath(getHref()); |
1730 | 1709 | 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(); | |
1733 | 1712 | |
1734 | 1713 | if (status == HttpStatus.SC_OK) { |
1735 | 1714 | LOGGER.debug("Updated event " + encodedHref); |
1742 | 1721 | if (responses.length == 1) { |
1743 | 1722 | encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl"); |
1744 | 1723 | 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(); | |
1747 | 1726 | if (status == HttpStatus.SC_OK) { |
1748 | 1727 | LOGGER.debug("Updated event " + encodedHref); |
1749 | 1728 | } 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()); | |
1751 | 1730 | } |
1752 | 1731 | } |
1753 | 1732 | } 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()); | |
1755 | 1734 | } |
1756 | 1735 | |
1757 | 1736 | // 440 means forbidden on Exchange |
1762 | 1741 | status = HttpStatus.SC_OK; |
1763 | 1742 | } |
1764 | 1743 | 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(); | |
1767 | 1746 | } |
1768 | 1747 | |
1769 | 1748 | // trigger activeSync push event, only if davmail.forceActiveSyncUpdate setting is true |
1774 | 1753 | propertyList.add(Field.createDavProperty("contentclass", contentClass)); |
1775 | 1754 | // ... but also set PR_INTERNET_CONTENT to preserve custom properties |
1776 | 1755 | 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 | } | |
1785 | 1766 | } |
1786 | 1767 | } |
1787 | 1768 | } |
1831 | 1812 | } |
1832 | 1813 | } else { |
1833 | 1814 | try { |
1834 | URI folderURI = new URI(href, false); | |
1815 | java.net.URI folderURI = new java.net.URI(href); | |
1835 | 1816 | folder.folderPath = folderURI.getPath(); |
1836 | 1817 | 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) { | |
1840 | 1821 | throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href); |
1841 | 1822 | } |
1842 | 1823 | } |
1874 | 1855 | */ |
1875 | 1856 | @Override |
1876 | 1857 | 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 | ||
1879 | 1863 | Folder folder = null; |
1880 | 1864 | if (responses.length > 0) { |
1881 | 1865 | folder = buildFolder(responses[0]); |
1918 | 1902 | } |
1919 | 1903 | propertyValues.add(Field.createPropertyValue("folderclass", folderClass)); |
1920 | 1904 | |
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) { | |
1923 | 1907 | @Override |
1924 | public String getName() { | |
1908 | public String getMethod() { | |
1925 | 1909 | return "MKCOL"; |
1926 | 1910 | } |
1927 | 1911 | }; |
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); | |
1932 | 1925 | return status; |
1933 | 1926 | } |
1934 | 1927 | |
1944 | 1937 | } |
1945 | 1938 | } |
1946 | 1939 | |
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 | } | |
1954 | 1954 | } |
1955 | 1955 | |
1956 | 1956 | /** |
1958 | 1958 | */ |
1959 | 1959 | @Override |
1960 | 1960 | 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 | } | |
1962 | 1968 | } |
1963 | 1969 | |
1964 | 1970 | /** |
1966 | 1972 | */ |
1967 | 1973 | @Override |
1968 | 1974 | 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)), | |
1970 | 1976 | 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(); | |
1973 | 1979 | if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) { |
1974 | 1980 | throw new HttpPreconditionFailedException(BundleMessage.format("EXCEPTION_UNABLE_TO_MOVE_FOLDER")); |
1975 | 1981 | } else if (statusCode != HttpStatus.SC_CREATED) { |
1976 | throw DavGatewayHttpClientFacade.buildHttpResponseException(method); | |
1982 | throw HttpClientAdapter.buildHttpResponseException(httpMove, response); | |
1977 | 1983 | } 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 | |
1979 | 1985 | getWellKnownFolders(); |
1980 | 1986 | } |
1981 | } finally { | |
1982 | method.releaseConnection(); | |
1983 | 1987 | } |
1984 | 1988 | } |
1985 | 1989 | |
1988 | 1992 | */ |
1989 | 1993 | @Override |
1990 | 1994 | 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)), | |
1992 | 1996 | 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(); | |
1999 | 2003 | if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) { |
2000 | 2004 | throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_ITEM"); |
2001 | 2005 | } 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 | } | |
2006 | 2008 | } |
2007 | 2009 | } |
2008 | 2010 | |
2246 | 2248 | } |
2247 | 2249 | searchRequest.append(" ORDER BY ").append(Field.getRequestPropertyString("imapUid")).append(" DESC"); |
2248 | 2250 | 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); | |
2251 | 2253 | DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_RESULT", responses.length)); |
2252 | 2254 | return responses; |
2253 | 2255 | } |
2278 | 2280 | String itemPath = getFolderPath(folderPath) + '/' + emlItemName; |
2279 | 2281 | MultiStatusResponse[] responses = null; |
2280 | 2282 | 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) { | |
2284 | 2287 | // ignore |
2285 | 2288 | } |
2286 | 2289 | if (responses == null || responses.length == 0 && isMainCalendar(folderPath)) { |
2288 | 2291 | itemName = itemName.substring(0, itemName.length() - 3) + "EML"; |
2289 | 2292 | } |
2290 | 2293 | // 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 | } | |
2292 | 2300 | } |
2293 | 2301 | if (responses == null || responses.length == 0) { |
2294 | 2302 | throw new HttpNotFoundException(itemPath + " not found"); |
2309 | 2317 | List<ExchangeSession.Event> events = getAllEvents(folderPath); |
2310 | 2318 | for (ExchangeSession.Event event : events) { |
2311 | 2319 | 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 | } | |
2313 | 2326 | break; |
2314 | 2327 | } |
2315 | 2328 | } |
2346 | 2359 | @Override |
2347 | 2360 | public ExchangeSession.ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException { |
2348 | 2361 | 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"); | |
2352 | 2365 | |
2353 | 2366 | 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())); | |
2358 | 2370 | } else { |
2359 | inputStream = method.getResponseBodyAsStream(); | |
2371 | inputStream = response.getEntity().getContent(); | |
2360 | 2372 | } |
2361 | 2373 | |
2362 | 2374 | contactPhoto = new ContactPhoto(); |
2374 | 2386 | LOGGER.debug(e); |
2375 | 2387 | } |
2376 | 2388 | } |
2377 | method.releaseConnection(); | |
2378 | 2389 | } |
2379 | 2390 | return contactPhoto; |
2380 | 2391 | } |
2395 | 2406 | @Override |
2396 | 2407 | public void deleteItem(String folderPath, String itemName) throws IOException { |
2397 | 2408 | 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 | } | |
2399 | 2414 | if (status == HttpStatus.SC_NOT_FOUND && isMainCalendar(folderPath)) { |
2400 | 2415 | // retry in tasks folder |
2401 | 2416 | 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 | } | |
2403 | 2421 | } |
2404 | 2422 | if (status == HttpStatus.SC_NOT_FOUND) { |
2405 | 2423 | LOGGER.debug("Unable to delete " + itemName + ": item not found"); |
2413 | 2431 | ArrayList<PropEntry> list = new ArrayList<>(); |
2414 | 2432 | list.add(Field.createDavProperty("processed", "true")); |
2415 | 2433 | 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 | } | |
2418 | 2438 | } |
2419 | 2439 | |
2420 | 2440 | @Override |
2434 | 2454 | |
2435 | 2455 | String fakeEventUrl = null; |
2436 | 2456 | 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)) { | |
2441 | 2464 | // create fake event |
2442 | int statusCode = httpClient.executeMethod(postMethod); | |
2465 | int statusCode = response.getStatusLine().getStatusCode(); | |
2443 | 2466 | 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>"); | |
2445 | 2468 | if (fakeEventUrl != null) { |
2446 | 2469 | fakeEventUrl = URIUtil.decode(fakeEventUrl); |
2447 | 2470 | } |
2448 | 2471 | } |
2449 | } finally { | |
2450 | postMethod.releaseConnection(); | |
2451 | 2472 | } |
2452 | 2473 | } |
2453 | 2474 | // failover for Exchange 2007, use PROPPATCH with forced timezone |
2468 | 2489 | propertyList.add(Field.createDavProperty("timezoneid", timezoneId)); |
2469 | 2490 | } |
2470 | 2491 | 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(); | |
2474 | 2495 | if (statusCode == HttpStatus.SC_MULTI_STATUS) { |
2475 | 2496 | fakeEventUrl = patchMethodUrl; |
2476 | 2497 | } |
2477 | } finally { | |
2478 | patchMethod.releaseConnection(); | |
2479 | 2498 | } |
2480 | 2499 | } |
2481 | 2500 | if (fakeEventUrl != null) { |
2482 | 2501 | // 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)) { | |
2487 | 2505 | 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") + | |
2489 | 2507 | "END:VTIMEZONE\r\n"); |
2490 | } finally { | |
2491 | getMethod.releaseConnection(); | |
2492 | 2508 | } |
2493 | 2509 | } |
2494 | 2510 | |
2598 | 2614 | @Override |
2599 | 2615 | public void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException { |
2600 | 2616 | String messageUrl = URIUtil.encodePathQuery(getFolderPath(folderPath) + '/' + messageName); |
2601 | PropPatchMethod patchMethod; | |
2617 | ||
2602 | 2618 | List<PropEntry> davProperties = buildProperties(properties); |
2603 | 2619 | |
2604 | 2620 | if (properties != null && properties.containsKey("draft")) { |
2612 | 2628 | davProperties.add(Field.createDavProperty("messageFormat", properties.get("messageFormat"))); |
2613 | 2629 | } |
2614 | 2630 | 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)) { | |
2617 | 2633 | // update message with blind carbon copy and other flags |
2618 | int statusCode = httpClient.executeMethod(patchMethod); | |
2634 | int statusCode = response.getStatusLine().getStatusCode(); | |
2619 | 2635 | 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 | ||
2625 | 2639 | } |
2626 | 2640 | } |
2627 | 2641 | |
2628 | 2642 | // 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"); | |
2632 | 2646 | |
2633 | 2647 | try { |
2634 | 2648 | // use same encoding as client socket reader |
2635 | 2649 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
2636 | 2650 | mimeMessage.writeTo(baos); |
2637 | 2651 | 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 | } | |
2640 | 2660 | |
2641 | 2661 | // workaround for misconfigured Exchange server |
2642 | 2662 | if (code == HttpStatus.SC_NOT_ACCEPTABLE) { |
2666 | 2686 | } else if (contentType.startsWith("text/html")) { |
2667 | 2687 | propertyList.add(Field.createDavProperty("htmldescription", (String) mimePart.getContent())); |
2668 | 2688 | } 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"); | |
2670 | 2690 | } |
2671 | 2691 | |
2672 | 2692 | 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(); | |
2676 | 2696 | if (patchStatus == HttpStatus.SC_MULTI_STATUS) { |
2677 | 2697 | code = HttpStatus.SC_OK; |
2678 | 2698 | } |
2679 | } finally { | |
2680 | propPatchMethod.releaseConnection(); | |
2681 | } | |
2682 | } | |
2699 | } | |
2700 | } | |
2701 | ||
2683 | 2702 | |
2684 | 2703 | if (code != HttpStatus.SC_OK && code != HttpStatus.SC_CREATED) { |
2685 | 2704 | |
2686 | 2705 | // first delete draft message |
2687 | 2706 | 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 | } | |
2690 | 2713 | } catch (IOException e) { |
2691 | 2714 | LOGGER.warn("Unable to delete draft message"); |
2692 | 2715 | } |
2693 | 2716 | } |
2694 | 2717 | if (code == HttpStatus.SC_INSUFFICIENT_STORAGE) { |
2695 | throw new InsufficientStorageException(putmethod.getStatusText()); | |
2718 | throw new InsufficientStorageException(reasonPhrase); | |
2696 | 2719 | } 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); | |
2698 | 2721 | } |
2699 | 2722 | } |
2700 | 2723 | } catch (MessagingException e) { |
2708 | 2731 | if (mimeMessage.getHeader("Bcc") != null) { |
2709 | 2732 | davProperties = new ArrayList<>(); |
2710 | 2733 | 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(); | |
2715 | 2738 | 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 | } | |
2721 | 2741 | } |
2722 | 2742 | } |
2723 | 2743 | } catch (MessagingException e) { |
2731 | 2751 | */ |
2732 | 2752 | @Override |
2733 | 2753 | 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)) { | |
2735 | 2755 | @Override |
2736 | protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) { | |
2756 | public MultiStatus getResponseBodyAsMultiStatus(HttpResponse response) { | |
2737 | 2757 | // ignore response body, sometimes invalid with exchange mapi properties |
2758 | throw new UnsupportedOperationException(); | |
2738 | 2759 | } |
2739 | 2760 | }; |
2740 | try { | |
2741 | int statusCode = httpClient.executeMethod(patchMethod); | |
2761 | try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) { | |
2762 | int statusCode = response.getStatusLine().getStatusCode(); | |
2742 | 2763 | if (statusCode != HttpStatus.SC_MULTI_STATUS) { |
2743 | 2764 | throw new DavMailException("EXCEPTION_UNABLE_TO_UPDATE_MESSAGE"); |
2744 | 2765 | } |
2745 | ||
2746 | } finally { | |
2747 | patchMethod.releaseConnection(); | |
2748 | 2766 | } |
2749 | 2767 | } |
2750 | 2768 | |
2754 | 2772 | @Override |
2755 | 2773 | public void deleteMessage(ExchangeSession.Message message) throws IOException { |
2756 | 2774 | 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 | } | |
2758 | 2782 | } |
2759 | 2783 | |
2760 | 2784 | /** |
2796 | 2820 | properties.put("messageFormat", "2"); |
2797 | 2821 | } |
2798 | 2822 | 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)), | |
2800 | 2824 | URIUtil.encodePath(getFolderPath(SENDMSG)), false); |
2801 | // set header if saveInSent is disabled | |
2825 | // set header if saveInSent is disabled | |
2802 | 2826 | if (!Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) { |
2803 | method.setRequestHeader("Saveinsent", "f"); | |
2804 | } | |
2805 | moveItem(method); | |
2827 | httpMove.setHeader("Saveinsent", "f"); | |
2828 | } | |
2829 | moveItem(httpMove); | |
2806 | 2830 | } catch (MessagingException e) { |
2807 | 2831 | throw new IOException(e.getMessage()); |
2808 | 2832 | } |
2857 | 2881 | messageProperties.add(Field.getPropertyName("date")); |
2858 | 2882 | messageProperties.add(Field.getPropertyName("htmldescription")); |
2859 | 2883 | 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"); | |
2896 | 2892 | if (propertyValue != null) { |
2897 | mimeMessage.setText(propertyValue); | |
2893 | mimeMessage.addHeader("Content-class", propertyValue); | |
2898 | 2894 | } |
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 | } | |
2901 | 2926 | } |
2902 | 2927 | if (LOGGER.isDebugEnabled()) { |
2903 | 2928 | LOGGER.debug("Rebuilt message content: " + new String(baos.toByteArray(), StandardCharsets.UTF_8)); |
2920 | 2945 | return baos.toByteArray(); |
2921 | 2946 | } |
2922 | 2947 | |
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; | |
2936 | 2968 | } |
2937 | 2969 | |
2938 | 2970 | protected InputStream getContentInputStream(String url) throws IOException { |
2939 | 2971 | String encodedUrl = encodeAndFixUrl(url); |
2940 | 2972 | |
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"); | |
2945 | 2977 | |
2946 | 2978 | 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()); | |
2951 | 2982 | } else { |
2952 | inputStream = method.getResponseBodyAsStream(); | |
2983 | inputStream = response.getEntity().getContent(); | |
2953 | 2984 | } |
2954 | 2985 | inputStream = new FilterInputStream(inputStream) { |
2955 | 2986 | int totalCount; |
2960 | 2991 | int count = super.read(buffer, offset, length); |
2961 | 2992 | totalCount += count; |
2962 | 2993 | 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())); | |
2964 | 2995 | DavGatewayTray.switchIcon(); |
2965 | 2996 | lastLogCount = totalCount; |
2966 | 2997 | } |
2972 | 3003 | try { |
2973 | 3004 | super.close(); |
2974 | 3005 | } finally { |
2975 | method.releaseConnection(); | |
3006 | httpGet.releaseConnection(); | |
2976 | 3007 | } |
2977 | 3008 | } |
2978 | 3009 | }; |
2979 | 3010 | |
2980 | } catch (HttpResponseException e) { | |
2981 | method.releaseConnection(); | |
3011 | } catch (IOException e) { | |
2982 | 3012 | LOGGER.warn("Unable to retrieve message at: " + url); |
2983 | 3013 | throw e; |
2984 | 3014 | } |
3000 | 3030 | |
3001 | 3031 | protected void moveMessage(String sourceUrl, String targetFolder) throws IOException { |
3002 | 3032 | 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); | |
3004 | 3034 | // 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) { | |
3009 | 3040 | throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_MESSAGE"); |
3010 | 3041 | } else if (statusCode != HttpStatus.SC_CREATED) { |
3011 | throw DavGatewayHttpClientFacade.buildHttpResponseException(method); | |
3042 | throw HttpClientAdapter.buildHttpResponseException(method, response); | |
3012 | 3043 | } |
3013 | 3044 | } finally { |
3014 | 3045 | method.releaseConnection(); |
3030 | 3061 | |
3031 | 3062 | protected void copyMessage(String sourceUrl, String targetFolder) throws IOException { |
3032 | 3063 | 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); | |
3034 | 3065 | // 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(); | |
3038 | 3069 | if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) { |
3039 | 3070 | throw new DavMailException("EXCEPTION_UNABLE_TO_COPY_MESSAGE"); |
3040 | 3071 | } else if (statusCode != HttpStatus.SC_CREATED) { |
3041 | throw DavGatewayHttpClientFacade.buildHttpResponseException(method); | |
3042 | } | |
3043 | } finally { | |
3044 | method.releaseConnection(); | |
3072 | throw HttpClientAdapter.buildHttpResponseException(httpCopy, response); | |
3073 | } | |
3045 | 3074 | } |
3046 | 3075 | } |
3047 | 3076 | |
3049 | 3078 | protected void moveToTrash(ExchangeSession.Message message) throws IOException { |
3050 | 3079 | String destination = URIUtil.encodePath(deleteditemsUrl) + '/' + UUID.randomUUID().toString(); |
3051 | 3080 | 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 | } | |
3062 | 3093 | } |
3063 | 3094 | |
3064 | 3095 | LOGGER.debug("Deleted to :" + destination); |
3068 | 3099 | String result = null; |
3069 | 3100 | DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet(); |
3070 | 3101 | 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 | ||
3091 | 3120 | return result; |
3092 | 3121 | } |
3093 | 3122 | |
3135 | 3164 | return value; |
3136 | 3165 | } |
3137 | 3166 | |
3167 | ||
3168 | @Override | |
3169 | public void close() { | |
3170 | httpClientAdapter.close(); | |
3171 | } | |
3138 | 3172 | |
3139 | 3173 | /** |
3140 | 3174 | * Format date to exchange search format. |
3198 | 3232 | } |
3199 | 3233 | return result; |
3200 | 3234 | } |
3201 | ||
3202 | /** | |
3203 | * Close session. | |
3204 | * Shutdown http client connection manager | |
3205 | */ | |
3206 | @Override | |
3207 | public void close() { | |
3208 | DavGatewayHttpClientFacade.close(httpClient); | |
3209 | } | |
3210 | 3235 | } |
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 | /* | |
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 | /* | |
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 | /* | |
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 | } |
568 | 568 | * @param alias field alias |
569 | 569 | * @param value field value |
570 | 570 | * @return property value object |
571 | * @see ExchangePropPatchMethod | |
571 | * @see davmail.http.request.ExchangePropPatchRequest | |
572 | 572 | */ |
573 | 573 | public static PropertyValue createPropertyValue(String alias, String value) { |
574 | 574 | Field field = Field.get(alias); |
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 | } |
18 | 18 | package davmail.exchange.ews; |
19 | 19 | |
20 | 20 | import davmail.Settings; |
21 | import org.apache.http.entity.AbstractHttpEntity; | |
21 | 22 | |
22 | 23 | /** |
23 | 24 | * Create Item method. |
35 | 36 | this.savedItemFolderId = savedItemFolderId; |
36 | 37 | this.item = item; |
37 | 38 | addMethodOption(messageDisposition); |
38 | setContentChunked(Settings.getBooleanProperty("davmail.enableChunkedRequest", false)); | |
39 | ((AbstractHttpEntity)getEntity()).setChunked(Settings.getBooleanProperty("davmail.enableChunkedRequest", false)); | |
39 | 40 | } |
40 | 41 | |
41 | 42 | /** |
20 | 20 | import davmail.BundleMessage; |
21 | 21 | import davmail.Settings; |
22 | 22 | import davmail.exchange.XMLStreamUtil; |
23 | import davmail.http.DavGatewayHttpClientFacade; | |
23 | import davmail.http.HttpClientAdapter; | |
24 | 24 | import davmail.ui.tray.DavGatewayTray; |
25 | 25 | import davmail.util.StringUtil; |
26 | 26 | 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; | |
33 | 34 | import org.apache.log4j.Level; |
34 | 35 | import org.apache.log4j.Logger; |
35 | 36 | import org.codehaus.stax2.typed.TypedXMLStreamReader; |
37 | 38 | import javax.xml.stream.XMLStreamConstants; |
38 | 39 | import javax.xml.stream.XMLStreamException; |
39 | 40 | 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; | |
41 | 50 | 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; | |
43 | 56 | import java.util.zip.GZIPInputStream; |
44 | 57 | |
45 | 58 | /** |
46 | 59 | * EWS SOAP method. |
47 | 60 | */ |
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(); | |
49 | 63 | protected static final Logger LOGGER = Logger.getLogger(EWSMethod.class); |
50 | 64 | protected static final int CHUNK_LENGTH = 131072; |
51 | 65 | |
91 | 105 | |
92 | 106 | protected String serverVersion; |
93 | 107 | protected String timezoneContext; |
108 | private HttpResponse response; | |
94 | 109 | |
95 | 110 | /** |
96 | 111 | * Build EWS method |
110 | 125 | * @param responseCollectionName item response collection name |
111 | 126 | */ |
112 | 127 | public EWSMethod(String itemType, String methodName, String responseCollectionName) { |
113 | super("/ews/exchange.asmx"); | |
128 | super(URI.create("/ews/exchange.asmx")); | |
114 | 129 | this.itemType = itemType; |
115 | 130 | this.methodName = methodName; |
116 | 131 | this.responseCollectionName = responseCollectionName; |
117 | 132 | if (Settings.getBooleanProperty("davmail.acceptEncodingGzip", true) && |
118 | 133 | !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() { | |
123 | 138 | byte[] content; |
124 | 139 | |
140 | @Override | |
125 | 141 | public boolean isRepeatable() { |
126 | 142 | return true; |
127 | 143 | } |
128 | 144 | |
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 { | |
130 | 163 | boolean firstPass = content == null; |
131 | 164 | if (content == null) { |
132 | 165 | content = generateSoapEnvelope(); |
150 | 183 | } |
151 | 184 | } |
152 | 185 | |
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); | |
170 | 194 | } |
171 | 195 | |
172 | 196 | protected void addAdditionalProperty(FieldURI additionalProperty) { |
770 | 794 | && !"ErrorMailRecipientNotFound".equals(errorDetail) |
771 | 795 | && !"ErrorItemNotFound".equals(errorDetail) |
772 | 796 | && !"ErrorCalendarOccurrenceIsDeletedFromRecurrence".equals(errorDetail) |
773 | ) { | |
797 | ) { | |
774 | 798 | throw new EWSException(errorDetail |
775 | 799 | + ' ' + ((errorDescription != null) ? errorDescription : "") |
776 | 800 | + ' ' + ((errorValue != null) ? errorValue : "") |
778 | 802 | } |
779 | 803 | } |
780 | 804 | 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 | ||
786 | 809 | public int getStatusCode() { |
787 | 810 | if ("ErrorAccessDenied".equals(errorDetail)) { |
788 | 811 | return HttpStatus.SC_FORBIDDEN; |
789 | 812 | } else if ("ErrorItemNotFound".equals(errorDetail)) { |
790 | 813 | return HttpStatus.SC_NOT_FOUND; |
791 | 814 | } else { |
792 | return super.getStatusCode(); | |
815 | return response.getStatusLine().getStatusCode(); | |
793 | 816 | } |
794 | 817 | } |
795 | 818 | |
850 | 873 | if (event == XMLStreamConstants.CHARACTERS) { |
851 | 874 | result.append(reader.getText()); |
852 | 875 | } 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++) { | |
854 | 877 | if (result.length() > 0) { |
855 | 878 | result.append(", "); |
856 | 879 | } |
883 | 906 | && !"ErrorNameResolutionMultipleResults".equals(result) |
884 | 907 | && !"ErrorNameResolutionNoResults".equals(result) |
885 | 908 | && !"ErrorFolderExists".equals(result) |
886 | ) { | |
909 | ) { | |
887 | 910 | errorDetail = result; |
888 | 911 | } |
889 | 912 | if (XMLStreamUtil.isStartTag(reader, "faultstring")) { |
1003 | 1026 | if (XMLStreamUtil.isStartTag(reader)) { |
1004 | 1027 | String tagLocalName = reader.getLocalName(); |
1005 | 1028 | if ("EmailAddress".equals(tagLocalName) && member == null) { |
1006 | member = "mailto:"+XMLStreamUtil.getElementText(reader); | |
1029 | member = "mailto:" + XMLStreamUtil.getElementText(reader); | |
1007 | 1030 | } |
1008 | 1031 | } |
1009 | 1032 | } |
1165 | 1188 | } |
1166 | 1189 | |
1167 | 1190 | @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"); | |
1170 | 1194 | 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)); | |
1174 | 1200 | } else { |
1175 | processResponseStream(getResponseBodyAsStream()); | |
1201 | processResponseStream(inputStream); | |
1176 | 1202 | } |
1177 | 1203 | } catch (IOException e) { |
1178 | 1204 | LOGGER.error("Error while parsing soap response: " + e, e); |
1179 | 1205 | } |
1180 | 1206 | } |
1207 | return this; | |
1181 | 1208 | } |
1182 | 1209 | |
1183 | 1210 | protected void processResponseStream(InputStream inputStream) { |
27 | 27 | import davmail.exchange.VObject; |
28 | 28 | import davmail.exchange.VProperty; |
29 | 29 | import davmail.exchange.auth.O365Token; |
30 | import davmail.http.DavGatewayHttpClientFacade; | |
30 | import davmail.http.HttpClientAdapter; | |
31 | import davmail.http.request.GetRequest; | |
31 | 32 | import davmail.ui.NotificationDialog; |
32 | 33 | import davmail.util.IOUtil; |
33 | 34 | 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; | |
38 | 37 | |
39 | 38 | import javax.mail.MessagingException; |
40 | 39 | import javax.mail.Session; |
137 | 136 | // Unable to map CANCELLED: cancelled events are directly deleted on Exchange |
138 | 137 | } |
139 | 138 | |
140 | protected HttpClient httpClient; | |
139 | protected HttpClientAdapter httpClient; | |
141 | 140 | |
142 | 141 | protected Map<String, String> folderIdMap; |
143 | 142 | protected boolean directEws; |
167 | 166 | } |
168 | 167 | } |
169 | 168 | |
170 | public EwsExchangeSession(HttpClient httpClient, String userName) throws IOException { | |
169 | public EwsExchangeSession(HttpClientAdapter httpClient, String userName) throws IOException { | |
171 | 170 | this.httpClient = httpClient; |
172 | 171 | this.userName = userName; |
173 | 172 | if (userName.contains("@")) { |
176 | 175 | buildSessionInfo(null); |
177 | 176 | } |
178 | 177 | |
179 | public EwsExchangeSession(HttpClient httpClient, URI uri, String userName) throws IOException { | |
178 | public EwsExchangeSession(HttpClientAdapter httpClient, URI uri, String userName) throws IOException { | |
180 | 179 | this.httpClient = httpClient; |
181 | 180 | this.userName = userName; |
182 | 181 | if (userName.contains("@")) { |
186 | 185 | buildSessionInfo(uri); |
187 | 186 | } |
188 | 187 | |
189 | public EwsExchangeSession(HttpClient httpClient, O365Token token, String userName) throws IOException { | |
188 | public EwsExchangeSession(HttpClientAdapter httpClient, O365Token token, String userName) throws IOException { | |
190 | 189 | this.httpClient = httpClient; |
191 | 190 | this.userName = userName; |
192 | 191 | if (userName.contains("@")) { |
197 | 196 | buildSessionInfo(null); |
198 | 197 | } |
199 | 198 | |
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 | ||
200 | 207 | /** |
201 | 208 | * EWS fetch page size. |
209 | * | |
202 | 210 | * @return page size |
203 | 211 | */ |
204 | 212 | private static int getPageSize() { |
205 | 213 | 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; | |
218 | 214 | } |
219 | 215 | |
220 | 216 | /** |
226 | 222 | GetFolderMethod checkMethod = new GetFolderMethod(BaseShape.ID_ONLY, |
227 | 223 | DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null); |
228 | 224 | 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 | ||
238 | 226 | if (status == HttpStatus.SC_UNAUTHORIZED) { |
239 | 227 | throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED"); |
240 | 228 | } else if (status != HttpStatus.SC_OK) { |
244 | 232 | |
245 | 233 | @Override |
246 | 234 | 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 | |
248 | 236 | checkEndPointUrl(); |
249 | 237 | |
250 | 238 | // new approach based on ConvertId to find primary email address |
263 | 251 | if (convertIdItem != null && !convertIdItem.isEmpty()) { |
264 | 252 | email = convertIdItem.get("Mailbox"); |
265 | 253 | alias = email.substring(0, email.indexOf('@')); |
254 | } else { | |
255 | LOGGER.error("Unable to resolve email from root folder"); | |
256 | throw new IOException(); | |
266 | 257 | } |
267 | 258 | |
268 | 259 | } catch (IOException e) { |
274 | 265 | || "/ews/services.wsdl".equalsIgnoreCase(uri.getPath()) |
275 | 266 | || "/ews/exchange.asmx".equalsIgnoreCase(uri.getPath()); |
276 | 267 | |
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 | ||
304 | 268 | 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 | } | |
327 | 269 | |
328 | 270 | try { |
329 | 271 | folderIdMap = new HashMap<>(); |
344 | 286 | } |
345 | 287 | |
346 | 288 | protected String getEmailSuffixFromHostname() { |
347 | String domain = httpClient.getHostConfiguration().getHost(); | |
289 | String domain = httpClient.getHost(); | |
348 | 290 | int start = domain.lastIndexOf('.', domain.lastIndexOf('.') - 1); |
349 | 291 | if (start >= 0) { |
350 | 292 | return '@' + domain.substring(start + 1); |
703 | 645 | resultCount = results.size(); |
704 | 646 | if (resultCount > 0 && LOGGER.isDebugEnabled()) { |
705 | 647 | 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())); | |
708 | 650 | } |
709 | 651 | |
710 | 652 | |
750 | 692 | |
751 | 693 | long highestUid = 0; |
752 | 694 | 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()); | |
754 | 696 | } |
755 | 697 | // Only add new result if not already available (concurrent folder changes issue) |
756 | 698 | 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()); | |
758 | 700 | if (imapUid > highestUid) { |
759 | 701 | results.add(item); |
760 | 702 | } |
762 | 704 | resultCount = results.size(); |
763 | 705 | if (resultCount > 0 && LOGGER.isDebugEnabled()) { |
764 | 706 | 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())); | |
767 | 709 | } |
768 | 710 | if (Thread.interrupted()) { |
769 | 711 | LOGGER.debug("Folder " + folderPath + " - Search items failed: Interrupted by client"); |
795 | 737 | } |
796 | 738 | |
797 | 739 | if (actualConditionCount > 1) { |
798 | buffer.append("</t:").append(operator.toString()).append('>'); | |
740 | buffer.append("</t:").append(operator).append('>'); | |
799 | 741 | } |
800 | 742 | } |
801 | 743 | } |
831 | 773 | |
832 | 774 | protected FieldURI getFieldURI() { |
833 | 775 | FieldURI fieldURI = Field.get(attributeName); |
776 | // check to detect broken field mapping | |
777 | //noinspection ConstantConditions | |
834 | 778 | if (fieldURI == null) { |
835 | 779 | throw new IllegalArgumentException("Unknown field: " + attributeName); |
836 | 780 | } |
878 | 822 | buffer.append("</t:FieldURIOrConstant>"); |
879 | 823 | } |
880 | 824 | |
881 | buffer.append("</t:").append(operator.toString()).append('>'); | |
825 | buffer.append("</t:").append(operator).append('>'); | |
882 | 826 | } |
883 | 827 | |
884 | 828 | public boolean isMatch(ExchangeSession.Contact contact) { |
2108 | 2052 | } |
2109 | 2053 | |
2110 | 2054 | // handle deleted occurrences |
2111 | if (!vCalendar.isTodo() && currentItemId != null && !isMeetingResponse && | |
2112 | Settings.getBooleanProperty("davmail.caldavRealUpdate", false)) { | |
2055 | if (!vCalendar.isTodo() && currentItemId != null && !isMeetingResponse) { | |
2113 | 2056 | handleExcludedDates(currentItemId, vCalendar); |
2114 | 2057 | handleModifiedOccurrences(currentItemId, vCalendar); |
2115 | 2058 | } |
2650 | 2593 | |
2651 | 2594 | @Override |
2652 | 2595 | public int sendEvent(String icsBody) throws IOException { |
2653 | String itemName = UUID.randomUUID().toString() + ".EML"; | |
2596 | String itemName = UUID.randomUUID() + ".EML"; | |
2654 | 2597 | byte[] mimeContent = new Event(DRAFTS, itemName, "urn:content-classes:calendarmessage", icsBody, null, null).createMimeContent(); |
2655 | 2598 | if (mimeContent == null) { |
2656 | 2599 | // no recipients, cancel |
2756 | 2699 | protected String getTimezoneidFromOptions() { |
2757 | 2700 | String result = null; |
2758 | 2701 | // 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 | ) { | |
2764 | 2708 | String line; |
2765 | 2709 | // find timezone |
2766 | 2710 | //noinspection StatementWithEmptyBody |
2780 | 2724 | } |
2781 | 2725 | } |
2782 | 2726 | } 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); | |
2793 | 2728 | } |
2794 | 2729 | |
2795 | 2730 | return result; |
2984 | 2919 | } |
2985 | 2920 | |
2986 | 2921 | 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(); | |
3000 | 2933 | } |
3001 | 2934 | |
3002 | 2935 | protected static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<>(); |
3218 | 3151 | * @return true if itemName is an EWS item id |
3219 | 3152 | */ |
3220 | 3153 | protected static boolean isItemId(String itemName) { |
3221 | return itemName.length() >= 152 | |
3154 | return itemName.length() >= 144 | |
3222 | 3155 | // 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$") | |
3224 | 3157 | && itemName.indexOf(' ') < 0; |
3225 | 3158 | } |
3226 | 3159 | |
3272 | 3205 | */ |
3273 | 3206 | @Override |
3274 | 3207 | public void close() { |
3275 | DavGatewayHttpClientFacade.close(httpClient); | |
3208 | httpClient.close(); | |
3276 | 3209 | } |
3277 | 3210 | |
3278 | 3211 | } |
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 | } |
22 | 22 | import org.apache.log4j.Logger; |
23 | 23 | |
24 | 24 | 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; | |
26 | 30 | import java.util.ArrayList; |
27 | 31 | import java.util.Collections; |
28 | 32 | import java.util.List; |
57 | 61 | return proxyes; |
58 | 62 | } else if (enableProxy |
59 | 63 | && proxyHost != null && proxyHost.length() > 0 && proxyPort > 0 |
60 | && !DavGatewayHttpClientFacade.isNoProxyFor(uri) | |
64 | && !isNoProxyFor(uri) | |
61 | 65 | && ("http".equals(scheme) || "https".equals(scheme))) { |
62 | 66 | // DavMail defined proxies |
63 | 67 | ArrayList<Proxy> proxies = new ArrayList<>(); |
68 | 72 | } |
69 | 73 | } |
70 | 74 | |
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 | ||
71 | 89 | @Override |
72 | 90 | 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); | |
74 | 92 | proxySelector.connectFailed(uri, sa, ioe); |
75 | 93 | } |
76 | 94 | } |
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 | } |
18 | 18 | |
19 | 19 | package davmail.http; |
20 | 20 | |
21 | import davmail.Settings; | |
22 | import davmail.ui.PasswordPromptDialog; | |
21 | 23 | import org.apache.log4j.Logger; |
22 | 24 | |
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; | |
23 | 30 | 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; | |
24 | 37 | import java.io.IOException; |
38 | import java.io.InputStreamReader; | |
25 | 39 | import java.net.InetAddress; |
26 | 40 | 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; | |
27 | 48 | |
28 | 49 | /** |
29 | 50 | * SSLSocketFactory implementation. |
32 | 53 | public class DavGatewaySSLSocketFactory extends SSLSocketFactory { |
33 | 54 | static final Logger LOGGER = Logger.getLogger(DavGatewaySSLSocketFactory.class); |
34 | 55 | |
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 | } | |
39 | 159 | } |
40 | 160 | |
41 | 161 | @Override |
42 | 162 | 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[]{}; | |
44 | 169 | } |
45 | 170 | |
46 | 171 | @Override |
47 | 172 | 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[]{}; | |
49 | 179 | } |
50 | 180 | |
51 | 181 | @Override |
52 | 182 | public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { |
53 | 183 | 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 | } | |
55 | 189 | } |
56 | 190 | |
57 | 191 | @Override |
58 | 192 | public Socket createSocket(String host, int port) throws IOException { |
59 | 193 | 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 | } | |
61 | 199 | } |
62 | 200 | |
63 | 201 | @Override |
64 | 202 | public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException { |
65 | 203 | 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 | } | |
67 | 209 | } |
68 | 210 | |
69 | 211 | @Override |
70 | 212 | public Socket createSocket(InetAddress host, int port) throws IOException { |
71 | 213 | 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 | } | |
73 | 219 | } |
74 | 220 | |
75 | 221 | @Override |
76 | 222 | public Socket createSocket(InetAddress host, int port, InetAddress clientHost, int clientPort) throws IOException { |
77 | 223 | 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 | } | |
79 | 229 | } |
80 | 230 | } |
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 | } |
55 | 55 | } |
56 | 56 | } |
57 | 57 | } |
58 | } catch (InterruptedException e) { | |
59 | LOGGER.warn("Thread interrupted", e); | |
60 | Thread.currentThread().interrupt(); | |
58 | 61 | } catch (final Exception ex) { |
59 | 62 | LOGGER.error(ex); |
60 | 63 | } |
301 | 301 | |
302 | 302 | private RequestConfig getRequestConfig() { |
303 | 303 | HashSet<String> authSchemes = new HashSet<>(); |
304 | authSchemes.add(AuthSchemes.NTLM); | |
305 | authSchemes.add(AuthSchemes.BASIC); | |
306 | authSchemes.add(AuthSchemes.DIGEST); | |
307 | 304 | if (Settings.getBooleanProperty("davmail.enableKerberos")) { |
308 | 305 | authSchemes.add(AuthSchemes.SPNEGO); |
309 | 306 | authSchemes.add(AuthSchemes.KERBEROS); |
307 | } else { | |
308 | authSchemes.add(AuthSchemes.NTLM); | |
309 | authSchemes.add(AuthSchemes.BASIC); | |
310 | authSchemes.add(AuthSchemes.DIGEST); | |
310 | 311 | } |
311 | 312 | return RequestConfig.custom() |
312 | 313 | // socket connect timeout |
620 | 621 | } |
621 | 622 | |
622 | 623 | public String getUserAgent() { |
623 | return Settings.getProperty("davmail.userAgent", DavGatewayHttpClientFacade.IE_USER_AGENT); | |
624 | return Settings.getUserAgent(); | |
624 | 625 | } |
625 | 626 | |
626 | 627 | public static HttpResponseException buildHttpResponseException(HttpRequestBase request, HttpResponse response) { |
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 | /* | |
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 | /* | |
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 | /* | |
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 | } |
19 | 19 | package davmail.http.request; |
20 | 20 | |
21 | 21 | import davmail.exchange.XMLStreamUtil; |
22 | import davmail.exchange.dav.ExchangeDavMethod; | |
23 | 22 | import org.apache.http.Header; |
24 | 23 | import org.apache.http.HttpResponse; |
25 | 24 | import org.apache.http.HttpStatus; |
44 | 43 | import java.util.List; |
45 | 44 | |
46 | 45 | 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); | |
48 | 47 | private static final String XML_CONTENT_TYPE = "text/xml; charset=UTF-8"; |
49 | 48 | |
50 | 49 | private HttpResponse response; |
27 | 27 | import davmail.exception.HttpForbiddenException; |
28 | 28 | import davmail.exception.HttpNotFoundException; |
29 | 29 | 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; | |
31 | 35 | import davmail.ui.tray.DavGatewayTray; |
32 | 36 | import davmail.util.IOUtil; |
33 | 37 | import davmail.util.StringUtil; |
35 | 39 | import org.apache.log4j.Logger; |
36 | 40 | |
37 | 41 | 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; | |
39 | 49 | 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; | |
41 | 56 | import java.net.Socket; |
42 | 57 | import java.net.SocketException; |
43 | 58 | import java.net.SocketTimeoutException; |
411 | 426 | throw e; |
412 | 427 | } catch (IOException e) { |
413 | 428 | DavGatewayTray.log(e); |
414 | LOGGER.warn("Ignore broken message " + rangeIterator.currentIndex+ ' ' +e.getMessage()); | |
429 | LOGGER.warn("Ignore broken message " + rangeIterator.currentIndex + ' ' + e.getMessage()); | |
415 | 430 | } |
416 | 431 | |
417 | 432 | } |
747 | 762 | } |
748 | 763 | |
749 | 764 | protected String encodeFolderPath(String folderPath) { |
750 | return BASE64MailboxEncoder.encode(folderPath).replaceAll("\"","\\\\\""); | |
765 | return BASE64MailboxEncoder.encode(folderPath).replaceAll("\"", "\\\\\""); | |
751 | 766 | } |
752 | 767 | |
753 | 768 | protected String decodeFolderPath(String folderPath) { |
801 | 816 | this.message = message; |
802 | 817 | } |
803 | 818 | |
804 | public int getMimeMessageSize() throws IOException, MessagingException, InterruptedException { | |
819 | public int getMimeMessageSize() throws IOException, MessagingException { | |
805 | 820 | loadMessage(); |
806 | 821 | return message.getMimeMessageSize(); |
807 | 822 | } |
809 | 824 | /** |
810 | 825 | * Monitor full message download |
811 | 826 | */ |
812 | protected void loadMessage() throws IOException, MessagingException, InterruptedException { | |
827 | protected void loadMessage() throws IOException, MessagingException { | |
813 | 828 | if (!message.isLoaded()) { |
814 | 829 | // flush current buffer |
815 | 830 | String flushString = buffer.toString(); |
820 | 835 | } |
821 | 836 | } |
822 | 837 | |
823 | public MimeMessage getMimeMessage() throws IOException, MessagingException, InterruptedException { | |
838 | public MimeMessage getMimeMessage() throws IOException, MessagingException { | |
824 | 839 | loadMessage(); |
825 | 840 | return message.getMimeMessage(); |
826 | 841 | } |
827 | 842 | |
828 | public InputStream getRawInputStream() throws IOException, MessagingException, InterruptedException { | |
843 | public InputStream getRawInputStream() throws IOException, MessagingException { | |
829 | 844 | loadMessage(); |
830 | 845 | return message.getRawInputStream(); |
831 | 846 | } |
832 | 847 | |
833 | public Enumeration getMatchingHeaderLines(String[] requestedHeaders) throws IOException, MessagingException, InterruptedException { | |
848 | public Enumeration getMatchingHeaderLines(String[] requestedHeaders) throws IOException, MessagingException { | |
834 | 849 | Enumeration result = message.getMatchingHeaderLinesFromHeaders(requestedHeaders); |
835 | 850 | if (result == null) { |
836 | 851 | loadMessage(); |
841 | 856 | } |
842 | 857 | |
843 | 858 | |
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 { | |
845 | 860 | StringBuilder buffer = new StringBuilder(); |
846 | 861 | MessageWrapper messageWrapper = new MessageWrapper(os, buffer, message); |
847 | 862 | buffer.append("* ").append(currentIndex).append(" FETCH (UID ").append(message.getImapUid()); |
854 | 869 | buffer.append(" FLAGS (").append(message.getImapFlags()).append(')'); |
855 | 870 | } else if ("RFC822.SIZE".equals(param)) { |
856 | 871 | int size; |
857 | if ( ( parameters.contains("BODY.PEEK[HEADER.FIELDS (") | |
872 | if ((parameters.contains("BODY.PEEK[HEADER.FIELDS (") | |
858 | 873 | // exclude mutt header request |
859 | && !parameters.contains("X-LABEL") ) | |
874 | && !parameters.contains("X-LABEL")) | |
860 | 875 | || 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 | |
863 | 877 | 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)); | |
865 | 879 | } else { |
866 | 880 | size = messageWrapper.getMimeMessageSize(); |
867 | 881 | } |
1117 | 1131 | |
1118 | 1132 | /** |
1119 | 1133 | * Check NOT UID condition. |
1134 | * | |
1120 | 1135 | * @param notUidRange excluded uid range |
1121 | * @param imapUid current message imap uid | |
1136 | * @param imapUid current message imap uid | |
1122 | 1137 | * @return true if not excluded |
1123 | 1138 | */ |
1124 | 1139 | private boolean isNotExcluded(String notUidRange, long imapUid) { |
1126 | 1141 | return true; |
1127 | 1142 | } |
1128 | 1143 | String imapUidAsString = String.valueOf(imapUid); |
1129 | for (String rangeValue: notUidRange.split(",")) { | |
1144 | for (String rangeValue : notUidRange.split(",")) { | |
1130 | 1145 | if (imapUidAsString.equals(rangeValue)) { |
1131 | 1146 | return false; |
1132 | 1147 | } |
1134 | 1149 | return true; |
1135 | 1150 | } |
1136 | 1151 | |
1137 | protected void appendEnvelope(StringBuilder buffer, MessageWrapper message) throws IOException, InterruptedException { | |
1152 | protected void appendEnvelope(StringBuilder buffer, MessageWrapper message) throws IOException { | |
1138 | 1153 | |
1139 | 1154 | try { |
1140 | 1155 | MimeMessage mimeMessage = message.getMimeMessage(); |
1225 | 1240 | |
1226 | 1241 | } |
1227 | 1242 | |
1228 | protected void appendBodyStructure(StringBuilder buffer, MessageWrapper message) throws IOException, InterruptedException { | |
1243 | protected void appendBodyStructure(StringBuilder buffer, MessageWrapper message) throws IOException { | |
1229 | 1244 | |
1230 | 1245 | buffer.append(" BODYSTRUCTURE "); |
1231 | 1246 | try { |
1344 | 1359 | buffer.append(" NIL"); |
1345 | 1360 | } |
1346 | 1361 | } |
1362 | int bodySize = getBodyPartSize(bodyPart); | |
1347 | 1363 | appendBodyStructureValue(buffer, bodyPart.getContentID()); |
1348 | 1364 | appendBodyStructureValue(buffer, bodyPart.getDescription()); |
1349 | 1365 | appendBodyStructureValue(buffer, bodyPart.getEncoding()); |
1350 | appendBodyStructureValue(buffer, bodyPart.getSize()); | |
1366 | appendBodyStructureValue(buffer, bodySize); | |
1351 | 1367 | if ("MESSAGE".equals(type) || "TEXT".equals(type)) { |
1352 | 1368 | // line count not implemented in JavaMail, return fake line count |
1353 | appendBodyStructureValue(buffer, bodyPart.getSize() / 80); | |
1369 | appendBodyStructureValue(buffer, bodySize / 80); | |
1354 | 1370 | } else { |
1355 | 1371 | // do not send line count for non text bodyparts |
1356 | 1372 | appendBodyStructureValue(buffer, -1); |
1357 | 1373 | } |
1358 | 1374 | 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; | |
1359 | 1396 | } |
1360 | 1397 | |
1361 | 1398 | protected void appendBodyStructureValue(StringBuilder buffer, String value) { |
1420 | 1457 | protected ExchangeSession.Condition appendNotSearchParams(String token, SearchConditions conditions) throws IOException { |
1421 | 1458 | ImapTokenizer innerTokens = new ImapTokenizer(token); |
1422 | 1459 | ExchangeSession.Condition cond = buildConditions(conditions, innerTokens); |
1423 | if (cond==null || cond.isEmpty()) { | |
1424 | return null; | |
1460 | if (cond == null || cond.isEmpty()) { | |
1461 | return null; | |
1425 | 1462 | } |
1426 | 1463 | return session.not(cond); |
1427 | 1464 | } |
1433 | 1470 | // conditions.deleted = Boolean.FALSE; |
1434 | 1471 | return session.isNull("deleted"); |
1435 | 1472 | } else if ("KEYWORD".equals(nextToken)) { |
1436 | return appendNotSearchParams(nextToken+" "+tokens.nextToken(), conditions); | |
1473 | return appendNotSearchParams(nextToken + " " + tokens.nextToken(), conditions); | |
1437 | 1474 | } else if ("UID".equals(nextToken)) { |
1438 | 1475 | conditions.notUidRange = tokens.nextToken(); |
1439 | 1476 | } else { |
1503 | 1540 | } |
1504 | 1541 | } else //noinspection StatementWithEmptyBody |
1505 | 1542 | 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 | } | |
1513 | 1550 | // client side search token |
1514 | 1551 | return null; |
1515 | 1552 | } |
1605 | 1642 | } |
1606 | 1643 | } else //noinspection StatementWithEmptyBody |
1607 | 1644 | 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 | } | |
1612 | 1649 | } |
1613 | 1650 | } else if ("+Flags".equalsIgnoreCase(action) || "+FLAGS.SILENT".equalsIgnoreCase(action)) { |
1614 | 1651 | ImapTokenizer flagtokenizer = new ImapTokenizer(flags); |
1646 | 1683 | } |
1647 | 1684 | } else //noinspection StatementWithEmptyBody |
1648 | 1685 | 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 | } | |
1653 | 1690 | } |
1654 | 1691 | } else if ("FLAGS".equalsIgnoreCase(action) || "FLAGS.SILENT".equalsIgnoreCase(action)) { |
1655 | 1692 | // flag list with default values |
1678 | 1715 | junk = true; |
1679 | 1716 | } else //noinspection StatementWithEmptyBody |
1680 | 1717 | 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 | } | |
1688 | 1725 | } |
1689 | 1726 | if (keywords != null) { |
1690 | 1727 | properties.put("keywords", message.setFlags(keywords)); |
1801 | 1838 | size++; |
1802 | 1839 | if (((state != State.BODY && writeHeaders) || (state == State.BODY && writeBody)) && |
1803 | 1840 | (size > startIndex) && (bufferSize < maxSize) |
1804 | ) { | |
1841 | ) { | |
1805 | 1842 | super.write(b); |
1806 | 1843 | bufferSize++; |
1807 | 1844 | } |
2070 | 2107 | } else if (currentQuote == '"' && currentChar == '"' || |
2071 | 2108 | currentQuote == '(' && currentChar == ')' || |
2072 | 2109 | currentQuote == '[' && currentChar == ']' |
2073 | ) { | |
2110 | ) { | |
2074 | 2111 | // end quote |
2075 | 2112 | quotes.pop(); |
2076 | 2113 | } else { |
2081 | 2118 | currentIndex++; |
2082 | 2119 | } |
2083 | 2120 | String result = new String(value, startIndex, currentIndex - startIndex); |
2084 | startIndex = currentIndex+1; | |
2121 | startIndex = currentIndex + 1; | |
2085 | 2122 | return result; |
2086 | 2123 | } |
2087 | 2124 | } |
552 | 552 | |
553 | 553 | rootLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("rootLogger")); |
554 | 554 | davmailLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("davmail")); |
555 | httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("org.apache.commons.httpclient")); | |
555 | httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient")); | |
556 | 556 | wireLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient.wire")); |
557 | 557 | |
558 | 558 | addSettingComponent(leftLoggingPanel, BundleMessage.format("UI_LOG_DEFAULT"), rootLoggingLevelField); |
668 | 668 | |
669 | 669 | rootLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("rootLogger")); |
670 | 670 | davmailLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("davmail")); |
671 | httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("org.apache.commons.httpclient")); | |
671 | httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient")); | |
672 | 672 | wireLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient.wire")); |
673 | 673 | logFilePathField.setText(Settings.getProperty("davmail.logFilePath")); |
674 | 674 | logFileSizeField.setText(Settings.getProperty("davmail.logFileSize")); |
839 | 839 | |
840 | 840 | Settings.setLoggingLevel("rootLogger", (Level) rootLoggingLevelField.getSelectedItem()); |
841 | 841 | Settings.setLoggingLevel("davmail", (Level) davmailLoggingLevelField.getSelectedItem()); |
842 | Settings.setLoggingLevel("org.apache.commons.httpclient", (Level) httpclientLoggingLevelField.getSelectedItem()); | |
842 | Settings.setLoggingLevel("httpclient", (Level) httpclientLoggingLevelField.getSelectedItem()); | |
843 | 843 | Settings.setLoggingLevel("httpclient.wire", (Level) wireLoggingLevelField.getSelectedItem()); |
844 | 844 | Settings.setProperty("davmail.logFilePath", logFilePathField.getText()); |
845 | 845 | Settings.setProperty("davmail.logFileSize", logFileSizeField.getText()); |
243 | 243 | } |
244 | 244 | |
245 | 245 | 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) { | |
249 | 247 | if ("Unity".equals(currentDesktop)) { |
250 | 248 | LOGGER.info("Detected Unity desktop, please follow instructions at " + |
251 | 249 | "http://davmail.sourceforge.net/linuxsetup.html to restore normal systray " + |
252 | 250 | "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"); | |
253 | 256 | } |
254 | 257 | if (Settings.O365_INTERACTIVE.equals(Settings.getProperty("davmail.mode"))) { |
255 | 258 | LOGGER.info("O365Interactive is not compatible with SWT, do not try to create SWT tray"); |
356 | 359 | boolean isXFCE = "XFCE".equals(xdgCurrentDesktop); |
357 | 360 | boolean isUnity = "Unity".equals(xdgCurrentDesktop); |
358 | 361 | boolean isCinnamon = "X-Cinnamon".equals(xdgCurrentDesktop); |
362 | boolean isGnome = xdgCurrentDesktop != null && xdgCurrentDesktop.contains("GNOME"); | |
359 | 363 | |
360 | 364 | if (backgroundColorString == null || backgroundColorString.length() == 0) { |
361 | 365 | // define color for default theme |
370 | 374 | } |
371 | 375 | if (isCinnamon) { |
372 | 376 | backgroundColorString = "#2E2E2E"; |
377 | } | |
378 | if (isGnome) { | |
379 | backgroundColorString = "#000000"; | |
373 | 380 | } |
374 | 381 | } |
375 | 382 | |
383 | 390 | imageType = BufferedImage.TYPE_INT_RGB; |
384 | 391 | } |
385 | 392 | |
386 | if (backgroundColor != null || isKDE || isUnity || isXFCE) { | |
393 | if (backgroundColor != null || isKDE || isUnity || isXFCE || isGnome) { | |
387 | 394 | int width = image.getWidth(); |
388 | 395 | int height = image.getHeight(); |
389 | 396 | int x = 0; |
398 | 405 | height = 24; |
399 | 406 | x = 4; |
400 | 407 | y = 4; |
401 | } else if (isCinnamon) { | |
408 | } else if (isCinnamon | isGnome) { | |
402 | 409 | width = 24; |
403 | 410 | height = 24; |
404 | 411 | x = 4; |
0 | 0 | # Warning : actual log levels set in davmail.properties |
1 | 1 | log4j.rootLogger=WARN, ConsoleAppender |
2 | 2 | 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 | |
5 | 4 | |
6 | 5 | # ConsoleAppender is set to be a ConsoleAppender. |
7 | 6 | log4j.appender.ConsoleAppender=org.apache.log4j.ConsoleAppender |
18 | 17 | #log4j.appender.ConnectionAppender.layout=org.apache.log4j.PatternLayout |
19 | 18 | #log4j.appender.ConnectionAppender.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c %x - %m%n |
20 | 19 | |
21 | #log4j.logger.org.apache.commons.httpclient.util.IdleConnectionHandler=DEBUG | |
22 | #log4j.logger.org.apache.commons.httpclient.util.MultiThreadedHttpConnectionManager=DEBUG | |
23 | ||
24 | 20 | #log4j.appender.defaultSocketAppender=org.apache.log4j.net.SocketAppender |
25 | 21 | #log4j.appender.defaultSocketAppender.RemoteHost=localhost |
26 | 22 | #log4j.appender.defaultSocketAppender.port=4560 |
331 | 331 | </p> |
332 | 332 | <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. |
333 | 333 | </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> | |
334 | 336 | <p> |
335 | 337 | <strong>Message deleted over IMAP still visible through OWA</strong> |
336 | 338 | </p> |
45 | 45 | <code>davmail -notray</code> |
46 | 46 | </subsection> |
47 | 47 | <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. | |
49 | 49 | If SWT is available it provides an improved tray icon. |
50 | 50 | If you do not want any tray icon run DavMail with the <code>-notray</code> option. |
51 | 51 | </p> |
10 | 10 | <body> |
11 | 11 | |
12 | 12 | <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. | |
14 | 14 | </p> |
15 | 15 | |
16 | 16 | <p>Davmail Gateway can run in server mode as a gateway between the mail |
118 | 118 | |
119 | 119 | # Delete messages immediately on IMAP STORE \Deleted flag |
120 | 120 | 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 | |
122 | 123 | davmail.imapIdleDelay= |
123 | 124 | # Always reply to IMAP RFC822.SIZE requests with Exchange approximate message size for performance reasons |
124 | 125 | davmail.imapAlwaysApproxMsgSize= |
220 | 221 | </section> |
221 | 222 | |
222 | 223 | <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 | |
224 | 225 | </p> |
225 | 226 | |
226 | 227 | <p>Davmail Gateway can now be deployed in any JEE application server using |
10 | 10 | <body> |
11 | 11 | |
12 | 12 | <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&architecture=x86-64-bit&package=jre-fx">Zulu JRE FX</a> | |
16 | is a good option to have OpenJFX available for O365Interactive. | |
16 | 17 | </p> |
17 | 18 | |
18 | 19 | <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 | } |